sillstone 0.0.0-stage โ 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +424 -0
- package/package.json +19 -4
- package/scripts/build.js +49 -0
- package/src/generated.c +511 -0
- package/src/node.def +16 -0
- package/src/node_api.h +104 -0
- package/test/checksum.test.js +65 -0
- package/test/reference.test.js +447 -0
- package/README.md +0 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert");
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const sillstone = require("../index.js");
|
|
5
|
+
|
|
6
|
+
test("version matches expected", () => {
|
|
7
|
+
assert.strictEqual(sillstone.version(), "0.2.0");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("checksum produces deterministic golden output", () => {
|
|
11
|
+
const data = Buffer.from("sillstone pure rust zero dep");
|
|
12
|
+
const sum = sillstone.bufferChecksum(data);
|
|
13
|
+
let h = 0xcbf29ce484222325n;
|
|
14
|
+
const prime = 0x0100000001b3n;
|
|
15
|
+
for (let i = 0; i < data.length; i++) {
|
|
16
|
+
h ^= BigInt(data[i]);
|
|
17
|
+
h = (h * prime) & 0xffffffffffffffffn;
|
|
18
|
+
}
|
|
19
|
+
assert.strictEqual(sum, h);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("digests produce correct outputs", () => {
|
|
23
|
+
const data = Buffer.from("abc");
|
|
24
|
+
assert.strictEqual(
|
|
25
|
+
sillstone.sha256(data).toString("hex"),
|
|
26
|
+
crypto.createHash("sha256").update(data).digest("hex")
|
|
27
|
+
);
|
|
28
|
+
assert.strictEqual(
|
|
29
|
+
sillstone.sha3_256(data).toString("hex"),
|
|
30
|
+
crypto.createHash("sha3-256").update(data).digest("hex")
|
|
31
|
+
);
|
|
32
|
+
assert.strictEqual(
|
|
33
|
+
sillstone.sha3_512(data).toString("hex"),
|
|
34
|
+
crypto.createHash("sha3-512").update(data).digest("hex")
|
|
35
|
+
);
|
|
36
|
+
assert.strictEqual(
|
|
37
|
+
sillstone.blake3(Buffer.alloc(0)).toString("hex"),
|
|
38
|
+
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("dst functions satisfy mathematical inversions", () => {
|
|
43
|
+
const x = [1.0, 2.0, 3.0, 4.0];
|
|
44
|
+
const d1 = sillstone.dst1(x);
|
|
45
|
+
assert.strictEqual(d1.length, 4);
|
|
46
|
+
const rec1 = sillstone.dst1(d1);
|
|
47
|
+
for (let i = 0; i < x.length; i++) {
|
|
48
|
+
assert.ok(Math.abs(x[i] - rec1[i]) < 1e-10);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const d2 = sillstone.dst2(x);
|
|
52
|
+
const rec2 = sillstone.dst3(d2);
|
|
53
|
+
for (let i = 0; i < x.length; i++) {
|
|
54
|
+
assert.ok(Math.abs(x[i] - rec2[i]) < 1e-10);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("unicode grapheme clustering and character categories", () => {
|
|
59
|
+
const fam = "๐จโ๐ฉโ๐งโ๐ฆ";
|
|
60
|
+
assert.strictEqual(sillstone.graphemeCount(fam), 1);
|
|
61
|
+
assert.strictEqual(sillstone.graphemeCount("Hello"), 5);
|
|
62
|
+
assert.strictEqual(sillstone.graphemeCount(""), 0);
|
|
63
|
+
const cat = sillstone.charCategory("A");
|
|
64
|
+
assert.ok(cat >= 0);
|
|
65
|
+
});
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 sillstone
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
// Hex-exact conformance: the committed reference vectors through koffi.
|
|
6
|
+
// Every one of the 45 vectors in the repository-root reference.json is
|
|
7
|
+
// replayed through the cdylib and compared against every recorded
|
|
8
|
+
// field โ detect codes, tier-2 sub-digests (recomputed SDK-side with
|
|
9
|
+
// the same folds the generator uses), tier-1 digests, facts and match
|
|
10
|
+
// slots. The same vectors the Rust `gen-reference verify` gate and the
|
|
11
|
+
// Python/Go SDKs check.
|
|
12
|
+
|
|
13
|
+
const test = require("node:test");
|
|
14
|
+
const assert = require("node:assert/strict");
|
|
15
|
+
const crypto = require("node:crypto");
|
|
16
|
+
const fs = require("node:fs");
|
|
17
|
+
const path = require("node:path");
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
FfiError,
|
|
21
|
+
MODALITIES,
|
|
22
|
+
FORMATS,
|
|
23
|
+
contentHash,
|
|
24
|
+
describe,
|
|
25
|
+
detect,
|
|
26
|
+
fnv1a64,
|
|
27
|
+
findCdylib,
|
|
28
|
+
matchSignatures,
|
|
29
|
+
signature,
|
|
30
|
+
} = require("../index.js");
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
|
|
33
|
+
const REFERENCE = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "reference.json"), "utf8")).vectors;
|
|
34
|
+
const BY_NAME = new Map(REFERENCE.map((v) => [v.name, v]));
|
|
35
|
+
|
|
36
|
+
const MASK64 = (1n << 64n) - 1n;
|
|
37
|
+
|
|
38
|
+
/** SplitMix64 next_u64 (BigInt, wrapping). @param {{s: bigint}} st */
|
|
39
|
+
function splitmixNext(st) {
|
|
40
|
+
st.s = (st.s + 0x9e3779b97f4a7c15n) & MASK64;
|
|
41
|
+
let z = st.s;
|
|
42
|
+
z = ((z ^ (z >> 30n)) * 0xbf58476d1ce4e5b9n) & MASK64;
|
|
43
|
+
z = ((z ^ (z >> 27n)) * 0x94d049bb133111ebn) & MASK64;
|
|
44
|
+
return z ^ (z >> 31n);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The upstream conformance byte recipe: one next_u64 per byte, low byte. */
|
|
48
|
+
function splitmixBytes(seed, length) {
|
|
49
|
+
const st = { s: BigInt(seed) };
|
|
50
|
+
const out = Buffer.alloc(length);
|
|
51
|
+
for (let i = 0; i < length; i++) out[i] = Number(splitmixNext(st) & 0xffn);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The 48 KiB pseudo-file recipe of the upstream facade suite. */
|
|
56
|
+
function blob48(seed) {
|
|
57
|
+
return splitmixBytes(seed, 48 * 1024);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const VOCAB = [
|
|
61
|
+
"hash", "image", "audio", "signal", "codec", "block", "frame", "sample", "stream",
|
|
62
|
+
"vector", "median", "kernel", "window", "raster", "pixel", "tone", "hash", "luma",
|
|
63
|
+
"delta", "chunk", "radix", "filter", "table", "edge",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** Deterministic ~bytes-sized prose from the seeded 24-word vocabulary. */
|
|
67
|
+
function prose(seed, bytes) {
|
|
68
|
+
const st = { s: BigInt(seed) };
|
|
69
|
+
let out = "";
|
|
70
|
+
while (out.length < bytes) {
|
|
71
|
+
out += VOCAB[Number(splitmixNext(st) % BigInt(VOCAB.length))];
|
|
72
|
+
out += " ";
|
|
73
|
+
}
|
|
74
|
+
return out.slice(0, bytes);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Smooth periodic RGB content with real low-frequency energy. */
|
|
78
|
+
function patternedPixels(seed, w, h) {
|
|
79
|
+
const st = { s: BigInt(seed) };
|
|
80
|
+
const fx = Number(splitmixNext(st) % 5n) + 1;
|
|
81
|
+
const fy = Number(splitmixNext(st) % 5n) + 1;
|
|
82
|
+
const p1 = Number(splitmixNext(st) % 628n) / 100.0;
|
|
83
|
+
const p2 = Number(splitmixNext(st) % 628n) / 100.0;
|
|
84
|
+
const TAU = 6.283185307179586;
|
|
85
|
+
const px = Buffer.alloc(w * h * 3);
|
|
86
|
+
let i = 0;
|
|
87
|
+
for (let y = 0; y < h; y++) {
|
|
88
|
+
for (let x = 0; x < w; x++) {
|
|
89
|
+
const u = x / w;
|
|
90
|
+
const v = y / h;
|
|
91
|
+
const r = 127.5 + 120.0 * Math.sin(u * fx * TAU + p1);
|
|
92
|
+
const g = 127.5 + 120.0 * Math.sin(v * fy * TAU + p2);
|
|
93
|
+
const b = 127.5 + 120.0 * Math.sin((u + v) * (fx + fy) + p1 + p2);
|
|
94
|
+
px[i++] = Math.trunc(r);
|
|
95
|
+
px[i++] = Math.trunc(g);
|
|
96
|
+
px[i++] = Math.trunc(b);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Encodes RGB triples as a bottom-up 24-bit BMP. */
|
|
103
|
+
function bmp24(w, h, rgb) {
|
|
104
|
+
const row = Math.ceil((w * 3) / 4) * 4;
|
|
105
|
+
const img = row * h;
|
|
106
|
+
const f = [Buffer.from("BM", "ascii")];
|
|
107
|
+
const head = Buffer.alloc(52);
|
|
108
|
+
head.writeUInt32LE(54 + img, 0); // file size
|
|
109
|
+
head.writeUInt32LE(54, 8); // pixel offset (4 reserved bytes at 4..8)
|
|
110
|
+
head.writeUInt32LE(40, 12); // DIB header
|
|
111
|
+
head.writeInt32LE(w, 16);
|
|
112
|
+
head.writeInt32LE(h, 20);
|
|
113
|
+
head.writeUInt16LE(1, 24); // planes
|
|
114
|
+
head.writeUInt16LE(24, 26); // bpp
|
|
115
|
+
f.push(head);
|
|
116
|
+
const pad = Buffer.alloc(row - w * 3);
|
|
117
|
+
for (let y = h - 1; y >= 0; y--) {
|
|
118
|
+
const rowBuf = Buffer.alloc(w * 3);
|
|
119
|
+
for (let x = 0; x < w; x++) {
|
|
120
|
+
const i = (y * w + x) * 3;
|
|
121
|
+
rowBuf[x * 3] = rgb[i + 2];
|
|
122
|
+
rowBuf[x * 3 + 1] = rgb[i + 1];
|
|
123
|
+
rowBuf[x * 3 + 2] = rgb[i];
|
|
124
|
+
}
|
|
125
|
+
f.push(rowBuf, pad);
|
|
126
|
+
}
|
|
127
|
+
return Buffer.concat(f);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fixture(name) {
|
|
131
|
+
return fs.readFileSync(path.join(REPO_ROOT, "tests", "fixtures", name));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The gen-reference corpus, name -> input bytes (cached).
|
|
135
|
+
const inputCache = new Map();
|
|
136
|
+
function input(name) {
|
|
137
|
+
if (!inputCache.has(name)) {
|
|
138
|
+
inputCache.set(name, makeInput(name));
|
|
139
|
+
}
|
|
140
|
+
return inputCache.get(name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The exact name->input mapping tools/gen-reference defines. */
|
|
144
|
+
function makeInput(name) {
|
|
145
|
+
switch (name) {
|
|
146
|
+
case "detect-png":
|
|
147
|
+
case "sig-image-phash_rgb8_48x40":
|
|
148
|
+
case "describe-image-phash_rgb8_48x40":
|
|
149
|
+
return fixture("phash_rgb8_48x40.png");
|
|
150
|
+
case "detect-jpeg":
|
|
151
|
+
case "sig-image-base_444-jpeg":
|
|
152
|
+
return fixture("base_444.jpg");
|
|
153
|
+
case "detect-bmp":
|
|
154
|
+
case "sig-image-bmp-patterned-48x40":
|
|
155
|
+
case "describe-image-bmp-patterned-48x40":
|
|
156
|
+
return bmp24(48, 40, patternedPixels(0xbeef0001, 48, 40));
|
|
157
|
+
case "detect-wav":
|
|
158
|
+
case "sig-audio-tone-wav":
|
|
159
|
+
case "describe-audio-tone-wav":
|
|
160
|
+
return fixture("tone.wav");
|
|
161
|
+
case "detect-flac":
|
|
162
|
+
case "sig-audio-tone-flac":
|
|
163
|
+
return fixture("tone.flac");
|
|
164
|
+
case "detect-mp3":
|
|
165
|
+
case "sig-audio-l3_short-mp3":
|
|
166
|
+
return fixture("l3_short.mp3");
|
|
167
|
+
case "detect-mp4":
|
|
168
|
+
case "sig-video-a_64x48-mp4":
|
|
169
|
+
case "describe-video-a_64x48-mp4":
|
|
170
|
+
return fixture("a_64x48.mp4");
|
|
171
|
+
case "detect-mov":
|
|
172
|
+
return fixture("a_64x48.mov");
|
|
173
|
+
case "detect-pdf":
|
|
174
|
+
case "sig-pdf-text_page":
|
|
175
|
+
case "describe-pdf-text_page":
|
|
176
|
+
return fixture("text_page.pdf");
|
|
177
|
+
case "detect-zip":
|
|
178
|
+
return Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
|
179
|
+
case "detect-gif":
|
|
180
|
+
return Buffer.from("GIF89a\x01\x00\x01\x00", "binary");
|
|
181
|
+
case "detect-text":
|
|
182
|
+
return Buffer.from("just some words", "binary");
|
|
183
|
+
case "detect-binary":
|
|
184
|
+
return Buffer.from([0x00, 0xff, 0x02, 0xfe]);
|
|
185
|
+
case "sig-image-phash_gray8_40x32":
|
|
186
|
+
return fixture("phash_gray8_40x32.png");
|
|
187
|
+
case "sig-image-base_444-png":
|
|
188
|
+
return fixture("base_444.png");
|
|
189
|
+
case "sig-text-prose-4kib":
|
|
190
|
+
case "describe-text-prose-4kib":
|
|
191
|
+
return Buffer.from(prose(0xd1, 4096), "binary");
|
|
192
|
+
case "sig-binary-splitmix64-48kib":
|
|
193
|
+
case "describe-binary-splitmix64-48kib":
|
|
194
|
+
return blob48(0xd15e5eed);
|
|
195
|
+
case "sig-binary-splitmix64-48kib-prefix-insert": {
|
|
196
|
+
const out = Buffer.alloc(1 + 48 * 1024);
|
|
197
|
+
out[0] = 0xaa;
|
|
198
|
+
blob48(0xd15e5eed).copy(out, 1);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
case "sig-binary-splitmix64-48kib-unrelated":
|
|
202
|
+
return blob48(0xbadc0ffe);
|
|
203
|
+
case "match-image-self":
|
|
204
|
+
case "match-image-distinct":
|
|
205
|
+
case "match-error-cross-modality":
|
|
206
|
+
return fixture("phash_rgb8_48x40.png");
|
|
207
|
+
case "match-image-png-vs-jpeg-same-pixels":
|
|
208
|
+
return fixture("base_444.png");
|
|
209
|
+
case "match-audio-self":
|
|
210
|
+
return fixture("tone.wav");
|
|
211
|
+
case "match-text-self":
|
|
212
|
+
case "match-text-distinct":
|
|
213
|
+
return Buffer.from(prose(0xd1, 4096), "binary");
|
|
214
|
+
case "match-binary-near-duplicate":
|
|
215
|
+
case "match-binary-unrelated":
|
|
216
|
+
return blob48(0xd15e5eed);
|
|
217
|
+
case "match-video-mp4-vs-mov":
|
|
218
|
+
return fixture("a_64x48.mp4");
|
|
219
|
+
default:
|
|
220
|
+
throw new Error(`no input recipe for ${name}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The right-hand input of each match vector. */
|
|
225
|
+
function matchRight(name) {
|
|
226
|
+
switch (name) {
|
|
227
|
+
case "match-image-distinct":
|
|
228
|
+
case "match-image-png-vs-jpeg-same-pixels":
|
|
229
|
+
return name === "match-image-distinct"
|
|
230
|
+
? fixture("phash_gray8_40x32.png")
|
|
231
|
+
: fixture("base_444.jpg");
|
|
232
|
+
case "match-text-distinct":
|
|
233
|
+
return Buffer.from(prose(0xd2, 4096), "binary");
|
|
234
|
+
case "match-binary-near-duplicate": {
|
|
235
|
+
const out = Buffer.alloc(1 + 48 * 1024);
|
|
236
|
+
out[0] = 0xaa;
|
|
237
|
+
blob48(0xd15e5eed).copy(out, 1);
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
case "match-binary-unrelated":
|
|
241
|
+
return blob48(0xbadc0ffe);
|
|
242
|
+
case "match-video-mp4-vs-mov":
|
|
243
|
+
return fixture("a_64x48.mov");
|
|
244
|
+
default:
|
|
245
|
+
return input(name); // self-matches
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** f64 bit pattern from a recorded 16-hex-digit string. @param {string} hex */
|
|
250
|
+
const bits = (hex) => BigInt(`0x${hex}`);
|
|
251
|
+
const fnvHex = (bytes) => fnv1a64(bytes).toString(16).padStart(16, "0");
|
|
252
|
+
const shaHex = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
253
|
+
|
|
254
|
+
/** Words folded little-endian, the canonical payload behind the digests. */
|
|
255
|
+
function wordsLe(words) {
|
|
256
|
+
const out = Buffer.alloc(words.length * 8);
|
|
257
|
+
words.forEach((w, i) => out.writeBigUInt64LE(w, i * 8));
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test("cdylib is discoverable", () => {
|
|
262
|
+
assert.ok(fs.statSync(findCdylib()).isFile());
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// --- detect: 13 verdict vectors --------------------------------------
|
|
266
|
+
for (const [name, v] of BY_NAME) {
|
|
267
|
+
if (!name.startsWith("detect-")) continue;
|
|
268
|
+
test(`${name} verdict is reproduced`, () => {
|
|
269
|
+
const det = detect(input(name));
|
|
270
|
+
assert.equal(det.format, v.format, name);
|
|
271
|
+
assert.equal(det.modality, v.modality, name);
|
|
272
|
+
assert.equal(det.pending, v.pending, name);
|
|
273
|
+
assert.ok(FORMATS.includes(det.format) && MODALITIES.includes(det.modality), name);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- signature: 14 fold vectors --------------------------------------
|
|
278
|
+
for (const [name, v] of BY_NAME) {
|
|
279
|
+
if (!name.startsWith("sig-")) continue;
|
|
280
|
+
test(`${name} signature folds are reproduced hex-exact`, () => {
|
|
281
|
+
const sig = signature(input(name));
|
|
282
|
+
assert.equal(sig.modality, v.modality, name);
|
|
283
|
+
if (sig.modality === "image") {
|
|
284
|
+
assert.equal(sig.phash, BigInt(v.phash), name);
|
|
285
|
+
} else if (sig.modality === "audio") {
|
|
286
|
+
assert.equal(sig.peakCount, v.peak_count, name);
|
|
287
|
+
assert.equal(sig.peakCount, sig.peaksBytes.length, name);
|
|
288
|
+
assert.equal(sig.peakCount % 6, 0, name);
|
|
289
|
+
assert.equal(sig.frames, v.frames, name);
|
|
290
|
+
assert.equal(fnvHex(sig.peaksBytes), v.peaks_fnv1a64, name);
|
|
291
|
+
assert.equal(shaHex(sig.peaksBytes), v.peaks_sha256, name);
|
|
292
|
+
} else if (sig.modality === "text") {
|
|
293
|
+
assert.equal(sig.wordCount, v.minhash_words, name);
|
|
294
|
+
assert.equal(sig.words.length * 8, v.minhash_words, name);
|
|
295
|
+
assert.equal(fnvHex(wordsLe(sig.words)), v.minhash_fnv1a64, name);
|
|
296
|
+
assert.equal(shaHex(wordsLe(sig.words)), v.minhash_sha256, name);
|
|
297
|
+
} else if (sig.modality === "binary") {
|
|
298
|
+
assert.equal(sig.chunkCount, v.chunk_count, name);
|
|
299
|
+
assert.equal(sig.chunkCount, sig.digestBytes.length, name);
|
|
300
|
+
assert.equal(sig.chunkCount % 32, 0, name);
|
|
301
|
+
assert.equal(fnvHex(sig.digestBytes), v.chunks_fnv1a64, name);
|
|
302
|
+
assert.equal(shaHex(sig.digestBytes), v.chunks_sha256, name);
|
|
303
|
+
} else {
|
|
304
|
+
const frames = wordsLe(sig.frameHashes);
|
|
305
|
+
const minhash = wordsLe(sig.minhash);
|
|
306
|
+
assert.equal(sig.frameCount, v.frame_count, name);
|
|
307
|
+
assert.equal(sig.frameCount, frames.length, name);
|
|
308
|
+
assert.equal(fnvHex(frames), v.frames_fnv1a64, name);
|
|
309
|
+
assert.equal(shaHex(frames), v.frames_sha256, name);
|
|
310
|
+
assert.equal(minhash.length, v.minhash_words, name);
|
|
311
|
+
assert.equal(fnvHex(minhash), v.minhash_fnv1a64, name);
|
|
312
|
+
assert.equal(sig.width, v.width, name);
|
|
313
|
+
assert.equal(sig.height, v.height, name);
|
|
314
|
+
assert.equal(sig.durationBits, bits(v.duration_bits), name);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- describe: 7 both-tiers vectors ----------------------------------
|
|
320
|
+
for (const [name, v] of BY_NAME) {
|
|
321
|
+
if (!name.startsWith("describe-")) continue;
|
|
322
|
+
test(`${name} description is reproduced hex-exact`, () => {
|
|
323
|
+
const d = describe(input(name));
|
|
324
|
+
assert.equal(d.format, v.format, name);
|
|
325
|
+
assert.equal(d.modality, v.modality, name);
|
|
326
|
+
assert.equal(d.tier1, v.tier1, name);
|
|
327
|
+
const s = d.signature;
|
|
328
|
+
if (s.modality === "image") {
|
|
329
|
+
assert.equal(s.phash, BigInt(v.phash), name);
|
|
330
|
+
assert.deepEqual(d.facts, { width: v.facts.width, height: v.facts.height, keypoints: v.facts.keypoints }, name);
|
|
331
|
+
} else if (s.modality === "audio") {
|
|
332
|
+
assert.equal(s.peakCount, v.peak_count, name);
|
|
333
|
+
assert.equal(fnvHex(s.peaksBytes), v.peaks_fnv1a64, name);
|
|
334
|
+
assert.equal(shaHex(s.peaksBytes), v.peaks_sha256, name);
|
|
335
|
+
assert.equal(s.frames, v.frames, name);
|
|
336
|
+
assert.deepEqual(
|
|
337
|
+
d.facts,
|
|
338
|
+
{ sampleRate: v.facts.sample_rate, channels: v.facts.channels, frames: v.facts.frames, peaks: v.facts.peaks },
|
|
339
|
+
name,
|
|
340
|
+
);
|
|
341
|
+
} else if (s.modality === "text") {
|
|
342
|
+
assert.equal(s.wordCount, v.minhash_words, name);
|
|
343
|
+
assert.equal(fnvHex(wordsLe(s.words)), v.minhash_fnv1a64, name);
|
|
344
|
+
assert.equal(shaHex(wordsLe(s.words)), v.minhash_sha256, name);
|
|
345
|
+
assert.deepEqual(d.facts, { words: v.facts.words, canonicalLen: v.facts.canonical_len }, name);
|
|
346
|
+
} else if (s.modality === "binary") {
|
|
347
|
+
assert.equal(s.chunkCount, v.chunk_count, name);
|
|
348
|
+
assert.equal(fnvHex(s.digestBytes), v.chunks_fnv1a64, name);
|
|
349
|
+
assert.equal(shaHex(s.digestBytes), v.chunks_sha256, name);
|
|
350
|
+
assert.deepEqual(d.facts, { len: v.facts.len, chunks: v.facts.chunks }, name);
|
|
351
|
+
} else {
|
|
352
|
+
const frames = wordsLe(s.frameHashes);
|
|
353
|
+
assert.equal(s.frameCount, v.frame_count, name);
|
|
354
|
+
assert.equal(fnvHex(frames), v.frames_fnv1a64, name);
|
|
355
|
+
assert.equal(shaHex(frames), v.frames_sha256, name);
|
|
356
|
+
assert.equal(wordsLe(s.minhash).length, v.minhash_words, name);
|
|
357
|
+
assert.equal(fnvHex(wordsLe(s.minhash)), v.minhash_fnv1a64, name);
|
|
358
|
+
assert.equal(s.width, v.width, name);
|
|
359
|
+
assert.equal(s.height, v.height, name);
|
|
360
|
+
assert.equal(s.durationBits, bits(v.duration_bits), name);
|
|
361
|
+
assert.deepEqual(
|
|
362
|
+
d.facts,
|
|
363
|
+
{
|
|
364
|
+
width: v.facts.width,
|
|
365
|
+
height: v.facts.height,
|
|
366
|
+
durationBits: bits(v.facts.duration_bits),
|
|
367
|
+
framesSampled: v.facts.frames_sampled,
|
|
368
|
+
},
|
|
369
|
+
name,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --- match: 10 outcome vectors ---------------------------------------
|
|
376
|
+
/** Recorded decimal delta_t -> the u64 two's-complement slot. */
|
|
377
|
+
const deltaSlot = (dt) => BigInt(dt < 0 ? dt + 0x10000000000000000 : dt);
|
|
378
|
+
|
|
379
|
+
for (const [name, v] of BY_NAME) {
|
|
380
|
+
if (!name.startsWith("match-")) continue;
|
|
381
|
+
if (v.outcome === "error") {
|
|
382
|
+
test(`${name} is refused with status -2`, () => {
|
|
383
|
+
assert.throws(() => matchSignatures(input(name), input("sig-text-prose-4kib")), (err) => {
|
|
384
|
+
assert.ok(err instanceof FfiError);
|
|
385
|
+
assert.equal(err.status, -2);
|
|
386
|
+
return true;
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
test(`${name} match slots are reproduced exactly`, () => {
|
|
392
|
+
const m = matchSignatures(input(name), matchRight(name));
|
|
393
|
+
assert.equal(m.matched, v.matched, name);
|
|
394
|
+
if (v.outcome === "image") {
|
|
395
|
+
assert.equal(m.tag, "image", name);
|
|
396
|
+
assert.equal(m.slots[0], BigInt(v.hamming), name);
|
|
397
|
+
} else if (v.outcome === "audio") {
|
|
398
|
+
assert.equal(m.tag, "audio", name);
|
|
399
|
+
assert.equal(m.slots[0], BigInt(v.votes), name);
|
|
400
|
+
assert.equal(m.slots[1], deltaSlot(v.delta_t), name);
|
|
401
|
+
} else if (v.outcome === "jaccard") {
|
|
402
|
+
assert.equal(m.tag, name.startsWith("match-text") ? "text" : "binary", name);
|
|
403
|
+
assert.equal(m.slots[0], bits(v.jaccard_bits), name);
|
|
404
|
+
} else {
|
|
405
|
+
assert.equal(m.tag, "video", name);
|
|
406
|
+
assert.equal(m.slots[0], bits(v.score_bits), name);
|
|
407
|
+
assert.equal(m.slots[1], bits(v.minhash_jaccard_bits), name);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --- error vector + refusals -----------------------------------------
|
|
413
|
+
test("non-AVC mp4 is refused with status -2", () => {
|
|
414
|
+
assert.throws(() => signature(fixture("e_mp4v.mp4")), (err) => {
|
|
415
|
+
assert.ok(err instanceof FfiError);
|
|
416
|
+
assert.equal(err.status, -2);
|
|
417
|
+
return true;
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("corrupt container is refused, not crashing", () => {
|
|
422
|
+
const corrupt = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.from("garbage")]);
|
|
423
|
+
assert.throws(() => signature(corrupt), (err) => {
|
|
424
|
+
assert.ok(err instanceof FfiError);
|
|
425
|
+
assert.equal(err.status, -2);
|
|
426
|
+
return true;
|
|
427
|
+
});
|
|
428
|
+
assert.throws(() => describe(corrupt), FfiError);
|
|
429
|
+
assert.throws(() => contentHash(corrupt), (err) => {
|
|
430
|
+
assert.ok(err instanceof FfiError);
|
|
431
|
+
assert.equal(err.status, -2);
|
|
432
|
+
return true;
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("content hash matches a rust-derived pinned value", () => {
|
|
437
|
+
// text_page.pdf's tier-1 digest, derived once from the built cdylib
|
|
438
|
+
// and pinned here so a wrongly regenerated reference.json cannot
|
|
439
|
+
// mask drift. The same pin lives in the Python and Go harnesses and
|
|
440
|
+
// the Rust ffi tests.
|
|
441
|
+
const digest = contentHash(fixture("text_page.pdf"));
|
|
442
|
+
assert.equal(digest.length, 32);
|
|
443
|
+
assert.equal(
|
|
444
|
+
digest.toString("hex"),
|
|
445
|
+
"849978a1682ba75372094611c8fc290b9c39b84d75641bf0b6129cdbaa880fa9",
|
|
446
|
+
);
|
|
447
|
+
});
|
package/README.md
DELETED