mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-23 02:00:00 +02:00
fix(make-pdf): adversarial-review wave — offline posture enforced, symlink-aware confinement, bounded reads
Codex adversarial + structured review findings: - Remote images are now BLOCKED with a visible placeholder instead of warn-and-keep — leaving the tag meant Chromium fetched the URL at print time anyway, so the offline posture was a lie (tracking pixels and internal-URL probes ran without --allow-network). - The out-of-tree read check compares REAL paths: a symlink inside the input dir pointing at ~/.ssh/... passed the string-prefix check, including under --strict. Ordered after the existence check (realpath of a missing file false-positives on macOS /var → /private/var). - Image reads are bounded BEFORE reading: statSync first, non-regular files (fifo/device/dir) and >64MB files degrade to placeholders instead of hanging or exhausting memory; malformed percent-encoding (foo%zz.png) degrades to missing-image instead of crashing decodeURIComponent. - browse shell-outs get a 120s timeout — a wedged daemon or hostile mermaid source fails the run instead of hanging it. - TOC entries link to the heading's ACTUAL id (pre-id'd raw-HTML headings previously got dead #toc-N links); per-side margins compose into the CSS @page shorthand so a landscape promotion flipping preferCSSPageSize no longer silently reverts --margin-left/right to defaults (Codex P2). - The image memo is a typed object — literal NUL-byte separators had made diagram-prepass.ts register as binary to text tooling. Codex structured review GATE: PASS (no P1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -277,14 +277,47 @@ describe("inlineLocalImages", () => {
|
||||
).toThrow(StrictModeError);
|
||||
});
|
||||
|
||||
test("remote image warns and is left untouched (offline posture)", () => {
|
||||
test("remote image is BLOCKED with a visible placeholder (offline posture)", () => {
|
||||
// Leaving the tag would make Chromium fetch it at print time anyway —
|
||||
// the offline posture must remove the src, not just warn about it.
|
||||
const warnings: string[] = [];
|
||||
const tag = `<img src="https://example.com/x.png">`;
|
||||
const out = inlineLocalImages(tag, { ...base, warn: (m) => warnings.push(m) });
|
||||
expect(out).toBe(tag);
|
||||
expect(out).not.toContain("https://example.com/x.png\"");
|
||||
expect(out).toContain("remote image blocked");
|
||||
expect(warnings[0]).toContain("offline");
|
||||
});
|
||||
|
||||
test("symlink escaping the input dir is caught by the realpath check", () => {
|
||||
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "prepass-symlink-"));
|
||||
fs.writeFileSync(path.join(outside, "secret.png"), tinyPng(5, 5));
|
||||
const link = path.join(dir, "innocent.png");
|
||||
try {
|
||||
fs.symlinkSync(path.join(outside, "secret.png"), link);
|
||||
const warnings: string[] = [];
|
||||
inlineLocalImages(`<img src="innocent.png">`, { ...base, warn: (m) => warnings.push(m) });
|
||||
expect(warnings.some((w) => w.includes("OUTSIDE the input directory"))).toBe(true);
|
||||
} finally {
|
||||
try { fs.unlinkSync(link); } catch { /* ignore */ }
|
||||
fs.rmSync(outside, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("special files and oversized images degrade to placeholders, never hang", () => {
|
||||
// Directory masquerading as an image — not a regular file.
|
||||
fs.mkdirSync(path.join(dir, "dir.png"), { recursive: true });
|
||||
const warnings: string[] = [];
|
||||
const out = inlineLocalImages(`<img src="dir.png">`, { ...base, warn: (m) => warnings.push(m) });
|
||||
expect(out).toContain("image-missing");
|
||||
expect(warnings.some((w) => w.includes("not a regular file"))).toBe(true);
|
||||
});
|
||||
|
||||
test("malformed percent-encoding degrades to missing-image, never throws", () => {
|
||||
const warnings: string[] = [];
|
||||
const out = inlineLocalImages(`<img src="foo%zz.png">`, { ...base, warn: (m) => warnings.push(m) });
|
||||
expect(out).toContain("image-missing");
|
||||
});
|
||||
|
||||
test("remote image + --allow-network passes silently", () => {
|
||||
const warnings: string[] = [];
|
||||
const tag = `<img src="https://example.com/x.png">`;
|
||||
|
||||
@@ -264,6 +264,13 @@ describe("printCss", () => {
|
||||
expect(css).toContain("margin: 72pt");
|
||||
});
|
||||
|
||||
test("per-side margins reach the CSS @page rule (preferCSSPageSize parity)", () => {
|
||||
// Under a landscape promotion Chromium honors the CSS margins, not the
|
||||
// CDP per-side options — render() must compose them into the shorthand.
|
||||
const r = render({ markdown: "# T", marginLeft: "0.5in", marginRight: "0.5in" });
|
||||
expect(r.printCss).toContain("margin: 1in 0.5in 1in 0.5in");
|
||||
});
|
||||
|
||||
test("emits letter page size by default", () => {
|
||||
const css = printCss();
|
||||
expect(css).toContain("size: letter");
|
||||
|
||||
Reference in New Issue
Block a user