Skip to content
Source
Sign inSign up

boundedZip.ts

78 lines2.7 KB

1import JSZip from "jszip";
2import { checkZipDirectory } from "./zipDirectory.ts";
3​
4export const ARCHIVE_MAX_BYTES = 50 * 1024 * 1024;
5export const ARCHIVE_ENTRY_MAX_BYTES = 10 * 1024 * 1024;
6export const ARCHIVE_MAX_ENTRIES = 2048;
7export const ARCHIVE_MAX_RATIO = 200;
8​
9export async function boundedZip(
10 bytes: ArrayBuffer,
11 entryLimit = ARCHIVE_MAX_BYTES,
12 limits: {
13 totalLimit?: number;
14 xmlLimit?: number;
15 ratioFloor?: number;
16 markupLimit?: number;
17 } = {},
18) {
19 if (bytes.byteLength > ARCHIVE_MAX_BYTES)
20 throw new Error("Preview limited to files under 50 MB.");
21 checkZipDirectory(bytes, ARCHIVE_MAX_ENTRIES);
22 const zip = await JSZip.loadAsync(bytes);
23 const entries = Object.values(zip.files);
24 if (entries.length > ARCHIVE_MAX_ENTRIES)
25 throw new Error("This archive contains more than 2,048 entries.");
26 const files = new Map<
27 string,
28 { bytes: Uint8Array<ArrayBuffer>; date: Date; directory: boolean }
29 >();
30 let total = 0;
31 let markup = 0;
32 for (const entry of entries) {
33 if (entry.name.length > 1024) throw new Error("An archive path is too long to preview.");
34 const chunks: Uint8Array<ArrayBuffer>[] = [];
35 let length = 0;
36 await new Promise<void>((resolve, reject) => {
37 const stream = (
38 entry as JSZip.JSZipObject & {
39 internalStream(type: "uint8array"): JSZip.JSZipStreamHelper<Uint8Array<ArrayBuffer>>;
40 }
41 ).internalStream("uint8array");
42 stream
43 .on("data", (chunk) => {
44 total += chunk.length;
45 length += chunk.length;
46 // Word relationship targets can use arbitrary extensions, so count every part.
47 if (limits.markupLimit !== undefined)
48 for (const byte of chunk) if (byte === 0x3c) markup++;
49 if (
50 markup > (limits.markupLimit ?? Infinity) ||
51 total > (limits.totalLimit ?? ARCHIVE_MAX_BYTES) ||
52 total >
53 Math.max(limits.ratioFloor ?? 0, Math.max(1, bytes.byteLength) * ARCHIVE_MAX_RATIO) ||
54 length > entryLimit ||
55 (entry.name.endsWith(".xml") && length > (limits.xmlLimit ?? 8 * 1024 * 1024))
56 ) {
57 stream.pause();
58 reject(
59 new Error(
60 "The expanded archive exceeds the preview size or compression ratio limit.",
61 ),
62 );
63 } else chunks.push(chunk);
64 })
65 .on("error", reject)
66 .on("end", resolve)
67 .resume();
68 });
69 const content = new Uint8Array(length);
70 let offset = 0;
71 for (const chunk of chunks) {
72 content.set(chunk, offset);
73 offset += chunk.length;
74 }
75 files.set(entry.name, { bytes: content, date: entry.date, directory: entry.dir });
76 }
77 return files;