import { describe, expect, it } from "vitest";
import {
OdfParseError,
parseOdt,
parseOpenDocument,
} from "../../../src/office/odf";
import {
corruptFirstCentralCrc,
makeManifest,
makeOdf,
MEDIA_TYPES,
odtContent,
odsContent,
XMLNS,
zipWithEncodedCollision,
zipWithTraversal,
} from "./fixtures";
function expectOdfError(
action: () => unknown,
code: OdfParseError["code"],
): void {
try {
action();
throw new Error("Expected ODF parser to reject the fixture");
} catch (error) {
expect(error).toBeInstanceOf(OdfParseError);
expect((error as OdfParseError).code).toBe(code);
}
}
describe("OpenDocument package and XML security boundaries", () => {
it("rejects non-ZIP input and unsafe traversal paths", () => {
expectOdfError(
() => parseOpenDocument(new Uint8Array([1, 2, 3]).buffer),
"invalid-zip",
);
expectOdfError(
() =>
parseOpenDocument(
zipWithTraversal(odtContent("Safe")),
),
"invalid-path",
);
expectOdfError(
() =>
parseOpenDocument(
zipWithEncodedCollision(odtContent("Safe")),
),
"invalid-path",
);
});
it("rejects CRC corruption", () => {
const fixture = makeOdf("odt", odtContent("Integrity"));
expectOdfError(
() => parseOpenDocument(corruptFirstCentralCrc(fixture)),
"invalid-zip",
);
});
it("rejects expected-format, filename, manifest, and body type mismatches", () => {
const ods = makeOdf("ods", odsContent(''));
expectOdfError(
() => parseOpenDocument(ods, { expectedFormat: "odt" }),
"type-mismatch",
);
expectOdfError(
() => parseOpenDocument(ods, { fileName: "wrong.odt" }),
"type-mismatch",
);
expectOdfError(
() =>
parseOpenDocument(
makeOdf("odt", odtContent("x"), {
manifest: makeManifest(MEDIA_TYPES.ods),
}),
),
"type-mismatch",
);
expectOdfError(
() =>
parseOdt(makeOdf("odt", odsContent(''))),
"type-mismatch",
);
});
it("rejects encrypted package declarations", () => {
const manifest = `
`;
expectOdfError(
() =>
parseOpenDocument(
makeOdf("odt", odtContent("x"), { manifest }),
),
"encrypted",
);
});
it("accepts manifest directories represented implicitly by child entries", () => {
const manifest = `
`;
const document = parseOpenDocument(
makeOdf("odt", odtContent("Safe"), {
manifest,
extra: {
"Object 1/content.xml": new TextEncoder().encode("unused"),
},
}),
);
expect(document.format).toBe("odt");
});
it("rejects malformed XML, DTDs/entities, and excessive XML depth", () => {
expectOdfError(
() => parseOpenDocument(makeOdf("odt", "")),
"invalid-xml",
);
const entity = `]>
&local;`;
expectOdfError(
() => parseOpenDocument(makeOdf("odt", entity)),
"invalid-xml",
);
const nested =
``.repeat(8) +
`x` +
``.repeat(8);
expectOdfError(
() =>
parseOpenDocument(makeOdf("odt", odtContent(nested)), {
limits: { maxXmlDepth: 6 },
}),
"limit-exceeded",
);
const compact = `x`;
expectOdfError(
() =>
parseOpenDocument(makeOdf("odt", compact, { manifest: false }), {
limits: { maxXmlNodes: 4 },
}),
"limit-exceeded",
);
});
it("rejects dangerous external images and active links", () => {
expectOdfError(
() =>
parseOpenDocument(
makeOdf(
"odt",
odtContent(
'',
),
),
),
"external-resource",
);
expectOdfError(
() =>
parseOpenDocument(
makeOdf(
"odt",
odtContent(
'bad',
),
),
),
"external-resource",
);
});
it("keeps embedded objects inert and reports their omission", () => {
const document = parseOpenDocument(
makeOdf("odt", odtContent('')),
);
if (document.format !== "odt") throw new Error("Expected an ODT model");
expect(document.blocks).toEqual([]);
expect(document.warnings).toContain(
"1 embedded object was kept inert and omitted",
);
});
it("enforces entry, total, text, asset, repeat, and expanded-cell limits", () => {
const basic = makeOdf("odt", odtContent("123456789"));
expectOdfError(
() => parseOpenDocument(basic, { limits: { maxEntryBytes: 32 } }),
"limit-exceeded",
);
expectOdfError(
() => parseOpenDocument(basic, { limits: { maxTotalBytes: 50 } }),
"limit-exceeded",
);
expectOdfError(
() => parseOpenDocument(basic, { limits: { maxEntries: 2 } }),
"limit-exceeded",
);
expectOdfError(
() => parseOpenDocument(basic, { limits: { maxTextChars: 5 } }),
"limit-exceeded",
);
const repeated = makeOdf(
"ods",
odsContent(
'x',
),
);
expectOdfError(
() => parseOpenDocument(repeated, { limits: { maxRepeat: 3 } }),
"limit-exceeded",
);
expectOdfError(
() => parseOpenDocument(repeated, { limits: { maxCells: 15 } }),
"limit-exceeded",
);
const image = new Uint8Array([137, 80, 78, 71, 1, 2, 3, 4]);
const imageDocument = makeOdf(
"odt",
odtContent(
'',
),
{ extra: { "Pictures/a.png": image } },
);
expectOdfError(
() => parseOpenDocument(imageDocument, { limits: { maxAssetBytes: 4 } }),
"limit-exceeded",
);
});
it("validates option limits before reading untrusted input", () => {
expect(() =>
parseOpenDocument(new ArrayBuffer(0), { limits: { maxEntries: 0 } }),
).toThrow(TypeError);
});
});