Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
7b631f7
test(odf.js): pin ods writer cell-run, cell-value, and canonicaliser …
Mearman Sep 12, 2026
cfce24e
test(odf.js): fix invalid fixture values in canonical* unit tests
Mearman Sep 12, 2026
d9630d0
test(odf.js): pin regular-polygon/rounded-rect geometry and per-vecto…
Mearman Sep 12, 2026
513184d
test(odf.js): pin the shared canonicalise.ts helpers directly, and dr…
Mearman Sep 13, 2026
1e9e95b
test(odf.js): cover parseOdfAngleDeg, isLengthUnit and expandExponent…
Mearman Sep 13, 2026
2aaa313
test(odf.js): add the first direct unit coverage for typed/shared/lis…
Mearman Sep 13, 2026
5c02f7e
test(odf.js): add the first direct unit coverage for typed/shared/exp…
Mearman Sep 13, 2026
54c5af6
chore(odf.js): raise the mutation break threshold to the re-measured …
Mearman Sep 13, 2026
541f9e8
test(odf.js): give the BOM check its own testable function in parsePa…
Mearman Sep 13, 2026
3d57dc4
test(odf.js): add the first direct unit coverage for typed/shared/for…
Mearman Sep 13, 2026
39e3f66
test(odf.js): kill every mutant in test-support/zip.ts's LE integer r…
Mearman Sep 13, 2026
2ace65d
test(odf.js): kill every mutant in test-support/document-tree.ts's he…
Mearman Sep 13, 2026
41790b9
test(odf.js): add the first direct unit coverage for xml/build.ts
Mearman Sep 13, 2026
936b07e
test(odf.js): kill xml/build.ts's remaining survivors
Mearman Sep 13, 2026
b548cd1
test(odf.js): add the first direct unit coverage for image/sniff.ts
Mearman Sep 13, 2026
cb079b3
test(odf.js): add the first direct unit coverage for util/base64.ts
Mearman Sep 13, 2026
bcf5cab
test(odf.js): cover columnLettersToIndex and TableCursor's own error …
Mearman Sep 13, 2026
49f71ac
test(odf.js): pin columnLettersToIndex's uppercase-only guard against…
Mearman Sep 13, 2026
f7571db
test(odf.js): kill typed/shared/text.ts's remaining survivors
Mearman Sep 13, 2026
4a3965c
test(odf.js): add the first direct unit coverage for package-io/scaff…
Mearman Sep 13, 2026
a929e2d
test(odf.js): add the first direct unit coverage for typed/shared/bor…
Mearman Sep 13, 2026
225f279
test(odf.js): kill typed/shared/list.ts's remaining survivors
Mearman Sep 13, 2026
2f89fac
test(odf.js): kill typed/shared/transform.ts's remaining survivors
Mearman Sep 13, 2026
db211f3
test(odf.js): kill the last two typed/shared/transform.ts survivors
Mearman Sep 13, 2026
132914f
test(odf.js): kill typed/shared/metadata.ts's remaining survivors
Mearman Sep 13, 2026
0c097bb
test(odf.js): kill ooo1/ns.ts's remaining survivors
Mearman Sep 13, 2026
b35e9f2
test(odf.js): add the first direct unit coverage for typed/draw/embed…
Mearman Sep 13, 2026
33e470f
test(odf.js): kill typed/odp/read.ts's remaining survivors
Mearman Sep 13, 2026
661c565
fix(odf.js): supply the required frame field in embedded-write's own …
Mearman Sep 13, 2026
505cb3f
test(odf.js): kill typed/shared/cascade.ts's remaining survivors
Mearman Sep 13, 2026
afd7019
test(odf.js): kill typed/shared/masterpage.ts's remaining survivors
Mearman Sep 13, 2026
3e602b7
test(odf.js): kill typed/shared/units.ts's remaining survivors
Mearman Sep 13, 2026
b9dbd60
test(odf.js): add the first direct unit coverage for typed/shared/ima…
Mearman Sep 13, 2026
260db15
test(odf.js): kill typed/odb/subdocument.ts's remaining survivor
Mearman Sep 13, 2026
d4a0644
test(odf.js): kill typed/odg/read.ts's remaining survivors
Mearman Sep 13, 2026
4a51ef3
test(odf.js): kill typed/odm/write.ts's remaining survivors
Mearman Sep 13, 2026
ce76942
test(odf.js): kill typed/shared/expression.ts's remaining survivor
Mearman Sep 13, 2026
2f42a98
test(odf.js): add the first direct unit coverage for package-io/write.ts
Mearman Sep 13, 2026
fb6c4b3
test(odf.js): kill styles/serialize.ts's remaining survivor
Mearman Sep 13, 2026
f5e7e92
test(odf.js): kill typed/formula/read.ts's remaining survivor
Mearman Sep 13, 2026
e6d7f90
test(odf.js): kill typed/odm/read.ts's remaining survivor
Mearman Sep 13, 2026
27d9322
test(odf.js): kill util/base64.ts's remaining bytesToBase64 survivors
Mearman Sep 13, 2026
49998fd
test(odf.js): kill package-io/write.ts's remaining survivor
Mearman Sep 13, 2026
4b00980
test(odf.js): pin that only the mimetype entry is stored uncompressed
Mearman Sep 13, 2026
b0abab6
test(odf.js): kill typed/odm/read.ts's remaining tag-check survivor
Mearman Sep 13, 2026
f8f60f6
test(odf.js): kill styles/span.ts's redundant-guard and undercovered …
Mearman Sep 13, 2026
068dc0e
test(odf.js): kill styles/registry.ts's redundant-guard and undercove…
Mearman Sep 13, 2026
95a1759
test(odf.js): kill typed/formula/write.ts's remaining survivors
Mearman Sep 13, 2026
068bea4
test(odf.js): kill typed/odg/write.ts's remaining survivors
Mearman Sep 13, 2026
da3ff2b
test(odf.js): kill typed/draw/embedded.ts's remaining survivors
Mearman Sep 13, 2026
7607edc
test(odf.js): reach 100% mutation coverage on typed/odb/report.ts
Mearman Sep 14, 2026
58ed0d8
test(odf.js): reach 100% mutation coverage on typed/ods/conditional-f…
Mearman Sep 14, 2026
2e77cb7
test(odf.js): kill readCondition's remaining rank-branch survivors
Mearman Sep 14, 2026
307faf6
test(odf.js): reach 100% mutation coverage on typed/ods/data-validati…
Mearman Sep 14, 2026
c4d9230
refactor(odf.js): simplify insertOdfConstructMarkers and export isEmb…
Mearman Sep 14, 2026
6c7b295
test(odf.js): add direct unit coverage for typed/shared/constructs.ts
Mearman Sep 14, 2026
d81e61f
test(odf.js): fix ContentControlDescriptor fixture's controlType value
Mearman Sep 14, 2026
39ffd41
test(odf.js): kill typed/shared/constructs.ts's remaining survivors
Mearman Sep 14, 2026
58dc015
test(odf.js): cover typed/shared/constructs.ts's remaining no-coverag…
Mearman Sep 14, 2026
d1c0f4b
docs(odf.js): note shared/constructs.ts's zero survivors in the break…
Mearman Sep 14, 2026
e2a5e75
test(odf.js): cover formatServerDatabaseUrl's missing db:type branch
Mearman Sep 14, 2026
9fc710d
refactor(odf.js): remove two unobservable branches from draw/shapes.ts
Mearman Sep 14, 2026
a35b0f3
test(odf.js): kill typed/draw/shapes.ts's remaining survivors
Mearman Sep 14, 2026
6a90468
test(odf.js): cover readOdbInventory's remaining connection/component…
Mearman Sep 14, 2026
3f48de4
refactor(odf.js): simplify transform.ts's colon check and drop a redu…
Mearman Sep 14, 2026
744dd4e
test(odf.js): cover transform.ts's carriesNoLength, href, and reverse…
Mearman Sep 14, 2026
0dfc0e0
refactor(odf.js): remove reverse-direction branches the caller's own …
Mearman Sep 14, 2026
de05fd2
test(odf.js): cover transform.ts's classAttributeName and remaining r…
Mearman Sep 14, 2026
da5f83d
docs(odf.js): note ooo1/transform.ts's zero survivors in the breakThr…
Mearman Sep 14, 2026
cb6d4af
fix(odf.js): clamp skipExpression's index advances to text.length
Mearman Sep 14, 2026
b49043d
test(odf.js): cover sheetCellStyle's decoration-present and no-decora…
Mearman Sep 14, 2026
e9e21e9
test(odf.js): cover data validation message and interning branches
Mearman Sep 14, 2026
f27d97f
test(odf.js): cover unsupportedConditionalFormatReason's refusal bran…
Mearman Sep 14, 2026
6de776c
refactor(odf.js): compute the used range's max row/column via Math.max
Mearman Sep 14, 2026
0d85607
test(odf.js): cover dataBar's unset showValue and an empty conditiona…
Mearman Sep 14, 2026
777dec8
test(odf.js): recompute readOdbInventory fresh per test and cover its…
Mearman Sep 14, 2026
46e2696
test(odf.js): cover typed/shared/table.ts's read/write decoration and…
Mearman Sep 14, 2026
ac31917
test(odf.js): add direct unit coverage for canonicalRun
Mearman Sep 14, 2026
0ecd8a2
test(odf.js): add direct XML-structure coverage for odb/write.ts
Mearman Sep 14, 2026
9bdcc80
test(odf.js): add the first direct unit coverage for typed/draw/write…
Mearman Sep 14, 2026
2e045c6
test(odf.js): cover paragraph.ts's note/annotation sequential minting
Mearman Sep 14, 2026
0f15ae9
test(odf.js): add the first direct unit coverage for ooo1/properties.ts
Mearman Sep 14, 2026
5ac43b2
test(odf.js): add the first direct unit coverage for model/node.ts
Mearman Sep 14, 2026
85f3cce
test(odf.js): add the first direct unit coverage for xml/parse.ts
Mearman Sep 14, 2026
9f3b731
test(odf.js): add the first direct unit coverage for typed/draw/write…
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
4 changes: 2 additions & 2 deletions packages/odf.js/eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default tseslint.config(
preferReadonlyParams: "off",
}),
{
// fast-xml-parser@5 deprecates the whole XMLBuilder class, not one of its options, and ships no replacement of its own -- it points at a separate `fast-xml-builder` package that is not a declared dependency here. Swapping it is a real dependency decision with round-trip fidelity to re-verify (this builder is what keeps XML byte-faithful), so it is tracked rather than guessed at inside a tooling change. Scoped to the one module that constructs the builder.
files: ["src/xml/build.ts"],
// fast-xml-parser@5 deprecates the whole XMLBuilder class, not one of its options, and ships no replacement of its own it points at a separate `fast-xml-builder` package that is not a declared dependency here. Swapping it is a real dependency decision with round-trip fidelity to re-verify (this builder is what keeps XML byte-faithful), so it is tracked rather than guessed at inside a tooling change. Scoped to the one module that constructs the builder, plus its own test file, which necessarily references the identical deprecated class to reach BUILDER's shared prototype.
files: ["src/xml/build.ts", "src/xml/build.test.ts"],
rules: { "@typescript-eslint/no-deprecated": "off" },
},
);
84 changes: 84 additions & 0 deletions packages/odf.js/src/image/sniff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import { sniffImageFormat } from "./sniff";

function bytesOf(values: number[]): Uint8Array<ArrayBuffer> {
return new Uint8Array(values);
}

function asciiBytes(text: string): number[] {
return Array.from(text, (c) => c.charCodeAt(0));
}

describe("sniffImageFormat", () => {
it("detects a PNG from its 8-byte magic signature", () => {
expect(
sniffImageFormat(
bytesOf([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff]),
),
).toBe("png");
});

it("detects a JPEG from its 3-byte magic signature", () => {
expect(sniffImageFormat(bytesOf([0xff, 0xd8, 0xff, 0xe0]))).toBe("jpeg");
});

it("detects a GIF87a header", () => {
expect(sniffImageFormat(bytesOf(asciiBytes("GIF87a").concat([0x00])))).toBe(
"gif",
);
});

it("detects a GIF89a header", () => {
expect(sniffImageFormat(bytesOf(asciiBytes("GIF89a").concat([0x00])))).toBe(
"gif",
);
});

it("returns undefined for bytes shorter than every signature it checks", () => {
expect(sniffImageFormat(bytesOf([0x89, 0x50]))).toBeUndefined();
});

it("returns undefined for an empty byte array", () => {
expect(sniffImageFormat(bytesOf([]))).toBeUndefined();
});

it("detects SVG from an XML prolog, with no '<svg' tag present in the sniffed window", () => {
const bytes = bytesOf(asciiBytes('<?xml version="1.0"?><notsvg/>'));
expect(sniffImageFormat(bytes)).toBe("svg");
});

it("detects SVG from a bare '<svg' root tag, with no XML prolog", () => {
const bytes = bytesOf(
asciiBytes('<svg xmlns="http://www.w3.org/2000/svg"></svg>'),
);
expect(sniffImageFormat(bytes)).toBe("svg");
});

it("skips leading whitespace before the '<?xml' prolog", () => {
const bytes = bytesOf(asciiBytes(' <?xml version="1.0"?><svg/>'));
expect(sniffImageFormat(bytes)).toBe("svg");
});

it("returns undefined for text that starts with neither '<?xml' nor '<svg'", () => {
const bytes = bytesOf(asciiBytes("<html><body/></html>"));
expect(sniffImageFormat(bytes)).toBeUndefined();
});

it("never finds a root element hidden behind more leading whitespace than the sniff window covers", () => {
// The sniff window is capped at a fixed size specifically so a caller can't be made to scan an unboundedly large file — a real SVG's root element always appears well within it (see sniff.ts's own comment), so padding past the window with plain spaces before the real tag is exactly the case the cap is meant to give up on, not a bug to work around.
const paddingLength = 2000;
const bytes = bytesOf([
...Array<number>(paddingLength).fill(0x20),
...asciiBytes("<?xml?>"),
]);
expect(bytes.length).toBeGreaterThan(1024);
expect(sniffImageFormat(bytes)).toBeUndefined();
});

it("does not match bytes that merely end with, rather than start with, an SVG marker", () => {
const bytes = bytesOf(
asciiBytes("<html>embeds a literal <?xml tag</html>"),
);
expect(sniffImageFormat(bytes)).toBeUndefined();
});
});
4 changes: 1 addition & 3 deletions packages/odf.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 "bytes too short" guard: when bytes.length < signature.length, some index i in the loop below reads past the end of bytes, and an out-of-bounds array read is `undefined` in JS -- which is never strictly equal to signature[i] (always a real 0-255 byte value), so the loop's own mismatch check already returns false for every too-short input. A dedicated length guard would only ever produce a result the loop already produces on its own.
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 Down
156 changes: 156 additions & 0 deletions packages/odf.js/src/model/node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { describe, expect, it } from "vitest";
import { isXmlNode } from "./node";

// isXmlNode is a hand-written recursive structural guard (used via z.custom, since a genuinely recursive Zod schema collapses to `unknown` under z.lazy in this pinned version) with no direct unit tests at all -- every place it runs is exercised only as a side effect of parsing a real XML document, which never constructs the malformed shapes below.

describe("isXmlNode: non-object/malformed input", () => {
it("rejects null, a primitive, and an array outright", () => {
expect(isXmlNode(null)).toBe(false);
expect(isXmlNode("a string")).toBe(false);
expect(isXmlNode(42)).toBe(false);
expect(isXmlNode([])).toBe(false);
});

it("rejects a plain object with no type field", () => {
expect(isXmlNode({})).toBe(false);
});

it("rejects an unrecognised type value", () => {
expect(isXmlNode({ type: "bogus" })).toBe(false);
});
});

describe("isXmlNode: text/cdata/comment", () => {
it.each(["text", "cdata", "comment"] as const)(
"accepts type %s with a string value",
(type) => {
expect(isXmlNode({ type, value: "hello" })).toBe(true);
},
);

it.each(["text", "cdata", "comment"] as const)(
"rejects type %s when value is not a string",
(type) => {
expect(isXmlNode({ type, value: 5 })).toBe(false);
expect(isXmlNode({ type })).toBe(false);
},
);
});

describe("isXmlNode: declaration", () => {
it("accepts a declaration with an empty attributes array", () => {
expect(isXmlNode({ type: "declaration", attributes: [] })).toBe(true);
});

it("accepts a declaration whose every attribute is a valid {name, value} pair", () => {
expect(
isXmlNode({
type: "declaration",
attributes: [{ name: "version", value: "1.0" }],
}),
).toBe(true);
});

it("rejects a declaration whose attributes is not an array", () => {
expect(isXmlNode({ type: "declaration", attributes: "nope" })).toBe(false);
});

it("rejects a declaration with one malformed attribute among otherwise-valid ones", () => {
expect(
isXmlNode({
type: "declaration",
attributes: [
{ name: "version", value: "1.0" },
{ name: "encoding" }, // missing value
],
}),
).toBe(false);
});
});

describe("isXmlNode: pi", () => {
it("accepts a pi with string target and content", () => {
expect(
isXmlNode({ type: "pi", target: "xml-stylesheet", content: "foo" }),
).toBe(true);
});

it("rejects a pi missing either target or content", () => {
expect(isXmlNode({ type: "pi", target: "x" })).toBe(false);
expect(isXmlNode({ type: "pi", content: "x" })).toBe(false);
});

it("rejects a pi whose target or content is not a string", () => {
expect(isXmlNode({ type: "pi", target: 1, content: "x" })).toBe(false);
expect(isXmlNode({ type: "pi", target: "x", content: 1 })).toBe(false);
});
});

describe("isXmlNode: element", () => {
function validElement(overrides: Record<string, unknown> = {}) {
return {
type: "element",
tag: "text:p",
attributes: [],
children: [],
...overrides,
};
}

it("accepts a leaf element with no attributes or children", () => {
expect(isXmlNode(validElement())).toBe(true);
});

it("rejects an element whose tag is not a string", () => {
expect(isXmlNode(validElement({ tag: 5 }))).toBe(false);
});

it("rejects an element whose attributes is not an array", () => {
expect(isXmlNode(validElement({ attributes: "nope" }))).toBe(false);
});

it("rejects an element with one malformed attribute", () => {
expect(isXmlNode(validElement({ attributes: [{ name: "x" }] }))).toBe(
false,
);
});

it("rejects an element whose children is not an array", () => {
expect(isXmlNode(validElement({ children: "nope" }))).toBe(false);
});

it("accepts an element whose children are all valid nodes, recursively", () => {
const child = { type: "text", value: "hi" };
expect(isXmlNode(validElement({ children: [child] }))).toBe(true);
});

it("rejects an element with one malformed child among otherwise-valid ones", () => {
const goodChild = { type: "text", value: "hi" };
const badChild = { type: "text", value: 5 };
expect(isXmlNode(validElement({ children: [goodChild, badChild] }))).toBe(
false,
);
});

it("rejects an element nested two levels deep whose innermost grandchild is malformed", () => {
const malformedGrandchild = { type: "comment", value: 5 };
const child = {
type: "element",
tag: "text:span",
attributes: [],
children: [malformedGrandchild],
};
expect(isXmlNode(validElement({ children: [child] }))).toBe(false);
});

it("accepts an element nested two levels deep whose every descendant is well-formed", () => {
const grandchild = { type: "text", value: "deep" };
const child = {
type: "element",
tag: "text:span",
attributes: [],
children: [grandchild],
};
expect(isXmlNode(validElement({ children: [child] }))).toBe(true);
});
});
15 changes: 15 additions & 0 deletions packages/odf.js/src/ooo1/ns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,21 @@ describe("isOoo1Package", () => {
expect(isOoo1Package(pkg)).toBe(true);
});

it("recognises a bare, default (non-prefixed) xmlns declaration too", () => {
const pkg = packageOf({
"content.xml": `<office:document-content xmlns="http://openoffice.org/2000/office"><office:body/></office:document-content>`,
});
expect(isOoo1Package(pkg)).toBe(true);
});

it("does not treat an ordinary attribute whose value happens to equal an OOo1 URI as a namespace declaration", () => {
// office:version here is neither "xmlns" nor "xmlns:"-prefixed -- only its VALUE coincides with a real OOo1 namespace URI, which must not be enough on its own.
const pkg = packageOf({
"content.xml": `<office:document-content office:version="http://openoffice.org/2000/office"><office:body/></office:document-content>`,
});
expect(isOoo1Package(pkg)).toBe(false);
});

it("rejects a real ODF package", () => {
const pkg = packageOf({
"content.xml": `<office:document-content xmlns:office="${ODF_NAMESPACES.office}" xmlns:text="${ODF_NAMESPACES.text}" office:version="1.3"><office:body><office:text><text:p>hi</text:p></office:text></office:body></office:document-content>`,
Expand Down
Loading