Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
840b71b
test(ooxml.js): cover base64 encode/decode boundaries and simplify de…
Mearman Sep 12, 2026
2cbe812
test(ooxml.js): cover buildXml's node kinds and drop unobservable bui…
Mearman Sep 12, 2026
16c12ca
test(ooxml.js): cover parseXml's internal validation helpers directly
Mearman Sep 12, 2026
79f714b
test(ooxml.js): cover isXmlNode's full truth table across every node …
Mearman Sep 12, 2026
8c73b6c
test(ooxml.js): cover looksLikeXml's BOM/whitespace skip and drop a r…
Mearman Sep 12, 2026
a5ba62a
test(ooxml.js): cover every sniffed image signature and drop a redund…
Mearman Sep 12, 2026
f6243b0
test(ooxml.js): cover relsPathFor/resolveRelTarget's path arithmetic
Mearman Sep 12, 2026
137e973
test(ooxml.js): cover serial.ts's date/time boundaries and remove two…
Mearman Sep 12, 2026
5c72ba1
test(ooxml.js): cover sqref parsing/formatting and simplify its white…
Mearman Sep 12, 2026
5522911
test(ooxml.js): cover captureResidualAttributes/residualAttributesFor…
Mearman Sep 12, 2026
510a8ac
test(ooxml.js): cover loadSharedStrings and SharedStringTable directly
Mearman Sep 12, 2026
ebd1bfb
test(ooxml.js): prove readXlsx omits the definitions key when there a…
Mearman Sep 12, 2026
278a07b
test(ooxml.js): cover readWorkbookDefinitions' relationship filtering…
Mearman Sep 12, 2026
0ebb6d9
test(ooxml.js): cover consecutive images with no candidate paragraph …
Mearman Sep 12, 2026
aca0d07
test(ooxml.js): cover shading's "none" colour tokens and single-colou…
Mearman Sep 12, 2026
a9e95bd
test(ooxml.js): cover threaded-comment id formatting, counter increme…
Mearman Sep 12, 2026
c353c9c
test(ooxml.js): cover page-size tolerance's exact boundary and remove…
Mearman Sep 12, 2026
3241e38
test(ooxml.js): cover numbering's overridden-level guard, namespace, …
Mearman Sep 12, 2026
c98e5a8
test(ooxml.js): cover flavour detection's own precondition directly
Mearman Sep 12, 2026
4fca859
fix(ooxml.js): populate the required displayText field on every test …
Mearman Sep 12, 2026
c15c711
refactor(ooxml.js): drop looksLikeSvg's redundant Math.min against by…
Mearman Sep 13, 2026
b001ae1
test(ooxml.js): prove isXmlNode's element branch gates on type, not s…
Mearman Sep 13, 2026
2c48742
test(ooxml.js): prove a reply's own counter increment never runs back…
Mearman Sep 13, 2026
6c39bde
test(ooxml.js): prove a table relationship is filtered by its own type
Mearman Sep 13, 2026
3af2738
refactor(ooxml.js): drop isoDateTimeToSerial's redundant no-separator…
Mearman Sep 13, 2026
6745c51
refactor(ooxml.js): drop parseSqref's redundant empty-token skip
Mearman Sep 13, 2026
792b285
refactor(ooxml.js): hoist buildXml's ignored pi/declaration child array
Mearman Sep 13, 2026
e30376e
test(ooxml.js): cover textContent's cdata concatenation, simplify rel…
Mearman Sep 13, 2026
9520aa7
test(ooxml.js): cover buildCellShading's unrecognised-kind default br…
Mearman Sep 13, 2026
4051f5f
test(ooxml.js): cover figure-captions' image gate, join separator
Mearman Sep 13, 2026
be2e633
test(ooxml.js): cover numbering's non-canonical ilvl/numId sort, unde…
Mearman Sep 13, 2026
75cbf0c
refactor(ooxml.js): drop bytesToBase64's redundant remainder-byte guards
Mearman Sep 13, 2026
a085060
test(ooxml.js): cover embedded-object root-entry precedence, Package …
Mearman Sep 13, 2026
cfb811c
fix(ooxml.js): correct a stale comment about the removed magic-byte gate
Mearman Sep 13, 2026
d77beb1
test(ooxml.js): add direct structural coverage for oleObjectBin
Mearman Sep 13, 2026
b8481a4
test(ooxml.js): cover inherit's rel-type filter, placeholder fallback…
Mearman Sep 13, 2026
5ecee8b
test(ooxml.js): assert italic is also undefined for an rPr with no attrs
Mearman Sep 13, 2026
98ac25b
test(ooxml.js): prove residualAttributesFor rejects a matching-first-…
Mearman Sep 13, 2026
f245629
refactor(ooxml.js): rewrite oleObjectBin's fixed-array copy loops as …
Mearman Sep 13, 2026
54618d8
test(ooxml.js): add byte-level coverage for oleObjectBin's remaining …
Mearman Sep 13, 2026
7198d8e
refactor(ooxml.js): drop oleObjectBin's three redundant zero-valued h…
Mearman Sep 13, 2026
8a9e53d
test(ooxml.js): cover defined-names' print-area/titles parse and buil…
Mearman Sep 13, 2026
7ea2f15
test(ooxml.js): cover data-validation's read/build attribute branches
Mearman Sep 13, 2026
60b098b
test(ooxml.js): add direct coverage for the tree-walk/attr/rels helpers
Mearman Sep 13, 2026
92de84e
test(ooxml.js): cover print-settings' margins, breaks, and fit/scale …
Mearman Sep 13, 2026
8e978f1
refactor(ooxml.js): drop defined-names' redundant guards and regex re…
Mearman Sep 13, 2026
f4ca53e
test(ooxml.js): cover every ST_DataValidationOperator vocabulary member
Mearman Sep 13, 2026
5740d3c
refactor(ooxml.js): drop print-settings' redundant scale-presence guard
Mearman Sep 13, 2026
4c53ec2
test(ooxml.js): add direct structural coverage for chart cache reading
Mearman Sep 13, 2026
45fd09f
test(ooxml.js): add direct structural coverage for diagram text walking
Mearman Sep 13, 2026
be6601e
test(ooxml.js): prove a startOverride with no w:val leaves startAt alone
Mearman Sep 13, 2026
da234f0
test(ooxml.js): distinguish a multi-level cache's last level from its…
Mearman Sep 13, 2026
15db048
test(ooxml.js): prove an unrecognised paragraph child contributes no run
Mearman Sep 13, 2026
3c6113e
refactor(ooxml.js): drop readToggle's redundant absent-value guard
Mearman Sep 13, 2026
f36c6a9
test(ooxml.js): close style-cascade gaps in type discrimination and m…
Mearman Sep 13, 2026
5aa96ca
test(ooxml.js): prove an unrecognised asciiTheme resolves to no font
Mearman Sep 13, 2026
78df127
refactor(ooxml.js): drop reading-order's provably redundant cut guards
Mearman Sep 13, 2026
7ef4b8f
test(ooxml.js): cover reading-order's axis-tie, recursion, and extent…
Mearman Sep 13, 2026
3902f9b
test(ooxml.js): close reading-order's touching-boundary and gap-arith…
Mearman Sep 13, 2026
d942ff5
test(ooxml.js): add direct structural coverage for the embedded-fixtu…
Mearman Sep 13, 2026
6660ccb
test(ooxml.js): close metadata's blank-value, keyword-parsing, and pe…
Mearman Sep 13, 2026
cd883bc
test(ooxml.js): add direct structural coverage for xlsx table/name de…
Mearman Sep 13, 2026
5ecc2df
test(ooxml.js): close color.ts's HSL boundary and gamma-threshold gaps
Mearman Sep 13, 2026
ef78cb0
test(ooxml.js): close xlsx.ts's rels-correlation and sheet-ordering gaps
Mearman Sep 13, 2026
fd9a6f8
test(ooxml.js): pick a non-coincidental (l, s) pair for the 1/6 hue b…
Mearman Sep 13, 2026
e90c504
test(ooxml.js): close drawingml's per-field, theme-fallback, and tran…
Mearman Sep 13, 2026
5fa061f
refactor(ooxml.js): drop localName's unreachable no-colon branch
Mearman Sep 13, 2026
8ead76c
refactor(ooxml.js): drop applyGroupTransform's redundant identity sho…
Mearman Sep 13, 2026
979e08a
docs(ooxml.js): document canonicalizeGroupRotation's irreducible +180…
Mearman Sep 13, 2026
d0f2809
test(ooxml.js): cover isCompactXmlNode's full type-code truth table d…
Mearman Sep 14, 2026
60a4c8f
refactor(ooxml.js): drop comments' redundant presence guards before a…
Mearman Sep 14, 2026
13fc751
test(ooxml.js): close comments.ts's relationship-type, local-name, an…
Mearman Sep 14, 2026
e67fdfe
test(ooxml.js): distinguish extentAlong's true earliest start from it…
Mearman Sep 14, 2026
abd8ac2
refactor(ooxml.js): drop constructs.ts's three redundant guards
Mearman Sep 14, 2026
8e29a3a
test(ooxml.js): close constructs.ts's paragraph-index, checkbox, and …
Mearman Sep 14, 2026
f356725
fix(ooxml.js): pin isBlockScopedHalf's own leading/trailing boundary …
Mearman Sep 14, 2026
ba7dacf
refactor(ooxml.js): drop drawings.ts's redundant column/row validity …
Mearman Sep 14, 2026
e60fb6b
refactor(ooxml.js): drop drawings.ts's remaining redundant NaN-fallba…
Mearman Sep 14, 2026
fefba10
test(ooxml.js): cover SheetGridGeometry's column/row lookups and edit…
Mearman Sep 15, 2026
1885aee
test(ooxml.js): close drawings.ts's remaining chart-frame and marker …
Mearman Sep 15, 2026
856c7dc
test(ooxml.js): add direct structural coverage for buildDrawing and f…
Mearman Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 155 additions & 1 deletion packages/ooxml.js/src/compact.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
CompactXmlNodeSchema,
decodeCompactPackage,
decodePackage,
encodeCompactPackage,
Expand All @@ -8,7 +9,7 @@ import {
toCompact,
zipPackage,
} from "./index";
import type { Package, XmlElement } from "./index";
import type { CompactPackage, Package, XmlElement } from "./index";

function enc(s: string): Uint8Array<ArrayBuffer> {
return new TextEncoder().encode(s);
Expand Down Expand Up @@ -179,6 +180,109 @@ describe("compact size", () => {
});
});

describe("isCompactXmlNode (via CompactXmlNodeSchema)", () => {
it("rejects a non-array value", () => {
expect(CompactXmlNodeSchema.safeParse("nope").success).toBe(false);
expect(CompactXmlNodeSchema.safeParse({ 0: 1, 1: 0 }).success).toBe(false);
});

it("accepts a text/cdata/comment node ([1|2|3, number])", () => {
expect(CompactXmlNodeSchema.safeParse([1, 0]).success).toBe(true);
expect(CompactXmlNodeSchema.safeParse([2, 0]).success).toBe(true);
expect(CompactXmlNodeSchema.safeParse([3, 0]).success).toBe(true);
});

it("rejects a text/cdata/comment node with the wrong tuple length", () => {
expect(CompactXmlNodeSchema.safeParse([1, 0, 0]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([2, 0, 0]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([3, 0, 0]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([1]).success).toBe(false);
});

it("rejects a text/cdata/comment node whose value slot is not a number", () => {
expect(CompactXmlNodeSchema.safeParse([1, "x"]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([2, "x"]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([3, "x"]).success).toBe(false);
});

it("accepts a declaration node ([4, attrPairs])", () => {
expect(CompactXmlNodeSchema.safeParse([4, [0, 1]]).success).toBe(true);
expect(CompactXmlNodeSchema.safeParse([4, []]).success).toBe(true);
});

it("rejects a declaration node with the wrong tuple length", () => {
expect(CompactXmlNodeSchema.safeParse([4, [0, 1], 9]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([4]).success).toBe(false);
});

it("rejects a declaration node whose attr pairs are not a valid CompactAttrPairs", () => {
expect(CompactXmlNodeSchema.safeParse([4, "not-an-array"]).success).toBe(
false,
);
expect(CompactXmlNodeSchema.safeParse([4, [0, "x"]]).success).toBe(false);
});

it("accepts a pi node ([5, number, number])", () => {
expect(CompactXmlNodeSchema.safeParse([5, 0, 1]).success).toBe(true);
});

it("rejects a pi node with the wrong tuple length", () => {
expect(CompactXmlNodeSchema.safeParse([5, 0]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([5, 0, 1, 2]).success).toBe(false);
});

it("rejects a pi node whose target or content slot is not a number", () => {
expect(CompactXmlNodeSchema.safeParse([5, "x", 1]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([5, 0, "x"]).success).toBe(false);
});

it("accepts an element node ([0, tag, attrPairs, children])", () => {
expect(CompactXmlNodeSchema.safeParse([0, 0, [], []]).success).toBe(true);
expect(
CompactXmlNodeSchema.safeParse([0, 0, [1, 2], [[1, 0]]]).success,
).toBe(true);
});

it("rejects an element node with the wrong tuple length", () => {
expect(CompactXmlNodeSchema.safeParse([0, 0, [], [], 9]).success).toBe(
false,
);
expect(CompactXmlNodeSchema.safeParse([0, 0, []]).success).toBe(false);
});

it("rejects an element node whose tag slot is not a number", () => {
expect(CompactXmlNodeSchema.safeParse([0, "x", [], []]).success).toBe(
false,
);
});

it("rejects an element node whose attr pairs are not a valid CompactAttrPairs", () => {
expect(
CompactXmlNodeSchema.safeParse([0, 0, "not-an-array", []]).success,
).toBe(false);
expect(CompactXmlNodeSchema.safeParse([0, 0, [0, "x"], []]).success).toBe(
false,
);
});

it("rejects an element node whose children slot is not an array", () => {
expect(
CompactXmlNodeSchema.safeParse([0, 0, [], "not-an-array"]).success,
).toBe(false);
});

it("rejects an element node whose children are not all valid compact nodes", () => {
expect(
CompactXmlNodeSchema.safeParse([0, 0, [], [["not-a-node"]]]).success,
).toBe(false);
});

it("rejects an unrecognised leading type code, even one that happens to satisfy the element-shape checks", () => {
expect(CompactXmlNodeSchema.safeParse([9]).success).toBe(false);
expect(CompactXmlNodeSchema.safeParse([9, 0, [], []]).success).toBe(false);
});
});

describe("compact adversarial cases", () => {
it("round-trips an empty Package", () => {
const pkg: Package = { parts: {} };
Expand Down Expand Up @@ -217,6 +321,56 @@ describe("compact adversarial cases", () => {
expect(fromCompact(toCompact(pkg))).toEqual(pkg);
});

it("round-trips a cdata node", () => {
const pkg: Package = {
parts: {
"word/document.xml": {
kind: "xml",
nodes: [{ type: "cdata", value: "<raw> & unescaped" }],
},
},
};
expect(fromCompact(toCompact(pkg))).toEqual(pkg);
});

it("round-trips a processing-instruction node", () => {
const pkg: Package = {
parts: {
"word/document.xml": {
kind: "xml",
nodes: [
{
type: "pi",
target: "mso-application",
content: 'progid="Word.Document"',
},
],
},
},
};
expect(fromCompact(toCompact(pkg))).toEqual(pkg);
});

it("throws with the out-of-range string index when a string-table lookup fails", () => {
const cpkg: CompactPackage = {
s: [],
p: { "word/document.xml": [[1, 5]] },
};
expect(() => fromCompact(cpkg)).toThrow(
"fromCompact: string table index 5 is out of range",
);
});

it("throws when an attribute index-pairs array has odd length", () => {
const cpkg: CompactPackage = {
s: ["name-only"],
p: { "word/document.xml": [[4, [0]]] },
};
expect(() => fromCompact(cpkg)).toThrow(
"fromCompact: attribute index pairs array has odd length",
);
});

it("round-trips a large base64 binary part as a single interned string", () => {
const largeBase64 = Buffer.from(new Uint8Array(64 * 1024).fill(7)).toString(
"base64",
Expand Down
105 changes: 90 additions & 15 deletions packages/ooxml.js/src/image/sniff.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,102 @@
import { describe, expect, it } from "vitest";
import { sniffImageFormat } from "./sniff";

// Ported verbatim from documents.js's src/image/sniff.test.ts.
describe("sniffImageFormat", () => {
it("recognises a PNG signature", () => {
expect(
sniffImageFormat(
new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0]),
),
).toBe("png");
function enc(s: string): number[] {
return Array.from(new TextEncoder().encode(s));
}

describe("sniffImageFormat: PNG", () => {
it("detects a genuine PNG signature", () => {
const bytes = new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3,
]);
expect(sniffImageFormat(bytes)).toBe("png");
});

it("does not match a truncated PNG signature (shorter than the real one)", () => {
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
expect(sniffImageFormat(bytes)).toBeUndefined();
});

it("does not match bytes that agree with the PNG signature's prefix but diverge partway through", () => {
const bytes = new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x00, 0x0a, 0x1a, 0x0a,
]);
expect(sniffImageFormat(bytes)).toBeUndefined();
});
});

describe("sniffImageFormat: JPEG", () => {
it("detects a genuine JPEG signature", () => {
expect(sniffImageFormat(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]))).toBe(
"jpeg",
);
});

it("recognises a JPEG signature", () => {
it("does not match a signature that diverges on the final byte", () => {
expect(
sniffImageFormat(new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0])),
).toBe("jpeg");
sniffImageFormat(new Uint8Array([0xff, 0xd8, 0x00])),
).toBeUndefined();
});
});

describe("sniffImageFormat: GIF", () => {
it("detects the GIF87a signature", () => {
const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 1, 2]);
expect(sniffImageFormat(bytes)).toBe("gif");
});

it("detects the GIF89a signature", () => {
const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 1, 2]);
expect(sniffImageFormat(bytes)).toBe("gif");
});

it("returns undefined for unrecognised bytes", () => {
expect(sniffImageFormat(new Uint8Array([1, 2, 3, 4]))).toBeUndefined();
it("does not match a GIF-like prefix that diverges on the version byte", () => {
const bytes = new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x30, 0x61]);
expect(sniffImageFormat(bytes)).toBeUndefined();
});
});

describe("sniffImageFormat: SVG", () => {
it("detects an SVG that opens directly with the root <svg> tag", () => {
const bytes = new Uint8Array(enc('<svg xmlns="x"><path/></svg>'));
expect(sniffImageFormat(bytes)).toBe("svg");
});

it("detects an SVG whose root tag is preceded by an XML prolog", () => {
const bytes = new Uint8Array(
enc('<?xml version="1.0"?><svg xmlns="x"></svg>'),
);
expect(sniffImageFormat(bytes)).toBe("svg");
});

it("detects an SVG whose root/prolog is preceded by leading whitespace", () => {
const bytes = new Uint8Array(enc(' \n\t<svg xmlns="x"></svg>'));
expect(sniffImageFormat(bytes)).toBe("svg");
});

it("does not detect an SVG signature in plain, unrelated text", () => {
const bytes = new Uint8Array(enc("just some text, not a document"));
expect(sniffImageFormat(bytes)).toBeUndefined();
});

it("does not detect an SVG signature in an empty byte array", () => {
expect(sniffImageFormat(new Uint8Array([]))).toBeUndefined();
});

it("only sniffs the leading 1024-byte window, never a '<svg' tag that appears only later in a longer document", () => {
// 2000 bytes of non-SVG filler, with a real '<svg' root tag starting well past the 1024-byte sniff window: the real function must never find it there.
const filler = new Uint8Array(2000).fill(0x2e); // '.'
const svgTail = enc("<svg xmlns='x'></svg>");
const bytes = new Uint8Array(2000 + svgTail.length);
bytes.set(filler, 0);
bytes.set(svgTail, 1500);
expect(sniffImageFormat(bytes)).toBeUndefined();
});
});

it("returns undefined for bytes shorter than the shortest signature", () => {
expect(sniffImageFormat(new Uint8Array([0xff, 0xd8]))).toBeUndefined();
describe("sniffImageFormat: no format recognised", () => {
it("returns undefined for bytes matching none of the known signatures", () => {
expect(sniffImageFormat(new Uint8Array([1, 2, 3, 4, 5]))).toBeUndefined();
});
});
7 changes: 3 additions & 4 deletions packages/ooxml.js/src/image/sniff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ const GIF89A_SIGNATURE: readonly number[] = [
0x47, 0x49, 0x46, 0x38, 0x39, 0x61,
];

// No separate length guard needed: bytes[i] is `undefined` for any index at or past bytes.length (an out-of-range read never throws), and undefined can never equal a real signature byte value -- so bytes shorter than the signature already fail this loop's own comparison at the first index past their own end.
function startsWith(
bytes: Uint8Array<ArrayBuffer>,
signature: readonly number[],
): boolean {
if (bytes.length < signature.length) {
return false;
}
for (let i = 0; i < signature.length; i++) {
if (bytes[i] !== signature[i]) {
return false;
Expand All @@ -31,7 +29,8 @@ function startsWith(
const SVG_SNIFF_WINDOW = 1024;

function looksLikeSvg(bytes: Uint8Array<ArrayBuffer>): boolean {
const window = bytes.subarray(0, Math.min(bytes.length, SVG_SNIFF_WINDOW));
// No Math.min against bytes.length needed: subarray's own end argument is clamped to the array's length regardless of what is asked for, so requesting SVG_SNIFF_WINDOW bytes from a shorter buffer already yields only the bytes that exist.
const window = bytes.subarray(0, SVG_SNIFF_WINDOW);
let text = "";
for (const byte of window) {
text += String.fromCharCode(byte);
Expand Down
Loading