"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const {
STATUS_REJECTED,
STATUS_UNSUPPORTED,
FfiError,
extractCanonical,
findCdylib,
parseCanonical,
} = require("../index.js");
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
const REFERENCE = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "reference.json"), "utf8"));
const FIXTURE_VECTORS = REFERENCE.vectors.filter(
(v) => "text_sha256" in v && !("input" in v),
);
const OPEN_REFUSALS = REFERENCE.errors.filter((e) => e.api === "Document::open");
const EXCLUDED_REFUSALS = REFERENCE.errors.filter((e) => e.api !== "Document::open");
test("cdylib is discoverable", () => {
assert.ok(fs.statSync(findCdylib()).isFile());
});
for (const vector of FIXTURE_VECTORS) {
test(`reference vector ${vector.name} is reproduced hex-exact`, () => {
const data = fs.readFileSync(path.join(REPO_ROOT, "tests", "fixtures", `${vector.name}.pdf`));
const canonical = parseCanonical(extractCanonical(data));
assert.equal(
crypto.createHash("sha256").update(canonical.textBytes).digest("hex"),
vector.text_sha256,
vector.name,
);
assert.equal(canonical.textBytes.length, vector.text_bytes, vector.name);
assert.equal(canonical.pages, vector.pages, vector.name);
assert.equal(canonical.rebuilt, vector.rebuilt, vector.name);
});
}
test("reference vector recovered_startxref is rebuilt from inline hex", () => {
const vector = REFERENCE.vectors.find((v) => v.name === "recovered_startxref");
const canonical = parseCanonical(extractCanonical(Buffer.from(vector.input, "hex")));
assert.equal(
crypto.createHash("sha256").update(canonical.textBytes).digest("hex"),
vector.text_sha256,
);
assert.equal(canonical.textBytes.length, vector.text_bytes);
assert.equal(canonical.pages, vector.pages);
assert.equal(canonical.rebuilt, true);
});
test("encrypted fixture refuses unsupported", () => {
const data = fs.readFileSync(path.join(REPO_ROOT, "tests", "fixtures", "encrypted.pdf"));
assert.throws(() => extractCanonical(data), (err) => {
assert.ok(err instanceof FfiError);
assert.equal(err.status, STATUS_UNSUPPORTED);
return true;
});
});
for (const row of OPEN_REFUSALS) {
test(`refusal vector ${row.name} is replayed`, () => {
assert.throws(() => extractCanonical(Buffer.from(row.input, "hex")), (err) => {
assert.ok(err instanceof FfiError);
const expected = row.error.includes("unsupported") ? STATUS_UNSUPPORTED : STATUS_REJECTED;
assert.equal(err.status, expected, row.name);
return true;
});
});
}
test("decode_stream error rows are excluded by design", () => {
assert.equal(EXCLUDED_REFUSALS.length + OPEN_REFUSALS.length, REFERENCE.errors.length);
assert.ok(EXCLUDED_REFUSALS.every((e) => e.api === "pith_pdf::decode_stream"));
assert.equal(EXCLUDED_REFUSALS.length, 11);
assert.equal(OPEN_REFUSALS.length, 5);
});
test("malformed input is refused, not crashing", () => {
assert.throws(() => extractCanonical(Buffer.from("not a pdf at all")), (err) => {
assert.ok(err instanceof FfiError);
assert.equal(err.status, STATUS_REJECTED);
return true;
});
});
test("empty input is refused", () => {
assert.throws(() => extractCanonical(Buffer.alloc(0)), FfiError);
});
test("full stream matches a rust-pinned value", () => {
const data = fs.readFileSync(path.join(REPO_ROOT, "tests", "fixtures", "empty_page.pdf"));
const raw = extractCanonical(data);
assert.equal(
crypto.createHash("sha256").update(raw).digest("hex"),
"5a8db37861d2ffa061f87ad2ec1b4c55181e695473ec20dbd228bad355b40e7e",
);
assert.deepEqual([...raw.subarray(0, 13)], [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 14]);
});