@ismail-elkorchi/bytefold 0.6.0
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/LICENSE +21 -0
- package/README.md +48 -0
- package/SPEC.md +285 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +33 -0
- package/dist/abort.js.map +1 -0
- package/dist/archive/errors.d.ts +34 -0
- package/dist/archive/errors.d.ts.map +1 -0
- package/dist/archive/errors.js +45 -0
- package/dist/archive/errors.js.map +1 -0
- package/dist/archive/httpArchiveErrors.d.ts +2 -0
- package/dist/archive/httpArchiveErrors.d.ts.map +1 -0
- package/dist/archive/httpArchiveErrors.js +25 -0
- package/dist/archive/httpArchiveErrors.js.map +1 -0
- package/dist/archive/index.d.ts +47 -0
- package/dist/archive/index.d.ts.map +1 -0
- package/dist/archive/index.js +1490 -0
- package/dist/archive/index.js.map +1 -0
- package/dist/archive/types.d.ts +91 -0
- package/dist/archive/types.d.ts.map +1 -0
- package/dist/archive/types.js +2 -0
- package/dist/archive/types.js.map +1 -0
- package/dist/archive/xzPreflight.d.ts +13 -0
- package/dist/archive/xzPreflight.d.ts.map +1 -0
- package/dist/archive/xzPreflight.js +44 -0
- package/dist/archive/xzPreflight.js.map +1 -0
- package/dist/archive/zipPreflight.d.ts +18 -0
- package/dist/archive/zipPreflight.d.ts.map +1 -0
- package/dist/archive/zipPreflight.js +50 -0
- package/dist/archive/zipPreflight.js.map +1 -0
- package/dist/binary.d.ts +12 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +59 -0
- package/dist/binary.js.map +1 -0
- package/dist/bun/index.d.ts +19 -0
- package/dist/bun/index.d.ts.map +1 -0
- package/dist/bun/index.js +427 -0
- package/dist/bun/index.js.map +1 -0
- package/dist/compress/errors.d.ts +30 -0
- package/dist/compress/errors.d.ts.map +1 -0
- package/dist/compress/errors.js +40 -0
- package/dist/compress/errors.js.map +1 -0
- package/dist/compress/index.d.ts +12 -0
- package/dist/compress/index.d.ts.map +1 -0
- package/dist/compress/index.js +339 -0
- package/dist/compress/index.js.map +1 -0
- package/dist/compress/types.d.ts +41 -0
- package/dist/compress/types.d.ts.map +1 -0
- package/dist/compress/types.js +2 -0
- package/dist/compress/types.js.map +1 -0
- package/dist/compression/bzip2.d.ts +9 -0
- package/dist/compression/bzip2.d.ts.map +1 -0
- package/dist/compression/bzip2.js +546 -0
- package/dist/compression/bzip2.js.map +1 -0
- package/dist/compression/codecs.d.ts +6 -0
- package/dist/compression/codecs.d.ts.map +1 -0
- package/dist/compression/codecs.js +82 -0
- package/dist/compression/codecs.js.map +1 -0
- package/dist/compression/deflate64.d.ts +3 -0
- package/dist/compression/deflate64.d.ts.map +1 -0
- package/dist/compression/deflate64.js +549 -0
- package/dist/compression/deflate64.js.map +1 -0
- package/dist/compression/node-backend.d.ts +9 -0
- package/dist/compression/node-backend.d.ts.map +1 -0
- package/dist/compression/node-backend.js +103 -0
- package/dist/compression/node-backend.js.map +1 -0
- package/dist/compression/registry.d.ts +10 -0
- package/dist/compression/registry.d.ts.map +1 -0
- package/dist/compression/registry.js +30 -0
- package/dist/compression/registry.js.map +1 -0
- package/dist/compression/streams.d.ts +31 -0
- package/dist/compression/streams.d.ts.map +1 -0
- package/dist/compression/streams.js +147 -0
- package/dist/compression/streams.js.map +1 -0
- package/dist/compression/types.d.ts +19 -0
- package/dist/compression/types.d.ts.map +1 -0
- package/dist/compression/types.js +2 -0
- package/dist/compression/types.js.map +1 -0
- package/dist/compression/xz.d.ts +21 -0
- package/dist/compression/xz.d.ts.map +1 -0
- package/dist/compression/xz.js +1455 -0
- package/dist/compression/xz.js.map +1 -0
- package/dist/compression/xzFilters.d.ts +14 -0
- package/dist/compression/xzFilters.d.ts.map +1 -0
- package/dist/compression/xzFilters.js +736 -0
- package/dist/compression/xzFilters.js.map +1 -0
- package/dist/compression/xzIndexPreflight.d.ts +20 -0
- package/dist/compression/xzIndexPreflight.d.ts.map +1 -0
- package/dist/compression/xzIndexPreflight.js +371 -0
- package/dist/compression/xzIndexPreflight.js.map +1 -0
- package/dist/compression/xzScan.d.ts +15 -0
- package/dist/compression/xzScan.d.ts.map +1 -0
- package/dist/compression/xzScan.js +310 -0
- package/dist/compression/xzScan.js.map +1 -0
- package/dist/cp437.d.ts +2 -0
- package/dist/cp437.d.ts.map +1 -0
- package/dist/cp437.js +31 -0
- package/dist/cp437.js.map +1 -0
- package/dist/crc32.d.ts +7 -0
- package/dist/crc32.d.ts.map +1 -0
- package/dist/crc32.js +37 -0
- package/dist/crc32.js.map +1 -0
- package/dist/crc64.d.ts +6 -0
- package/dist/crc64.d.ts.map +1 -0
- package/dist/crc64.js +32 -0
- package/dist/crc64.js.map +1 -0
- package/dist/crypto/ctr.d.ts +11 -0
- package/dist/crypto/ctr.d.ts.map +1 -0
- package/dist/crypto/ctr.js +56 -0
- package/dist/crypto/ctr.js.map +1 -0
- package/dist/crypto/sha256.d.ts +16 -0
- package/dist/crypto/sha256.d.ts.map +1 -0
- package/dist/crypto/sha256.js +152 -0
- package/dist/crypto/sha256.js.map +1 -0
- package/dist/crypto/winzip-aes.d.ts +17 -0
- package/dist/crypto/winzip-aes.d.ts.map +1 -0
- package/dist/crypto/winzip-aes.js +98 -0
- package/dist/crypto/winzip-aes.js.map +1 -0
- package/dist/crypto/zipcrypto.d.ts +23 -0
- package/dist/crypto/zipcrypto.d.ts.map +1 -0
- package/dist/crypto/zipcrypto.js +99 -0
- package/dist/crypto/zipcrypto.js.map +1 -0
- package/dist/deno/index.d.ts +19 -0
- package/dist/deno/index.d.ts.map +1 -0
- package/dist/deno/index.js +422 -0
- package/dist/deno/index.js.map +1 -0
- package/dist/dosTime.d.ts +7 -0
- package/dist/dosTime.d.ts.map +1 -0
- package/dist/dosTime.js +21 -0
- package/dist/dosTime.js.map +1 -0
- package/dist/errorContext.d.ts +2 -0
- package/dist/errorContext.d.ts.map +1 -0
- package/dist/errorContext.js +24 -0
- package/dist/errorContext.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraFields.d.ts +29 -0
- package/dist/extraFields.d.ts.map +1 -0
- package/dist/extraFields.js +201 -0
- package/dist/extraFields.js.map +1 -0
- package/dist/generated/unicodeCaseFolding.d.ts +4 -0
- package/dist/generated/unicodeCaseFolding.d.ts.map +1 -0
- package/dist/generated/unicodeCaseFolding.js +1594 -0
- package/dist/generated/unicodeCaseFolding.js.map +1 -0
- package/dist/http/errors.d.ts +26 -0
- package/dist/http/errors.d.ts.map +1 -0
- package/dist/http/errors.js +33 -0
- package/dist/http/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/limits.d.ts +22 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +39 -0
- package/dist/limits.js.map +1 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +448 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/zip/RandomAccess.d.ts +12 -0
- package/dist/node/zip/RandomAccess.d.ts.map +1 -0
- package/dist/node/zip/RandomAccess.js +38 -0
- package/dist/node/zip/RandomAccess.js.map +1 -0
- package/dist/node/zip/Sink.d.ts +17 -0
- package/dist/node/zip/Sink.d.ts.map +1 -0
- package/dist/node/zip/Sink.js +45 -0
- package/dist/node/zip/Sink.js.map +1 -0
- package/dist/node/zip/ZipReader.d.ts +51 -0
- package/dist/node/zip/ZipReader.d.ts.map +1 -0
- package/dist/node/zip/ZipReader.js +1540 -0
- package/dist/node/zip/ZipReader.js.map +1 -0
- package/dist/node/zip/ZipWriter.d.ts +21 -0
- package/dist/node/zip/ZipWriter.d.ts.map +1 -0
- package/dist/node/zip/ZipWriter.js +196 -0
- package/dist/node/zip/ZipWriter.js.map +1 -0
- package/dist/node/zip/entryStream.d.ts +22 -0
- package/dist/node/zip/entryStream.d.ts.map +1 -0
- package/dist/node/zip/entryStream.js +241 -0
- package/dist/node/zip/entryStream.js.map +1 -0
- package/dist/node/zip/entryWriter.d.ts +54 -0
- package/dist/node/zip/entryWriter.d.ts.map +1 -0
- package/dist/node/zip/entryWriter.js +512 -0
- package/dist/node/zip/entryWriter.js.map +1 -0
- package/dist/node/zip/index.d.ts +8 -0
- package/dist/node/zip/index.d.ts.map +1 -0
- package/dist/node/zip/index.js +5 -0
- package/dist/node/zip/index.js.map +1 -0
- package/dist/reader/RandomAccess.d.ts +55 -0
- package/dist/reader/RandomAccess.d.ts.map +1 -0
- package/dist/reader/RandomAccess.js +528 -0
- package/dist/reader/RandomAccess.js.map +1 -0
- package/dist/reader/ZipReader.d.ts +89 -0
- package/dist/reader/ZipReader.d.ts.map +1 -0
- package/dist/reader/ZipReader.js +1359 -0
- package/dist/reader/ZipReader.js.map +1 -0
- package/dist/reader/centralDirectory.d.ts +40 -0
- package/dist/reader/centralDirectory.d.ts.map +1 -0
- package/dist/reader/centralDirectory.js +311 -0
- package/dist/reader/centralDirectory.js.map +1 -0
- package/dist/reader/entryStream.d.ts +22 -0
- package/dist/reader/entryStream.d.ts.map +1 -0
- package/dist/reader/entryStream.js +122 -0
- package/dist/reader/entryStream.js.map +1 -0
- package/dist/reader/eocd.d.ts +22 -0
- package/dist/reader/eocd.d.ts.map +1 -0
- package/dist/reader/eocd.js +184 -0
- package/dist/reader/eocd.js.map +1 -0
- package/dist/reader/httpZipErrors.d.ts +4 -0
- package/dist/reader/httpZipErrors.d.ts.map +1 -0
- package/dist/reader/httpZipErrors.js +48 -0
- package/dist/reader/httpZipErrors.js.map +1 -0
- package/dist/reader/localHeader.d.ts +15 -0
- package/dist/reader/localHeader.d.ts.map +1 -0
- package/dist/reader/localHeader.js +37 -0
- package/dist/reader/localHeader.js.map +1 -0
- package/dist/reportSchema.d.ts +3 -0
- package/dist/reportSchema.d.ts.map +1 -0
- package/dist/reportSchema.js +3 -0
- package/dist/reportSchema.js.map +1 -0
- package/dist/streams/adapters.d.ts +10 -0
- package/dist/streams/adapters.d.ts.map +1 -0
- package/dist/streams/adapters.js +54 -0
- package/dist/streams/adapters.js.map +1 -0
- package/dist/streams/buffer.d.ts +5 -0
- package/dist/streams/buffer.d.ts.map +1 -0
- package/dist/streams/buffer.js +44 -0
- package/dist/streams/buffer.js.map +1 -0
- package/dist/streams/crcTransform.d.ts +15 -0
- package/dist/streams/crcTransform.d.ts.map +1 -0
- package/dist/streams/crcTransform.js +30 -0
- package/dist/streams/crcTransform.js.map +1 -0
- package/dist/streams/emit.d.ts +7 -0
- package/dist/streams/emit.d.ts.map +1 -0
- package/dist/streams/emit.js +13 -0
- package/dist/streams/emit.js.map +1 -0
- package/dist/streams/limits.d.ts +16 -0
- package/dist/streams/limits.d.ts.map +1 -0
- package/dist/streams/limits.js +39 -0
- package/dist/streams/limits.js.map +1 -0
- package/dist/streams/measure.d.ts +5 -0
- package/dist/streams/measure.d.ts.map +1 -0
- package/dist/streams/measure.js +9 -0
- package/dist/streams/measure.js.map +1 -0
- package/dist/streams/progress.d.ts +8 -0
- package/dist/streams/progress.d.ts.map +1 -0
- package/dist/streams/progress.js +69 -0
- package/dist/streams/progress.js.map +1 -0
- package/dist/streams/web.d.ts +5 -0
- package/dist/streams/web.d.ts.map +1 -0
- package/dist/streams/web.js +33 -0
- package/dist/streams/web.js.map +1 -0
- package/dist/tar/TarReader.d.ts +41 -0
- package/dist/tar/TarReader.d.ts.map +1 -0
- package/dist/tar/TarReader.js +930 -0
- package/dist/tar/TarReader.js.map +1 -0
- package/dist/tar/TarWriter.d.ts +25 -0
- package/dist/tar/TarWriter.d.ts.map +1 -0
- package/dist/tar/TarWriter.js +307 -0
- package/dist/tar/TarWriter.js.map +1 -0
- package/dist/tar/index.d.ts +4 -0
- package/dist/tar/index.d.ts.map +1 -0
- package/dist/tar/index.js +3 -0
- package/dist/tar/index.js.map +1 -0
- package/dist/tar/types.d.ts +67 -0
- package/dist/tar/types.d.ts.map +1 -0
- package/dist/tar/types.js +2 -0
- package/dist/tar/types.js.map +1 -0
- package/dist/text/caseFold.d.ts +7 -0
- package/dist/text/caseFold.d.ts.map +1 -0
- package/dist/text/caseFold.js +45 -0
- package/dist/text/caseFold.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web/index.d.ts +11 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +95 -0
- package/dist/web/index.js.map +1 -0
- package/dist/writer/Sink.d.ts +21 -0
- package/dist/writer/Sink.d.ts.map +1 -0
- package/dist/writer/Sink.js +24 -0
- package/dist/writer/Sink.js.map +1 -0
- package/dist/writer/ZipWriter.d.ts +27 -0
- package/dist/writer/ZipWriter.d.ts.map +1 -0
- package/dist/writer/ZipWriter.js +153 -0
- package/dist/writer/ZipWriter.js.map +1 -0
- package/dist/writer/centralDirectoryWriter.d.ts +8 -0
- package/dist/writer/centralDirectoryWriter.d.ts.map +1 -0
- package/dist/writer/centralDirectoryWriter.js +77 -0
- package/dist/writer/centralDirectoryWriter.js.map +1 -0
- package/dist/writer/entryWriter.d.ts +54 -0
- package/dist/writer/entryWriter.d.ts.map +1 -0
- package/dist/writer/entryWriter.js +327 -0
- package/dist/writer/entryWriter.js.map +1 -0
- package/dist/writer/finalize.d.ts +10 -0
- package/dist/writer/finalize.d.ts.map +1 -0
- package/dist/writer/finalize.js +56 -0
- package/dist/writer/finalize.js.map +1 -0
- package/dist/zip/index.d.ts +8 -0
- package/dist/zip/index.d.ts.map +1 -0
- package/dist/zip/index.js +5 -0
- package/dist/zip/index.js.map +1 -0
- package/jsr.json +41 -0
- package/package.json +117 -0
- package/schemas/audit-report.schema.json +38 -0
- package/schemas/capabilities-report.schema.json +25 -0
- package/schemas/detection-report.schema.json +23 -0
- package/schemas/error.schema.json +22 -0
- package/schemas/normalize-report.schema.json +47 -0
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
import { ZipError } from '../errors.js';
|
|
2
|
+
import { mergeSignals, throwIfAborted } from '../abort.js';
|
|
3
|
+
import { BYTEFOLD_REPORT_SCHEMA_VERSION } from '../reportSchema.js';
|
|
4
|
+
import { BufferRandomAccess, HttpRandomAccess } from './RandomAccess.js';
|
|
5
|
+
import { wrapRandomAccessForZip } from './httpZipErrors.js';
|
|
6
|
+
import { findEocd } from './eocd.js';
|
|
7
|
+
import { iterCentralDirectory } from './centralDirectory.js';
|
|
8
|
+
import { openEntryStream, openRawStream } from './entryStream.js';
|
|
9
|
+
import { buildAesExtra, parseAesExtra } from '../extraFields.js';
|
|
10
|
+
import { readLocalHeader } from './localHeader.js';
|
|
11
|
+
import { readableFromBytes } from '../streams/web.js';
|
|
12
|
+
import { readAllBytes } from '../streams/buffer.js';
|
|
13
|
+
import { createCrcTransform } from '../streams/crcTransform.js';
|
|
14
|
+
import { createProgressTracker, createProgressTransform } from '../streams/progress.js';
|
|
15
|
+
import { normalizePathForCollision, toCollisionKey } from '../text/caseFold.js';
|
|
16
|
+
import { WebWritableSink } from '../writer/Sink.js';
|
|
17
|
+
import { writeCentralDirectory } from '../writer/centralDirectoryWriter.js';
|
|
18
|
+
import { finalizeArchive } from '../writer/finalize.js';
|
|
19
|
+
import { writeRawEntry } from '../writer/entryWriter.js';
|
|
20
|
+
import { getCompressionCodec, hasCompressionCodec } from '../compression/registry.js';
|
|
21
|
+
import { AGENT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS } from '../limits.js';
|
|
22
|
+
const DEFAULT_LIMITS = DEFAULT_RESOURCE_LIMITS;
|
|
23
|
+
const AGENT_LIMITS = AGENT_RESOURCE_LIMITS;
|
|
24
|
+
/** Read ZIP archives from bytes, streams, or URLs. */
|
|
25
|
+
export class ZipReader {
|
|
26
|
+
/** @internal */
|
|
27
|
+
profile;
|
|
28
|
+
/** @internal */
|
|
29
|
+
strict;
|
|
30
|
+
/** @internal */
|
|
31
|
+
limits;
|
|
32
|
+
/** @internal */
|
|
33
|
+
warningsList = [];
|
|
34
|
+
/** @internal */
|
|
35
|
+
entriesList = null;
|
|
36
|
+
/** @internal */
|
|
37
|
+
password;
|
|
38
|
+
/** @internal */
|
|
39
|
+
storeEntries;
|
|
40
|
+
/** @internal */
|
|
41
|
+
eocd = null;
|
|
42
|
+
/** @internal */
|
|
43
|
+
signal;
|
|
44
|
+
/** @internal */
|
|
45
|
+
reader;
|
|
46
|
+
/** @internal */
|
|
47
|
+
constructor(reader, options) {
|
|
48
|
+
const resolved = resolveReaderProfile(options);
|
|
49
|
+
this.reader = reader;
|
|
50
|
+
this.profile = resolved.profile;
|
|
51
|
+
this.strict = resolved.strict;
|
|
52
|
+
this.limits = resolved.limits;
|
|
53
|
+
this.password = options?.password;
|
|
54
|
+
this.storeEntries = options?.shouldStoreEntries ?? true;
|
|
55
|
+
this.signal = mergeSignals(options?.signal, options?.http?.signal);
|
|
56
|
+
}
|
|
57
|
+
/** @internal */
|
|
58
|
+
static async fromRandomAccess(reader, options) {
|
|
59
|
+
const instance = new ZipReader(wrapRandomAccessForZip(reader), options);
|
|
60
|
+
await instance.init();
|
|
61
|
+
return instance;
|
|
62
|
+
}
|
|
63
|
+
/** Create a reader from in-memory bytes. */
|
|
64
|
+
static async fromUint8Array(data, options) {
|
|
65
|
+
const reader = new BufferRandomAccess(data);
|
|
66
|
+
const instance = new ZipReader(wrapRandomAccessForZip(reader), options);
|
|
67
|
+
await instance.init();
|
|
68
|
+
return instance;
|
|
69
|
+
}
|
|
70
|
+
/** Create a reader from a readable stream. */
|
|
71
|
+
static async fromStream(stream, options) {
|
|
72
|
+
const readOptions = {};
|
|
73
|
+
if (options?.signal)
|
|
74
|
+
readOptions.signal = options.signal;
|
|
75
|
+
if (options?.limits?.maxInputBytes !== undefined) {
|
|
76
|
+
readOptions.maxBytes = options.limits.maxInputBytes;
|
|
77
|
+
}
|
|
78
|
+
else if (options?.limits?.maxTotalDecompressedBytes !== undefined) {
|
|
79
|
+
readOptions.maxBytes = options.limits.maxTotalDecompressedBytes;
|
|
80
|
+
}
|
|
81
|
+
else if (options?.limits?.maxTotalUncompressedBytes !== undefined) {
|
|
82
|
+
readOptions.maxBytes = options.limits.maxTotalUncompressedBytes;
|
|
83
|
+
}
|
|
84
|
+
const data = await readAllBytes(stream, readOptions);
|
|
85
|
+
return ZipReader.fromUint8Array(data, options);
|
|
86
|
+
}
|
|
87
|
+
/** Create a reader from a URL using HTTP range requests when possible. */
|
|
88
|
+
static async fromUrl(url, options) {
|
|
89
|
+
const httpSignal = mergeSignals(options?.signal, options?.http?.signal);
|
|
90
|
+
const httpOptions = options?.http ? { ...options.http } : {};
|
|
91
|
+
if (httpSignal) {
|
|
92
|
+
httpOptions.signal = httpSignal;
|
|
93
|
+
}
|
|
94
|
+
const reader = new HttpRandomAccess(url, Object.keys(httpOptions).length > 0 ? httpOptions : undefined);
|
|
95
|
+
const instance = new ZipReader(wrapRandomAccessForZip(reader), options);
|
|
96
|
+
await instance.init();
|
|
97
|
+
return instance;
|
|
98
|
+
}
|
|
99
|
+
/** Return stored entries (requires shouldStoreEntries=true). */
|
|
100
|
+
entries() {
|
|
101
|
+
if (!this.storeEntries) {
|
|
102
|
+
throw new ZipError('ZIP_ENTRIES_NOT_STORED', 'Entries are not stored; use iterEntries() or enable shouldStoreEntries');
|
|
103
|
+
}
|
|
104
|
+
if (!this.entriesList)
|
|
105
|
+
return [];
|
|
106
|
+
return this.entriesList.map((entry) => ({ ...entry }));
|
|
107
|
+
}
|
|
108
|
+
/** Return non-fatal warnings encountered during parsing. */
|
|
109
|
+
warnings() {
|
|
110
|
+
return [...this.warningsList];
|
|
111
|
+
}
|
|
112
|
+
/** Iterate entries lazily (or from cached entries). */
|
|
113
|
+
async *iterEntries(options) {
|
|
114
|
+
const signal = this.resolveSignal(options?.signal);
|
|
115
|
+
throwIfAborted(signal);
|
|
116
|
+
if (this.entriesList) {
|
|
117
|
+
for (const entry of this.entriesList) {
|
|
118
|
+
throwIfAborted(signal);
|
|
119
|
+
yield { ...entry };
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!this.eocd) {
|
|
124
|
+
throw new ZipError('ZIP_BAD_CENTRAL_DIRECTORY', 'Central directory has not been initialized');
|
|
125
|
+
}
|
|
126
|
+
const entries = [];
|
|
127
|
+
const totals = { totalUncompressed: 0n };
|
|
128
|
+
for await (const entry of iterCentralDirectory(this.reader, this.eocd.cdOffset, this.eocd.cdSize, this.eocd.totalEntries, {
|
|
129
|
+
strict: this.strict,
|
|
130
|
+
maxEntries: this.limits.maxEntries,
|
|
131
|
+
onWarning: (warning) => this.warningsList.push(warning),
|
|
132
|
+
...(signal ? { signal } : {})
|
|
133
|
+
})) {
|
|
134
|
+
throwIfAborted(signal);
|
|
135
|
+
this.applyEntryLimits(entry, totals);
|
|
136
|
+
if (this.storeEntries)
|
|
137
|
+
entries.push(entry);
|
|
138
|
+
yield { ...entry };
|
|
139
|
+
}
|
|
140
|
+
if (this.storeEntries) {
|
|
141
|
+
this.entriesList = entries;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Iterate entries with a callback. */
|
|
145
|
+
async forEachEntry(fn, options) {
|
|
146
|
+
for await (const entry of this.iterEntries(options)) {
|
|
147
|
+
await fn(entry);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Open a decoded stream for an entry. */
|
|
151
|
+
async open(entry, options) {
|
|
152
|
+
const strict = options?.isStrict ?? this.strict;
|
|
153
|
+
const signal = this.resolveSignal(options?.signal);
|
|
154
|
+
const totals = { totalUncompressed: 0n };
|
|
155
|
+
const params = {
|
|
156
|
+
strict,
|
|
157
|
+
onWarning: (warning) => this.warningsList.push(warning)
|
|
158
|
+
};
|
|
159
|
+
const password = options?.password ?? this.password;
|
|
160
|
+
if (password !== undefined) {
|
|
161
|
+
params.password = password;
|
|
162
|
+
}
|
|
163
|
+
return this.openEntryStream(entry, {
|
|
164
|
+
...params,
|
|
165
|
+
...(signal ? { signal } : {}),
|
|
166
|
+
...progressParams(options),
|
|
167
|
+
limits: this.limits,
|
|
168
|
+
totals
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Open a raw (compressed) stream for an entry. */
|
|
172
|
+
async openRaw(entry, options) {
|
|
173
|
+
const signal = this.resolveSignal(options?.signal);
|
|
174
|
+
const { stream } = await openRawStream(this.reader, entry, {
|
|
175
|
+
...(signal ? { signal } : {}),
|
|
176
|
+
...progressParams(options)
|
|
177
|
+
});
|
|
178
|
+
return stream;
|
|
179
|
+
}
|
|
180
|
+
/** @internal */
|
|
181
|
+
async openEntryStream(entry, options) {
|
|
182
|
+
return openEntryStream(this.reader, entry, options);
|
|
183
|
+
}
|
|
184
|
+
/** Normalize to a writable stream, producing a report. */
|
|
185
|
+
async normalizeToWritable(writable, options) {
|
|
186
|
+
const sink = new WebWritableSink(writable);
|
|
187
|
+
return this.normalizeToSink(sink, options);
|
|
188
|
+
}
|
|
189
|
+
/** Audit the archive and return a report of issues. */
|
|
190
|
+
async audit(options) {
|
|
191
|
+
const settings = this.resolveAuditSettings(options);
|
|
192
|
+
const signal = this.resolveSignal(options?.signal);
|
|
193
|
+
throwIfAborted(signal);
|
|
194
|
+
const issues = [];
|
|
195
|
+
const summary = {
|
|
196
|
+
entries: 0,
|
|
197
|
+
encryptedEntries: 0,
|
|
198
|
+
unsupportedEntries: 0,
|
|
199
|
+
warnings: 0,
|
|
200
|
+
errors: 0
|
|
201
|
+
};
|
|
202
|
+
const unsupportedEntries = new Set();
|
|
203
|
+
const ranges = [];
|
|
204
|
+
const seenNames = new Map();
|
|
205
|
+
const seenNfc = new Map();
|
|
206
|
+
const seenCase = new Map();
|
|
207
|
+
let totalUncompressed = 0n;
|
|
208
|
+
let totalExceeded = false;
|
|
209
|
+
const addIssue = (issue) => {
|
|
210
|
+
issues.push(issue);
|
|
211
|
+
if (issue.severity === 'warning')
|
|
212
|
+
summary.warnings += 1;
|
|
213
|
+
if (issue.severity === 'error')
|
|
214
|
+
summary.errors += 1;
|
|
215
|
+
};
|
|
216
|
+
const addParseWarning = (warning) => {
|
|
217
|
+
const severity = settings.strict ? 'error' : 'warning';
|
|
218
|
+
addIssue({
|
|
219
|
+
code: warning.code,
|
|
220
|
+
severity,
|
|
221
|
+
message: warning.message,
|
|
222
|
+
...(warning.entryName ? { entryName: warning.entryName } : {})
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
let size;
|
|
226
|
+
try {
|
|
227
|
+
size = await this.reader.size(signal);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
addIssue(issueFromError(err));
|
|
231
|
+
return finalizeAuditReport(issues, summary);
|
|
232
|
+
}
|
|
233
|
+
let eocd;
|
|
234
|
+
try {
|
|
235
|
+
eocd = await findEocd(this.reader, false, signal, {
|
|
236
|
+
maxSearchBytes: settings.limits.maxZipEocdSearchBytes,
|
|
237
|
+
maxCommentBytes: settings.limits.maxZipCommentBytes,
|
|
238
|
+
maxCentralDirectoryBytes: settings.limits.maxZipCentralDirectoryBytes,
|
|
239
|
+
maxEntries: settings.limits.maxEntries,
|
|
240
|
+
rejectMultiDisk: true
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
addIssue(issueFromError(err));
|
|
245
|
+
return finalizeAuditReport(issues, summary);
|
|
246
|
+
}
|
|
247
|
+
for (const warning of eocd.warnings) {
|
|
248
|
+
addParseWarning(warning);
|
|
249
|
+
}
|
|
250
|
+
const eocdEnd = eocd.eocdOffset + 22n + BigInt(eocd.comment.length);
|
|
251
|
+
const trailingBytes = size > eocdEnd ? size - eocdEnd : 0n;
|
|
252
|
+
if (trailingBytes > 0n) {
|
|
253
|
+
const severity = settings.rejectTrailingBytes ? 'error' : 'warning';
|
|
254
|
+
const trailingBytesNumber = toSafeNumber(trailingBytes);
|
|
255
|
+
if (trailingBytesNumber !== undefined) {
|
|
256
|
+
summary.trailingBytes = trailingBytesNumber;
|
|
257
|
+
}
|
|
258
|
+
addIssue({
|
|
259
|
+
code: 'ZIP_TRAILING_BYTES',
|
|
260
|
+
severity,
|
|
261
|
+
message: `Trailing bytes after EOCD: ${trailingBytes.toString()}`,
|
|
262
|
+
offset: eocdEnd.toString(),
|
|
263
|
+
details: { trailingBytes: trailingBytes.toString() }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const cdEnd = eocd.cdOffset + eocd.cdSize;
|
|
267
|
+
if (eocd.cdOffset < 0n || cdEnd > size) {
|
|
268
|
+
addIssue({
|
|
269
|
+
code: 'ZIP_OUT_OF_RANGE',
|
|
270
|
+
severity: 'error',
|
|
271
|
+
message: 'Central directory is outside file bounds',
|
|
272
|
+
offset: eocd.cdOffset.toString(),
|
|
273
|
+
details: {
|
|
274
|
+
cdOffset: eocd.cdOffset.toString(),
|
|
275
|
+
cdSize: eocd.cdSize.toString(),
|
|
276
|
+
fileSize: size.toString()
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
for await (const entry of iterCentralDirectory(this.reader, eocd.cdOffset, eocd.cdSize, eocd.totalEntries, {
|
|
282
|
+
strict: false,
|
|
283
|
+
maxEntries: settings.limits.maxEntries,
|
|
284
|
+
onWarning: addParseWarning,
|
|
285
|
+
...(signal ? { signal } : {})
|
|
286
|
+
})) {
|
|
287
|
+
throwIfAborted(signal);
|
|
288
|
+
summary.entries += 1;
|
|
289
|
+
if (entry.encrypted)
|
|
290
|
+
summary.encryptedEntries += 1;
|
|
291
|
+
const normalizedName = normalizePathForCollision(entry.name, entry.isDirectory);
|
|
292
|
+
if (normalizedName) {
|
|
293
|
+
const existing = seenNames.get(normalizedName);
|
|
294
|
+
if (existing) {
|
|
295
|
+
existing.count += 1;
|
|
296
|
+
addIssue({
|
|
297
|
+
code: 'ZIP_DUPLICATE_ENTRY',
|
|
298
|
+
severity: 'warning',
|
|
299
|
+
message: `Duplicate entry name: ${existing.original} vs ${entry.name}`,
|
|
300
|
+
entryName: entry.name,
|
|
301
|
+
details: {
|
|
302
|
+
occurrences: existing.count,
|
|
303
|
+
otherName: existing.original,
|
|
304
|
+
key: normalizedName,
|
|
305
|
+
collisionKind: 'duplicate'
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
seenNames.set(normalizedName, { count: 1, original: entry.name });
|
|
311
|
+
const nfcName = normalizedName.normalize('NFC');
|
|
312
|
+
const caseKey = toCollisionKey(normalizedName, entry.isDirectory);
|
|
313
|
+
const existingNfc = seenNfc.get(nfcName);
|
|
314
|
+
if (existingNfc && existingNfc.normalized !== normalizedName) {
|
|
315
|
+
addIssue({
|
|
316
|
+
code: 'ZIP_UNICODE_COLLISION',
|
|
317
|
+
severity: 'error',
|
|
318
|
+
message: `Unicode normalization collision: ${existingNfc.original} vs ${entry.name}`,
|
|
319
|
+
entryName: entry.name,
|
|
320
|
+
details: { otherName: existingNfc.original, key: nfcName, collisionKind: 'unicode_nfc' }
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const existingCase = seenCase.get(caseKey);
|
|
325
|
+
if (existingCase && existingCase.nfc !== nfcName) {
|
|
326
|
+
addIssue({
|
|
327
|
+
code: 'ZIP_CASE_COLLISION',
|
|
328
|
+
severity: 'warning',
|
|
329
|
+
message: `Case-insensitive name collision: ${existingCase.original} vs ${entry.name}`,
|
|
330
|
+
entryName: entry.name,
|
|
331
|
+
details: { otherName: existingCase.original, key: caseKey, collisionKind: 'casefold' }
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
seenNfc.set(nfcName, { original: entry.name, normalized: normalizedName });
|
|
336
|
+
seenCase.set(caseKey, { original: entry.name, nfc: nfcName });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const issue of entryPathIssues(entry.name)) {
|
|
340
|
+
addIssue(issue);
|
|
341
|
+
}
|
|
342
|
+
if (entry.isSymlink) {
|
|
343
|
+
addIssue({
|
|
344
|
+
code: 'ZIP_SYMLINK_PRESENT',
|
|
345
|
+
severity: settings.symlinkSeverity,
|
|
346
|
+
message: 'Symlink entry present',
|
|
347
|
+
entryName: entry.name
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const aesExtra = entry.method === 99 ? parseAesExtra(entry.extra.get(0x9901) ?? new Uint8Array(0)) : undefined;
|
|
351
|
+
if (entry.encrypted) {
|
|
352
|
+
if ((entry.flags & 0x40) !== 0) {
|
|
353
|
+
addIssue({
|
|
354
|
+
code: 'ZIP_UNSUPPORTED_ENCRYPTION',
|
|
355
|
+
severity: 'error',
|
|
356
|
+
message: 'Strong encryption is not supported',
|
|
357
|
+
entryName: entry.name
|
|
358
|
+
});
|
|
359
|
+
unsupportedEntries.add(entry.name);
|
|
360
|
+
}
|
|
361
|
+
else if (entry.method === 99 && !aesExtra) {
|
|
362
|
+
addIssue({
|
|
363
|
+
code: 'ZIP_UNSUPPORTED_ENCRYPTION',
|
|
364
|
+
severity: 'error',
|
|
365
|
+
message: 'AES encryption extra field missing or invalid',
|
|
366
|
+
entryName: entry.name
|
|
367
|
+
});
|
|
368
|
+
unsupportedEntries.add(entry.name);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const methodToCheck = entry.method === 99 ? aesExtra?.actualMethod : entry.method;
|
|
372
|
+
if (methodToCheck !== undefined && !hasCompressionCodec(methodToCheck)) {
|
|
373
|
+
addIssue({
|
|
374
|
+
code: 'ZIP_UNSUPPORTED_METHOD',
|
|
375
|
+
severity: 'error',
|
|
376
|
+
message: `Unsupported compression method ${methodToCheck}`,
|
|
377
|
+
entryName: entry.name,
|
|
378
|
+
details: { method: methodToCheck }
|
|
379
|
+
});
|
|
380
|
+
unsupportedEntries.add(entry.name);
|
|
381
|
+
}
|
|
382
|
+
if (entry.uncompressedSize > settings.limits.maxUncompressedEntryBytes) {
|
|
383
|
+
addIssue({
|
|
384
|
+
code: 'ZIP_LIMIT_EXCEEDED',
|
|
385
|
+
severity: 'error',
|
|
386
|
+
message: 'Entry exceeds max uncompressed size',
|
|
387
|
+
entryName: entry.name,
|
|
388
|
+
details: {
|
|
389
|
+
limit: settings.limits.maxUncompressedEntryBytes.toString(),
|
|
390
|
+
size: entry.uncompressedSize.toString()
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
totalUncompressed += entry.uncompressedSize;
|
|
395
|
+
if (!totalExceeded && totalUncompressed > settings.limits.maxTotalUncompressedBytes) {
|
|
396
|
+
totalExceeded = true;
|
|
397
|
+
addIssue({
|
|
398
|
+
code: 'ZIP_LIMIT_EXCEEDED',
|
|
399
|
+
severity: 'error',
|
|
400
|
+
message: 'Total uncompressed size exceeds limit',
|
|
401
|
+
details: {
|
|
402
|
+
limit: settings.limits.maxTotalUncompressedBytes.toString(),
|
|
403
|
+
size: totalUncompressed.toString()
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (entry.compressedSize > 0n) {
|
|
408
|
+
const ratio = Number(entry.uncompressedSize) / Number(entry.compressedSize);
|
|
409
|
+
if (ratio > settings.limits.maxCompressionRatio) {
|
|
410
|
+
addIssue({
|
|
411
|
+
code: 'ZIP_LIMIT_EXCEEDED',
|
|
412
|
+
severity: settings.strict ? 'error' : 'warning',
|
|
413
|
+
message: 'Compression ratio exceeds safety limit',
|
|
414
|
+
entryName: entry.name,
|
|
415
|
+
details: { ratio, limit: settings.limits.maxCompressionRatio }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (entry.offset < 0n || entry.offset >= size) {
|
|
420
|
+
addIssue({
|
|
421
|
+
code: 'ZIP_OUT_OF_RANGE',
|
|
422
|
+
severity: 'error',
|
|
423
|
+
message: 'Local header offset is outside file bounds',
|
|
424
|
+
entryName: entry.name,
|
|
425
|
+
offset: entry.offset.toString(),
|
|
426
|
+
details: { offset: entry.offset.toString(), fileSize: size.toString() }
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const local = await readLocalHeader(this.reader, entry, signal);
|
|
432
|
+
const mismatchDetails = collectHeaderMismatches(entry, local);
|
|
433
|
+
if (mismatchDetails) {
|
|
434
|
+
addIssue({
|
|
435
|
+
code: 'ZIP_HEADER_MISMATCH',
|
|
436
|
+
severity: 'error',
|
|
437
|
+
message: 'Local header does not match central directory',
|
|
438
|
+
entryName: entry.name,
|
|
439
|
+
offset: entry.offset.toString(),
|
|
440
|
+
details: mismatchDetails
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const dataEnd = local.dataOffset + entry.compressedSize;
|
|
444
|
+
if (dataEnd > size) {
|
|
445
|
+
addIssue({
|
|
446
|
+
code: 'ZIP_OUT_OF_RANGE',
|
|
447
|
+
severity: 'error',
|
|
448
|
+
message: 'Entry data extends beyond file bounds',
|
|
449
|
+
entryName: entry.name,
|
|
450
|
+
offset: local.dataOffset.toString(),
|
|
451
|
+
details: {
|
|
452
|
+
dataOffset: local.dataOffset.toString(),
|
|
453
|
+
dataEnd: dataEnd.toString(),
|
|
454
|
+
fileSize: size.toString()
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
ranges.push({ start: entry.offset, end: dataEnd, entryName: entry.name });
|
|
460
|
+
if (dataEnd > eocd.cdOffset) {
|
|
461
|
+
addIssue({
|
|
462
|
+
code: 'ZIP_OUT_OF_RANGE',
|
|
463
|
+
severity: 'error',
|
|
464
|
+
message: 'Entry data overlaps central directory',
|
|
465
|
+
entryName: entry.name,
|
|
466
|
+
details: {
|
|
467
|
+
dataEnd: dataEnd.toString(),
|
|
468
|
+
cdOffset: eocd.cdOffset.toString()
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
const details = errorDetails(err);
|
|
476
|
+
addIssue({
|
|
477
|
+
code: 'ZIP_HEADER_MISMATCH',
|
|
478
|
+
severity: 'error',
|
|
479
|
+
message: 'Failed to read local header',
|
|
480
|
+
entryName: entry.name,
|
|
481
|
+
offset: entry.offset.toString(),
|
|
482
|
+
...(details ? { details } : {})
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
addIssue(issueFromError(err));
|
|
489
|
+
}
|
|
490
|
+
ranges.sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : 0));
|
|
491
|
+
for (let i = 1; i < ranges.length; i += 1) {
|
|
492
|
+
const prev = ranges[i - 1];
|
|
493
|
+
const curr = ranges[i];
|
|
494
|
+
if (curr.start < prev.end) {
|
|
495
|
+
addIssue({
|
|
496
|
+
code: 'ZIP_OVERLAPPING_ENTRIES',
|
|
497
|
+
severity: 'error',
|
|
498
|
+
message: 'Entry data ranges overlap',
|
|
499
|
+
entryName: curr.entryName,
|
|
500
|
+
details: {
|
|
501
|
+
previousEntry: prev.entryName,
|
|
502
|
+
previousEnd: prev.end.toString(),
|
|
503
|
+
currentStart: curr.start.toString()
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
summary.unsupportedEntries = unsupportedEntries.size;
|
|
509
|
+
return finalizeAuditReport(issues, summary);
|
|
510
|
+
}
|
|
511
|
+
/** Audit and throw if the archive fails the selected profile. */
|
|
512
|
+
async assertSafe(options) {
|
|
513
|
+
const report = await this.audit(options);
|
|
514
|
+
const profile = options?.profile ?? this.profile;
|
|
515
|
+
const treatWarningsAsErrors = profile === 'agent';
|
|
516
|
+
const ok = report.ok && (!treatWarningsAsErrors || report.summary.warnings === 0);
|
|
517
|
+
if (ok)
|
|
518
|
+
return;
|
|
519
|
+
const message = treatWarningsAsErrors
|
|
520
|
+
? 'ZIP audit reported warnings or errors'
|
|
521
|
+
: 'ZIP audit reported errors';
|
|
522
|
+
throw new ZipError('ZIP_AUDIT_FAILED', message, { cause: report });
|
|
523
|
+
}
|
|
524
|
+
/** Release underlying resources held by the reader. */
|
|
525
|
+
async close() {
|
|
526
|
+
await this.reader.close();
|
|
527
|
+
}
|
|
528
|
+
/** Async dispose hook for using with `using` in supported runtimes. */
|
|
529
|
+
async [Symbol.asyncDispose]() {
|
|
530
|
+
await this.close();
|
|
531
|
+
}
|
|
532
|
+
/** @internal */
|
|
533
|
+
async init() {
|
|
534
|
+
const eocd = await findEocd(this.reader, this.strict, this.signal, {
|
|
535
|
+
maxSearchBytes: this.limits.maxZipEocdSearchBytes,
|
|
536
|
+
maxCommentBytes: this.limits.maxZipCommentBytes,
|
|
537
|
+
maxCentralDirectoryBytes: this.limits.maxZipCentralDirectoryBytes,
|
|
538
|
+
maxEntries: this.limits.maxEntries,
|
|
539
|
+
rejectMultiDisk: true
|
|
540
|
+
});
|
|
541
|
+
this.warningsList.push(...eocd.warnings);
|
|
542
|
+
this.eocd = eocd;
|
|
543
|
+
if (this.storeEntries) {
|
|
544
|
+
await this.loadEntries();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** @internal */
|
|
548
|
+
async loadEntries() {
|
|
549
|
+
if (this.entriesList)
|
|
550
|
+
return;
|
|
551
|
+
for await (const _ of this.iterEntries()) {
|
|
552
|
+
// iterEntries populates entriesList when storeEntries is enabled.
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** @internal */
|
|
556
|
+
applyEntryLimits(entry, totals) {
|
|
557
|
+
if (entry.uncompressedSize > this.limits.maxUncompressedEntryBytes) {
|
|
558
|
+
throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Entry exceeds max uncompressed size', {
|
|
559
|
+
entryName: entry.name
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
totals.totalUncompressed += entry.uncompressedSize;
|
|
563
|
+
if (totals.totalUncompressed > this.limits.maxTotalUncompressedBytes) {
|
|
564
|
+
throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Total uncompressed size exceeds limit');
|
|
565
|
+
}
|
|
566
|
+
if (entry.compressedSize > 0n) {
|
|
567
|
+
const ratio = Number(entry.uncompressedSize) / Number(entry.compressedSize);
|
|
568
|
+
if (ratio > this.limits.maxCompressionRatio) {
|
|
569
|
+
const message = 'Compression ratio exceeds safety limit';
|
|
570
|
+
if (this.strict) {
|
|
571
|
+
throw new ZipError('ZIP_LIMIT_EXCEEDED', message, { entryName: entry.name });
|
|
572
|
+
}
|
|
573
|
+
this.warningsList.push({ code: 'ZIP_LIMIT_EXCEEDED', message, entryName: entry.name });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** @internal */
|
|
578
|
+
resolveAuditSettings(options) {
|
|
579
|
+
const profile = options?.profile ?? this.profile;
|
|
580
|
+
const defaults = profile === this.profile
|
|
581
|
+
? { strict: this.strict, limits: this.limits }
|
|
582
|
+
: resolveProfileDefaults(profile);
|
|
583
|
+
const strict = options?.isStrict ?? defaults.strict;
|
|
584
|
+
const limits = normalizeLimits(options?.limits, defaults.limits);
|
|
585
|
+
return {
|
|
586
|
+
profile,
|
|
587
|
+
strict,
|
|
588
|
+
limits,
|
|
589
|
+
rejectTrailingBytes: profile === 'agent',
|
|
590
|
+
symlinkSeverity: profile === 'agent' ? 'error' : 'warning'
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** @internal */
|
|
594
|
+
resolveSignal(signal) {
|
|
595
|
+
return mergeSignals(this.signal, signal);
|
|
596
|
+
}
|
|
597
|
+
/** @internal */
|
|
598
|
+
async normalizeToSink(sink, options) {
|
|
599
|
+
const signal = this.resolveSignal(options?.signal);
|
|
600
|
+
throwIfAborted(signal);
|
|
601
|
+
const mode = options?.mode ?? 'safe';
|
|
602
|
+
const deterministic = options?.isDeterministic ?? true;
|
|
603
|
+
const onDuplicate = options?.onDuplicate ?? 'error';
|
|
604
|
+
const onCaseCollision = options?.onCaseCollision ?? 'error';
|
|
605
|
+
const onUnsupported = options?.onUnsupported ?? 'error';
|
|
606
|
+
const onSymlink = options?.onSymlink ?? 'error';
|
|
607
|
+
const preserveComments = options?.shouldPreserveComments ?? false;
|
|
608
|
+
const preserveTrailingBytes = options?.shouldPreserveTrailingBytes ?? false;
|
|
609
|
+
const limits = normalizeLimits(options?.limits, this.limits);
|
|
610
|
+
const outputMethod = options?.method ?? 8;
|
|
611
|
+
const password = options?.password ?? this.password;
|
|
612
|
+
const fixedMtime = new Date(1980, 0, 1, 0, 0, 0);
|
|
613
|
+
const issues = [];
|
|
614
|
+
const summary = {
|
|
615
|
+
entries: 0,
|
|
616
|
+
encryptedEntries: 0,
|
|
617
|
+
unsupportedEntries: 0,
|
|
618
|
+
warnings: 0,
|
|
619
|
+
errors: 0,
|
|
620
|
+
outputEntries: 0,
|
|
621
|
+
droppedEntries: 0,
|
|
622
|
+
renamedEntries: 0,
|
|
623
|
+
recompressedEntries: 0,
|
|
624
|
+
preservedEntries: 0
|
|
625
|
+
};
|
|
626
|
+
const addIssue = (issue) => {
|
|
627
|
+
issues.push(issue);
|
|
628
|
+
if (issue.severity === 'warning')
|
|
629
|
+
summary.warnings += 1;
|
|
630
|
+
if (issue.severity === 'error')
|
|
631
|
+
summary.errors += 1;
|
|
632
|
+
};
|
|
633
|
+
const normalizedEntries = await this.collectNormalizedEntries({
|
|
634
|
+
...(signal ? { signal } : {}),
|
|
635
|
+
deterministic,
|
|
636
|
+
onDuplicate,
|
|
637
|
+
onCaseCollision,
|
|
638
|
+
onSymlink,
|
|
639
|
+
issues,
|
|
640
|
+
addIssue,
|
|
641
|
+
summary
|
|
642
|
+
});
|
|
643
|
+
const results = [];
|
|
644
|
+
const totals = { totalUncompressed: 0n };
|
|
645
|
+
try {
|
|
646
|
+
for (const item of normalizedEntries) {
|
|
647
|
+
throwIfAborted(signal);
|
|
648
|
+
if (item.dropped)
|
|
649
|
+
continue;
|
|
650
|
+
const entry = item.entry;
|
|
651
|
+
if (entry.encrypted)
|
|
652
|
+
summary.encryptedEntries += 1;
|
|
653
|
+
const name = item.normalizedName;
|
|
654
|
+
const mtime = deterministic ? fixedMtime : entry.mtime;
|
|
655
|
+
const externalAttributes = deterministic ? (entry.isDirectory ? 0x10 : 0) : entry.externalAttributes;
|
|
656
|
+
const comment = preserveComments && !deterministic ? entry.comment : undefined;
|
|
657
|
+
const aesExtra = entry.method === 99 ? parseAesExtra(entry.extra.get(0x9901) ?? new Uint8Array(0)) : undefined;
|
|
658
|
+
if (entry.method === 99 && !aesExtra) {
|
|
659
|
+
addIssue({
|
|
660
|
+
code: 'ZIP_UNSUPPORTED_ENCRYPTION',
|
|
661
|
+
severity: 'error',
|
|
662
|
+
message: 'AES extra field missing; cannot normalize entry',
|
|
663
|
+
entryName: entry.name
|
|
664
|
+
});
|
|
665
|
+
summary.unsupportedEntries += 1;
|
|
666
|
+
if (onUnsupported === 'drop') {
|
|
667
|
+
summary.droppedEntries += 1;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
throw new ZipError('ZIP_UNSUPPORTED_ENCRYPTION', 'Missing AES extra field', {
|
|
671
|
+
entryName: entry.name
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
if (entry.isSymlink) {
|
|
675
|
+
if (onSymlink === 'drop') {
|
|
676
|
+
summary.droppedEntries += 1;
|
|
677
|
+
addIssue({
|
|
678
|
+
code: 'ZIP_SYMLINK_PRESENT',
|
|
679
|
+
severity: 'warning',
|
|
680
|
+
message: 'Symlink entry dropped during normalization',
|
|
681
|
+
entryName: entry.name
|
|
682
|
+
});
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (onSymlink === 'error') {
|
|
686
|
+
addIssue({
|
|
687
|
+
code: 'ZIP_SYMLINK_PRESENT',
|
|
688
|
+
severity: 'error',
|
|
689
|
+
message: 'Symlink entries are not allowed during normalization',
|
|
690
|
+
entryName: entry.name
|
|
691
|
+
});
|
|
692
|
+
throw new ZipError('ZIP_SYMLINK_DISALLOWED', 'Symlink entries are not allowed during normalization', {
|
|
693
|
+
entryName: entry.name
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (mode === 'safe') {
|
|
698
|
+
const methodToCheck = entry.method === 99 ? aesExtra?.actualMethod : entry.method;
|
|
699
|
+
if (methodToCheck !== undefined && !hasCompressionCodec(methodToCheck)) {
|
|
700
|
+
addIssue({
|
|
701
|
+
code: 'ZIP_UNSUPPORTED_METHOD',
|
|
702
|
+
severity: 'error',
|
|
703
|
+
message: `Unsupported compression method ${methodToCheck}`,
|
|
704
|
+
entryName: entry.name,
|
|
705
|
+
details: { method: methodToCheck }
|
|
706
|
+
});
|
|
707
|
+
summary.unsupportedEntries += 1;
|
|
708
|
+
if (onUnsupported === 'drop') {
|
|
709
|
+
summary.droppedEntries += 1;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
throw new ZipError('ZIP_UNSUPPORTED_METHOD', `Unsupported compression method ${methodToCheck}`, {
|
|
713
|
+
entryName: entry.name,
|
|
714
|
+
method: methodToCheck
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const method = entry.isDirectory ? 0 : outputMethod;
|
|
718
|
+
const codec = getCompressionCodec(method);
|
|
719
|
+
if (!codec || !codec.createCompressStream) {
|
|
720
|
+
addIssue({
|
|
721
|
+
code: 'ZIP_UNSUPPORTED_METHOD',
|
|
722
|
+
severity: 'error',
|
|
723
|
+
message: `Unsupported compression method ${method}`,
|
|
724
|
+
entryName: entry.name,
|
|
725
|
+
details: { method }
|
|
726
|
+
});
|
|
727
|
+
summary.unsupportedEntries += 1;
|
|
728
|
+
if (onUnsupported === 'drop') {
|
|
729
|
+
summary.droppedEntries += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
throw new ZipError('ZIP_UNSUPPORTED_METHOD', `Unsupported compression method ${method}`, {
|
|
733
|
+
entryName: entry.name,
|
|
734
|
+
method
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (entry.encrypted) {
|
|
738
|
+
addIssue({
|
|
739
|
+
code: 'ZIP_UNSUPPORTED_ENCRYPTION',
|
|
740
|
+
severity: 'error',
|
|
741
|
+
message: 'Encrypted entries are not supported during normalization in this runtime',
|
|
742
|
+
entryName: entry.name
|
|
743
|
+
});
|
|
744
|
+
summary.unsupportedEntries += 1;
|
|
745
|
+
if (onUnsupported === 'drop') {
|
|
746
|
+
summary.droppedEntries += 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
throw new ZipError('ZIP_UNSUPPORTED_ENCRYPTION', 'Encrypted entries are not supported', {
|
|
750
|
+
entryName: entry.name
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
let source;
|
|
754
|
+
if (entry.isDirectory) {
|
|
755
|
+
source = readableFromBytes(new Uint8Array(0));
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
source = await this.openEntryStream(entry, {
|
|
759
|
+
strict: true,
|
|
760
|
+
onWarning: (warning) => addIssue({
|
|
761
|
+
code: warning.code,
|
|
762
|
+
severity: 'warning',
|
|
763
|
+
message: warning.message,
|
|
764
|
+
...(warning.entryName ? { entryName: warning.entryName } : {})
|
|
765
|
+
}),
|
|
766
|
+
...(password !== undefined ? { password } : {}),
|
|
767
|
+
...(signal ? { signal } : {}),
|
|
768
|
+
...progressParams(options),
|
|
769
|
+
limits,
|
|
770
|
+
totals
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const spool = await spoolCompressedEntryToMemory({
|
|
774
|
+
source,
|
|
775
|
+
method,
|
|
776
|
+
entryName: name,
|
|
777
|
+
...(signal ? { signal } : {}),
|
|
778
|
+
progress: progressParams(options)
|
|
779
|
+
});
|
|
780
|
+
const dataStream = readableFromBytes(spool.data);
|
|
781
|
+
const flags = 0x800;
|
|
782
|
+
const result = await writeRawEntry(sink, {
|
|
783
|
+
name,
|
|
784
|
+
source: dataStream,
|
|
785
|
+
method,
|
|
786
|
+
flags,
|
|
787
|
+
crc32: spool.crc32,
|
|
788
|
+
compressedSize: spool.compressedSize,
|
|
789
|
+
uncompressedSize: spool.uncompressedSize,
|
|
790
|
+
mtime,
|
|
791
|
+
comment,
|
|
792
|
+
externalAttributes,
|
|
793
|
+
zip64Mode: 'auto',
|
|
794
|
+
forceZip64: false,
|
|
795
|
+
...(signal ? { signal } : {}),
|
|
796
|
+
...(options ? { progress: progressParams(options) } : {})
|
|
797
|
+
});
|
|
798
|
+
results.push(result);
|
|
799
|
+
summary.outputEntries += 1;
|
|
800
|
+
summary.recompressedEntries += entry.isDirectory ? 0 : 1;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
// lossless mode
|
|
804
|
+
const methodToCheck = entry.method === 99 ? aesExtra?.actualMethod : entry.method;
|
|
805
|
+
if (methodToCheck !== undefined && !hasCompressionCodec(methodToCheck)) {
|
|
806
|
+
summary.unsupportedEntries += 1;
|
|
807
|
+
addIssue({
|
|
808
|
+
code: 'ZIP_UNSUPPORTED_METHOD',
|
|
809
|
+
severity: 'warning',
|
|
810
|
+
message: `Unsupported compression method ${methodToCheck} preserved in lossless mode`,
|
|
811
|
+
entryName: entry.name,
|
|
812
|
+
details: { method: methodToCheck }
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
const { stream: rawStream } = await openRawStream(this.reader, entry, {
|
|
816
|
+
...(signal ? { signal } : {}),
|
|
817
|
+
...progressParams(options)
|
|
818
|
+
});
|
|
819
|
+
const flags = 0x800 | (entry.encrypted ? 0x01 : 0);
|
|
820
|
+
const aesExtraBytes = aesExtra
|
|
821
|
+
? buildAesExtra({
|
|
822
|
+
vendorVersion: aesExtra.vendorVersion,
|
|
823
|
+
strength: aesExtra.strength,
|
|
824
|
+
actualMethod: aesExtra.actualMethod
|
|
825
|
+
})
|
|
826
|
+
: undefined;
|
|
827
|
+
const result = await writeRawEntry(sink, {
|
|
828
|
+
name,
|
|
829
|
+
source: rawStream,
|
|
830
|
+
method: entry.method,
|
|
831
|
+
flags,
|
|
832
|
+
crc32: entry.crc32,
|
|
833
|
+
compressedSize: entry.compressedSize,
|
|
834
|
+
uncompressedSize: entry.uncompressedSize,
|
|
835
|
+
mtime,
|
|
836
|
+
comment,
|
|
837
|
+
externalAttributes,
|
|
838
|
+
zip64Mode: 'auto',
|
|
839
|
+
forceZip64: false,
|
|
840
|
+
...(aesExtraBytes ? { aesExtra: aesExtraBytes } : {}),
|
|
841
|
+
...(signal ? { signal } : {}),
|
|
842
|
+
...(options ? { progress: progressParams(options) } : {})
|
|
843
|
+
});
|
|
844
|
+
results.push(result);
|
|
845
|
+
summary.outputEntries += 1;
|
|
846
|
+
summary.preservedEntries += 1;
|
|
847
|
+
}
|
|
848
|
+
const cdInfo = await writeCentralDirectory(sink, results, signal);
|
|
849
|
+
const finalizeOptions = {
|
|
850
|
+
entryCount: BigInt(results.length),
|
|
851
|
+
cdOffset: cdInfo.offset,
|
|
852
|
+
cdSize: cdInfo.size,
|
|
853
|
+
forceZip64: false,
|
|
854
|
+
hasZip64Entries: results.some((entry) => entry.zip64)
|
|
855
|
+
};
|
|
856
|
+
await finalizeArchive(sink, finalizeOptions, signal);
|
|
857
|
+
if (preserveTrailingBytes) {
|
|
858
|
+
const trailing = await readTrailingBytes(this.reader, this.eocd, signal);
|
|
859
|
+
if (trailing.length > 0) {
|
|
860
|
+
await sink.write(trailing);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
await sink.close();
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
await sink.close().catch(() => { });
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
869
|
+
summary.entries = normalizedEntries.length;
|
|
870
|
+
const report = finalizeNormalizeReport(issues, summary);
|
|
871
|
+
return report;
|
|
872
|
+
}
|
|
873
|
+
/** @internal */
|
|
874
|
+
async collectNormalizedEntries(params) {
|
|
875
|
+
const entries = [];
|
|
876
|
+
const nameIndex = new Map();
|
|
877
|
+
const caseIndex = new Map();
|
|
878
|
+
const nfcIndex = new Map();
|
|
879
|
+
const originalNames = new Map();
|
|
880
|
+
const iterOptions = params.signal ? { signal: params.signal } : undefined;
|
|
881
|
+
for await (const entry of this.iterEntries(iterOptions)) {
|
|
882
|
+
params.summary.entries += 1;
|
|
883
|
+
const normalizedName = normalizeEntryName(entry.name, entry.isDirectory, params.addIssue);
|
|
884
|
+
let targetName = normalizedName;
|
|
885
|
+
let renamed = false;
|
|
886
|
+
const existingIndex = nameIndex.get(targetName);
|
|
887
|
+
if (existingIndex !== undefined) {
|
|
888
|
+
if (params.onDuplicate === 'error') {
|
|
889
|
+
const existingName = originalNames.get(targetName) ?? targetName;
|
|
890
|
+
params.addIssue({
|
|
891
|
+
code: 'ZIP_DUPLICATE_ENTRY',
|
|
892
|
+
severity: 'error',
|
|
893
|
+
message: `Duplicate entry name: ${existingName} vs ${entry.name}`,
|
|
894
|
+
entryName: entry.name,
|
|
895
|
+
details: { collisionKind: 'duplicate', otherName: existingName, key: targetName }
|
|
896
|
+
});
|
|
897
|
+
throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (duplicate). Rename entries to avoid collisions.', {
|
|
898
|
+
entryName: entry.name,
|
|
899
|
+
context: buildCollisionContext('duplicate', existingName, entry.name, targetName, 'zip')
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (params.onDuplicate === 'last-wins') {
|
|
903
|
+
entries[existingIndex].dropped = true;
|
|
904
|
+
params.summary.droppedEntries += 1;
|
|
905
|
+
params.addIssue({
|
|
906
|
+
code: 'ZIP_DUPLICATE_ENTRY',
|
|
907
|
+
severity: 'warning',
|
|
908
|
+
message: `Duplicate entry name replaced by last occurrence: ${targetName}`,
|
|
909
|
+
entryName: entry.name
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
else if (params.onDuplicate === 'rename') {
|
|
913
|
+
targetName = resolveConflictName(targetName, nameIndex, caseIndex);
|
|
914
|
+
renamed = true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const nfcName = targetName.normalize('NFC');
|
|
918
|
+
const existingNfc = nfcIndex.get(nfcName);
|
|
919
|
+
if (existingNfc && existingNfc.target !== targetName) {
|
|
920
|
+
params.addIssue({
|
|
921
|
+
code: 'ZIP_UNICODE_COLLISION',
|
|
922
|
+
severity: 'error',
|
|
923
|
+
message: `Unicode normalization collision: ${existingNfc.original} vs ${entry.name}`,
|
|
924
|
+
entryName: entry.name,
|
|
925
|
+
details: { collisionKind: 'unicode_nfc', otherName: existingNfc.original, key: nfcName }
|
|
926
|
+
});
|
|
927
|
+
throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (unicode_nfc). Rename entries to avoid collisions.', {
|
|
928
|
+
entryName: entry.name,
|
|
929
|
+
context: buildCollisionContext('unicode_nfc', existingNfc.original, entry.name, nfcName, 'zip')
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
const caseKey = toCollisionKey(targetName, entry.isDirectory);
|
|
933
|
+
const existingCase = caseIndex.get(caseKey);
|
|
934
|
+
if (existingCase && existingCase.target !== targetName) {
|
|
935
|
+
if (params.onCaseCollision === 'error') {
|
|
936
|
+
params.addIssue({
|
|
937
|
+
code: 'ZIP_CASE_COLLISION',
|
|
938
|
+
severity: 'error',
|
|
939
|
+
message: `Case-insensitive name collision: ${existingCase.original} vs ${entry.name}`,
|
|
940
|
+
entryName: entry.name,
|
|
941
|
+
details: { collisionKind: 'casefold', otherName: existingCase.original, key: caseKey }
|
|
942
|
+
});
|
|
943
|
+
throw new ZipError('ZIP_NAME_COLLISION', 'Name collision detected (case). Rename entries to avoid collisions.', {
|
|
944
|
+
entryName: entry.name,
|
|
945
|
+
context: buildCollisionContext('case', existingCase.original, entry.name, caseKey, 'zip')
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (params.onCaseCollision === 'last-wins') {
|
|
949
|
+
const previous = nameIndex.get(existingCase.target);
|
|
950
|
+
if (previous !== undefined) {
|
|
951
|
+
entries[previous].dropped = true;
|
|
952
|
+
params.summary.droppedEntries += 1;
|
|
953
|
+
}
|
|
954
|
+
params.addIssue({
|
|
955
|
+
code: 'ZIP_CASE_COLLISION',
|
|
956
|
+
severity: 'warning',
|
|
957
|
+
message: `Case-insensitive collision replaced by last occurrence: ${targetName}`,
|
|
958
|
+
entryName: entry.name
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
else if (params.onCaseCollision === 'rename') {
|
|
962
|
+
targetName = resolveConflictName(targetName, nameIndex, caseIndex);
|
|
963
|
+
renamed = true;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
nameIndex.set(targetName, entries.length);
|
|
967
|
+
originalNames.set(targetName, entry.name);
|
|
968
|
+
const finalNfc = targetName.normalize('NFC');
|
|
969
|
+
nfcIndex.set(finalNfc, { original: entry.name, target: targetName });
|
|
970
|
+
caseIndex.set(toCollisionKey(targetName, entry.isDirectory), {
|
|
971
|
+
original: entry.name,
|
|
972
|
+
target: targetName,
|
|
973
|
+
nfc: finalNfc
|
|
974
|
+
});
|
|
975
|
+
if (renamed) {
|
|
976
|
+
params.summary.renamedEntries += 1;
|
|
977
|
+
params.addIssue({
|
|
978
|
+
code: 'ZIP_NORMALIZED_NAME',
|
|
979
|
+
severity: 'info',
|
|
980
|
+
message: `Entry renamed to ${targetName}`,
|
|
981
|
+
entryName: entry.name,
|
|
982
|
+
details: { normalizedName: targetName }
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
else if (targetName !== entry.name) {
|
|
986
|
+
params.addIssue({
|
|
987
|
+
code: 'ZIP_NORMALIZED_NAME',
|
|
988
|
+
severity: 'info',
|
|
989
|
+
message: `Entry name normalized to ${targetName}`,
|
|
990
|
+
entryName: entry.name,
|
|
991
|
+
details: { normalizedName: targetName }
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
entries.push({
|
|
995
|
+
entry: entry,
|
|
996
|
+
normalizedName: targetName,
|
|
997
|
+
dropped: false
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (params.deterministic) {
|
|
1001
|
+
entries.sort((a, b) => (a.normalizedName < b.normalizedName ? -1 : a.normalizedName > b.normalizedName ? 1 : 0));
|
|
1002
|
+
}
|
|
1003
|
+
return entries;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function normalizeEntryName(entryName, isDirectory, addIssue) {
|
|
1007
|
+
if (entryName.includes('\u0000')) {
|
|
1008
|
+
addIssue({
|
|
1009
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1010
|
+
severity: 'error',
|
|
1011
|
+
message: 'Entry name contains NUL byte',
|
|
1012
|
+
entryName
|
|
1013
|
+
});
|
|
1014
|
+
throw new ZipError('ZIP_PATH_TRAVERSAL', 'Entry name contains NUL byte', { entryName });
|
|
1015
|
+
}
|
|
1016
|
+
const normalized = entryName.replace(/\\/g, '/');
|
|
1017
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized)) {
|
|
1018
|
+
addIssue({
|
|
1019
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1020
|
+
severity: 'error',
|
|
1021
|
+
message: 'Absolute paths are not allowed in ZIP entries',
|
|
1022
|
+
entryName
|
|
1023
|
+
});
|
|
1024
|
+
throw new ZipError('ZIP_PATH_TRAVERSAL', 'Absolute paths are not allowed in ZIP entries', { entryName });
|
|
1025
|
+
}
|
|
1026
|
+
const parts = normalized.split('/').filter((part) => part.length > 0 && part !== '.');
|
|
1027
|
+
if (parts.some((part) => part === '..')) {
|
|
1028
|
+
addIssue({
|
|
1029
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1030
|
+
severity: 'error',
|
|
1031
|
+
message: 'Path traversal detected in ZIP entry',
|
|
1032
|
+
entryName
|
|
1033
|
+
});
|
|
1034
|
+
throw new ZipError('ZIP_PATH_TRAVERSAL', 'Path traversal detected in ZIP entry', { entryName });
|
|
1035
|
+
}
|
|
1036
|
+
let name = parts.join('/');
|
|
1037
|
+
if (isDirectory && !name.endsWith('/')) {
|
|
1038
|
+
name = name.length > 0 ? `${name}/` : '';
|
|
1039
|
+
}
|
|
1040
|
+
if (name.length === 0) {
|
|
1041
|
+
addIssue({
|
|
1042
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1043
|
+
severity: 'error',
|
|
1044
|
+
message: 'Entry name resolves to empty path',
|
|
1045
|
+
entryName
|
|
1046
|
+
});
|
|
1047
|
+
throw new ZipError('ZIP_PATH_TRAVERSAL', 'Entry name resolves to empty path', { entryName });
|
|
1048
|
+
}
|
|
1049
|
+
return name;
|
|
1050
|
+
}
|
|
1051
|
+
function resolveConflictName(name, nameIndex, lowerIndex) {
|
|
1052
|
+
const trailingSlash = name.endsWith('/');
|
|
1053
|
+
const trimmed = trailingSlash ? name.slice(0, -1) : name;
|
|
1054
|
+
const slashIndex = trimmed.lastIndexOf('/');
|
|
1055
|
+
const dir = slashIndex >= 0 ? trimmed.slice(0, slashIndex + 1) : '';
|
|
1056
|
+
const file = slashIndex >= 0 ? trimmed.slice(slashIndex + 1) : trimmed;
|
|
1057
|
+
const dotIndex = file.lastIndexOf('.');
|
|
1058
|
+
const base = dotIndex > 0 ? file.slice(0, dotIndex) : file;
|
|
1059
|
+
const ext = dotIndex > 0 ? file.slice(dotIndex) : '';
|
|
1060
|
+
let counter = 1;
|
|
1061
|
+
while (true) {
|
|
1062
|
+
const candidate = `${dir}${base}~${counter}${ext}${trailingSlash ? '/' : ''}`;
|
|
1063
|
+
const caseKey = toCollisionKey(candidate, trailingSlash);
|
|
1064
|
+
if (!nameIndex.has(candidate) && !lowerIndex.has(caseKey)) {
|
|
1065
|
+
return candidate;
|
|
1066
|
+
}
|
|
1067
|
+
counter += 1;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function buildCollisionContext(collisionType, nameA, nameB, key, format, collisionKind = collisionType === 'case' ? 'casefold' : collisionType) {
|
|
1071
|
+
return {
|
|
1072
|
+
collisionType,
|
|
1073
|
+
collisionKind,
|
|
1074
|
+
nameA,
|
|
1075
|
+
nameB,
|
|
1076
|
+
key,
|
|
1077
|
+
format
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
async function spoolCompressedEntryToMemory(options) {
|
|
1081
|
+
const codec = getCompressionCodec(options.method);
|
|
1082
|
+
if (!codec || !codec.createCompressStream) {
|
|
1083
|
+
throw new ZipError('ZIP_UNSUPPORTED_METHOD', `Unsupported compression method ${options.method}`, {
|
|
1084
|
+
entryName: options.entryName,
|
|
1085
|
+
method: options.method
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
const crcResult = { crc32: 0, bytes: 0n };
|
|
1089
|
+
const compressTracker = createProgressTracker(options.progress, {
|
|
1090
|
+
kind: 'compress',
|
|
1091
|
+
entryName: options.entryName
|
|
1092
|
+
});
|
|
1093
|
+
let stream = options.source;
|
|
1094
|
+
stream = stream.pipeThrough(createCrcTransform(crcResult, { strict: true }));
|
|
1095
|
+
stream = stream.pipeThrough(createProgressTransform(compressTracker));
|
|
1096
|
+
const transform = await codec.createCompressStream();
|
|
1097
|
+
stream = stream.pipeThrough(transform);
|
|
1098
|
+
const readOptions = {};
|
|
1099
|
+
if (options.signal)
|
|
1100
|
+
readOptions.signal = options.signal;
|
|
1101
|
+
const data = await readAllBytes(stream, readOptions);
|
|
1102
|
+
return {
|
|
1103
|
+
data,
|
|
1104
|
+
compressedSize: BigInt(data.length),
|
|
1105
|
+
uncompressedSize: crcResult.bytes,
|
|
1106
|
+
crc32: crcResult.crc32
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async function readTrailingBytes(reader, eocd, signal) {
|
|
1110
|
+
if (!eocd)
|
|
1111
|
+
return new Uint8Array(0);
|
|
1112
|
+
const size = await reader.size(signal);
|
|
1113
|
+
const eocdEnd = eocd.eocdOffset + 22n + BigInt(eocd.comment.length);
|
|
1114
|
+
if (size <= eocdEnd)
|
|
1115
|
+
return new Uint8Array(0);
|
|
1116
|
+
const trailing = size - eocdEnd;
|
|
1117
|
+
const trailingNumber = toSafeNumber(trailing);
|
|
1118
|
+
if (trailingNumber === undefined) {
|
|
1119
|
+
throw new ZipError('ZIP_LIMIT_EXCEEDED', 'Trailing bytes too large to preserve');
|
|
1120
|
+
}
|
|
1121
|
+
return reader.read(eocdEnd, trailingNumber, signal);
|
|
1122
|
+
}
|
|
1123
|
+
function finalizeNormalizeReport(issues, summary) {
|
|
1124
|
+
const sanitizedIssues = issues.map((issue) => ({
|
|
1125
|
+
...issue,
|
|
1126
|
+
...(issue.details ? { details: sanitizeDetails(issue.details) } : {})
|
|
1127
|
+
}));
|
|
1128
|
+
const report = {
|
|
1129
|
+
schemaVersion: BYTEFOLD_REPORT_SCHEMA_VERSION,
|
|
1130
|
+
ok: summary.errors === 0,
|
|
1131
|
+
summary,
|
|
1132
|
+
issues: sanitizedIssues
|
|
1133
|
+
};
|
|
1134
|
+
report.toJSON = () => ({
|
|
1135
|
+
schemaVersion: BYTEFOLD_REPORT_SCHEMA_VERSION,
|
|
1136
|
+
ok: report.ok,
|
|
1137
|
+
summary: report.summary,
|
|
1138
|
+
issues: sanitizedIssues.map(issueToJson)
|
|
1139
|
+
});
|
|
1140
|
+
return report;
|
|
1141
|
+
}
|
|
1142
|
+
function resolveReaderProfile(options) {
|
|
1143
|
+
const profile = options?.profile ?? 'strict';
|
|
1144
|
+
const defaults = profile === 'agent' ? AGENT_LIMITS : DEFAULT_LIMITS;
|
|
1145
|
+
const strictDefault = profile === 'compat' ? false : true;
|
|
1146
|
+
const strict = options?.isStrict ?? strictDefault;
|
|
1147
|
+
const limits = normalizeLimits(options?.limits, defaults);
|
|
1148
|
+
return { profile, strict, limits };
|
|
1149
|
+
}
|
|
1150
|
+
function resolveProfileDefaults(profile) {
|
|
1151
|
+
if (profile === 'compat') {
|
|
1152
|
+
return { strict: false, limits: DEFAULT_LIMITS };
|
|
1153
|
+
}
|
|
1154
|
+
if (profile === 'agent') {
|
|
1155
|
+
return { strict: true, limits: AGENT_LIMITS };
|
|
1156
|
+
}
|
|
1157
|
+
return { strict: true, limits: DEFAULT_LIMITS };
|
|
1158
|
+
}
|
|
1159
|
+
/** @internal */
|
|
1160
|
+
export function __getZipDefaultsForProfile(profile) {
|
|
1161
|
+
return resolveProfileDefaults(profile).limits;
|
|
1162
|
+
}
|
|
1163
|
+
function normalizeLimits(limits, defaults = DEFAULT_LIMITS) {
|
|
1164
|
+
const maxTotal = toBigInt(limits?.maxTotalDecompressedBytes ?? limits?.maxTotalUncompressedBytes) ??
|
|
1165
|
+
defaults.maxTotalUncompressedBytes;
|
|
1166
|
+
return {
|
|
1167
|
+
maxEntries: limits?.maxEntries ?? defaults.maxEntries,
|
|
1168
|
+
maxUncompressedEntryBytes: toBigInt(limits?.maxUncompressedEntryBytes) ?? defaults.maxUncompressedEntryBytes,
|
|
1169
|
+
maxTotalUncompressedBytes: maxTotal,
|
|
1170
|
+
maxTotalDecompressedBytes: maxTotal,
|
|
1171
|
+
maxCompressionRatio: limits?.maxCompressionRatio ?? defaults.maxCompressionRatio,
|
|
1172
|
+
maxDictionaryBytes: toBigInt(limits?.maxDictionaryBytes) ?? defaults.maxDictionaryBytes,
|
|
1173
|
+
maxXzDictionaryBytes: toBigInt(limits?.maxXzDictionaryBytes ?? limits?.maxDictionaryBytes) ?? defaults.maxXzDictionaryBytes,
|
|
1174
|
+
maxXzBufferedBytes: typeof limits?.maxXzBufferedBytes === 'number' && Number.isFinite(limits.maxXzBufferedBytes)
|
|
1175
|
+
? Math.max(1, Math.floor(limits.maxXzBufferedBytes))
|
|
1176
|
+
: defaults.maxXzBufferedBytes,
|
|
1177
|
+
maxXzIndexRecords: typeof limits?.maxXzIndexRecords === 'number' && Number.isFinite(limits.maxXzIndexRecords)
|
|
1178
|
+
? Math.max(1, Math.floor(limits.maxXzIndexRecords))
|
|
1179
|
+
: defaults.maxXzIndexRecords,
|
|
1180
|
+
maxXzIndexBytes: typeof limits?.maxXzIndexBytes === 'number' && Number.isFinite(limits.maxXzIndexBytes)
|
|
1181
|
+
? Math.max(8, Math.floor(limits.maxXzIndexBytes))
|
|
1182
|
+
: defaults.maxXzIndexBytes,
|
|
1183
|
+
maxXzPreflightBlockHeaders: typeof limits?.maxXzPreflightBlockHeaders === 'number' && Number.isFinite(limits.maxXzPreflightBlockHeaders)
|
|
1184
|
+
? Math.max(0, Math.floor(limits.maxXzPreflightBlockHeaders))
|
|
1185
|
+
: defaults.maxXzPreflightBlockHeaders,
|
|
1186
|
+
maxZipCentralDirectoryBytes: typeof limits?.maxZipCentralDirectoryBytes === 'number' && Number.isFinite(limits.maxZipCentralDirectoryBytes)
|
|
1187
|
+
? Math.max(0, Math.floor(limits.maxZipCentralDirectoryBytes))
|
|
1188
|
+
: defaults.maxZipCentralDirectoryBytes,
|
|
1189
|
+
maxZipCommentBytes: typeof limits?.maxZipCommentBytes === 'number' && Number.isFinite(limits.maxZipCommentBytes)
|
|
1190
|
+
? Math.max(0, Math.floor(limits.maxZipCommentBytes))
|
|
1191
|
+
: defaults.maxZipCommentBytes,
|
|
1192
|
+
maxZipEocdSearchBytes: typeof limits?.maxZipEocdSearchBytes === 'number' && Number.isFinite(limits.maxZipEocdSearchBytes)
|
|
1193
|
+
? Math.max(22, Math.floor(limits.maxZipEocdSearchBytes))
|
|
1194
|
+
: defaults.maxZipEocdSearchBytes,
|
|
1195
|
+
maxBzip2BlockSize: typeof limits?.maxBzip2BlockSize === 'number' && Number.isFinite(limits.maxBzip2BlockSize)
|
|
1196
|
+
? Math.max(1, Math.min(9, Math.floor(limits.maxBzip2BlockSize)))
|
|
1197
|
+
: defaults.maxBzip2BlockSize,
|
|
1198
|
+
maxInputBytes: toBigInt(limits?.maxInputBytes) ?? defaults.maxInputBytes
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function progressParams(options) {
|
|
1202
|
+
if (!options)
|
|
1203
|
+
return {};
|
|
1204
|
+
const out = {};
|
|
1205
|
+
if (options.onProgress)
|
|
1206
|
+
out.onProgress = options.onProgress;
|
|
1207
|
+
if (options.progressIntervalMs !== undefined)
|
|
1208
|
+
out.progressIntervalMs = options.progressIntervalMs;
|
|
1209
|
+
if (options.progressChunkInterval !== undefined)
|
|
1210
|
+
out.progressChunkInterval = options.progressChunkInterval;
|
|
1211
|
+
return out;
|
|
1212
|
+
}
|
|
1213
|
+
function toBigInt(value) {
|
|
1214
|
+
if (value === undefined)
|
|
1215
|
+
return undefined;
|
|
1216
|
+
return typeof value === 'bigint' ? value : BigInt(value);
|
|
1217
|
+
}
|
|
1218
|
+
function entryPathIssues(entryName) {
|
|
1219
|
+
const issues = [];
|
|
1220
|
+
if (entryName.includes('\u0000')) {
|
|
1221
|
+
issues.push({
|
|
1222
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1223
|
+
severity: 'error',
|
|
1224
|
+
message: 'Entry name contains NUL byte',
|
|
1225
|
+
entryName
|
|
1226
|
+
});
|
|
1227
|
+
return issues;
|
|
1228
|
+
}
|
|
1229
|
+
const normalized = entryName.replace(/\\/g, '/');
|
|
1230
|
+
if (normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized)) {
|
|
1231
|
+
issues.push({
|
|
1232
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1233
|
+
severity: 'error',
|
|
1234
|
+
message: 'Absolute paths are not allowed in ZIP entries',
|
|
1235
|
+
entryName
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
const parts = normalized.split('/').filter((part) => part.length > 0);
|
|
1239
|
+
if (parts.some((part) => part === '..')) {
|
|
1240
|
+
issues.push({
|
|
1241
|
+
code: 'ZIP_PATH_TRAVERSAL',
|
|
1242
|
+
severity: 'error',
|
|
1243
|
+
message: 'Path traversal detected in ZIP entry',
|
|
1244
|
+
entryName
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
return issues;
|
|
1248
|
+
}
|
|
1249
|
+
function collectHeaderMismatches(entry, local) {
|
|
1250
|
+
const details = {};
|
|
1251
|
+
if (entry.flags !== local.flags) {
|
|
1252
|
+
details.flags = { local: local.flags, central: entry.flags };
|
|
1253
|
+
}
|
|
1254
|
+
if (entry.method !== local.method) {
|
|
1255
|
+
details.method = { local: local.method, central: entry.method };
|
|
1256
|
+
}
|
|
1257
|
+
if (local.nameLen !== entry.rawNameBytes.length) {
|
|
1258
|
+
details.nameLength = { local: local.nameLen, central: entry.rawNameBytes.length };
|
|
1259
|
+
}
|
|
1260
|
+
if (!bytesEqual(local.nameBytes, entry.rawNameBytes)) {
|
|
1261
|
+
details.nameBytes = { mismatch: true };
|
|
1262
|
+
}
|
|
1263
|
+
if (local.extraLen !== entry.extraLength) {
|
|
1264
|
+
details.extraLength = { local: local.extraLen, central: entry.extraLength };
|
|
1265
|
+
}
|
|
1266
|
+
return Object.keys(details).length > 0 ? details : undefined;
|
|
1267
|
+
}
|
|
1268
|
+
function bytesEqual(a, b) {
|
|
1269
|
+
if (a.length !== b.length)
|
|
1270
|
+
return false;
|
|
1271
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
1272
|
+
if (a[i] !== b[i])
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
function issueFromError(err) {
|
|
1278
|
+
if (err instanceof ZipError) {
|
|
1279
|
+
return {
|
|
1280
|
+
code: err.code,
|
|
1281
|
+
severity: 'error',
|
|
1282
|
+
message: err.message,
|
|
1283
|
+
...(err.entryName ? { entryName: err.entryName } : {}),
|
|
1284
|
+
...(err.offset !== undefined ? { offset: err.offset.toString() } : {}),
|
|
1285
|
+
...(err.cause ? { details: { cause: String(err.cause) } } : {})
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (err instanceof Error) {
|
|
1289
|
+
return {
|
|
1290
|
+
code: 'ZIP_AUDIT_ERROR',
|
|
1291
|
+
severity: 'error',
|
|
1292
|
+
message: err.message,
|
|
1293
|
+
details: { name: err.name }
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
code: 'ZIP_AUDIT_ERROR',
|
|
1298
|
+
severity: 'error',
|
|
1299
|
+
message: 'Unknown audit error'
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function errorDetails(err) {
|
|
1303
|
+
if (err instanceof ZipError) {
|
|
1304
|
+
return { code: err.code, message: err.message };
|
|
1305
|
+
}
|
|
1306
|
+
if (err instanceof Error) {
|
|
1307
|
+
return { name: err.name, message: err.message };
|
|
1308
|
+
}
|
|
1309
|
+
return { value: String(err) };
|
|
1310
|
+
}
|
|
1311
|
+
function toSafeNumber(value) {
|
|
1312
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
1313
|
+
return undefined;
|
|
1314
|
+
return Number(value);
|
|
1315
|
+
}
|
|
1316
|
+
function finalizeAuditReport(issues, summary) {
|
|
1317
|
+
const sanitizedIssues = issues.map((issue) => ({
|
|
1318
|
+
...issue,
|
|
1319
|
+
...(issue.details ? { details: sanitizeDetails(issue.details) } : {})
|
|
1320
|
+
}));
|
|
1321
|
+
const report = {
|
|
1322
|
+
schemaVersion: BYTEFOLD_REPORT_SCHEMA_VERSION,
|
|
1323
|
+
ok: summary.errors === 0,
|
|
1324
|
+
summary,
|
|
1325
|
+
issues: sanitizedIssues
|
|
1326
|
+
};
|
|
1327
|
+
report.toJSON = () => ({
|
|
1328
|
+
schemaVersion: BYTEFOLD_REPORT_SCHEMA_VERSION,
|
|
1329
|
+
ok: report.ok,
|
|
1330
|
+
summary: report.summary,
|
|
1331
|
+
issues: sanitizedIssues.map(issueToJson)
|
|
1332
|
+
});
|
|
1333
|
+
return report;
|
|
1334
|
+
}
|
|
1335
|
+
function issueToJson(issue) {
|
|
1336
|
+
return {
|
|
1337
|
+
code: issue.code,
|
|
1338
|
+
severity: issue.severity,
|
|
1339
|
+
message: issue.message,
|
|
1340
|
+
...(issue.entryName ? { entryName: issue.entryName } : {}),
|
|
1341
|
+
...(issue.offset !== undefined ? { offset: issue.offset.toString() } : {}),
|
|
1342
|
+
...(issue.details ? { details: sanitizeDetails(issue.details) } : {})
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
function sanitizeDetails(value) {
|
|
1346
|
+
if (typeof value === 'bigint')
|
|
1347
|
+
return value.toString();
|
|
1348
|
+
if (Array.isArray(value))
|
|
1349
|
+
return value.map(sanitizeDetails);
|
|
1350
|
+
if (value && typeof value === 'object') {
|
|
1351
|
+
const out = {};
|
|
1352
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1353
|
+
out[key] = sanitizeDetails(val);
|
|
1354
|
+
}
|
|
1355
|
+
return out;
|
|
1356
|
+
}
|
|
1357
|
+
return value;
|
|
1358
|
+
}
|
|
1359
|
+
//# sourceMappingURL=ZipReader.js.map
|