


























@@ -62,11 +62,55 @@ export const DEFAULT_MAX_EXTRACTED_BYTES = 512 * 1024 * 1024;
6262/** @internal */
6363export const DEFAULT_MAX_ENTRY_BYTES = 256 * 1024 * 1024;
646465-const ERROR_ARCHIVE_SIZE_EXCEEDS_LIMIT = "archive size exceeds limit";
66-const ERROR_ARCHIVE_ENTRY_COUNT_EXCEEDS_LIMIT = "archive entry count exceeds limit";
67-const ERROR_ARCHIVE_ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT =
68-"archive entry extracted size exceeds limit";
69-const ERROR_ARCHIVE_EXTRACTED_SIZE_EXCEEDS_LIMIT = "archive extracted size exceeds limit";
65+export const ARCHIVE_LIMIT_ERROR_CODE = {
66+ARCHIVE_SIZE_EXCEEDS_LIMIT: "archive-size-exceeds-limit",
67+ENTRY_COUNT_EXCEEDS_LIMIT: "archive-entry-count-exceeds-limit",
68+ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-entry-extracted-size-exceeds-limit",
69+EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-extracted-size-exceeds-limit",
70+} as const;
71+72+export type ArchiveLimitErrorCode =
73+(typeof ARCHIVE_LIMIT_ERROR_CODE)[keyof typeof ARCHIVE_LIMIT_ERROR_CODE];
74+75+const ARCHIVE_LIMIT_ERROR_MESSAGE = {
76+[ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT]: "archive size exceeds limit",
77+[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT]: "archive entry count exceeds limit",
78+[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT]:
79+"archive entry extracted size exceeds limit",
80+[ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive extracted size exceeds limit",
81+} as const satisfies Record<ArchiveLimitErrorCode, string>;
82+83+export class ArchiveLimitError extends Error {
84+readonly code: ArchiveLimitErrorCode;
85+86+constructor(code: ArchiveLimitErrorCode) {
87+super(ARCHIVE_LIMIT_ERROR_MESSAGE[code]);
88+this.name = "ArchiveLimitError";
89+this.code = code;
90+}
91+}
92+93+const ZIP_EOCD_SIGNATURE = 0x06054b50;
94+const ZIP64_EOCD_SIGNATURE = 0x06064b50;
95+const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
96+const ZIP_EOCD_MIN_BYTES = 22;
97+const ZIP_EOCD_MAX_COMMENT_BYTES = 0xffff;
98+const ZIP64_ENTRY_COUNT_SENTINEL = 0xffff;
99+const ZIP64_UINT32_SENTINEL = 0xffffffff;
100+const ZIP_CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
101+const ZIP_CENTRAL_FILE_HEADER_MIN_BYTES = 46;
102+const ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET = 28;
103+const ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET = 30;
104+const ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET = 32;
105+const ZIP_EOCD_TOTAL_ENTRIES_OFFSET = 10;
106+const ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 12;
107+const ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 16;
108+const ZIP_EOCD_COMMENT_LENGTH_OFFSET = 20;
109+const ZIP64_EOCD_LOCATOR_BYTES = 20;
110+const ZIP64_EOCD_OFFSET_OFFSET = 8;
111+const ZIP64_EOCD_TOTAL_ENTRIES_OFFSET = 32;
112+const ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 40;
113+const ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 48;
70114const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
71115const OPEN_WRITE_CREATE_FLAGS =
72116fsConstants.O_WRONLY |
@@ -188,8 +232,185 @@ function assertArchiveEntryCountWithinLimit(
188232limits: ResolvedArchiveExtractLimits,
189233) {
190234if (entryCount > limits.maxEntries) {
191-throw new Error(ERROR_ARCHIVE_ENTRY_COUNT_EXCEEDS_LIMIT);
235+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT);
236+}
237+}
238+239+function asBufferView(buffer: Buffer | Uint8Array): Buffer {
240+if (Buffer.isBuffer(buffer)) {
241+return buffer;
242+}
243+return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
244+}
245+246+function readSafeUInt64LE(buffer: Buffer, offset: number): number {
247+const value = buffer.readBigUInt64LE(offset);
248+if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
249+return Number.MAX_SAFE_INTEGER;
250+}
251+return Number(value);
252+}
253+254+function findZipEndOfCentralDirectory(buffer: Buffer): number {
255+if (buffer.byteLength < ZIP_EOCD_MIN_BYTES) {
256+return -1;
257+}
258+const minOffset = Math.max(
259+0,
260+buffer.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_EOCD_MAX_COMMENT_BYTES,
261+);
262+for (let offset = buffer.byteLength - ZIP_EOCD_MIN_BYTES; offset >= minOffset; offset -= 1) {
263+if (buffer.readUInt32LE(offset) !== ZIP_EOCD_SIGNATURE) {
264+continue;
265+}
266+const commentLength = buffer.readUInt16LE(offset + ZIP_EOCD_COMMENT_LENGTH_OFFSET);
267+if (offset + ZIP_EOCD_MIN_BYTES + commentLength === buffer.byteLength) {
268+return offset;
269+}
270+}
271+return -1;
272+}
273+274+type ZipCentralDirectoryInfo = {
275+declaredEntryCount: number;
276+centralDirectoryOffset: number;
277+centralDirectorySize: number;
278+endOfCentralDirectoryOffset: number;
279+};
280+281+function readZip64CentralDirectoryInfo(
282+buffer: Buffer,
283+eocdOffset: number,
284+): ZipCentralDirectoryInfo | null {
285+const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_BYTES;
286+if (locatorOffset < 0 || buffer.readUInt32LE(locatorOffset) !== ZIP64_EOCD_LOCATOR_SIGNATURE) {
287+return null;
288+}
289+const zip64EocdOffset = readSafeUInt64LE(buffer, locatorOffset + ZIP64_EOCD_OFFSET_OFFSET);
290+if (
291+zip64EocdOffset < 0 ||
292+zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET + 8 > buffer.byteLength ||
293+buffer.readUInt32LE(zip64EocdOffset) !== ZIP64_EOCD_SIGNATURE
294+) {
295+return null;
296+}
297+return {
298+declaredEntryCount: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_TOTAL_ENTRIES_OFFSET),
299+centralDirectorySize: readSafeUInt64LE(
300+buffer,
301+zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET,
302+),
303+centralDirectoryOffset: readSafeUInt64LE(
304+buffer,
305+zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET,
306+),
307+endOfCentralDirectoryOffset: eocdOffset,
308+};
309+}
310+311+function readZipCentralDirectoryInfo(buffer: Buffer): ZipCentralDirectoryInfo | null {
312+const eocdOffset = findZipEndOfCentralDirectory(buffer);
313+if (eocdOffset < 0) {
314+return null;
315+}
316+const declaredEntryCount = buffer.readUInt16LE(eocdOffset + ZIP_EOCD_TOTAL_ENTRIES_OFFSET);
317+const centralDirectorySize = buffer.readUInt32LE(
318+eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET,
319+);
320+const centralDirectoryOffset = buffer.readUInt32LE(
321+eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET,
322+);
323+const usesZip64 =
324+declaredEntryCount === ZIP64_ENTRY_COUNT_SENTINEL ||
325+centralDirectorySize === ZIP64_UINT32_SENTINEL ||
326+centralDirectoryOffset === ZIP64_UINT32_SENTINEL;
327+if (usesZip64) {
328+return (
329+readZip64CentralDirectoryInfo(buffer, eocdOffset) ?? {
330+ declaredEntryCount,
331+ centralDirectoryOffset,
332+ centralDirectorySize,
333+endOfCentralDirectoryOffset: eocdOffset,
334+}
335+);
336+}
337+return {
338+ declaredEntryCount,
339+ centralDirectoryOffset,
340+ centralDirectorySize,
341+endOfCentralDirectoryOffset: eocdOffset,
342+};
343+}
344+345+function countZipCentralDirectoryHeaders(
346+buffer: Buffer,
347+info: ZipCentralDirectoryInfo,
348+): number | null {
349+const start = info.centralDirectoryOffset;
350+const declaredEnd = start + info.centralDirectorySize;
351+const scanEnd = info.endOfCentralDirectoryOffset;
352+if (
353+!Number.isSafeInteger(start) ||
354+!Number.isSafeInteger(declaredEnd) ||
355+!Number.isSafeInteger(scanEnd) ||
356+start < 0 ||
357+declaredEnd < start ||
358+scanEnd < start ||
359+scanEnd > buffer.byteLength
360+) {
361+return null;
362+}
363+let offset = start;
364+let count = 0;
365+while (offset < scanEnd) {
366+if (scanEnd - offset < ZIP_CENTRAL_FILE_HEADER_MIN_BYTES) {
367+break;
368+}
369+if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_FILE_HEADER_SIGNATURE) {
370+break;
371+}
372+const nameLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET);
373+const extraLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET);
374+const commentLength = buffer.readUInt16LE(
375+offset + ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET,
376+);
377+const nextOffset =
378+offset + ZIP_CENTRAL_FILE_HEADER_MIN_BYTES + nameLength + extraLength + commentLength;
379+if (nextOffset <= offset || nextOffset > scanEnd) {
380+return null;
381+}
382+count += 1;
383+offset = nextOffset;
384+}
385+return count > 0 || info.declaredEntryCount === 0 ? count : null;
386+}
387+388+/** @internal */
389+export function readZipCentralDirectoryEntryCount(buffer: Buffer | Uint8Array): number | null {
390+const view = asBufferView(buffer);
391+const info = readZipCentralDirectoryInfo(view);
392+if (!info) {
393+return null;
394+}
395+const countedEntryCount = countZipCentralDirectoryHeaders(view, info);
396+return countedEntryCount === null
397+ ? info.declaredEntryCount
398+ : Math.max(info.declaredEntryCount, countedEntryCount);
399+}
400+401+export async function loadZipArchiveWithPreflight(
402+buffer: Buffer | Uint8Array,
403+limits?: ArchiveExtractLimits,
404+): Promise<JSZip> {
405+const resolvedLimits = resolveExtractLimits(limits);
406+if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
407+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
408+}
409+const entryCount = readZipCentralDirectoryEntryCount(buffer);
410+if (entryCount !== null) {
411+assertArchiveEntryCountWithinLimit(entryCount, resolvedLimits);
192412}
413+return await JSZip.loadAsync(buffer);
193414}
194415195416function createByteBudgetTracker(limits: ResolvedArchiveExtractLimits): {
@@ -207,11 +428,11 @@ function createByteBudgetTracker(limits: ResolvedArchiveExtractLimits): {
207428}
208429entryBytes += b;
209430if (entryBytes > limits.maxEntryBytes) {
210-throw new Error(ERROR_ARCHIVE_ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
431+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
211432}
212433extractedBytes += b;
213434if (extractedBytes > limits.maxExtractedBytes) {
214-throw new Error(ERROR_ARCHIVE_EXTRACTED_SIZE_EXCEEDS_LIMIT);
435+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT);
215436}
216437};
217438@@ -223,7 +444,7 @@ function createByteBudgetTracker(limits: ResolvedArchiveExtractLimits): {
223444addEntrySize(size: number) {
224445const s = Math.max(0, Math.floor(size));
225446if (s > limits.maxEntryBytes) {
226-throw new Error(ERROR_ARCHIVE_ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
447+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
227448}
228449// Note: tar budgets are based on the header-declared size.
229450addBytes(s);
@@ -462,11 +683,11 @@ async function extractZip(params: {
462683const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
463684const stat = await fs.stat(params.archivePath);
464685if (stat.size > limits.maxArchiveBytes) {
465-throw new Error(ERROR_ARCHIVE_SIZE_EXCEEDS_LIMIT);
686+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
466687}
467688468689const buffer = await fs.readFile(params.archivePath);
469-const zip = await JSZip.loadAsync(buffer);
690+const zip = await loadZipArchiveWithPreflight(buffer, limits);
470691const entries = Object.values(zip.files) as ZipEntry[];
471692const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
472693@@ -594,7 +815,7 @@ export async function extractArchive(params: {
594815const limits = resolveExtractLimits(params.limits);
595816const stat = await fs.stat(params.archivePath);
596817if (stat.size > limits.maxArchiveBytes) {
597-throw new Error(ERROR_ARCHIVE_SIZE_EXCEEDS_LIMIT);
818+throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
598819}
599820600821const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。