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); }); });