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