import { strToU8, zipSync, type Zippable } from "fflate"; import type { OdfFormat } from "../../../src/office/odf"; export const MEDIA_TYPES: Readonly> = Object.freeze({ odt: "application/vnd.oasis.opendocument.text", ods: "application/vnd.oasis.opendocument.spreadsheet", odp: "application/vnd.oasis.opendocument.presentation", }); export const XMLNS = [ 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"', 'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"', 'xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"', 'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"', 'xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0"', 'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"', 'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"', 'xmlns:xlink="http://www.w3.org/1999/xlink"', 'xmlns:dc="http://purl.org/dc/elements/1.1/"', 'xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"', 'xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"', 'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"', ].join(" "); interface FixtureOptions { extra?: Record; manifest?: string | false; meta?: string; styles?: string; mediaType?: string; } export function makeOdf( format: OdfFormat, content: string, options: FixtureOptions = {}, ): ArrayBuffer { const mediaType = options.mediaType ?? MEDIA_TYPES[format]; const extra = options.extra ?? {}; const manifest = options.manifest === false ? undefined : (options.manifest ?? makeManifest(mediaType, Object.keys(extra))); const files: Zippable = { mimetype: [strToU8(mediaType), { level: 0 }], "content.xml": strToU8(content), }; if (manifest) files["META-INF/manifest.xml"] = strToU8(manifest); if (options.meta) files["meta.xml"] = strToU8(options.meta); if (options.styles) files["styles.xml"] = strToU8(options.styles); for (const [path, value] of Object.entries(extra)) files[path] = value; return ownedBuffer(zipSync(files, { level: 6 })); } export function makeManifest( mediaType: string, extraPaths: readonly string[] = [], ): string { return ` ${extraPaths .map( (path) => ``, ) .join("")} `; } export function odtContent(body: string): string { return ` ${body} `; } export function odsContent(body: string): string { return ` ${body} `; } export function odpContent(body: string): string { return ` ${body} `; } export function zipWithTraversal(content: string): ArrayBuffer { const files: Zippable = { mimetype: [strToU8(MEDIA_TYPES.odt), { level: 0 }], "content.xml": strToU8(content), "../escape.txt": strToU8("no"), }; return ownedBuffer(zipSync(files)); } export function zipWithEncodedCollision(content: string): ArrayBuffer { const files: Zippable = { mimetype: [strToU8(MEDIA_TYPES.odt), { level: 0 }], "content.xml": strToU8(content), "Pictures/a.png": new Uint8Array([1]), "Pictures/%61.png": new Uint8Array([2]), }; return ownedBuffer(zipSync(files)); } export function corruptFirstCentralCrc(input: ArrayBuffer): ArrayBuffer { const bytes = new Uint8Array(input.slice(0)); const view = new DataView(bytes.buffer); const end = findSignature(bytes, 0x06054b50); const centralOffset = view.getUint32(end + 16, true); view.setUint32( centralOffset + 16, view.getUint32(centralOffset + 16, true) ^ 1, true, ); return bytes.buffer; } function findSignature(bytes: Uint8Array, signature: number): number { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); for (let offset = bytes.byteLength - 4; offset >= 0; offset -= 1) { if (view.getUint32(offset, true) === signature) return offset; } throw new Error("Signature not found in test ZIP"); } function mediaFor(path: string): string { if (path.endsWith(".png")) return "image/png"; if (path.endsWith(".svg")) return "image/svg+xml"; return "application/octet-stream"; } function ownedBuffer(bytes: Uint8Array): ArrayBuffer { const copy = new Uint8Array(bytes.byteLength); copy.set(bytes); return copy.buffer; }