diff --git a/coupling_runtime.exp b/coupling_runtime.exp index 9b1cd85..9ecc287 100644 Binary files a/coupling_runtime.exp and b/coupling_runtime.exp differ diff --git a/coupling_runtime.lib b/coupling_runtime.lib index 238813e..e278454 100644 Binary files a/coupling_runtime.lib and b/coupling_runtime.lib differ diff --git a/src/couplingSeed.test.ts b/src/couplingSeed.test.ts index d7bbb8c..40b5aa1 100644 --- a/src/couplingSeed.test.ts +++ b/src/couplingSeed.test.ts @@ -4,6 +4,9 @@ process.env.COUPLING_SERVICE_SECRET = process.env.COUPLING_SERVICE_SECRET || 'se process.env.COUPLING_WM_MASTER_KEY = '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff'; +const { config } = await import('./config'); +config.wmMasterKeyHex = process.env.COUPLING_WM_MASTER_KEY; + const { deriveCouplingSeedHex, isCouplingSeedConfigured, COUPLING_SEED_HEX_LENGTH } = await import( './couplingSeed' ); diff --git a/src/ffi.test.ts b/src/ffi.test.ts index d8c5e4c..4419ffa 100644 --- a/src/ffi.test.ts +++ b/src/ffi.test.ts @@ -24,7 +24,7 @@ afterEach(() => { describe('normalizeTokenHexForFfi', () => { it('lowercases valid token hex before sending it to the native runtime', () => { - expect(normalizeTokenHexForFfi('ABCDEF123456')).toBe('abcdef123456'); + expect(normalizeTokenHexForFfi('ABCDEF1234567890')).toBe('abcdef1234567890'); }); it('rejects non-hex token values', () => { @@ -37,12 +37,12 @@ describe('normalizeTokenHexForFfi', () => { ); }); - it('rejects token values outside the supported native bounds', () => { + it('rejects token values outside the seeded v2 token width', () => { expect(() => normalizeTokenHexForFfi('abcd')).toThrow( - 'tokenHex must be between 8 and 64 hex characters' + 'tokenHex must be exactly 16 hex characters' ); - expect(() => normalizeTokenHexForFfi('a'.repeat(66))).toThrow( - 'tokenHex must be between 8 and 64 hex characters' + expect(() => normalizeTokenHexForFfi('a'.repeat(32))).toThrow( + 'tokenHex must be exactly 16 hex characters' ); }); diff --git a/src/ffi.ts b/src/ffi.ts index ef540fe..d762889 100644 --- a/src/ffi.ts +++ b/src/ffi.ts @@ -3,20 +3,18 @@ import { createHash } from 'node:crypto'; import { existsSync, readFileSync } from 'node:fs'; import { config } from './config'; -export type CouplingAssetType = 'png' | 'fbx' | 'text'; +export type CouplingAssetType = 'png' | 'fbx'; export type CouplingDecodeInput = { filePath: string; assetType: CouplingAssetType; - expectedTokenLength?: number; - // Per-job seed (64 hex) that placed the v2 mark. Required to decode a v2 image/mesh mark; when - // absent, only the legacy (pre-seed) decoders are attempted. - seedHex?: string; + // Per-job seed (64 hex) that placed the mark — required. Decode is seed-only; there is no blind path. + seedHex: string; }; export type CouplingEncodeInput = { filePath: string; - assetType: Exclude; + assetType: CouplingAssetType; tokenHex: string; // Per-job seed (64 hex) from the server; the sole placement secret (no key is baked into the client). seedHex: string; @@ -26,21 +24,11 @@ export type CouplingDecodeResult = { decoderKind: string; tokenHex: string; tokenLength: number; - // 2 = v2 (seeded DCT/vertex-norm), 1 = legacy/text. Lets forensics tell which engine matched. + // Always 2 (the v2 seeded engine). Retained for forensic reporting / future versioning. wmVersion: number; }; type NativeSymbols = { - xg_0115: (filePathUtf8: number, tokenHexUtf8: number) => number; - xg_0118: (filePathUtf8: number, outHexUtf8: number, outCap: number) => number; - xg_0119: (filePathUtf8: number, outHexUtf8: number, outCap: number) => number; - xg_0120: (filePathUtf8: number, tokenHexUtf8: number) => number; - xg_0121: ( - filePathUtf8: number, - expectedHexLength: number, - outHexUtf8: number, - outCap: number - ) => number; // v2 image (DCT-domain QIM, all raster formats). Encode(path, token, seed): 0 ok, 1 skip, <0 err. xg_0122: (filePathUtf8: number, tokenHexUtf8: number, seedHexUtf8: number) => number; xg_0123: (filePathUtf8: number, seedHexUtf8: number, outHexUtf8: number, outCap: number) => number; @@ -54,62 +42,10 @@ type NativeLibrary = { close(): void; }; -const MIN_TOKEN_HEX_LENGTH = 8; -const MAX_TOKEN_HEX_LENGTH = 64; -const SEEDED_TOKEN_HEX_LENGTH = 16; -const OUTPUT_CAPACITY = MAX_TOKEN_HEX_LENGTH + 1; +const TOKEN_HEX_LENGTH = 16; +const OUTPUT_CAPACITY = TOKEN_HEX_LENGTH + 1; const HEX_RE = /^[0-9a-f]+$/; -const PNG_ERRORS: Record = { - [-1]: 'native sodium initialization failed', - [-2]: 'native PNG decoder received invalid arguments', - [-3]: 'native PNG decoder could not read the asset bytes', - [-4]: 'native PNG decoder requires a non-empty PNG file', - [-5]: 'native PNG decoder rejected the PNG signature', - [-6]: 'native PNG decoder found malformed PNG chunks', - [-7]: 'native PNG decoder could not unmask lane 0', - [-8]: 'native PNG decoder could not unmask lane 1', - [-9]: 'native PNG decoder could not unmask lane 2', - [-10]: 'native PNG decoder could not reach token quorum', -}; - -const TEXT_ERRORS: Record = { - [-1]: 'native text decoder failed sodium initialization', - [-2]: 'native text decoder received invalid arguments', - [-3]: 'native text decoder could not read the asset bytes', - [-4]: 'native text decoder only supports files between 1 byte and 8 MiB', - [-5]: 'native text decoder could not unmask lane 0', - [-6]: 'native text decoder could not unmask lane 1', - [-7]: 'native text decoder could not unmask lane 2', - [-8]: 'native text decoder could not reach token quorum', -}; - -const FBX_ERRORS: Record = { - [-1]: 'native FBX decoder failed sodium initialization', - [-2]: 'native FBX decoder received invalid arguments', - [-3]: 'native FBX decoder expected an even token length between 8 and 64', - [-4]: 'native FBX decoder output buffer was too small', - [-5]: 'native FBX decoder could not read the asset bytes', - [-6]: 'native FBX decoder only supports files between 1 byte and 64 MiB', - [-7]: 'native FBX decoder rejected binary bytes while using the text path', - [-8]: 'native FBX decoder found no carrier data', - [-9]: 'native FBX decoder did not find all three carrier lanes', - [-10]: 'native FBX decoder could not parse carrier values', - [-11]: 'native FBX decoder could not allocate its extraction buffer', - [-12]: 'native FBX decoder does not have enough carrier data for the requested token length', - [-13]: 'native FBX decoder could not build lane permutations', - [-14]: 'native FBX decoder failed while converting extracted bits to hex', - [-41]: 'native FBX decoder could not parse binary FBX nodes', - [-42]: 'native FBX decoder could not collect binary FBX carrier data', - [-43]: 'native FBX decoder did not find all three binary FBX carrier lanes', - [-44]: 'native FBX decoder could not convert binary FBX carrier values', - [-45]: 'native FBX decoder could not allocate its binary extraction buffer', - [-46]: 'native FBX decoder does not have enough binary carrier data for the requested token length', - [-47]: 'native FBX decoder could not build binary lane permutations', - [-48]: 'native FBX decoder failed while converting binary extracted bits to hex', - [-49]: 'native FBX decoder could not build binary lane seed material', -}; - export class NativeCouplingError extends Error { readonly code: number; readonly decoderKind: string; @@ -179,26 +115,6 @@ function loadNativeLibrary(): NativeLibrary { verifyConfiguredDllIntegrity(); try { nativeLibrary = dlopen(config.dllPath, { - xg_0115: { - args: ['ptr', 'ptr'], - returns: 'i32', - }, - xg_0118: { - args: ['ptr', 'ptr', 'u32'], - returns: 'i32', - }, - xg_0119: { - args: ['ptr', 'ptr', 'u32'], - returns: 'i32', - }, - xg_0120: { - args: ['ptr', 'ptr'], - returns: 'i32', - }, - xg_0121: { - args: ['ptr', 'u32', 'ptr', 'u32'], - returns: 'i32', - }, xg_0122: { args: ['ptr', 'ptr', 'ptr'], returns: 'i32', @@ -232,6 +148,9 @@ function decodeBufferToHex(buffer: Uint8Array): string { if (!HEX_RE.test(tokenHex)) { throw new Error('Coupling runtime returned invalid token data'); } + if (tokenHex.length !== TOKEN_HEX_LENGTH) { + throw new Error('Coupling runtime returned token data with invalid length'); + } return tokenHex; } @@ -243,11 +162,8 @@ export function normalizeTokenHexForFfi(tokenHex: string): string { if (normalized.length % 2 !== 0) { throw new Error('tokenHex must contain an even number of hex characters'); } - if ( - normalized.length < MIN_TOKEN_HEX_LENGTH || - normalized.length > MAX_TOKEN_HEX_LENGTH - ) { - throw new Error('tokenHex must be between 8 and 64 hex characters'); + if (normalized.length !== TOKEN_HEX_LENGTH) { + throw new Error('tokenHex must be exactly 16 hex characters'); } return normalized; } @@ -266,92 +182,31 @@ function toNativeUtf8Buffer(value: string): Buffer { return Buffer.from(`${value}\0`, 'utf8'); } -function getDefaultExpectedTokenLength(assetType: CouplingAssetType): number | undefined { - if (assetType === 'fbx') { - return 32; - } - return undefined; -} - export function getDecoderKind(assetType: CouplingAssetType): string { - switch (assetType) { - case 'png': - return 'coupling-png-ffi'; - case 'fbx': - return 'coupling-fbx-ffi'; - case 'text': - return 'coupling-text-ffi'; - } -} - -function getErrorMap(assetType: CouplingAssetType): Record { - switch (assetType) { - case 'png': - return PNG_ERRORS; - case 'fbx': - return FBX_ERRORS; - case 'text': - return TEXT_ERRORS; - } + return assetType === 'png' ? 'coupling-png-v2-ffi' : 'coupling-fbx-v2-ffi'; } +// Seed-only v2 decode: the per-job seed that placed the mark is required, and there is no blind or +// legacy fallback. A miss (wrong seed, tampered, or unmarked asset) throws NativeCouplingError. function callNativeDecoder(input: CouplingDecodeInput): CouplingDecodeResult { const library = loadNativeLibrary(); const output = new Uint8Array(OUTPUT_CAPACITY); const outputPointer = ptr(output); - const filePathBuffer = toNativeUtf8Buffer(input.filePath); - const filePathPointer = ptr(filePathBuffer); + const filePathPointer = ptr(toNativeUtf8Buffer(input.filePath)); + const seedPointer = ptr(toNativeUtf8Buffer(normalizeSeedHexForFfi(input.seedHex))); const decoderKind = getDecoderKind(input.assetType); - const seedHex = input.seedHex ? normalizeSeedHexForFfi(input.seedHex) : undefined; - - // A v2 mark can only be read with the per-job seed that placed it; without one we go straight to - // the legacy decoders. - const seedPointer = seedHex ? ptr(toNativeUtf8Buffer(seedHex)) : null; - - let exitCode: number; - if (input.assetType === 'png') { - if (seedPointer !== null) { - exitCode = library.symbols.xg_0123(filePathPointer, seedPointer, outputPointer, output.byteLength); - if (exitCode === 0) { - const tokenHex = decodeBufferToHex(output); - return { decoderKind: 'coupling-png-v2-ffi', tokenHex, tokenLength: tokenHex.length, wmVersion: 2 }; - } - } - exitCode = library.symbols.xg_0118(filePathPointer, outputPointer, output.byteLength); - } else if (input.assetType === 'text') { - exitCode = library.symbols.xg_0119(filePathPointer, outputPointer, output.byteLength); - } else { - if (seedPointer !== null) { - exitCode = library.symbols.xg_0125(filePathPointer, seedPointer, outputPointer, output.byteLength); - if (exitCode === 0) { - const tokenHex = decodeBufferToHex(output); - return { decoderKind: 'coupling-fbx-v2-ffi', tokenHex, tokenLength: tokenHex.length, wmVersion: 2 }; - } - } - const expectedTokenLength = input.expectedTokenLength ?? getDefaultExpectedTokenLength('fbx'); - if (!expectedTokenLength) { - throw new Error(`Missing expected token length for ${decoderKind}`); - } - exitCode = library.symbols.xg_0121( - filePathPointer, - expectedTokenLength, - outputPointer, - output.byteLength - ); - } + + const exitCode = + input.assetType === 'png' + ? library.symbols.xg_0123(filePathPointer, seedPointer, outputPointer, output.byteLength) + : library.symbols.xg_0125(filePathPointer, seedPointer, outputPointer, output.byteLength); if (exitCode !== 0) { - const errorMessage = getErrorMap(input.assetType)[exitCode] ?? `native decoder failed with code ${exitCode}`; - throw new NativeCouplingError(decoderKind, exitCode, errorMessage); + throw new NativeCouplingError(decoderKind, exitCode, `native v2 decoder failed with code ${exitCode}`); } const tokenHex = decodeBufferToHex(output); - return { - decoderKind, - tokenHex, - tokenLength: tokenHex.length, - wmVersion: 1, - }; + return { decoderKind, tokenHex, tokenLength: tokenHex.length, wmVersion: 2 }; } export function decodeCouplingAsset(input: CouplingDecodeInput): CouplingDecodeResult { @@ -365,23 +220,12 @@ export function encodeCouplingAsset(input: CouplingEncodeInput): void { const filePathPointer = ptr(toNativeUtf8Buffer(input.filePath)); const tokenHexPointer = ptr(toNativeUtf8Buffer(tokenHex)); const seedHexPointer = ptr(toNativeUtf8Buffer(seedHex)); - const useSeededV2 = tokenHex.length === SEEDED_TOKEN_HEX_LENGTH; const decoderKind = - input.assetType === 'png' - ? useSeededV2 - ? 'coupling-png-v2-encode-ffi' - : 'coupling-png-legacy-encode-ffi' - : useSeededV2 - ? 'coupling-fbx-v2-encode-ffi' - : 'coupling-fbx-legacy-encode-ffi'; + input.assetType === 'png' ? 'coupling-png-v2-encode-ffi' : 'coupling-fbx-v2-encode-ffi'; const exitCode = input.assetType === 'png' - ? useSeededV2 - ? library.symbols.xg_0122(filePathPointer, tokenHexPointer, seedHexPointer) - : library.symbols.xg_0115(filePathPointer, tokenHexPointer) - : useSeededV2 - ? library.symbols.xg_0124(filePathPointer, tokenHexPointer, seedHexPointer) - : library.symbols.xg_0120(filePathPointer, tokenHexPointer); + ? library.symbols.xg_0122(filePathPointer, tokenHexPointer, seedHexPointer) + : library.symbols.xg_0124(filePathPointer, tokenHexPointer, seedHexPointer); // 0 = applied, 1 = skipped (uncarryable asset — never an error, never blocks), <0 = real failure. if (exitCode !== 0 && exitCode !== 1) { throw new NativeCouplingError( diff --git a/src/materialize.test.ts b/src/materialize.test.ts index df77501..a276284 100644 --- a/src/materialize.test.ts +++ b/src/materialize.test.ts @@ -396,7 +396,7 @@ describe('materializeProtectedPayload', () => { } }); - it('materializes legacy-length coupling job tokens without forcing v2 encoding', async () => { + it('materializes coupling job assets with v2 seeded embedding', async () => { const workspaceDir = await mkdtemp(path.join(tmpdir(), 'coupling-materialize-')); try { const fixture = await createMaterializationFixture(workspaceDir); @@ -411,7 +411,7 @@ describe('materializeProtectedPayload', () => { globalThis.fetch = createMaterializationFetchStub({ descriptor: fixture.descriptor, contentKeyBase64: fixture.contentKeyBase64, - couplingJobs: [{ assetPath: 'Assets/Protected/artifact.png', tokenHex: 'a'.repeat(32) }], + couplingJobs: [{ assetPath: 'Assets/Protected/artifact.png', tokenHex: 'a'.repeat(16) }], licenseSubject: 'lic-test', redeemCounts, receiptCounts, diff --git a/src/nativeExports.test.ts b/src/nativeExports.test.ts new file mode 100644 index 0000000..87f9a72 --- /dev/null +++ b/src/nativeExports.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'bun:test'; +import path from 'node:path'; + +describe('Windows native export ordinals', () => { + it('preserves surviving public DLL ordinals after removing legacy exports', async () => { + const defPath = path.resolve(import.meta.dir, '..', 'yucp_coupling', 'yucp_coupling.def'); + const def = await Bun.file(defPath).text(); + + expect(def).toContain('xg_0122 @4'); + expect(def).toContain('xg_0123 @5'); + expect(def).toContain('xg_0124 @6'); + expect(def).toContain('xg_0125 @7'); + }); + + it('preserves surviving runtime-helper ordinals after removing legacy exports', async () => { + const defPath = path.resolve( + import.meta.dir, + '..', + 'yucp_coupling', + 'yucp_coupling.runtime-helper.def' + ); + const def = await Bun.file(defPath).text(); + + expect(def).toContain('xg_0122 @3'); + expect(def).toContain('xg_0124 @4'); + }); +}); diff --git a/src/roundtrip.test.ts b/src/roundtrip.test.ts index 1e25a77..920a501 100644 --- a/src/roundtrip.test.ts +++ b/src/roundtrip.test.ts @@ -3,6 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { deflateSync } from 'node:zlib'; +import { createHash } from 'node:crypto'; const SERVICE_SECRET = 'roundtrip-secret'; const SERVICE_PORT = 8791; @@ -139,6 +140,34 @@ const serviceProcess = Bun.spawn({ stdout: 'pipe', stderr: 'pipe', }); +const serviceStderrText = captureStreamText(serviceProcess.stderr); + +function captureStreamText(stream: ReadableStream, maxChars = 8192): () => string { + const decoder = new TextDecoder(); + let captured = ''; + void (async () => { + const reader = stream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + captured += decoder.decode(value, { stream: true }); + if (captured.length > maxChars) { + captured = captured.slice(-maxChars); + } + } + captured += decoder.decode(); + if (captured.length > maxChars) { + captured = captured.slice(-maxChars); + } + } catch { + captured += decoder.decode(); + } + })(); + return () => captured.trim(); +} async function waitForService(): Promise { const deadline = Date.now() + 15_000; @@ -149,7 +178,23 @@ async function waitForService(): Promise { } await Bun.sleep(200); } - const stderrText = await new Response(serviceProcess.stderr).text().catch(() => ''); + const stderrText = serviceStderrText(); + throw new Error(`coupling service did not become healthy${stderrText ? `: ${stderrText.trim()}` : ''}`); +} + +async function waitForServiceOnPort( + port: number, + getStderrText: () => string +): Promise { + const deadline = Date.now() + 15_000; + while (Date.now() < deadline) { + const response = await fetch(`http://127.0.0.1:${port}/v1/health`).catch(() => null); + if (response?.ok) { + return; + } + await Bun.sleep(200); + } + const stderrText = getStderrText(); throw new Error(`coupling service did not become healthy${stderrText ? `: ${stderrText.trim()}` : ''}`); } @@ -420,10 +465,10 @@ describe('coupling FBX 7500 support', () => { }) ).not.toThrow(); - // A mesh with no embedded mark misses cleanly (v2 decode then legacy fallback both miss). + // A mesh with no embedded mark misses cleanly under seed-only decode. let decodeThrown: unknown; try { - decodeCouplingAsset({ filePath: assetPath, assetType: 'fbx' }); + decodeCouplingAsset({ filePath: assetPath, assetType: 'fbx', seedHex: PNG_SEED_HEX }); } catch (error) { decodeThrown = error; } @@ -553,3 +598,160 @@ describe('runtime artifact manifest', () => { expect(payload.ciphertextSize).toBe(payload.plaintextSize); }); }); + +describe('coupling attribution (seed-iteration)', () => { + it('rejects oversized candidate sets instead of partially matching a truncated list', async () => { + await waitForService(); + const headers = { Authorization: `Bearer ${SERVICE_SECRET}`, 'Content-Type': 'application/json' }; + const candidates = Array.from({ length: 20_001 }, (_, index) => ({ + assetPath: `Assets/Test/${index}.png`, + licenseSubject: `buyer-${index}`, + tokenHash: '0'.repeat(64), + })); + + const response = await fetch(`http://127.0.0.1:${SERVICE_PORT}/v1/coupling/attribute`, { + method: 'POST', + headers, + body: JSON.stringify({ assets: [], candidates }), + }); + const payload = (await response.json()) as { error?: { code?: string } }; + + expect(response.status).toBe(413); + expect(payload.error?.code).toBe('too_many_candidates'); + }); + + it('rejects malformed base64 content before candidate attribution', async () => { + await waitForService(); + const headers = { Authorization: `Bearer ${SERVICE_SECRET}`, 'Content-Type': 'application/json' }; + + const response = await fetch(`http://127.0.0.1:${SERVICE_PORT}/v1/coupling/attribute`, { + method: 'POST', + headers, + body: JSON.stringify({ + assets: [{ assetPath: 'Assets/Test/leak.png', assetType: 'png', contentBase64: 'Zm9v!!!!' }], + candidates: [ + { + assetPath: 'Assets/Test/leak.png', + licenseSubject: 'buyer-A', + tokenHash: '0'.repeat(64), + }, + ], + }), + }); + const payload = (await response.json()) as { error?: { code?: string } }; + + expect(response.status).toBe(400); + expect(payload.error?.code).toBe('invalid_asset_content'); + }); + + it('fails the request when candidate seed derivation configuration is invalid', async () => { + const brokenPort = SERVICE_PORT + 1; + const brokenService = Bun.spawn({ + cmd: ['bun', 'src/server.ts'], + cwd: path.resolve(import.meta.dir, '..'), + env: { + ...process.env, + COUPLING_SERVICE_SECRET: SERVICE_SECRET, + COUPLING_SERVICE_PORT: String(brokenPort), + COUPLING_WM_MASTER_KEY: 'zz', + }, + stdout: 'pipe', + stderr: 'pipe', + }); + const brokenStderrText = captureStreamText(brokenService.stderr); + try { + await waitForServiceOnPort(brokenPort, brokenStderrText); + const response = await fetch(`http://127.0.0.1:${brokenPort}/v1/coupling/attribute`, { + method: 'POST', + headers: { Authorization: `Bearer ${SERVICE_SECRET}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + assets: [ + { + assetPath: 'Assets/Test/leak.png', + assetType: 'png', + contentBase64: Buffer.from(makeGrayPng(PNG_FIXTURE_SIZE, 0x80)).toString('base64'), + }, + ], + candidates: [ + { + assetPath: 'Assets/Test/leak.png', + licenseSubject: 'buyer-A', + tokenHash: '0'.repeat(64), + }, + ], + }), + }); + const payload = (await response.json()) as { error?: { code?: string } }; + + expect(response.status).toBe(500); + expect(payload.error?.code).toBe('internal_error'); + } finally { + brokenService.kill(); + await brokenService.exited.catch(() => undefined); + } + }); + + it('identifies the buyer from a leaked asset and rejects decoy-only candidates', async () => { + await waitForService(); + const assetRel = 'Assets/Test/leak.png'; + const buyer = 'buyer-A'; + const headers = { Authorization: `Bearer ${SERVICE_SECRET}`, 'Content-Type': 'application/json' }; + + // 1) Server derives the per-(asset, buyer) seed. + const deriveRes = await fetch(`http://127.0.0.1:${SERVICE_PORT}/v1/coupling/internal/derive-seeds`, { + method: 'POST', + headers, + body: JSON.stringify({ licenseSubject: buyer, assetPaths: [assetRel] }), + }); + const seedHex = ((await deriveRes.json()) as { seeds: Array<{ seedHex: string }> }).seeds[0]?.seedHex; + expect(seedHex).toMatch(/^[0-9a-f]{64}$/); + if (!seedHex) throw new Error('no seed derived'); + + const workspaceDir = await mkdtemp(path.join(tmpdir(), 'coupling-attr-')); + try { + // 2) Embed a token with that seed; tokenHash is what the trace would store. + const assetPath = path.join(workspaceDir, 'leak.png'); + await writeFile(assetPath, makeGrayPng(PNG_FIXTURE_SIZE, 0x80)); + const token = PNG_TOKEN_HEX; + encodeCouplingAsset({ filePath: assetPath, assetType: 'png', tokenHex: token, seedHex }); + const tokenHash = createHash('sha256').update(token, 'utf8').digest('hex'); + const contentBase64 = Buffer.from(await readFile(assetPath)).toString('base64'); + const decoyHash = createHash('sha256').update('feedfacefeedface', 'utf8').digest('hex'); + + // 3) Attribute with a decoy first + the real candidate -> names the buyer. + const res = await fetch(`http://127.0.0.1:${SERVICE_PORT}/v1/coupling/attribute`, { + method: 'POST', + headers, + body: JSON.stringify({ + assets: [{ assetPath: assetRel, assetType: 'png', contentBase64 }], + candidates: [ + { assetPath: assetRel, licenseSubject: 'buyer-OTHER', tokenHash: decoyHash }, + { assetPath: assetRel, licenseSubject: buyer, tokenHash }, + ], + }), + }); + const payload = (await res.json()) as { + results: Array<{ matched: boolean; matchedLicenseSubject?: string; tokenHex?: string }>; + }; + expect(payload.results[0]).toMatchObject({ + matched: true, + matchedLicenseSubject: buyer, + tokenHex: token, + }); + + // 4) Decoy-only candidates -> no match (no blind fallback). + const res2 = await fetch(`http://127.0.0.1:${SERVICE_PORT}/v1/coupling/attribute`, { + method: 'POST', + headers, + body: JSON.stringify({ + assets: [{ assetPath: assetRel, assetType: 'png', contentBase64 }], + candidates: [{ assetPath: assetRel, licenseSubject: 'buyer-OTHER', tokenHash: decoyHash }], + }), + }); + const payload2 = (await res2.json()) as { results: Array<{ matched: boolean }> }; + expect(payload2.results[0]?.matched).toBe(false); + } finally { + await rm(workspaceDir, { recursive: true, force: true }); + } + }); +}); diff --git a/src/server.ts b/src/server.ts index e12f86e..ebec9f6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { timingSafeEqual } from 'node:crypto'; +import { createHash, timingSafeEqual } from 'node:crypto'; import { mkdir, rm } from 'node:fs/promises'; import path from 'node:path'; import { @@ -41,8 +41,6 @@ type CouplingScanManifestAsset = { field?: unknown; assetPath?: unknown; assetType?: unknown; - expectedTokenLength?: unknown; - tokenLength?: unknown; contentBase64?: unknown; seedHex?: unknown; }; @@ -51,8 +49,7 @@ type CouplingScanTask = { fieldName: string; assetPath: string; assetType: CouplingAssetType; - expectedTokenLength?: number; - seedHex?: string; + seedHex: string; filePath: string; }; @@ -151,35 +148,20 @@ function buildScanError(task: CouplingScanTask, error: unknown): CouplingScanErr } const message = error instanceof Error ? error.message : ''; - const publicMessage = (() => { - if (message.startsWith('Decoded token length ')) { - return 'Decoded token length did not match expectedTokenLength'; - } - if (message.startsWith('Missing expected token length')) { - return 'Asset decode request was missing an expected token length'; - } - if ( - message === 'Coupling runtime returned invalid token data' || - message === 'Coupling runtime returned an empty token' || - message === 'Coupling runtime library integrity check failed' || - message === 'Coupling runtime library failed to load' || - message === 'Coupling runtime library is unavailable on this host' - ) { - return message; - } - return 'Asset decoding failed'; - })(); + const publicMessage = + message === 'Coupling runtime returned invalid token data' || + message === 'Coupling runtime returned an empty token' || + message === 'Coupling runtime library integrity check failed' || + message === 'Coupling runtime library failed to load' || + message === 'Coupling runtime library is unavailable on this host' + ? message + : 'Asset decoding failed'; return { assetPath: task.assetPath, assetType: task.assetType, decoderKind: getDecoderKind(task.assetType), - code: - publicMessage === 'Decoded token length did not match expectedTokenLength' - ? 'token_length_mismatch' - : publicMessage === 'Asset decode request was missing an expected token length' - ? 'missing_expected_token_length' - : 'decode_failed', + code: 'decode_failed', message: publicMessage, }; } @@ -188,18 +170,9 @@ function decodeScanTask(task: CouplingScanTask): CouplingScanResult { const decoded = decodeCouplingAsset({ filePath: task.filePath, assetType: task.assetType, - ...(task.expectedTokenLength === undefined - ? {} - : { expectedTokenLength: task.expectedTokenLength }), - ...(task.seedHex === undefined ? {} : { seedHex: task.seedHex }), + seedHex: task.seedHex, }); - if (task.expectedTokenLength !== undefined && decoded.tokenLength !== task.expectedTokenLength) { - throw new Error( - `Decoded token length ${decoded.tokenLength} did not match expectedTokenLength ${task.expectedTokenLength}` - ); - } - return { assetPath: task.assetPath, assetType: task.assetType, @@ -211,10 +184,7 @@ function decodeScanTask(task: CouplingScanTask): CouplingScanResult { } const PNG_LIKELY_STRIPPED_CODES = new Set([-7, -8, -9, -10]); -const TEXT_LIKELY_STRIPPED_CODES = new Set([-5, -6, -7, -8]); -const FBX_LIKELY_STRIPPED_CODES = new Set([ - -8, -9, -10, -12, -13, -14, -42, -43, -44, -46, -47, -48, -]); +const FBX_LIKELY_STRIPPED_CODES = new Set([-8, -9, -10]); function classifyNativeDecodeFailure( assetType: CouplingAssetType, @@ -225,8 +195,6 @@ function classifyNativeDecodeFailure( return PNG_LIKELY_STRIPPED_CODES.has(nativeCode) ? 'likely-stripped' : 'no-signal'; case 'fbx': return FBX_LIKELY_STRIPPED_CODES.has(nativeCode) ? 'likely-stripped' : 'no-signal'; - case 'text': - return TEXT_LIKELY_STRIPPED_CODES.has(nativeCode) ? 'likely-stripped' : 'no-signal'; } } @@ -389,19 +357,6 @@ function inferAssetType(assetPath: string, uploadName: string): CouplingAssetTyp if (extension === '.fbx') { return 'fbx'; } - if ( - extension === '.txt' || - extension === '.md' || - extension === '.json' || - extension === '.csv' || - extension === '.yaml' || - extension === '.yml' || - extension === '.xml' || - extension === '.ini' || - extension === '.log' - ) { - return 'text'; - } } return null; } @@ -413,12 +368,9 @@ function normalizeAssetType( ): CouplingAssetType { if (typeof rawType === 'string') { const normalized = rawType.trim().toLowerCase(); - if (normalized === 'png' || normalized === 'fbx' || normalized === 'text') { + if (normalized === 'png' || normalized === 'fbx') { return normalized; } - if (normalized === 'txt' || normalized === 'plain') { - return 'text'; - } throw new HttpError( 400, 'unsupported_asset_type', @@ -436,31 +388,10 @@ function normalizeAssetType( return inferred; } -function normalizeExpectedTokenLength( - rawLength: unknown, - assetType: CouplingAssetType, - hasSeed = false -): number | undefined { - if (rawLength === undefined || rawLength === null || rawLength === '') { - return assetType === 'fbx' && !hasSeed ? 32 : undefined; - } - const value = Number(rawLength); - if (!Number.isInteger(value) || value < 8 || value > 64 || value % 2 !== 0) { - throw new HttpError( - 400, - 'invalid_expected_token_length', - 'expectedTokenLength must be an even integer between 8 and 64' - ); - } - return value; -} - -function normalizeOptionalSeedHex(rawSeed: unknown, assetPath: string): string | undefined { - if (rawSeed === undefined || rawSeed === null || rawSeed === '') { - return undefined; - } - if (typeof rawSeed !== 'string') { - throw new HttpError(400, 'invalid_seed', `seedHex for "${assetPath}" must be a string`); +// Decode is seed-only, so a scan asset MUST carry its 64-hex placement seed. +function normalizeRequiredSeedHex(rawSeed: unknown, assetPath: string): string { + if (typeof rawSeed !== 'string' || !rawSeed.trim()) { + throw new HttpError(400, 'missing_seed', `seedHex is required for "${assetPath}"`); } const normalized = rawSeed.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/.test(normalized)) { @@ -469,6 +400,21 @@ function normalizeOptionalSeedHex(rawSeed: unknown, assetPath: string): string | return normalized; } +const STANDARD_BASE64_RE = + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + +function decodeAssetContentBase64(contentBase64: string, assetPath: string): Uint8Array { + const normalized = contentBase64.trim(); + if (!STANDARD_BASE64_RE.test(normalized)) { + throw new HttpError(400, 'invalid_asset_content', `contentBase64 is invalid for asset "${assetPath}"`); + } + const decoded = Buffer.from(normalized, 'base64'); + if (decoded.toString('base64') !== normalized) { + throw new HttpError(400, 'invalid_asset_content', `contentBase64 is invalid for asset "${assetPath}"`); + } + return Uint8Array.from(decoded); +} + function getSafeExtension(assetPath: string, uploadName: string, assetType: CouplingAssetType): string { const candidates = [assetPath, uploadName]; for (const candidate of candidates) { @@ -482,8 +428,6 @@ function getSafeExtension(assetPath: string, uploadName: string, assetType: Coup return '.png'; case 'fbx': return '.fbx'; - case 'text': - return '.txt'; } } @@ -603,12 +547,7 @@ async function buildScanTasks( } const assetType = normalizeAssetType(asset.assetType, assetPath, upload.name); - const seedHex = normalizeOptionalSeedHex(asset.seedHex, assetPath); - const expectedTokenLength = normalizeExpectedTokenLength( - asset.expectedTokenLength ?? asset.tokenLength, - assetType, - seedHex !== undefined - ); + const seedHex = normalizeRequiredSeedHex(asset.seedHex, assetPath); const filePath = buildStoredAssetPath(requestDir, index, assetPath, upload.name, assetType); await Bun.write(filePath, upload); @@ -616,8 +555,7 @@ async function buildScanTasks( fieldName, assetPath, assetType, - ...(expectedTokenLength === undefined ? {} : { expectedTokenLength }), - ...(seedHex === undefined ? {} : { seedHex }), + seedHex, filePath, }); } @@ -675,16 +613,7 @@ async function buildJsonScanTasks( ); } - let bytes: Uint8Array; - try { - bytes = Uint8Array.from(Buffer.from(asset.contentBase64, 'base64')); - } catch { - throw new HttpError( - 400, - 'invalid_asset_content', - `contentBase64 is invalid for asset "${assetPath}"` - ); - } + const bytes = decodeAssetContentBase64(asset.contentBase64, assetPath); if (bytes.byteLength <= 0) { throw new HttpError(400, 'empty_asset_file', `Asset "${assetPath}" is empty`); } @@ -698,20 +627,14 @@ async function buildJsonScanTasks( const uploadName = typeof asset.fieldName === 'string' ? asset.fieldName : assetPath; const assetType = normalizeAssetType(asset.assetType, assetPath, uploadName); - const seedHex = normalizeOptionalSeedHex(asset.seedHex, assetPath); - const expectedTokenLength = normalizeExpectedTokenLength( - asset.expectedTokenLength ?? asset.tokenLength, - assetType, - seedHex !== undefined - ); + const seedHex = normalizeRequiredSeedHex(asset.seedHex, assetPath); const filePath = buildStoredAssetPath(requestDir, index, assetPath, uploadName, assetType); await Bun.write(filePath, bytes); tasks.push({ fieldName: `asset${index}`, assetPath, assetType, - ...(expectedTokenLength === undefined ? {} : { expectedTokenLength }), - ...(seedHex === undefined ? {} : { seedHex }), + seedHex, filePath, }); } @@ -748,7 +671,7 @@ async function handleHealth(): Promise { ok: true, status: 'healthy', service: 'coupling-service', - assetTypes: ['png', 'fbx', 'text'], + assetTypes: ['png', 'fbx'], materializationConfigured: Boolean( config.controlPlaneBaseUrl?.trim() && config.brokerSharedSecret?.trim() ), @@ -894,6 +817,175 @@ async function handleDeriveSeeds(request: Request): Promise { return jsonResponse(200, { success: true, epoch, seeds }); } +const MAX_ATTRIBUTE_CANDIDATES = 20000; + +type AttributeCandidate = { assetPath: string; licenseSubject: string; tokenHash: string }; + +function sha256Hex(input: string): string { + return createHash('sha256').update(input, 'utf8').digest('hex'); +} + +// Seed-iteration attribution: decode a leaked asset by trying each candidate buyer's per-job seed +// (re-derived from the recorded assetPath + licenseSubject). The seed that yields a CRC-valid token +// whose sha256 matches that candidate's tokenHash identifies the buyer. Master key + native stay here. +async function handleAttribute(request: Request): Promise { + assertScanAuthorized(request); + assertNativeRuntimeAvailable(); + if (!isCouplingSeedConfigured()) { + throw new HttpError( + 503, + 'seed_master_unconfigured', + 'COUPLING_WM_MASTER_KEY is not configured on the coupling service' + ); + } + const contentType = request.headers.get('content-type') ?? ''; + if (!contentType.toLowerCase().includes('application/json')) { + throw new HttpError(415, 'unsupported_media_type', 'POST /v1/coupling/attribute requires application/json'); + } + + let body: { assets?: unknown; candidates?: unknown; epoch?: unknown }; + try { + body = (await request.json()) as typeof body; + } catch { + throw new HttpError(400, 'invalid_request', 'request body must be valid JSON'); + } + + if (!Array.isArray(body.candidates) || body.candidates.length === 0) { + throw new HttpError(400, 'missing_candidates', 'candidates must be a non-empty array'); + } + if (body.candidates.length > MAX_ATTRIBUTE_CANDIDATES) { + throw new HttpError( + 413, + 'too_many_candidates', + `An attribute request can include at most ${MAX_ATTRIBUTE_CANDIDATES} candidates` + ); + } + const candidates: AttributeCandidate[] = []; + for (const raw of body.candidates) { + const c = raw as { assetPath?: unknown; licenseSubject?: unknown; tokenHash?: unknown }; + const assetPath = typeof c.assetPath === 'string' ? c.assetPath.trim() : ''; + const licenseSubject = typeof c.licenseSubject === 'string' ? c.licenseSubject.trim() : ''; + const tokenHash = typeof c.tokenHash === 'string' ? c.tokenHash.trim().toLowerCase() : ''; + if (!assetPath || !licenseSubject || !/^[0-9a-f]{64}$/.test(tokenHash)) { + throw new HttpError( + 400, + 'invalid_candidate', + 'each candidate requires assetPath, licenseSubject, and a 64-hex tokenHash' + ); + } + candidates.push({ assetPath, licenseSubject, tokenHash }); + } + const epoch = + typeof body.epoch === 'number' && Number.isInteger(body.epoch) ? body.epoch : COUPLING_SEED_EPOCH; + + if (!Array.isArray(body.assets) || body.assets.length === 0) { + throw new HttpError(400, 'missing_assets', 'assets must be a non-empty array'); + } + if (body.assets.length > config.maxAssetsPerRequest) { + throw new HttpError( + 413, + 'too_many_assets', + `An attribute request can include at most ${config.maxAssetsPerRequest} assets` + ); + } + + const requestId = crypto.randomUUID(); + const requestDir = path.join(config.workRoot, 'requests', requestId); + await mkdir(requestDir, { recursive: true }); + + try { + const results: Array<{ + assetPath: string; + assetType: CouplingAssetType; + matched: boolean; + tokenHex?: string; + matchedLicenseSubject?: string; + wmVersion?: number; + attempted: number; + }> = []; + + for (let index = 0; index < body.assets.length; index += 1) { + const a = body.assets[index] as { + assetPath?: unknown; + assetType?: unknown; + contentBase64?: unknown; + }; + const rawAssetPath = typeof a.assetPath === 'string' ? a.assetPath : ''; + const assetPath = normalizeRelativeAssetPath(rawAssetPath); + if (!isSafeRelativeAssetPath(assetPath)) { + throw new HttpError( + 400, + 'invalid_asset_path', + `assetPath must be a safe relative path, received "${rawAssetPath}"` + ); + } + if (typeof a.contentBase64 !== 'string' || !a.contentBase64.trim()) { + throw new HttpError(400, 'missing_asset_content', `contentBase64 is required for asset "${assetPath}"`); + } + const bytes = decodeAssetContentBase64(a.contentBase64, assetPath); + if (bytes.byteLength <= 0) { + throw new HttpError(400, 'empty_asset_file', `Asset "${assetPath}" is empty`); + } + if (bytes.byteLength > config.maxFileBytes) { + throw new HttpError(413, 'asset_too_large', `Asset "${assetPath}" exceeds ${config.maxFileBytes} bytes`); + } + + const assetType = normalizeAssetType(a.assetType, assetPath, assetPath); + const filePath = buildStoredAssetPath(requestDir, index, assetPath, assetPath, assetType); + await Bun.write(filePath, bytes); + + // Try candidates whose recorded basename matches the leaked file first, then the rest. + const base = path.posix.basename(assetPath).toLowerCase(); + const ordered = [...candidates].sort((x, y) => { + const bx = path.posix.basename(x.assetPath).toLowerCase() === base ? 0 : 1; + const by = path.posix.basename(y.assetPath).toLowerCase() === base ? 0 : 1; + return bx - by; + }); + + let attempted = 0; + let hit: { tokenHex: string; licenseSubject: string; wmVersion: number } | null = null; + for (const cand of ordered) { + attempted += 1; + try { + const seedHex = await deriveCouplingSeedHex(cand.assetPath, cand.licenseSubject, epoch); + const decoded = decodeCouplingAsset({ filePath, assetType, seedHex }); + if (sha256Hex(decoded.tokenHex) === cand.tokenHash) { + hit = { + tokenHex: decoded.tokenHex, + licenseSubject: cand.licenseSubject, + wmVersion: decoded.wmVersion, + }; + break; + } + } catch (error) { + if (!(error instanceof NativeCouplingError)) { + throw error; + } + // Wrong seed / unmarked / tampered for this candidate — try the next. + } + } + + results.push( + hit + ? { + assetPath, + assetType, + matched: true, + tokenHex: hit.tokenHex, + matchedLicenseSubject: hit.licenseSubject, + wmVersion: hit.wmVersion, + attempted, + } + : { assetPath, assetType, matched: false, attempted } + ); + } + + return jsonResponse(200, { requestId, results }); + } finally { + await rm(requestDir, { recursive: true, force: true }).catch(() => undefined); + } +} + async function handleForensicScore(request: Request): Promise { assertScanAuthorized(request); assertNativeRuntimeAvailable(); @@ -1115,6 +1207,9 @@ async function routeRequest(request: Request): Promise { if (request.method === 'POST' && url.pathname === '/v1/coupling/internal/derive-seeds') { return await handleDeriveSeeds(request); } + if (request.method === 'POST' && url.pathname === '/v1/coupling/attribute') { + return await handleAttribute(request); + } if ( request.method === 'POST' && (url.pathname === '/materialize' || url.pathname === '/v1/coupling/materialize') diff --git a/yucp_coupling/coupling_runtime.c b/yucp_coupling/coupling_runtime.c index 144bc7f..072bd07 100644 --- a/yucp_coupling/coupling_runtime.c +++ b/yucp_coupling/coupling_runtime.c @@ -7,13 +7,11 @@ #include /* Translation unit for the coupling watermark runtime. It includes guard.c and, for the client - * (runtime-helper) profile, re-exports the legacy encoders under stable names. The legacy rundll32 - * manifest entry point was removed: the importer invokes the encoders in-process via GetProcAddress - * and never writes tokens to a disk manifest. */ + * (runtime-helper) profile, re-exports the v2 seeded encoders under stable undecorated names. The + * importer invokes them in-process via GetProcAddress; there is no rundll32/manifest path and no + * legacy (pre-seed) encoder. */ #if defined(YUCP_RUNTIME_HELPER_EXPORTS) -#define xg_0115 yucp_runtime_helper_encode_png -#define xg_0120 yucp_runtime_helper_encode_fbx #define xg_0122 yucp_runtime_helper_encode_seeded_png #define xg_0124 yucp_runtime_helper_encode_seeded_fbx #define XG_EXPORT @@ -30,36 +28,8 @@ #undef XG_EXPORT #if defined(YUCP_RUNTIME_HELPER_EXPORTS) -#undef xg_0115 -#undef xg_0120 #undef xg_0122 #undef xg_0124 -#if defined(_WIN32) && defined(__cplusplus) -extern "C" __declspec(dllexport) -#elif defined(_WIN32) -__declspec(dllexport) -#elif defined(__cplusplus) -extern "C" __attribute__((visibility("default"))) -#else -__attribute__((visibility("default"))) -#endif -int xg_0115(const char* file_path_utf8, const char* token_hex_utf8) { - return yucp_runtime_helper_encode_png(file_path_utf8, token_hex_utf8); -} - -#if defined(_WIN32) && defined(__cplusplus) -extern "C" __declspec(dllexport) -#elif defined(_WIN32) -__declspec(dllexport) -#elif defined(__cplusplus) -extern "C" __attribute__((visibility("default"))) -#else -__attribute__((visibility("default"))) -#endif -int xg_0120(const char* file_path_utf8, const char* token_hex_utf8) { - return yucp_runtime_helper_encode_fbx(file_path_utf8, token_hex_utf8); -} - #if defined(_WIN32) && defined(__cplusplus) extern "C" __declspec(dllexport) #elif defined(_WIN32) diff --git a/yucp_coupling/guard.c b/yucp_coupling/guard.c index d3b1609..0340dad 100644 --- a/yucp_coupling/guard.c +++ b/yucp_coupling/guard.c @@ -1092,137 +1092,6 @@ static int xw_pack_path(char* out_utf8, size_t out_cap) { #endif } -static int xw_pack_load(uint8_t** out_pack, size_t* out_len, uint8_t out_seed32[32]) { - if (!out_pack || !out_len || !out_seed32) return -1; - *out_pack = NULL; - *out_len = 0; - - char p0[4096]; - int pr = xw_pack_path(p0, sizeof(p0)); - if (pr != 0) return -2; - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(p0, &b0, &n0) != 0 || !b0) return -3; - if (n0 < 64u || n0 > (32u * 1024u * 1024u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -4; - } - - const uint8_t mg[8] = { 'X', 'G', 'Q', 'W', '1', 0u, 0u, 0u }; - if (memcmp(b0, mg, 8u) != 0) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -5; - } - uint32_t v = rd_u32_le(b0 + 8u); - uint32_t bl = rd_u32_le(b0 + 12u); - if (v != 1u || bl == 0u) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -6; - } - if ((size_t)bl != (n0 - 48u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -7; - } - - crypto_hash_sha256(out_seed32, b0, (unsigned long long)n0); - *out_pack = b0; - *out_len = n0; - return 0; -} - -static int xw_eval(const uint8_t* rgba, uint32_t w, uint32_t h, - const char* token_hex_utf8, - const uint8_t* pack, size_t pack_len, - const uint8_t seed32[32], - uint32_t timeout_ms, - uint32_t* out_milli) { - if (!rgba || !token_hex_utf8 || !pack || pack_len == 0u || !seed32 || !out_milli) return -1; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || !hex_only(token_hex_utf8)) return -2; - if (w == 0u || h == 0u) return -3; - if (w > UINT32_MAX / h) return -4; - size_t px = (size_t)w * (size_t)h; - if (px == 0u) return -5; - - size_t nbits = tk * 4u; - size_t reps = px / (nbits * 2u); - if (reps < 8u) reps = 8u; - if (reps > 64u) reps = 64u; - - ULONGLONG t0 = GetTickCount64(); - size_t good = 0u; - for (size_t bi = 0u; bi < nbits; bi++) { - if ((bi & 15u) == 0u) { - ULONGLONG dt = GetTickCount64() - t0; - if (dt > (ULONGLONG)timeout_ms) return -6; - } - - uint8_t expb = xw_hex_bit(token_hex_utf8, bi); - uint64_t s0 = xw_seed64(seed32, (uint64_t)bi * 0xD6E8FEB86659FD93ull + 0xA0761D6478BD642Full); - uint64_t s1 = xw_seed64(seed32, (uint64_t)bi * 0x94D049BB133111EBull + 0xE7037ED1A0B428DBull); - int vote = 0; - for (size_t ri = 0u; ri < reps; ri++) { - uint64_t rv = xw_rng(&s0, &s1); - size_t idx = (size_t)(rv % (uint64_t)px); - size_t qi = (size_t)((rv >> 16) % (uint64_t)pack_len); - uint8_t m = (uint8_t)(((rv >> 33) ^ (uint64_t)pack[qi] ^ (uint64_t)seed32[(bi + ri) & 31u]) & 1u); - uint8_t obs = (uint8_t)(((rgba[idx * 4u + 2u] & 1u) ^ m) & 1u); - vote += obs ? 1 : -1; - } - uint8_t decb = vote >= 0 ? 1u : 0u; - good += (decb == expb) ? 1u : 0u; - } - - *out_milli = (uint32_t)((good * 1000u) / nbits); - return 0; -} - -static int xw_embed(uint8_t* rgba, uint32_t w, uint32_t h, - const char* token_hex_utf8, - const uint8_t* pack, size_t pack_len, - const uint8_t seed32[32], - uint32_t timeout_ms) { - if (!rgba || !token_hex_utf8 || !pack || pack_len == 0u || !seed32) return -1; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || !hex_only(token_hex_utf8)) return -2; - if (w == 0u || h == 0u) return -3; - if (w > UINT32_MAX / h) return -4; - size_t px = (size_t)w * (size_t)h; - if (px == 0u) return -5; - - size_t nbits = tk * 4u; - size_t reps = px / (nbits * 2u); - if (reps < 8u) reps = 8u; - if (reps > 64u) reps = 64u; - - ULONGLONG t0 = GetTickCount64(); - for (size_t bi = 0u; bi < nbits; bi++) { - if ((bi & 15u) == 0u) { - ULONGLONG dt = GetTickCount64() - t0; - if (dt > (ULONGLONG)timeout_ms) return -6; - } - - uint8_t expb = xw_hex_bit(token_hex_utf8, bi); - uint64_t s0 = xw_seed64(seed32, (uint64_t)bi * 0xD6E8FEB86659FD93ull + 0xA0761D6478BD642Full); - uint64_t s1 = xw_seed64(seed32, (uint64_t)bi * 0x94D049BB133111EBull + 0xE7037ED1A0B428DBull); - for (size_t ri = 0u; ri < reps; ri++) { - uint64_t rv = xw_rng(&s0, &s1); - size_t idx = (size_t)(rv % (uint64_t)px); - size_t qi = (size_t)((rv >> 16) % (uint64_t)pack_len); - uint8_t m = (uint8_t)(((rv >> 33) ^ (uint64_t)pack[qi] ^ (uint64_t)seed32[(bi + ri) & 31u]) & 1u); - uint8_t wb = (uint8_t)((expb ^ m) & 1u); - size_t po = idx * 4u + 2u; - rgba[po] = (uint8_t)((rgba[po] & 0xFEu) | wb); - } - } - return 0; -} - static void hkdf_sha256(const uint8_t* ikm, size_t ikm_len, const uint8_t* salt, size_t salt_len, const uint8_t* info, size_t info_len, @@ -2295,414 +2164,6 @@ XG_EXPORT int xg_0114(const uint8_t seed32[32], const char* rel_path_utf8, char return 0; } -static int xw_tok_xormask(const char* in_hex, uint8_t mask_nib, char* out_hex, size_t out_cap) { - if (!in_hex || !out_hex) return -1; - size_t n = strlen(in_hex); - if (n < 8u || n > 64u || (n & 1u) != 0u) return -2; - if (out_cap < n + 1u) return -3; - static const char hexd[17] = "0123456789abcdef"; - for (size_t i = 0u; i < n; i++) { - int h = hex_nibble(in_hex[i]); - if (h < 0) return -4; - out_hex[i] = hexd[(uint8_t)(h ^ (mask_nib & 0x0Fu)) & 0x0Fu]; - } - out_hex[n] = '\0'; - return 0; -} - -static int xw_tok_vote_quorum(const char* a, const char* b, const char* c, char* out_hex, size_t out_cap) { - if (!out_hex) return -1; - const char* src[3] = { a, b, c }; - const char* in[3] = { NULL, NULL, NULL }; - int n = 0; - size_t len = 0u; - for (int i = 0; i < 3; i++) { - if (!src[i] || src[i][0] == '\0') continue; - size_t l = strlen(src[i]); - if (l < 8u || l > 64u || (l & 1u) != 0u || !hex_only(src[i])) return -2; - if (n == 0) len = l; - else if (l != len) return -3; - in[n++] = src[i]; - } - if (n < 2) return -4; - if (out_cap < len + 1u) return -5; - - static const char hexd[17] = "0123456789abcdef"; - for (size_t p = 0u; p < len; p++) { - int cnt[16] = { 0 }; - for (int i = 0; i < n; i++) { - int h = hex_nibble(in[i][p]); - if (h < 0) return -6; - cnt[h]++; - } - int best_v = -1; - int best_c = 0; - for (int v = 0; v < 16; v++) { - if (cnt[v] > best_c) { - best_c = cnt[v]; - best_v = v; - } - } - if (best_v < 0 || best_c < 2) return -7; - out_hex[p] = hexd[best_v]; - } - out_hex[len] = '\0'; - return 0; -} - -static int xw_png_xw_key_kind(const uint8_t* d, uint32_t l0, int* out_kind, size_t* out_v_off, size_t* out_v_len) { - if (!d || !out_kind || !out_v_off || !out_v_len) return -1; - size_t z = 0u; - while (z < (size_t)l0 && d[z] != 0u) z++; - if (z + 1u > (size_t)l0) return -2; - if (z == 2u && d[0] == 'x' && d[1] == 'w') { - *out_kind = -2; - *out_v_off = z + 1u; - *out_v_len = (size_t)l0 - (z + 1u); - return 0; - } - if (z == 3u && d[0] == 'x' && d[1] == 'w' && d[2] >= '0' && d[2] <= '2') { - *out_kind = (int)(d[2] - '0'); - *out_v_off = z + 1u; - *out_v_len = (size_t)l0 - (z + 1u); - return 0; - } - return 1; -} - -XG_EXPORT int xg_0115(const char* file_path_utf8, const char* token_hex_utf8) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !token_hex_utf8) return -2; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || (tk & 1u) != 0u || !hex_only(token_hex_utf8)) return -3; - - char tok[3][65]; - if (xw_tok_xormask(token_hex_utf8, 0x0u, tok[0], sizeof(tok[0])) != 0 || - xw_tok_xormask(token_hex_utf8, 0x5u, tok[1], sizeof(tok[1])) != 0 || - xw_tok_xormask(token_hex_utf8, 0xAu, tok[2], sizeof(tok[2])) != 0) { - return -4; - } - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -5; - if (n0 < 20u) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -6; - } - const uint8_t sig[8] = { 0x89u, 0x50u, 0x4Eu, 0x47u, 0x0Du, 0x0Au, 0x1Au, 0x0Au }; - if (memcmp(b0, sig, 8) != 0) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -7; - } - - size_t cap = n0 + 3u * (12u + 4u + tk) + 64u; - uint8_t* out = (uint8_t*)malloc(cap); - if (!out) { - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -8; - } - if (n0 < 8u) { - sodium_memzero(out, cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -9; - } - - size_t p = 0u; - memcpy(out, b0, 8u); - p = 8u; - - int saw_iend = 0; - size_t off = 8u; - while (off + 12u <= n0) { - uint32_t l0 = rd_u32_be(b0 + off); - size_t next = off + 12u + (size_t)l0; - if (next > n0) { - sodium_memzero(out, cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -10; - } - - const uint8_t* typ = b0 + off + 4u; - if (memcmp(typ, "IEND", 4u) == 0) { - for (int si = 0; si < 3; si++) { - size_t dlen = 4u + tk; - size_t clen = 12u + dlen; - if (p + clen > cap) { - sodium_memzero(out, cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -11; - } - wr_u32_be(out + p, (uint32_t)dlen); - memcpy(out + p + 4u, "tEXt", 4u); - out[p + 8u] = 'x'; - out[p + 9u] = 'w'; - out[p + 10u] = (uint8_t)('0' + si); - out[p + 11u] = 0u; - memcpy(out + p + 12u, tok[si], tk); - uint32_t crc = crc32_png(out + p + 4u, dlen + 4u); - wr_u32_be(out + p + 8u + dlen, crc); - p += clen; - } - size_t tail = n0 - off; - if (p + tail > cap) { - sodium_memzero(out, cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -12; - } - memcpy(out + p, b0 + off, tail); - p += tail; - saw_iend = 1; - break; - } - - int skip = 0; - if (memcmp(typ, "tEXt", 4u) == 0 && l0 > 3u) { - const uint8_t* d = b0 + off + 8u; - int kind = -99; - size_t vo = 0u, vl = 0u; - int kr = xw_png_xw_key_kind(d, l0, &kind, &vo, &vl); - if (kr == 0 && kind >= -2 && kind <= 2) { - skip = 1; - } - } - - if (!skip) { - size_t clen = next - off; - if (p + clen > cap) { - sodium_memzero(out, cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -13; - } - memcpy(out + p, b0 + off, clen); - p += clen; - } - off = next; - } - if (!saw_iend) { - sodium_memzero(out, cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -14; - } - - int wr = write_all_bytes_utf8(file_path_utf8, out, p); - sodium_memzero(out, cap); - free(out); - sodium_memzero(tok, sizeof(tok)); - sodium_memzero(b0, n0 + 1u); - free(b0); - return (wr == 0) ? 0 : -15; -} - -XG_EXPORT int xg_0116(const char* file_path_utf8, const char* token_hex_utf8) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !token_hex_utf8) return -2; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || (tk & 1u) != 0u || !hex_only(token_hex_utf8)) return -3; - - char tok[3][65]; - if (xw_tok_xormask(token_hex_utf8, 0x0u, tok[0], sizeof(tok[0])) != 0 || - xw_tok_xormask(token_hex_utf8, 0x5u, tok[1], sizeof(tok[1])) != 0 || - xw_tok_xormask(token_hex_utf8, 0xAu, tok[2], sizeof(tok[2])) != 0) { - return -4; - } - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -5; - if (n0 == 0 || n0 > (8u * 1024u * 1024u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -6; - } - size_t chk = n0 < 512u ? n0 : 512u; - for (size_t i = 0; i < chk; i++) { - if (b0[i] == 0u) { - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -7; - } - } - - size_t marker_len = 5u + tk; - size_t out_cap = n0 + (3u * (marker_len + 1u)) + 32u; - char* out = (char*)malloc(out_cap); - if (!out) { - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -8; - } - - size_t p = 0; - size_t i = 0; - while (i < n0) { - size_t line_start = i; - while (i < n0 && b0[i] != '\n') i++; - size_t line_end = i; - while (line_end > line_start && b0[line_end - 1] == '\r') line_end--; - size_t line_len = line_end - line_start; - int is_tag = (line_len >= 3u && - b0[line_start] == '#' && - b0[line_start + 1] == 'x' && - b0[line_start + 2] == 'w'); - - if (!is_tag) { - if (p + line_len + 1u > out_cap) { - sodium_memzero(out, out_cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -9; - } - memcpy(out + p, b0 + line_start, line_len); - p += line_len; - out[p++] = '\n'; - } - - if (i < n0 && b0[i] == '\n') i++; - } - for (int si = 0; si < 3; si++) { - if (p + marker_len + 1u > out_cap) { - sodium_memzero(out, out_cap); - free(out); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(tok, sizeof(tok)); - return -10; - } - out[p++] = '#'; - out[p++] = 'x'; - out[p++] = 'w'; - out[p++] = (char)('0' + si); - out[p++] = ':'; - memcpy(out + p, tok[si], tk); - p += tk; - out[p++] = '\n'; - } - - int wr = write_all_bytes_utf8(file_path_utf8, (const uint8_t*)out, p); - sodium_memzero(out, out_cap); - free(out); - sodium_memzero(tok, sizeof(tok)); - sodium_memzero(b0, n0 + 1u); - free(b0); - return (wr == 0) ? 0 : -11; -} - -XG_EXPORT int xg_0117(const char* file_path_utf8, - const char* token_hex_utf8, - const char* model_utf8, - uint32_t min_acc_milli, - uint32_t timeout_ms) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !token_hex_utf8 || !model_utf8) return -2; - if (min_acc_milli == 0u || min_acc_milli > 1000u) return -3; - if (timeout_ms < 1000u || timeout_ms > 600000u) return -4; - if (_stricmp(model_utf8, "pixelseal") != 0) return -5; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || !hex_only(token_hex_utf8)) return -6; - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -7; - if (n0 < 20u || n0 > (64u * 1024u * 1024u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -8; - } - - int w = 0, h = 0, c = 0; - uint8_t* px = stbi_load_from_memory((const stbi_uc*)b0, (int)n0, &w, &h, &c, 4); - if (!px || w <= 0 || h <= 0) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -9; - } - - uint8_t* pack = NULL; - size_t pack_len = 0; - uint8_t seed32[32]; - int pr = xw_pack_load(&pack, &pack_len, seed32); - if (pr != 0) { - stbi_image_free(px); - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(seed32, sizeof(seed32)); - return -10; - } - - int er = xw_embed(px, (uint32_t)w, (uint32_t)h, token_hex_utf8, pack, pack_len, seed32, timeout_ms); - if (er != 0) { - sodium_memzero(seed32, sizeof(seed32)); - sodium_memzero(pack, pack_len + 1u); - free(pack); - stbi_image_free(px); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -11; - } - - uint32_t acc = 0; - int vr = xw_eval(px, (uint32_t)w, (uint32_t)h, token_hex_utf8, pack, pack_len, seed32, timeout_ms, &acc); - if (vr != 0 || acc < min_acc_milli) { - sodium_memzero(seed32, sizeof(seed32)); - sodium_memzero(pack, pack_len + 1u); - free(pack); - stbi_image_free(px); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -12; - } - - xw_o out = { 0 }; - int wr0 = stbi_write_png_to_func(xw_wcb, &out, w, h, 4, px, w * 4); - sodium_memzero(seed32, sizeof(seed32)); - sodium_memzero(pack, pack_len + 1u); - free(pack); - stbi_image_free(px); - sodium_memzero(b0, n0 + 1u); - free(b0); - - if (!wr0 || out.e || !out.b || out.n == 0u) { - if (out.b) { - sodium_memzero(out.b, out.c); - free(out.b); - } - return -13; - } - - int wr = write_all_bytes_utf8(file_path_utf8, out.b, out.n); - sodium_memzero(out.b, out.c); - free(out.b); - return (wr == 0) ? 0 : -14; -} - /* ===================== Watermark v2 image: 8x8 block DCT-domain QIM ===================== * Robust pixel-domain replacement for the strippable tEXt metadata watermark. Bits are placed * in a mid-band DCT coefficient of the blue channel, one coded bit per keyed pseudo-random 8x8 @@ -3035,169 +2496,6 @@ XG_EXPORT int xg_0123(const char* file_path_utf8, const char* seed_hex_utf8, cha return rc; } -XG_EXPORT int xg_0118(const char* file_path_utf8, char* out_hex_utf8, uint32_t out_cap) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !out_hex_utf8 || out_cap < 9u) return -2; - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -3; - if (n0 < 20u) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -4; - } - const uint8_t sig[8] = { 0x89u, 0x50u, 0x4Eu, 0x47u, 0x0Du, 0x0Au, 0x1Au, 0x0Au }; - if (memcmp(b0, sig, 8) != 0) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -5; - } - - char raw[3][65]; - memset(raw, 0, sizeof(raw)); - size_t off = 8u; - while (off + 12u <= n0) { - uint32_t l0 = rd_u32_be(b0 + off); - size_t next = off + 12u + (size_t)l0; - if (next > n0) { - sodium_memzero(b0, n0 + 1u); - free(b0); - sodium_memzero(raw, sizeof(raw)); - return -6; - } - - if (memcmp(b0 + off + 4u, "tEXt", 4) == 0 && l0 > 3u) { - const uint8_t* d = b0 + off + 8u; - int kind = -99; - size_t vo = 0u, vl = 0u; - int kr = xw_png_xw_key_kind(d, l0, &kind, &vo, &vl); - if (kr == 0 && kind >= 0 && kind <= 2) { - if (vl >= 8u && vl <= 64u && (vl & 1u) == 0u) { - int ok = 1; - for (size_t i = 0; i < vl; i++) { - uint8_t c = d[vo + i]; - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { - ok = 0; - break; - } - } - if (ok) { - memcpy(raw[kind], d + vo, vl); - raw[kind][vl] = '\0'; - } - } - } - } - if (memcmp(b0 + off + 4u, "IEND", 4) == 0) break; - off = next; - } - - sodium_memzero(b0, n0 + 1u); - free(b0); - - char dec[3][65]; - memset(dec, 0, sizeof(dec)); - if (raw[0][0] != '\0' && xw_tok_xormask(raw[0], 0x0u, dec[0], sizeof(dec[0])) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -7; - } - if (raw[1][0] != '\0' && xw_tok_xormask(raw[1], 0x5u, dec[1], sizeof(dec[1])) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -8; - } - if (raw[2][0] != '\0' && xw_tok_xormask(raw[2], 0xAu, dec[2], sizeof(dec[2])) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -9; - } - if (xw_tok_vote_quorum(dec[0], dec[1], dec[2], out_hex_utf8, out_cap) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -10; - } - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return 0; -} - -XG_EXPORT int xg_0119(const char* file_path_utf8, char* out_hex_utf8, uint32_t out_cap) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !out_hex_utf8 || out_cap < 9u) return -2; - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -3; - if (n0 == 0 || n0 > (8u * 1024u * 1024u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -4; - } - - char raw[3][65]; - memset(raw, 0, sizeof(raw)); - size_t i = 0; - while (i < n0) { - size_t ls = i; - while (i < n0 && b0[i] != '\n') i++; - size_t le = i; - while (le > ls && b0[le - 1] == '\r') le--; - size_t ll = le - ls; - - if (ll >= 13u && b0[ls] == '#' && b0[ls + 1] == 'x' && b0[ls + 2] == 'w' && - b0[ls + 3] >= '0' && b0[ls + 3] <= '2' && b0[ls + 4] == ':') { - int kind = (int)(b0[ls + 3] - '0'); - size_t vlen = ll - 5u; - if (vlen >= 8u && vlen <= 64u && (vlen & 1u) == 0u) { - int ok = 1; - for (size_t j = 0; j < vlen; j++) { - uint8_t c = b0[ls + 5u + j]; - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { - ok = 0; - break; - } - } - if (ok) { - memcpy(raw[kind], b0 + ls + 5u, vlen); - raw[kind][vlen] = '\0'; - } - } - } - if (i < n0 && b0[i] == '\n') i++; - } - - sodium_memzero(b0, n0 + 1u); - free(b0); - - char dec[3][65]; - memset(dec, 0, sizeof(dec)); - if (raw[0][0] != '\0' && xw_tok_xormask(raw[0], 0x0u, dec[0], sizeof(dec[0])) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -5; - } - if (raw[1][0] != '\0' && xw_tok_xormask(raw[1], 0x5u, dec[1], sizeof(dec[1])) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -6; - } - if (raw[2][0] != '\0' && xw_tok_xormask(raw[2], 0xAu, dec[2], sizeof(dec[2])) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -7; - } - if (xw_tok_vote_quorum(dec[0], dec[1], dec[2], out_hex_utf8, out_cap) != 0) { - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return -8; - } - sodium_memzero(raw, sizeof(raw)); - sodium_memzero(dec, sizeof(dec)); - return 0; -} - #endif /* !YUCP_RUNTIME_HELPER_EXPORTS — end image/text/legacy decoders */ typedef struct xw_bp { @@ -3868,297 +3166,6 @@ static int xw_bn_write(const xw_bn* nd, uint32_t ver, xw_o* o) { return 0; } -static int xw_bin_embed(const char* file_path_utf8, const uint8_t* b0, size_t n0, const char* token_hex_utf8) { - if (!file_path_utf8 || !b0 || !token_hex_utf8 || !xw_fbx_bin(b0, n0)) return -1; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || !hex_only(token_hex_utf8) || (tk & 1u) != 0u) return -2; - - xw_br br; - int rc = xw_br_parse(b0, n0, &br); - if (rc != 0) return -31; - - xw_bc* c0 = NULL; - size_t cN = 0u, cC = 0u; - xw_v lanes[3]; - memset(lanes, 0, sizeof(lanes)); - int v_seen = 0; - for (size_t i = 0u; i < br.rn; i++) { - rc = xw_bn_collect(&br.r[i], &v_seen, &c0, &cN, &cC, lanes); - if (rc != 0) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -32; - } - } - if (cN == 0u || lanes[0].n == 0u || lanes[1].n == 0u || lanes[2].n == 0u) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -33; - } - - double* vals = NULL; - rc = xw_bc_vals(c0, cN, &vals); - if (rc != 0 || !vals) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -34; - } - - const char* lnames[3] = { "vtx", "wgt", "bsh" }; - const double q0[3] = { 1.0e-5, 1.0e-4, 2.0e-5 }; - const size_t reps[3] = { 3u, 3u, 3u }; - char skey[96]; - int skn = _snprintf_s(skey, sizeof(skey), _TRUNCATE, "c:%zu|%zu|%zu", lanes[0].n, lanes[1].n, lanes[2].n); - if (skn <= 0) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -49; - } - size_t bit_count = tk * 4u; - size_t* perm[3] = { NULL, NULL, NULL }; - size_t need[3] = { bit_count * reps[0], bit_count * reps[1], bit_count * reps[2] }; - for (size_t li = 0u; li < 3u; li++) { - if (lanes[li].n < need[li]) { - for (int pi = 0; pi < 3; pi++) { if (perm[pi]) free(perm[pi]); } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -35; - } - uint64_t s0 = xw_lane_seed(skey, (size_t)skn, lnames[li], (uint32_t)tk); - uint64_t s1 = s0 ^ 0x9E3779B97F4A7C15ull ^ ((uint64_t)li << 33); - if (s1 == 0u) s1 = 0xD1342543DE82EF95ull; - int pr = xw_lane_perm(&lanes[li], s0, s1, &perm[li]); - if (pr != 0 || !perm[li]) { - for (int pi = 0; pi < 3; pi++) { if (perm[pi]) free(perm[pi]); } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -36; - } - } - - for (size_t li = 0u; li < 3u; li++) { - for (size_t bi = 0u; bi < bit_count; bi++) { - uint8_t bit = xw_hex_bit(token_hex_utf8, bi); - for (size_t ri = 0u; ri < reps[li]; ri++) { - size_t ci = perm[li][bi * reps[li] + ri]; - vals[ci] = xw_qe(vals[ci], q0[li], bit); - } - } - } - - rc = xw_bc_apply(c0, cN, vals); - if (rc != 0) { - for (int pi = 0; pi < 3; pi++) { if (perm[pi]) free(perm[pi]); } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -37; - } - - for (size_t i = 0u; i < br.rn; i++) { - rc = xw_bn_build(&br.r[i], br.ver); - if (rc != 0) { - for (int pi = 0; pi < 3; pi++) { if (perm[pi]) free(perm[pi]); } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -38; - } - } - uint64_t off = 27u; - for (size_t i = 0u; i < br.rn; i++) { - rc = xw_bn_assign(&br.r[i], br.ver, off); - if (rc != 0) { - for (int pi = 0; pi < 3; pi++) { if (perm[pi]) free(perm[pi]); } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -39; - } - off = br.r[i].eo; - } - off += (uint64_t)xw_fbx_null_record_size(br.ver); - (void)off; - - xw_o out; - memset(&out, 0, sizeof(out)); - rc = xw_o_push(&out, br.h, sizeof(br.h)); - if (rc == 0) { - for (size_t i = 0u; i < br.rn; i++) { - rc = xw_bn_write(&br.r[i], br.ver, &out); - if (rc != 0) break; - } - } - if (rc == 0) { - uint8_t z25[25] = {0}; - rc = xw_o_push(&out, z25, xw_fbx_null_record_size(br.ver)); - } - if (rc == 0) rc = xw_o_push(&out, br.tail, br.tn); - if (rc == 0) rc = write_all_bytes_utf8(file_path_utf8, out.b, out.n); - - for (int pi = 0; pi < 3; pi++) { - if (perm[pi]) { - sodium_memzero(perm[pi], lanes[pi].n * sizeof(size_t)); - free(perm[pi]); - } - } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) { - sodium_memzero(c0, cC * sizeof(xw_bc)); - free(c0); - } - if (out.b) { - sodium_memzero(out.b, out.c); - free(out.b); - } - xw_br_free(&br); - return rc == 0 ? 0 : -40; -} - -static int xw_bin_extract(const char* file_path_utf8, const uint8_t* b0, size_t n0, uint32_t expected_hex_len, char* out_hex_utf8, uint32_t out_cap) { - if (!file_path_utf8 || !b0 || !out_hex_utf8 || !xw_fbx_bin(b0, n0)) return -1; - if (expected_hex_len < 8u || expected_hex_len > 64u || (expected_hex_len & 1u) != 0u) return -2; - if (out_cap < expected_hex_len + 1u) return -3; - - xw_br br; - int rc = xw_br_parse(b0, n0, &br); - if (rc != 0) return -41; - - xw_bc* c0 = NULL; - size_t cN = 0u, cC = 0u; - xw_v lanes[3]; - memset(lanes, 0, sizeof(lanes)); - int v_seen = 0; - for (size_t i = 0u; i < br.rn; i++) { - rc = xw_bn_collect(&br.r[i], &v_seen, &c0, &cN, &cC, lanes); - if (rc != 0) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -42; - } - } - if (cN == 0u || lanes[0].n == 0u || lanes[1].n == 0u || lanes[2].n == 0u) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -43; - } - - double* vals = NULL; - rc = xw_bc_vals(c0, cN, &vals); - if (rc != 0 || !vals) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - free(c0); - return -44; - } - - size_t bit_count = (size_t)expected_hex_len * 4u; - uint8_t* bits = (uint8_t*)malloc(bit_count); - if (!bits) { - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - free(c0); - return -45; - } - - const char* lnames[3] = { "vtx", "wgt", "bsh" }; - const double q0[3] = { 1.0e-5, 1.0e-4, 2.0e-5 }; - const size_t reps[3] = { 3u, 3u, 3u }; - char skey[96]; - int skn = _snprintf_s(skey, sizeof(skey), _TRUNCATE, "c:%zu|%zu|%zu", lanes[0].n, lanes[1].n, lanes[2].n); - if (skn <= 0) { - xw_br_free(&br); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) free(c0); - return -49; - } - size_t* perm[3] = { NULL, NULL, NULL }; - size_t need[3] = { bit_count * reps[0], bit_count * reps[1], bit_count * reps[2] }; - for (size_t li = 0u; li < 3u; li++) { - if (lanes[li].n < need[li]) { - for (int pi = 0; pi < 3; pi++) if (perm[pi]) free(perm[pi]); - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -46; - } - uint64_t s0 = xw_lane_seed(skey, (size_t)skn, lnames[li], expected_hex_len); - uint64_t s1 = s0 ^ 0x9E3779B97F4A7C15ull ^ ((uint64_t)li << 33); - if (s1 == 0u) s1 = 0xD1342543DE82EF95ull; - int pr = xw_lane_perm(&lanes[li], s0, s1, &perm[li]); - if (pr != 0 || !perm[li]) { - for (int pi = 0; pi < 3; pi++) if (perm[pi]) free(perm[pi]); - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - xw_br_free(&br); - for (int l2 = 0; l2 < 3; l2++) xw_vfree(&lanes[l2]); - free(c0); - return -47; - } - } - - for (size_t bi = 0u; bi < bit_count; bi++) { - int ones = 0; - int zeros = 0; - for (size_t li = 0u; li < 3u; li++) { - for (size_t ri = 0u; ri < reps[li]; ri++) { - size_t ci = perm[li][bi * reps[li] + ri]; - uint8_t b = xw_qd(vals[ci], q0[li]); - if (b) ones++; - else zeros++; - } - } - bits[bi] = (ones >= zeros) ? 1u : 0u; - } - - rc = xw_bits_to_hex(bits, bit_count, out_hex_utf8, out_cap); - for (int pi = 0; pi < 3; pi++) { - if (perm[pi]) { - sodium_memzero(perm[pi], lanes[pi].n * sizeof(size_t)); - free(perm[pi]); - } - } - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - for (int li = 0; li < 3; li++) xw_vfree(&lanes[li]); - if (c0) { - sodium_memzero(c0, cC * sizeof(xw_bc)); - free(c0); - } - xw_br_free(&br); - return rc == 0 ? 0 : -48; -} - /* ============== Watermark v2 mesh: vertex-norm distribution histogram QIM ============== * Blind, universal (every mesh has vertices), robust to vertex reorder + similarity transform. * Embeds one bit per equal-frequency norm bin by mean-modulation (Cho-Prost-Jung Type I): a fixed @@ -4485,338 +3492,8 @@ XG_EXPORT int xg_0125(const char* file_path_utf8, const char* seed_hex_utf8, cha } #endif /* !YUCP_RUNTIME_HELPER_EXPORTS — end mesh v2 decoder */ -XG_EXPORT int xg_0120(const char* file_path_utf8, const char* token_hex_utf8) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !token_hex_utf8) return -2; - size_t tk = strlen(token_hex_utf8); - if (tk < 8u || tk > 64u || !hex_only(token_hex_utf8) || (tk & 1u) != 0u) return -3; - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -4; - if (n0 == 0u || n0 > (64u * 1024u * 1024u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -5; - } - - if (xw_fbx_bin(b0, n0)) { - int br = xw_bin_embed(file_path_utf8, b0, n0, token_hex_utf8); - sodium_memzero(b0, n0 + 1u); - free(b0); - return br; - } - - size_t chk = n0 < 4096u ? n0 : 4096u; - for (size_t i = 0; i < chk; i++) { - if (b0[i] == 0u) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -6; - } - } - - xw_c* c0 = NULL; - size_t cN = 0u, cC = 0u; - xw_v lanes[3]; - memset(lanes, 0, sizeof(lanes)); - int rc = xw_collect_fbx_carriers(b0, n0, &c0, &cN, &cC, lanes); - if (rc != 0 || cN == 0u) { - if (c0) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - } - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -7; - } - rc = xw_reindex_lanes(c0, cN, lanes); - if (rc != 0) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -7; - } - - if (lanes[0].n == 0u || lanes[1].n == 0u || lanes[2].n == 0u) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -8; - } - - double* vals = NULL; - rc = xw_parse_vals(b0, n0, c0, cN, &vals); - if (rc != 0 || !vals) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -9; - } - - const char* lnames[3] = { "vtx", "wgt", "bsh" }; - const double q0[3] = { 1.0e-5, 1.0e-4, 2.0e-5 }; - const size_t reps[3] = { 3u, 3u, 3u }; - char skey[96]; - int skn = _snprintf_s(skey, sizeof(skey), _TRUNCATE, "c:%zu|%zu|%zu", lanes[0].n, lanes[1].n, lanes[2].n); - if (skn <= 0) { - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -10; - } - size_t bit_count = tk * 4u; - size_t* perm[3] = { NULL, NULL, NULL }; - size_t need[3] = { bit_count * reps[0], bit_count * reps[1], bit_count * reps[2] }; - for (size_t li = 0u; li < 3u; li++) { - if (lanes[li].n < need[li]) { - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -10; - } - uint64_t s0 = xw_lane_seed(skey, (size_t)skn, lnames[li], (uint32_t)tk); - uint64_t s1 = s0 ^ 0x9E3779B97F4A7C15ull ^ ((uint64_t)li << 33); - if (s1 == 0u) s1 = 0xD1342543DE82EF95ull; - int pr = xw_lane_perm(&lanes[li], s0, s1, &perm[li]); - if (pr != 0 || !perm[li]) { - for (int pi = 0; pi < 3; pi++) { - if (perm[pi]) { - sodium_memzero(perm[pi], lanes[pi].n * sizeof(size_t)); - free(perm[pi]); - } - } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -11; - } - } - - for (size_t li = 0u; li < 3u; li++) { - for (size_t bi = 0u; bi < bit_count; bi++) { - uint8_t bit = xw_hex_bit(token_hex_utf8, bi); - for (size_t ri = 0u; ri < reps[li]; ri++) { - size_t ci = perm[li][bi * reps[li] + ri]; - vals[ci] = xw_qe(vals[ci], q0[li], bit); - } - } - } - - rc = xw_write_vals(file_path_utf8, b0, n0, c0, cN, vals); - for (int pi = 0; pi < 3; pi++) { - if (perm[pi]) { - sodium_memzero(perm[pi], lanes[pi].n * sizeof(size_t)); - free(perm[pi]); - } - } - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return rc == 0 ? 0 : -12; -} - /* Legacy FBX decoder — server engine only (see decoder note above). */ #if !defined(YUCP_RUNTIME_HELPER_EXPORTS) -XG_EXPORT int xg_0121(const char* file_path_utf8, uint32_t expected_hex_len, char* out_hex_utf8, uint32_t out_cap) { - if (sodium_init() < 0) return -1; - if (!file_path_utf8 || !out_hex_utf8) return -2; - if (expected_hex_len < 8u || expected_hex_len > 64u || (expected_hex_len & 1u) != 0u) return -3; - if (out_cap < expected_hex_len + 1u) return -4; - - uint8_t* b0 = NULL; - size_t n0 = 0; - if (read_all_bytes_utf8(file_path_utf8, &b0, &n0) != 0 || !b0) return -5; - if (n0 == 0u || n0 > (64u * 1024u * 1024u)) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -6; - } - - if (xw_fbx_bin(b0, n0)) { - int br = xw_bin_extract(file_path_utf8, b0, n0, expected_hex_len, out_hex_utf8, out_cap); - sodium_memzero(b0, n0 + 1u); - free(b0); - return br; - } - - size_t chk = n0 < 4096u ? n0 : 4096u; - for (size_t i = 0; i < chk; i++) { - if (b0[i] == 0u) { - sodium_memzero(b0, n0 + 1u); - free(b0); - return -7; - } - } - - xw_c* c0 = NULL; - size_t cN = 0u, cC = 0u; - xw_v lanes[3]; - memset(lanes, 0, sizeof(lanes)); - int rc = xw_collect_fbx_carriers(b0, n0, &c0, &cN, &cC, lanes); - if (rc != 0 || cN == 0u) { - if (c0) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - } - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -8; - } - rc = xw_reindex_lanes(c0, cN, lanes); - if (rc != 0) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -8; - } - if (lanes[0].n == 0u || lanes[1].n == 0u || lanes[2].n == 0u) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -9; - } - - double* vals = NULL; - rc = xw_parse_vals(b0, n0, c0, cN, &vals); - if (rc != 0 || !vals) { - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -10; - } - - size_t bit_count = (size_t)expected_hex_len * 4u; - uint8_t* bits = (uint8_t*)malloc(bit_count); - if (!bits) { - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -11; - } - - const char* lnames[3] = { "vtx", "wgt", "bsh" }; - const double q0[3] = { 1.0e-5, 1.0e-4, 2.0e-5 }; - const size_t reps[3] = { 3u, 3u, 3u }; - char skey[96]; - int skn = _snprintf_s(skey, sizeof(skey), _TRUNCATE, "c:%zu|%zu|%zu", lanes[0].n, lanes[1].n, lanes[2].n); - if (skn <= 0) { - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -12; - } - size_t* perm[3] = { NULL, NULL, NULL }; - size_t need[3] = { bit_count * reps[0], bit_count * reps[1], bit_count * reps[2] }; - for (size_t li = 0u; li < 3u; li++) { - if (lanes[li].n < need[li]) { - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -12; - } - uint64_t s0 = xw_lane_seed(skey, (size_t)skn, lnames[li], expected_hex_len); - uint64_t s1 = s0 ^ 0x9E3779B97F4A7C15ull ^ ((uint64_t)li << 33); - if (s1 == 0u) s1 = 0xD1342543DE82EF95ull; - int pr = xw_lane_perm(&lanes[li], s0, s1, &perm[li]); - if (pr != 0 || !perm[li]) { - for (int pi = 0; pi < 3; pi++) { - if (perm[pi]) { - sodium_memzero(perm[pi], lanes[pi].n * sizeof(size_t)); - free(perm[pi]); - } - } - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return -13; - } - } - - for (size_t bi = 0u; bi < bit_count; bi++) { - int ones = 0; - int zeros = 0; - for (size_t li = 0u; li < 3u; li++) { - for (size_t ri = 0u; ri < reps[li]; ri++) { - size_t ci = perm[li][bi * reps[li] + ri]; - uint8_t b = xw_qd(vals[ci], q0[li]); - if (b) ones++; - else zeros++; - } - } - bits[bi] = (ones >= zeros) ? 1u : 0u; - } - - rc = xw_bits_to_hex(bits, bit_count, out_hex_utf8, out_cap); - for (int pi = 0; pi < 3; pi++) { - if (perm[pi]) { - sodium_memzero(perm[pi], lanes[pi].n * sizeof(size_t)); - free(perm[pi]); - } - } - sodium_memzero(bits, bit_count); - free(bits); - sodium_memzero(vals, cN * sizeof(double)); - free(vals); - sodium_memzero(c0, cC * sizeof(xw_c)); - free(c0); - for (int i = 0; i < 3; i++) xw_vfree(&lanes[i]); - sodium_memzero(b0, n0 + 1u); - free(b0); - return rc == 0 ? 0 : -14; -} #endif /* !YUCP_RUNTIME_HELPER_EXPORTS — end legacy FBX decoder */ XG_EXPORT void xg_010f(void* hh) { diff --git a/yucp_coupling/out/win-x64/Release/coupling_runtime.obj b/yucp_coupling/out/win-x64/Release/coupling_runtime.obj index a04c2a1..5cc4090 100644 Binary files a/yucp_coupling/out/win-x64/Release/coupling_runtime.obj and b/yucp_coupling/out/win-x64/Release/coupling_runtime.obj differ diff --git a/yucp_coupling/out/win-x64/Release/runtime-helper/coupling_runtime.obj b/yucp_coupling/out/win-x64/Release/runtime-helper/coupling_runtime.obj index 99e7f43..07585e0 100644 Binary files a/yucp_coupling/out/win-x64/Release/runtime-helper/coupling_runtime.obj and b/yucp_coupling/out/win-x64/Release/runtime-helper/coupling_runtime.obj differ diff --git a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.compile.pdb b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.compile.pdb index 20b4cd5..b7a6fed 100644 Binary files a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.compile.pdb and b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.compile.pdb differ diff --git a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.dll b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.dll index 7455827..082bd86 100644 Binary files a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.dll and b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.dll differ diff --git a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.pdb b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.pdb index 4e0352e..fb2f445 100644 Binary files a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.pdb and b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.pdb differ diff --git a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.sha256 b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.sha256 index 5dda26e..1a182f6 100644 --- a/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.sha256 +++ b/yucp_coupling/out/win-x64/Release/runtime-helper/yucp_coupling.sha256 @@ -1 +1 @@ -ec7e6abc52603bb788e387501f89a61391007bbe831882ca5f6aa9b07b065c8e +b227def5f2bbc82e3ddf0ae88e44add4434301ddc3b2c618d17715d81f5dd699 diff --git a/yucp_coupling/out/win-x64/Release/yucp_coupling.compile.pdb b/yucp_coupling/out/win-x64/Release/yucp_coupling.compile.pdb index a9bed85..55fe6bb 100644 Binary files a/yucp_coupling/out/win-x64/Release/yucp_coupling.compile.pdb and b/yucp_coupling/out/win-x64/Release/yucp_coupling.compile.pdb differ diff --git a/yucp_coupling/out/win-x64/Release/yucp_coupling.dll b/yucp_coupling/out/win-x64/Release/yucp_coupling.dll index 3ffeb8a..3c72f9e 100644 Binary files a/yucp_coupling/out/win-x64/Release/yucp_coupling.dll and b/yucp_coupling/out/win-x64/Release/yucp_coupling.dll differ diff --git a/yucp_coupling/out/win-x64/Release/yucp_coupling.pdb b/yucp_coupling/out/win-x64/Release/yucp_coupling.pdb index be04172..41f53ca 100644 Binary files a/yucp_coupling/out/win-x64/Release/yucp_coupling.pdb and b/yucp_coupling/out/win-x64/Release/yucp_coupling.pdb differ diff --git a/yucp_coupling/out/win-x64/Release/yucp_coupling.sha256 b/yucp_coupling/out/win-x64/Release/yucp_coupling.sha256 index 6b5482c..80892c0 100644 --- a/yucp_coupling/out/win-x64/Release/yucp_coupling.sha256 +++ b/yucp_coupling/out/win-x64/Release/yucp_coupling.sha256 @@ -1 +1 @@ -ba0a2b9b0164f894b78c24eca47c64833e4dc8afa58a21dcb8b39eaed8176f32 +8dcb77ecc9ec219ae445d63ba7bfa49e038a66cabdc342f8c722b8a1b6877928 diff --git a/yucp_coupling/yucp_coupling.def b/yucp_coupling/yucp_coupling.def index 7c612ac..d1f95c0 100644 --- a/yucp_coupling/yucp_coupling.def +++ b/yucp_coupling/yucp_coupling.def @@ -1,8 +1,5 @@ LIBRARY yucp_coupling EXPORTS - xg_0118 @1 - xg_0119 @2 - xg_0121 @3 xg_0122 @4 xg_0123 @5 xg_0124 @6 diff --git a/yucp_coupling/yucp_coupling.runtime-helper.def b/yucp_coupling/yucp_coupling.runtime-helper.def index 15a58aa..69a8301 100644 --- a/yucp_coupling/yucp_coupling.runtime-helper.def +++ b/yucp_coupling/yucp_coupling.runtime-helper.def @@ -1,6 +1,4 @@ LIBRARY yucp_coupling EXPORTS - xg_0115 @1 - xg_0120 @2 xg_0122 @3 xg_0124 @4