Files
office-tools/tests/office/odf/fixtures.ts
T

150 lines
5.4 KiB
TypeScript

import { strToU8, zipSync, type Zippable } from "fflate";
import type { OdfFormat } from "../../../src/office/odf";
export const MEDIA_TYPES: Readonly<Record<OdfFormat, string>> = 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<string, Uint8Array>;
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 `<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
<manifest:file-entry manifest:full-path="/" manifest:media-type="${mediaType}"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
${extraPaths
.map(
(path) =>
`<manifest:file-entry manifest:full-path="${path}" manifest:media-type="${mediaFor(path)}"/>`,
)
.join("")}
</manifest:manifest>`;
}
export function odtContent(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<office:document-content ${XMLNS} office:version="1.3">
<office:automatic-styles>
<style:style style:name="InlineBold" style:family="text">
<style:text-properties fo:font-weight="bold"/>
</style:style>
</office:automatic-styles>
<office:body><office:text>${body}</office:text></office:body>
</office:document-content>`;
}
export function odsContent(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<office:document-content ${XMLNS} office:version="1.3">
<office:body><office:spreadsheet>${body}</office:spreadsheet></office:body>
</office:document-content>`;
}
export function odpContent(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<office:document-content ${XMLNS} office:version="1.3">
<office:body><office:presentation>${body}</office:presentation></office:body>
</office:document-content>`;
}
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;
}