From 9fb6d411f2eedfc3303ecd824c2f8388e53e03f6 Mon Sep 17 00:00:00 2001 From: Joshua Khoury Date: Thu, 24 Sep 2026 09:32:01 +0400 Subject: [PATCH] feat: validate artifact package structure --- packages/artifact-verify/README.md | 2 ++ packages/artifact-verify/src/index.mjs | 31 +++++++++++++++-- .../artifact-verify/test/artifactci.test.mjs | 34 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/artifact-verify/README.md b/packages/artifact-verify/README.md index 900c3e3..8ad34e5 100644 --- a/packages/artifact-verify/README.md +++ b/packages/artifact-verify/README.md @@ -23,3 +23,5 @@ The command exits non-zero when a check fails and can emit a JSON evidence repor Current scope is intentionally deterministic: package structure, required/forbidden text, placeholder detection, cached Excel formula errors and basic PDF checks. It does not claim full semantic or visual correctness. Repository: https://github.com/yashkhou/verify + +Artifact Verify also validates core Office package roots, internal `.rels` targets, cached Excel formula errors, and basic PDF trailer structure (`startxref` + terminal `%%EOF`) without claiming full rendering or workbook recalculation. diff --git a/packages/artifact-verify/src/index.mjs b/packages/artifact-verify/src/index.mjs index 23a053f..26898c1 100644 --- a/packages/artifact-verify/src/index.mjs +++ b/packages/artifact-verify/src/index.mjs @@ -33,13 +33,32 @@ function inspectOffice(buffer, ext) { [...x.matchAll(/(#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|NULL!))<\/v>/g)] .map(m => m[1]) ); + const brokenRelationships = []; + for (const entry of entries.filter(e => e.entryName.endsWith(".rels"))) { + const source = entry.entryName === "_rels/.rels" + ? "" + : entry.entryName.replace(/\/_rels\/([^/]+)\.rels$/, "/$1"); + const baseDir = source ? path.posix.dirname(source) : ""; + const relXml = entry.getData().toString("utf8"); + for (const match of relXml.matchAll(/]*\bTarget=["']([^"']+)["'][^>]*>/g)) { + const tag = match[0]; + const target = match[1]; + if (/\bTargetMode=["']External["']/.test(tag) || target.startsWith("#")) continue; + const cleanTarget = target.split("#", 1)[0].split("?", 1)[0]; + const resolved = cleanTarget.startsWith("/") + ? path.posix.normalize(cleanTarget.slice(1)) + : path.posix.normalize(path.posix.join(baseDir, cleanTarget)); + if (!names.has(resolved)) brokenRelationships.push({ relationshipPart: entry.entryName, target, resolved }); + } + } return { text, names, xml, expectedRoot, rootPresent: names.has(expectedRoot), - formulaErrors + formulaErrors, + brokenRelationships }; } @@ -47,7 +66,8 @@ function inspectPdf(buffer) { const raw = buffer.toString("latin1"); const pageCount = (raw.match(/\/Type\s*\/Page(?!s)\b/g) || []).length; const strings = [...raw.matchAll(/\(([^()]*)\)\s*Tj/g)].map(m => m[1]); - return { pageCount, text: normalizeText(strings.join(" ")) }; + const trailerPresent = /startxref\s+\d+\s+%%EOF\s*$/.test(raw); + return { pageCount, text: normalizeText(strings.join(" ")), trailerPresent }; } export function verifyArtifact(filePath, rules = {}) { const absolute = path.resolve(filePath); @@ -73,6 +93,8 @@ export function verifyArtifact(filePath, rules = {}) { text = pdf.text; checks.push(result("pdf.header", buffer.subarray(0, 5).toString() === "%PDF-" ? "pass" : "fail", "PDF header signature")); + checks.push(result("pdf.trailer", pdf.trailerPresent ? "pass" : "fail", + pdf.trailerPresent ? "Found startxref and terminal %%EOF" : "Missing valid startxref / terminal %%EOF trailer")); if (rules.minPages != null) checks.push(result("pdf.minPages", pdf.pageCount >= rules.minPages ? "pass" : "fail", `${pdf.pageCount} pages`)); if (rules.maxPages != null) checks.push(result("pdf.maxPages", @@ -87,6 +109,11 @@ export function verifyArtifact(filePath, rules = {}) { text = office.text; checks.push(result("office.structure", office.rootPresent ? "pass" : "fail", office.rootPresent ? `Found ${office.expectedRoot}` : `Missing ${office.expectedRoot}`)); + checks.push(result("office.relationships", office.brokenRelationships.length === 0 ? "pass" : "fail", + office.brokenRelationships.length === 0 + ? "Internal package relationships resolve" + : `Broken internal relationships: ${office.brokenRelationships.map(rel => `${rel.relationshipPart} -> ${rel.target}`).join(", ")}`, + { brokenRelationships: office.brokenRelationships })); if (ext === ".xlsx") checks.push(result("xlsx.formulaErrors", office.formulaErrors.length === 0 ? "pass" : "fail", office.formulaErrors.length ? `Formula errors: ${office.formulaErrors.join(", ")}` : "No cached Excel formula errors found")); diff --git a/packages/artifact-verify/test/artifactci.test.mjs b/packages/artifact-verify/test/artifactci.test.mjs index c3fa924..eccdecd 100644 --- a/packages/artifact-verify/test/artifactci.test.mjs +++ b/packages/artifact-verify/test/artifactci.test.mjs @@ -56,3 +56,37 @@ test("fails a corrupt Office container without throwing", () => { assert.equal(report.ok, false); assert.equal(report.checks.some(check => check.id === "office.open" && check.status === "fail"), true); }); + + +test("detects broken internal Office relationships", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "artifact-verify-")); + const file = path.join(dir, "broken-rel.docx"); + const zip = new AdmZip(); + zip.addFile("word/document.xml", Buffer.from("Verified")); + zip.addFile("word/_rels/document.xml.rels", Buffer.from('')); + zip.writeZip(file); + const report = verifyArtifact(file, { forbidPlaceholders: false }); + const check = report.checks.find(item => item.id === "office.relationships"); + assert.equal(check.status, "fail"); + assert.equal(check.evidence.brokenRelationships[0].resolved, "word/media/missing.png"); +}); + +test("allows external Office relationships without requiring a package entry", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "artifact-verify-")); + const file = path.join(dir, "external-rel.docx"); + const zip = new AdmZip(); + zip.addFile("word/document.xml", Buffer.from("Verified")); + zip.addFile("word/_rels/document.xml.rels", Buffer.from('')); + zip.writeZip(file); + const report = verifyArtifact(file, { forbidPlaceholders: false }); + assert.equal(report.checks.find(item => item.id === "office.relationships").status, "pass"); +}); + +test("requires a PDF startxref and terminal EOF trailer", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "artifact-verify-")); + const file = path.join(dir, "broken.pdf"); + fs.writeFileSync(file, "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n"); + const report = verifyArtifact(file, { forbidPlaceholders: false }); + assert.equal(report.checks.find(item => item.id === "pdf.header").status, "pass"); + assert.equal(report.checks.find(item => item.id === "pdf.trailer").status, "fail"); +});