@happyvertical/files 0.78.2 → 0.78.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createWriteStream, existsSync, statSync, constants } from "node:fs";
3
- import { rename, rm, lstat, realpath, writeFile, stat, chmod, chown, mkdir, readFile, readdir, access, rmdir, unlink, copyFile } from "node:fs/promises";
3
+ import { lstat, realpath, rename, rm, writeFile, stat, chmod, chown, mkdir, readFile, readdir, access, rmdir, unlink, copyFile } from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { join, dirname, basename, resolve, isAbsolute, relative, sep, extname } from "node:path";
6
6
  import { Readable, Transform } from "node:stream";
@@ -8,6 +8,752 @@ import { pipeline } from "node:stream/promises";
8
8
  import { getTempDirectory } from "@happyvertical/utils";
9
9
  import { URL as URL$1 } from "node:url";
10
10
  import { S3Client, ListObjectsV2Command, HeadObjectCommand, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand } from "@aws-sdk/client-s3";
11
+ const CENTRAL_DIRECTORY_SIGNATURE = 33639248;
12
+ const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101010256;
13
+ const LOCAL_FILE_HEADER_SIGNATURE = 67324752;
14
+ const ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101075792;
15
+ const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE = 117853008;
16
+ const ZIP64_EXTRA_FIELD_ID = 1;
17
+ const PKWARE_EXTENDED_LANGUAGE_ENCODING_EXTRA_FIELD_ID = 8;
18
+ const PKWARE_UNIX_EXTRA_FIELD_ID = 13;
19
+ const STRONG_ENCRYPTION_EXTRA_FIELD_ID = 23;
20
+ const XCEED_UNICODE_PATH_EXTRA_FIELD_ID = 21838;
21
+ const LIBARCHIVE_EXTRA_FIELD_ID = 27768;
22
+ const UNICODE_PATH_EXTRA_FIELD_ID = 28789;
23
+ const ASI_UNIX_EXTRA_FIELD_ID = 30062;
24
+ const WINZIP_AES_EXTRA_FIELD_ID = 39169;
25
+ const WINZIP_AES_COMPRESSION_METHOD = 99;
26
+ const END_OF_CENTRAL_DIRECTORY_SIZE = 22;
27
+ const MAX_ZIP_COMMENT_BYTES = 65535;
28
+ const CENTRAL_DIRECTORY_ENTRY_SIZE = 46;
29
+ const LOCAL_FILE_HEADER_SIZE = 30;
30
+ const ENCRYPTED_FLAG = 1;
31
+ const STRONG_ENCRYPTION_FLAG = 64;
32
+ const DATA_DESCRIPTOR_FLAG = 8;
33
+ const MASKED_LOCAL_HEADER_FLAG = 8192;
34
+ const UNIX_FILE_TYPE_MASK = 61440;
35
+ const UNIX_DIRECTORY_TYPE = 16384;
36
+ const UNIX_REGULAR_FILE_TYPE = 32768;
37
+ const UNIX_SYMLINK_TYPE = 40960;
38
+ const DOS_DIRECTORY_ATTRIBUTE = 16;
39
+ const WINDOWS_REPARSE_POINT_ATTRIBUTE = 1024;
40
+ const UNIX_CREATOR_SYSTEM = 3;
41
+ const DARWIN_CREATOR_SYSTEM = 19;
42
+ const WINDOWS_RESERVED_DEVICE_BASENAME = /^(?:aux|con|conin\$|conout\$|nul|prn|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])$/iu;
43
+ const utf8Decoder = new TextDecoder("utf-8", {
44
+ fatal: true,
45
+ ignoreBOM: true
46
+ });
47
+ const DEFAULT_ZIP_MANIFEST_LIMITS = Object.freeze({
48
+ maxEntries: 1e4,
49
+ maxEntryUncompressedBytes: 200 * 1024 * 1024,
50
+ maxTotalUncompressedBytes: 2 * 1024 * 1024 * 1024,
51
+ maxPathBytes: 1024
52
+ });
53
+ class ZipManifestError extends Error {
54
+ constructor(message, code, entryPath) {
55
+ super(message);
56
+ this.code = code;
57
+ this.entryPath = entryPath;
58
+ this.name = "ZipManifestError";
59
+ }
60
+ }
61
+ class InvalidZipArchiveError extends ZipManifestError {
62
+ constructor(message) {
63
+ super(message, "ZIP_INVALID");
64
+ this.name = "InvalidZipArchiveError";
65
+ }
66
+ }
67
+ class UnsafeZipEntryError extends ZipManifestError {
68
+ constructor(reason, entryPath, message) {
69
+ super(message, "ZIP_UNSAFE_ENTRY", entryPath);
70
+ this.reason = reason;
71
+ this.name = "UnsafeZipEntryError";
72
+ }
73
+ }
74
+ class ZipManifestLimitError extends ZipManifestError {
75
+ constructor(limit, actual, maximum, message, entryPath) {
76
+ super(message, "ZIP_LIMIT_EXCEEDED", entryPath);
77
+ this.limit = limit;
78
+ this.actual = actual;
79
+ this.maximum = maximum;
80
+ this.name = "ZipManifestLimitError";
81
+ }
82
+ }
83
+ class UnsupportedZipFeatureError extends ZipManifestError {
84
+ constructor(feature, message, entryPath) {
85
+ super(message, "ZIP_UNSUPPORTED_FEATURE", entryPath);
86
+ this.feature = feature;
87
+ this.name = "UnsupportedZipFeatureError";
88
+ }
89
+ }
90
+ function inspectZipManifest(data, limits = {}) {
91
+ if (!(data instanceof Uint8Array)) {
92
+ throw new TypeError("inspectZipManifest data must be a Uint8Array");
93
+ }
94
+ const resolvedLimits = resolveLimits(limits);
95
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
96
+ const endRecord = findEndOfCentralDirectory(view, resolvedLimits.maxEntries);
97
+ assertClassicZip(endRecord, view);
98
+ if (endRecord.totalEntries > resolvedLimits.maxEntries) {
99
+ throw new ZipManifestLimitError(
100
+ "entry-count",
101
+ endRecord.totalEntries,
102
+ resolvedLimits.maxEntries,
103
+ `ZIP archive contains ${endRecord.totalEntries} entries; the limit is ${resolvedLimits.maxEntries}.`
104
+ );
105
+ }
106
+ if (endRecord.centralDirectoryOffset > endRecord.offset || endRecord.centralDirectorySize > endRecord.offset - endRecord.centralDirectoryOffset) {
107
+ throw new InvalidZipArchiveError(
108
+ "ZIP central directory points outside the archive metadata bounds."
109
+ );
110
+ }
111
+ const centralDirectoryEnd = endRecord.centralDirectoryOffset + endRecord.centralDirectorySize;
112
+ if (centralDirectoryEnd !== endRecord.offset) {
113
+ throw new InvalidZipArchiveError(
114
+ "ZIP central directory size or offset does not match the end record."
115
+ );
116
+ }
117
+ const entries = [];
118
+ const localEntryRanges = [];
119
+ const normalizedPaths = /* @__PURE__ */ new Set();
120
+ const portablePaths = /* @__PURE__ */ new Map();
121
+ let totalUncompressedBytes = 0;
122
+ let fileCount = 0;
123
+ let directoryCount = 0;
124
+ let offset = endRecord.centralDirectoryOffset;
125
+ for (let index = 0; index < endRecord.totalEntries; index += 1) {
126
+ assertRange(
127
+ offset,
128
+ CENTRAL_DIRECTORY_ENTRY_SIZE,
129
+ centralDirectoryEnd,
130
+ "ZIP central directory is truncated."
131
+ );
132
+ if (view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIGNATURE) {
133
+ throw new InvalidZipArchiveError(
134
+ `ZIP central directory entry ${index + 1} has an invalid signature.`
135
+ );
136
+ }
137
+ const versionMadeBy = view.getUint16(offset + 4, true);
138
+ const flags = view.getUint16(offset + 8, true);
139
+ const compressionMethod = view.getUint16(offset + 10, true);
140
+ const crc32 = view.getUint32(offset + 16, true);
141
+ const compressedSize = view.getUint32(offset + 20, true);
142
+ const uncompressedSize = view.getUint32(offset + 24, true);
143
+ const fileNameLength = view.getUint16(offset + 28, true);
144
+ const extraFieldLength = view.getUint16(offset + 30, true);
145
+ const commentLength = view.getUint16(offset + 32, true);
146
+ const startingDisk = view.getUint16(offset + 34, true);
147
+ const externalAttributes = view.getUint32(offset + 38, true);
148
+ const localHeaderOffset = view.getUint32(offset + 42, true);
149
+ if (compressedSize === 4294967295 || uncompressedSize === 4294967295 || localHeaderOffset === 4294967295 || startingDisk === 65535) {
150
+ throw zip64Error();
151
+ }
152
+ if (startingDisk !== 0) {
153
+ throw multiDiskError();
154
+ }
155
+ assertNotEncrypted(flags);
156
+ assertNotAesCompression(compressionMethod);
157
+ const nameStart = offset + CENTRAL_DIRECTORY_ENTRY_SIZE;
158
+ const nameEnd = nameStart + fileNameLength;
159
+ const extraEnd = nameEnd + extraFieldLength;
160
+ const entryEnd = extraEnd + commentLength;
161
+ assertRange(
162
+ offset,
163
+ entryEnd - offset,
164
+ centralDirectoryEnd,
165
+ "ZIP central directory entry metadata is truncated."
166
+ );
167
+ if (fileNameLength > resolvedLimits.maxPathBytes) {
168
+ throw new ZipManifestLimitError(
169
+ "path-bytes",
170
+ fileNameLength,
171
+ resolvedLimits.maxPathBytes,
172
+ `ZIP entry path is ${fileNameLength} bytes; the limit is ${resolvedLimits.maxPathBytes}.`
173
+ );
174
+ }
175
+ assertExtraFields(data, nameEnd, extraEnd, "central directory");
176
+ const rawPathBytes = data.subarray(nameStart, nameEnd);
177
+ const rawPath = decodeEntryPath(rawPathBytes);
178
+ const path2 = normalizeEntryPath(rawPath);
179
+ const unixMode = externalAttributes >>> 16 & 65535;
180
+ const unixFileType = unixMode & UNIX_FILE_TYPE_MASK;
181
+ const creatorSystem = versionMadeBy >>> 8;
182
+ if ((externalAttributes & WINDOWS_REPARSE_POINT_ATTRIBUTE) !== 0) {
183
+ throw new UnsafeZipEntryError(
184
+ "reparse-point",
185
+ rawPath,
186
+ `ZIP entry "${rawPath}" is a Windows reparse point, which is not allowed.`
187
+ );
188
+ }
189
+ if (unixFileType !== 0 && creatorSystem !== UNIX_CREATOR_SYSTEM && creatorSystem !== DARWIN_CREATOR_SYSTEM) {
190
+ throw new UnsupportedZipFeatureError(
191
+ "ambiguous-metadata",
192
+ `ZIP entry "${rawPath}" declares Unix file-type metadata from a non-Unix creator system.`,
193
+ rawPath
194
+ );
195
+ }
196
+ if (unixFileType === UNIX_SYMLINK_TYPE) {
197
+ throw new UnsafeZipEntryError(
198
+ "symlink",
199
+ rawPath,
200
+ `ZIP entry "${rawPath}" is a symbolic link, which is not allowed.`
201
+ );
202
+ }
203
+ if (unixFileType !== 0 && unixFileType !== UNIX_DIRECTORY_TYPE && unixFileType !== UNIX_REGULAR_FILE_TYPE) {
204
+ throw new UnsafeZipEntryError(
205
+ "special-file",
206
+ rawPath,
207
+ `ZIP entry "${rawPath}" uses an unsupported Unix special file type.`
208
+ );
209
+ }
210
+ if (compressionMethod === 0 && compressedSize !== uncompressedSize) {
211
+ throw new InvalidZipArchiveError(
212
+ `Stored ZIP entry "${path2}" declares different compressed and uncompressed sizes.`
213
+ );
214
+ }
215
+ const hasDirectoryPathMarker = rawPath.endsWith("/") || rawPath.endsWith("\\");
216
+ const hasDosDirectoryAttribute = (externalAttributes & DOS_DIRECTORY_ATTRIBUTE) !== 0;
217
+ if (unixFileType === UNIX_REGULAR_FILE_TYPE && (hasDirectoryPathMarker || hasDosDirectoryAttribute)) {
218
+ throw new UnsupportedZipFeatureError(
219
+ "ambiguous-metadata",
220
+ `ZIP entry "${rawPath}" has contradictory Unix file and directory metadata.`,
221
+ rawPath
222
+ );
223
+ }
224
+ const type = hasDirectoryPathMarker || unixFileType === UNIX_DIRECTORY_TYPE || hasDosDirectoryAttribute ? "directory" : "file";
225
+ if (normalizedPaths.has(path2)) {
226
+ throw new UnsafeZipEntryError(
227
+ "duplicate-path",
228
+ rawPath,
229
+ `ZIP entry "${rawPath}" collides with another normalized path ("${path2}").`
230
+ );
231
+ }
232
+ const portableKey = portablePathKey(path2);
233
+ const existingPortablePath = portablePaths.get(portableKey);
234
+ if (existingPortablePath !== void 0) {
235
+ throw new UnsafeZipEntryError(
236
+ "portable-path-collision",
237
+ rawPath,
238
+ `ZIP entry "${rawPath}" collides with "${existingPortablePath}" on a case-insensitive or Unicode-normalizing filesystem.`
239
+ );
240
+ }
241
+ normalizedPaths.add(path2);
242
+ portablePaths.set(portableKey, path2);
243
+ const localDataEnd = validateLocalHeader(data, view, {
244
+ centralDirectoryOffset: endRecord.centralDirectoryOffset,
245
+ centralCrc32: crc32,
246
+ centralFlags: flags,
247
+ centralNameStart: nameStart,
248
+ centralNameEnd: nameEnd,
249
+ compressedSize,
250
+ compressionMethod,
251
+ localHeaderOffset,
252
+ rawPath,
253
+ uncompressedSize
254
+ });
255
+ localEntryRanges.push({
256
+ end: localDataEnd,
257
+ path: path2,
258
+ start: localHeaderOffset
259
+ });
260
+ if (uncompressedSize > resolvedLimits.maxEntryUncompressedBytes) {
261
+ throw new ZipManifestLimitError(
262
+ "entry-uncompressed-size",
263
+ uncompressedSize,
264
+ resolvedLimits.maxEntryUncompressedBytes,
265
+ `ZIP entry "${path2}" declares ${uncompressedSize} uncompressed bytes; the per-entry limit is ${resolvedLimits.maxEntryUncompressedBytes}.`,
266
+ path2
267
+ );
268
+ }
269
+ totalUncompressedBytes += uncompressedSize;
270
+ if (totalUncompressedBytes > resolvedLimits.maxTotalUncompressedBytes) {
271
+ throw new ZipManifestLimitError(
272
+ "total-uncompressed-size",
273
+ totalUncompressedBytes,
274
+ resolvedLimits.maxTotalUncompressedBytes,
275
+ `ZIP archive declares ${totalUncompressedBytes} uncompressed bytes; the aggregate limit is ${resolvedLimits.maxTotalUncompressedBytes}.`,
276
+ path2
277
+ );
278
+ }
279
+ if (type === "directory") {
280
+ directoryCount += 1;
281
+ } else {
282
+ fileCount += 1;
283
+ }
284
+ entries.push({
285
+ path: path2,
286
+ type,
287
+ size: uncompressedSize,
288
+ compressedSize,
289
+ compressionMethod
290
+ });
291
+ offset = entryEnd;
292
+ }
293
+ if (offset !== centralDirectoryEnd) {
294
+ throw new InvalidZipArchiveError(
295
+ "ZIP central directory entry count does not match its declared size."
296
+ );
297
+ }
298
+ assertLocalEntryRangesCoverArchiveData(
299
+ localEntryRanges,
300
+ endRecord.centralDirectoryOffset
301
+ );
302
+ assertNoFileDescendantConflicts(entries);
303
+ return {
304
+ entries,
305
+ entryCount: entries.length,
306
+ fileCount,
307
+ directoryCount,
308
+ totalUncompressedBytes
309
+ };
310
+ }
311
+ function assertLocalEntryRangesCoverArchiveData(ranges, centralDirectoryOffset) {
312
+ const sortedRanges = [...ranges].sort(
313
+ (left, right) => left.start - right.start || left.end - right.end
314
+ );
315
+ let expectedStart = 0;
316
+ for (const range of sortedRanges) {
317
+ if (range.start < expectedStart) {
318
+ throw new InvalidZipArchiveError(
319
+ `ZIP local entry ranges overlap at "${range.path}".`
320
+ );
321
+ }
322
+ if (range.start > expectedStart) {
323
+ throw new InvalidZipArchiveError(
324
+ "ZIP contains data before the central directory that is not described by a central-directory entry."
325
+ );
326
+ }
327
+ expectedStart = range.end;
328
+ }
329
+ if (expectedStart !== centralDirectoryOffset) {
330
+ throw new InvalidZipArchiveError(
331
+ "ZIP contains data before the central directory that is not described by a central-directory entry."
332
+ );
333
+ }
334
+ }
335
+ function assertNoFileDescendantConflicts(entries) {
336
+ const sortedEntries = entries.map((entry) => ({ entry, portableKey: portablePathKey(entry.path) })).sort(
337
+ (left, right) => left.portableKey < right.portableKey ? -1 : left.portableKey > right.portableKey ? 1 : 0
338
+ );
339
+ for (const { entry, portableKey } of sortedEntries) {
340
+ if (entry.type !== "file") {
341
+ continue;
342
+ }
343
+ const descendantPrefix = `${portableKey}/`;
344
+ let low = 0;
345
+ let high = sortedEntries.length;
346
+ while (low < high) {
347
+ const middle = low + Math.floor((high - low) / 2);
348
+ if (sortedEntries[middle].portableKey < descendantPrefix) {
349
+ low = middle + 1;
350
+ } else {
351
+ high = middle;
352
+ }
353
+ }
354
+ const descendant = sortedEntries[low];
355
+ if (descendant?.portableKey.startsWith(descendantPrefix)) {
356
+ throw new UnsafeZipEntryError(
357
+ "path-conflict",
358
+ entry.path,
359
+ `ZIP file entry "${entry.path}" conflicts with descendant entry "${descendant.entry.path}".`
360
+ );
361
+ }
362
+ }
363
+ }
364
+ function resolveLimits(limits) {
365
+ return {
366
+ maxEntries: validateLimit(
367
+ "maxEntries",
368
+ limits.maxEntries ?? DEFAULT_ZIP_MANIFEST_LIMITS.maxEntries
369
+ ),
370
+ maxEntryUncompressedBytes: validateLimit(
371
+ "maxEntryUncompressedBytes",
372
+ limits.maxEntryUncompressedBytes ?? DEFAULT_ZIP_MANIFEST_LIMITS.maxEntryUncompressedBytes
373
+ ),
374
+ maxTotalUncompressedBytes: validateLimit(
375
+ "maxTotalUncompressedBytes",
376
+ limits.maxTotalUncompressedBytes ?? DEFAULT_ZIP_MANIFEST_LIMITS.maxTotalUncompressedBytes
377
+ ),
378
+ maxPathBytes: validateLimit(
379
+ "maxPathBytes",
380
+ limits.maxPathBytes ?? DEFAULT_ZIP_MANIFEST_LIMITS.maxPathBytes
381
+ )
382
+ };
383
+ }
384
+ function validateLimit(name, value) {
385
+ if (!Number.isSafeInteger(value) || value < 0) {
386
+ throw new RangeError(`${name} must be a non-negative safe integer`);
387
+ }
388
+ return value;
389
+ }
390
+ function findEndOfCentralDirectory(view, maxEntries) {
391
+ if (view.byteLength < END_OF_CENTRAL_DIRECTORY_SIZE) {
392
+ throw new InvalidZipArchiveError(
393
+ "Data is too small to contain a ZIP end-of-central-directory record."
394
+ );
395
+ }
396
+ const earliestOffset = Math.max(
397
+ 0,
398
+ view.byteLength - END_OF_CENTRAL_DIRECTORY_SIZE - MAX_ZIP_COMMENT_BYTES
399
+ );
400
+ const validationBudget = {
401
+ maxEntries,
402
+ remainingEntryChecks: Math.min(maxEntries, 65534) + 1
403
+ };
404
+ let endRecord;
405
+ for (let offset = view.byteLength - END_OF_CENTRAL_DIRECTORY_SIZE; offset >= earliestOffset; offset -= 1) {
406
+ if (view.getUint32(offset, true) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
407
+ continue;
408
+ }
409
+ const commentLength = view.getUint16(offset + 20, true);
410
+ if (offset + END_OF_CENTRAL_DIRECTORY_SIZE + commentLength !== view.byteLength) {
411
+ continue;
412
+ }
413
+ const candidate = {
414
+ offset,
415
+ diskNumber: view.getUint16(offset + 4, true),
416
+ centralDirectoryDisk: view.getUint16(offset + 6, true),
417
+ entriesOnDisk: view.getUint16(offset + 8, true),
418
+ totalEntries: view.getUint16(offset + 10, true),
419
+ centralDirectorySize: view.getUint32(offset + 12, true),
420
+ centralDirectoryOffset: view.getUint32(offset + 16, true)
421
+ };
422
+ if (isStructurallyPlausibleEndRecord(view, candidate, validationBudget)) {
423
+ if (endRecord) {
424
+ throw new InvalidZipArchiveError(
425
+ "ZIP archive contains multiple structurally valid end-of-central-directory records."
426
+ );
427
+ }
428
+ endRecord = candidate;
429
+ }
430
+ }
431
+ if (endRecord) {
432
+ return endRecord;
433
+ }
434
+ throw new InvalidZipArchiveError(
435
+ "ZIP end-of-central-directory record is missing or truncated."
436
+ );
437
+ }
438
+ function isStructurallyPlausibleEndRecord(view, endRecord, budget) {
439
+ if (hasStructurallyValidZip64Chain(view, endRecord)) {
440
+ return true;
441
+ }
442
+ if (endRecord.centralDirectoryOffset === 4294967295 || endRecord.centralDirectorySize === 4294967295) {
443
+ return false;
444
+ }
445
+ const centralDirectoryEnd = endRecord.centralDirectoryOffset + endRecord.centralDirectorySize;
446
+ if (endRecord.centralDirectoryOffset > endRecord.offset || centralDirectoryEnd !== endRecord.offset) {
447
+ return false;
448
+ }
449
+ if (endRecord.entriesOnDisk === 65535 || endRecord.totalEntries === 65535) {
450
+ return false;
451
+ }
452
+ if (endRecord.totalEntries > budget.maxEntries) {
453
+ return endRecord.centralDirectorySize >= endRecord.totalEntries * CENTRAL_DIRECTORY_ENTRY_SIZE && endRecord.centralDirectoryOffset <= endRecord.offset - CENTRAL_DIRECTORY_ENTRY_SIZE && view.getUint32(endRecord.centralDirectoryOffset, true) === CENTRAL_DIRECTORY_SIGNATURE;
454
+ }
455
+ let offset = endRecord.centralDirectoryOffset;
456
+ for (let index = 0; index < endRecord.totalEntries; index += 1) {
457
+ if (budget.remainingEntryChecks === 0) {
458
+ throw new InvalidZipArchiveError(
459
+ "ZIP end-record candidate validation exceeded the configured entry-check budget."
460
+ );
461
+ }
462
+ budget.remainingEntryChecks -= 1;
463
+ if (offset > endRecord.offset - CENTRAL_DIRECTORY_ENTRY_SIZE || view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIGNATURE) {
464
+ return false;
465
+ }
466
+ const fileNameLength = view.getUint16(offset + 28, true);
467
+ const extraFieldLength = view.getUint16(offset + 30, true);
468
+ const commentLength = view.getUint16(offset + 32, true);
469
+ const entryLength = CENTRAL_DIRECTORY_ENTRY_SIZE + fileNameLength + extraFieldLength + commentLength;
470
+ if (entryLength > endRecord.offset - offset) {
471
+ return false;
472
+ }
473
+ offset += entryLength;
474
+ }
475
+ return offset === endRecord.offset;
476
+ }
477
+ function hasStructurallyValidZip64Chain(view, endRecord) {
478
+ const locatorOffset = endRecord.offset - 20;
479
+ if (locatorOffset < 0 || view.getUint32(locatorOffset, true) !== ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE) {
480
+ return false;
481
+ }
482
+ const zip64Offset = readSafeUint64(view, locatorOffset + 8);
483
+ if (zip64Offset === void 0 || zip64Offset > locatorOffset - 56 || view.getUint32(zip64Offset, true) !== ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
484
+ return false;
485
+ }
486
+ const recordSize = readSafeUint64(view, zip64Offset + 4);
487
+ return recordSize !== void 0 && recordSize >= 44 && zip64Offset + 12 + recordSize === locatorOffset;
488
+ }
489
+ function readSafeUint64(view, offset) {
490
+ const low = view.getUint32(offset, true);
491
+ const high = view.getUint32(offset + 4, true);
492
+ const value = high * 4294967296 + low;
493
+ return Number.isSafeInteger(value) ? value : void 0;
494
+ }
495
+ function assertClassicZip(endRecord, view) {
496
+ const hasZip64Locator = hasStructurallyValidZip64Chain(view, endRecord);
497
+ if (endRecord.entriesOnDisk === 65535 || endRecord.totalEntries === 65535 || endRecord.centralDirectorySize === 4294967295 || endRecord.centralDirectoryOffset === 4294967295 || hasZip64Locator) {
498
+ throw zip64Error();
499
+ }
500
+ if (endRecord.diskNumber !== 0 || endRecord.centralDirectoryDisk !== 0 || endRecord.entriesOnDisk !== endRecord.totalEntries) {
501
+ throw multiDiskError();
502
+ }
503
+ }
504
+ function validateLocalHeader(data, view, entry) {
505
+ assertRange(
506
+ entry.localHeaderOffset,
507
+ LOCAL_FILE_HEADER_SIZE,
508
+ entry.centralDirectoryOffset,
509
+ "ZIP local file header is missing or truncated."
510
+ );
511
+ if (view.getUint32(entry.localHeaderOffset, true) !== LOCAL_FILE_HEADER_SIGNATURE) {
512
+ throw new InvalidZipArchiveError(
513
+ "ZIP central directory points to an invalid local file header."
514
+ );
515
+ }
516
+ const localFlags = view.getUint16(entry.localHeaderOffset + 6, true);
517
+ const localCompressionMethod = view.getUint16(
518
+ entry.localHeaderOffset + 8,
519
+ true
520
+ );
521
+ const localCrc32 = view.getUint32(entry.localHeaderOffset + 14, true);
522
+ const localCompressedSize = view.getUint32(
523
+ entry.localHeaderOffset + 18,
524
+ true
525
+ );
526
+ const localUncompressedSize = view.getUint32(
527
+ entry.localHeaderOffset + 22,
528
+ true
529
+ );
530
+ const localNameLength = view.getUint16(entry.localHeaderOffset + 26, true);
531
+ const localExtraLength = view.getUint16(entry.localHeaderOffset + 28, true);
532
+ if (localCompressedSize === 4294967295 || localUncompressedSize === 4294967295) {
533
+ throw zip64Error();
534
+ }
535
+ assertNotEncrypted(localFlags);
536
+ if (localFlags !== entry.centralFlags || localCompressionMethod !== entry.compressionMethod) {
537
+ throw new InvalidZipArchiveError(
538
+ "ZIP local file header does not match its central-directory entry."
539
+ );
540
+ }
541
+ if ((localFlags & DATA_DESCRIPTOR_FLAG) !== 0) {
542
+ throw new UnsupportedZipFeatureError(
543
+ "ambiguous-metadata",
544
+ `ZIP entry "${entry.rawPath}" uses a data descriptor whose payload boundary cannot be verified without decompression or extractor-specific scanning.`,
545
+ entry.rawPath
546
+ );
547
+ }
548
+ const localNameStart = entry.localHeaderOffset + LOCAL_FILE_HEADER_SIZE;
549
+ const localNameEnd = localNameStart + localNameLength;
550
+ const localExtraEnd = localNameEnd + localExtraLength;
551
+ assertRange(
552
+ entry.localHeaderOffset,
553
+ localExtraEnd - entry.localHeaderOffset,
554
+ entry.centralDirectoryOffset,
555
+ "ZIP local file header metadata is truncated."
556
+ );
557
+ const centralNameLength = entry.centralNameEnd - entry.centralNameStart;
558
+ if (localNameLength !== centralNameLength || !equalBytes(
559
+ data.subarray(localNameStart, localNameEnd),
560
+ data.subarray(entry.centralNameStart, entry.centralNameEnd)
561
+ )) {
562
+ throw new InvalidZipArchiveError(
563
+ "ZIP local and central-directory entry paths do not match."
564
+ );
565
+ }
566
+ assertExtraFields(data, localNameEnd, localExtraEnd, "local file header");
567
+ if (localCrc32 !== entry.centralCrc32 || localCompressedSize !== entry.compressedSize || localUncompressedSize !== entry.uncompressedSize) {
568
+ throw new InvalidZipArchiveError(
569
+ "ZIP local file sizes do not match the central-directory entry."
570
+ );
571
+ }
572
+ if (entry.compressedSize > entry.centralDirectoryOffset - localExtraEnd) {
573
+ throw new InvalidZipArchiveError(
574
+ "ZIP entry body is truncated or overlaps the central directory."
575
+ );
576
+ }
577
+ return localExtraEnd + entry.compressedSize;
578
+ }
579
+ function assertExtraFields(data, start, end, location) {
580
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
581
+ let offset = start;
582
+ while (offset < end) {
583
+ if (end - offset < 4) {
584
+ throw new InvalidZipArchiveError(
585
+ `ZIP ${location} contains a truncated extra field.`
586
+ );
587
+ }
588
+ const headerId = view.getUint16(offset, true);
589
+ const dataSize = view.getUint16(offset + 2, true);
590
+ const nextOffset = offset + 4 + dataSize;
591
+ if (nextOffset > end) {
592
+ throw new InvalidZipArchiveError(
593
+ `ZIP ${location} contains a truncated extra field.`
594
+ );
595
+ }
596
+ if (headerId === ZIP64_EXTRA_FIELD_ID) {
597
+ throw zip64Error();
598
+ }
599
+ if (headerId === STRONG_ENCRYPTION_EXTRA_FIELD_ID || headerId === WINZIP_AES_EXTRA_FIELD_ID) {
600
+ throw encryptionError();
601
+ }
602
+ if (headerId === PKWARE_EXTENDED_LANGUAGE_ENCODING_EXTRA_FIELD_ID || headerId === XCEED_UNICODE_PATH_EXTRA_FIELD_ID || headerId === UNICODE_PATH_EXTRA_FIELD_ID) {
603
+ throw new UnsupportedZipFeatureError(
604
+ "ambiguous-metadata",
605
+ "ZIP alternate path-encoding extra fields are not supported because entry names must have one unambiguous representation."
606
+ );
607
+ }
608
+ if (headerId === PKWARE_UNIX_EXTRA_FIELD_ID || headerId === ASI_UNIX_EXTRA_FIELD_ID || headerId === LIBARCHIVE_EXTRA_FIELD_ID) {
609
+ throw new UnsupportedZipFeatureError(
610
+ "ambiguous-metadata",
611
+ "ZIP Unix link and file-type extra fields are not supported because entry type must have one unambiguous representation."
612
+ );
613
+ }
614
+ offset = nextOffset;
615
+ }
616
+ }
617
+ function assertNotEncrypted(flags) {
618
+ if ((flags & (ENCRYPTED_FLAG | STRONG_ENCRYPTION_FLAG | MASKED_LOCAL_HEADER_FLAG)) !== 0) {
619
+ throw encryptionError();
620
+ }
621
+ }
622
+ function assertNotAesCompression(compressionMethod) {
623
+ if (compressionMethod === WINZIP_AES_COMPRESSION_METHOD) {
624
+ throw encryptionError();
625
+ }
626
+ }
627
+ function decodeEntryPath(bytes) {
628
+ let path2;
629
+ try {
630
+ path2 = utf8Decoder.decode(bytes);
631
+ } catch {
632
+ throw new InvalidZipArchiveError(
633
+ "ZIP entry path is not valid UTF-8. Legacy non-UTF-8 names are not supported."
634
+ );
635
+ }
636
+ if (path2.startsWith("\uFEFF")) {
637
+ throw new UnsupportedZipFeatureError(
638
+ "ambiguous-metadata",
639
+ "ZIP entry path starts with a UTF-8 BOM, which filename decoders interpret inconsistently.",
640
+ path2
641
+ );
642
+ }
643
+ return path2;
644
+ }
645
+ function normalizeEntryPath(rawPath) {
646
+ if (rawPath.length === 0) {
647
+ throw new UnsafeZipEntryError(
648
+ "empty-path",
649
+ rawPath,
650
+ "ZIP entry path is empty."
651
+ );
652
+ }
653
+ if (rawPath.includes("\0")) {
654
+ throw new UnsafeZipEntryError(
655
+ "nul-byte",
656
+ rawPath,
657
+ "ZIP entry path contains a NUL byte."
658
+ );
659
+ }
660
+ const unifiedPath = rawPath.replace(/\\/g, "/");
661
+ if (/^[a-zA-Z]:/.test(unifiedPath)) {
662
+ throw new UnsafeZipEntryError(
663
+ "drive-qualified-path",
664
+ rawPath,
665
+ `ZIP entry "${rawPath}" uses a drive-qualified path.`
666
+ );
667
+ }
668
+ if (unifiedPath.includes(":")) {
669
+ throw new UnsafeZipEntryError(
670
+ "alternate-data-stream",
671
+ rawPath,
672
+ `ZIP entry "${rawPath}" contains a colon that can select an NTFS alternate data stream.`
673
+ );
674
+ }
675
+ if (unifiedPath.startsWith("/")) {
676
+ throw new UnsafeZipEntryError(
677
+ "absolute-path",
678
+ rawPath,
679
+ `ZIP entry "${rawPath}" uses an absolute path.`
680
+ );
681
+ }
682
+ const segments = unifiedPath.split("/");
683
+ if (segments.includes("..")) {
684
+ throw new UnsafeZipEntryError(
685
+ "path-traversal",
686
+ rawPath,
687
+ `ZIP entry "${rawPath}" contains a parent-directory traversal segment.`
688
+ );
689
+ }
690
+ for (const segment of segments) {
691
+ if (segment === "" || segment === ".") {
692
+ continue;
693
+ }
694
+ if (/[. ]$/u.test(segment)) {
695
+ throw new UnsafeZipEntryError(
696
+ "windows-reserved-path",
697
+ rawPath,
698
+ `ZIP entry "${rawPath}" contains a path segment ending in a dot or space, which is not portable to Windows.`
699
+ );
700
+ }
701
+ const basename2 = segment.split(".", 1)[0].replace(/[. ]+$/u, "");
702
+ if (WINDOWS_RESERVED_DEVICE_BASENAME.test(basename2)) {
703
+ throw new UnsafeZipEntryError(
704
+ "windows-reserved-path",
705
+ rawPath,
706
+ `ZIP entry "${rawPath}" contains the Windows-reserved device name "${segment}".`
707
+ );
708
+ }
709
+ }
710
+ const normalizedPath = segments.filter((segment) => segment !== "" && segment !== ".").join("/");
711
+ if (normalizedPath.length === 0) {
712
+ throw new UnsafeZipEntryError(
713
+ "empty-path",
714
+ rawPath,
715
+ `ZIP entry "${rawPath}" normalizes to an empty path.`
716
+ );
717
+ }
718
+ return normalizedPath;
719
+ }
720
+ function portablePathKey(path2) {
721
+ return path2.normalize("NFC").toUpperCase().toLowerCase().normalize("NFC");
722
+ }
723
+ function assertRange(offset, length, boundary, message) {
724
+ if (offset < 0 || length < 0 || offset > boundary || length > boundary - offset) {
725
+ throw new InvalidZipArchiveError(message);
726
+ }
727
+ }
728
+ function equalBytes(left, right) {
729
+ if (left.length !== right.length) {
730
+ return false;
731
+ }
732
+ for (let index = 0; index < left.length; index += 1) {
733
+ if (left[index] !== right[index]) {
734
+ return false;
735
+ }
736
+ }
737
+ return true;
738
+ }
739
+ function zip64Error() {
740
+ return new UnsupportedZipFeatureError(
741
+ "zip64",
742
+ "ZIP64 archives are not supported; use a classic ZIP archive within the configured limits."
743
+ );
744
+ }
745
+ function multiDiskError() {
746
+ return new UnsupportedZipFeatureError(
747
+ "multi-disk",
748
+ "Multi-disk ZIP archives are not supported."
749
+ );
750
+ }
751
+ function encryptionError() {
752
+ return new UnsupportedZipFeatureError(
753
+ "encryption",
754
+ "Encrypted ZIP entries are not supported because their metadata cannot be safely inspected without decryption."
755
+ );
756
+ }
11
757
  function createTempFilePath(filepath) {
12
758
  return join(
13
759
  dirname(filepath),
@@ -2680,16 +3426,22 @@ Promise.resolve().then(() => factory).then(({ initializeProviders: initializePro
2680
3426
  });
2681
3427
  const PACKAGE_VERSION_INITIALIZED = true;
2682
3428
  export {
3429
+ DEFAULT_ZIP_MANIFEST_LIMITS,
2683
3430
  DirectoryNotEmptyError,
2684
3431
  FileNotFoundError,
2685
3432
  FilesystemAdapter,
2686
3433
  FilesystemError,
2687
3434
  GoogleDriveProvider,
2688
3435
  InvalidPathError,
3436
+ InvalidZipArchiveError,
2689
3437
  LocalFilesystemProvider,
2690
3438
  PACKAGE_VERSION_INITIALIZED,
2691
3439
  PermissionError,
2692
3440
  S3FilesystemProvider,
3441
+ UnsafeZipEntryError,
3442
+ UnsupportedZipFeatureError,
3443
+ ZipManifestError,
3444
+ ZipManifestLimitError,
2693
3445
  addRateLimit,
2694
3446
  factory as default,
2695
3447
  download,
@@ -2706,6 +3458,7 @@ export {
2706
3458
  getProviderInfo,
2707
3459
  getRateLimit,
2708
3460
  initializeProviders,
3461
+ inspectZipManifest,
2709
3462
  isDirectory,
2710
3463
  isFile,
2711
3464
  isProviderAvailable,