neozipkit 1.0.2 → 1.0.5
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/NEOZIP_APPNOTE.md +527 -0
- package/README.md +8 -5
- package/WHATS_NEW.md +42 -10
- package/dist/browser/ZipkitBrowser.d.ts.map +1 -1
- package/dist/browser/ZipkitBrowser.js +4 -6
- package/dist/browser/ZipkitBrowser.js.map +1 -1
- package/dist/browser-esm/index.js +424 -82
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +424 -82
- package/dist/browser-umd/index.js.map +4 -4
- package/dist/browser-umd/index.min.js +3 -3
- package/dist/core/ZipCompress.d.ts.map +1 -1
- package/dist/core/ZipCompress.js +12 -11
- package/dist/core/ZipCompress.js.map +1 -1
- package/dist/core/ZipDecompress.d.ts.map +1 -1
- package/dist/core/ZipDecompress.js +13 -8
- package/dist/core/ZipDecompress.js.map +1 -1
- package/dist/core/ZipEntry.d.ts +6 -0
- package/dist/core/ZipEntry.d.ts.map +1 -1
- package/dist/core/ZipEntry.js +7 -1
- package/dist/core/ZipEntry.js.map +1 -1
- package/dist/core/Zipkit.d.ts +43 -11
- package/dist/core/Zipkit.d.ts.map +1 -1
- package/dist/core/Zipkit.js +106 -63
- package/dist/core/Zipkit.js.map +1 -1
- package/dist/core/components/HashCalculator.d.ts +22 -1
- package/dist/core/components/HashCalculator.d.ts.map +1 -1
- package/dist/core/components/HashCalculator.js +49 -6
- package/dist/core/components/HashCalculator.js.map +1 -1
- package/dist/core/components/Util.d.ts +3 -4
- package/dist/core/components/Util.d.ts.map +1 -1
- package/dist/core/components/Util.js +10 -8
- package/dist/core/components/Util.js.map +1 -1
- package/dist/core/constants/Headers.d.ts +1 -3
- package/dist/core/constants/Headers.d.ts.map +1 -1
- package/dist/core/constants/Headers.js +14 -5
- package/dist/core/constants/Headers.js.map +1 -1
- package/dist/core/constants/MetaPaths.d.ts +38 -0
- package/dist/core/constants/MetaPaths.d.ts.map +1 -0
- package/dist/core/constants/MetaPaths.js +79 -0
- package/dist/core/constants/MetaPaths.js.map +1 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +14 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/merkle/MerkleRoot.d.ts +109 -0
- package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
- package/dist/core/merkle/MerkleRoot.js +275 -0
- package/dist/core/merkle/MerkleRoot.js.map +1 -0
- package/dist/core/version.js +1 -1
- package/dist/node/ZipCompressNode.d.ts.map +1 -1
- package/dist/node/ZipCompressNode.js +40 -49
- package/dist/node/ZipCompressNode.js.map +1 -1
- package/dist/node/ZipDecompressNode.d.ts.map +1 -1
- package/dist/node/ZipDecompressNode.js +17 -1
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +9 -0
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +67 -4
- package/dist/node/ZipkitNode.js.map +1 -1
- package/package.json +3 -2
- package/src/browser/ZipkitBrowser.ts +4 -6
- package/src/core/ZipCompress.ts +14 -13
- package/src/core/ZipDecompress.ts +14 -8
- package/src/core/ZipEntry.ts +8 -1
- package/src/core/Zipkit.ts +149 -76
- package/src/core/components/HashCalculator.ts +58 -6
- package/src/core/components/Util.ts +16 -7
- package/src/core/constants/Headers.ts +14 -4
- package/src/core/constants/MetaPaths.ts +86 -0
- package/src/core/index.ts +14 -0
- package/src/core/merkle/MerkleRoot.ts +312 -0
- package/src/core/version.ts +1 -1
- package/src/node/ZipCompressNode.ts +40 -49
- package/src/node/ZipDecompressNode.ts +13 -1
- package/src/node/ZipkitNode.ts +82 -4
|
@@ -6166,6 +6166,53 @@ var Errors_default = {
|
|
|
6166
6166
|
ENCRYPT_FAILED: "Encryption failed"
|
|
6167
6167
|
};
|
|
6168
6168
|
|
|
6169
|
+
// src/core/constants/MetaPaths.ts
|
|
6170
|
+
var META_TOKEN_NZIP = "META-INF/TOKEN.NZIP";
|
|
6171
|
+
var META_TOKEN_NZIP_LEGACY = "META-INF/NZIP.TOKEN";
|
|
6172
|
+
var META_TS_SUBMIT_NZIP = "META-INF/TS-SUBMIT.NZIP";
|
|
6173
|
+
var META_TIMESTAMP_NZIP = "META-INF/TIMESTAMP.NZIP";
|
|
6174
|
+
var META_TS_SUBMIT_OTS = "META-INF/TS-SUBMIT.OTS";
|
|
6175
|
+
var META_TIMESTAMP_OTS = "META-INF/TIMESTAMP.OTS";
|
|
6176
|
+
var META_MANIFEST_JSON = "META-INF/manifest.json";
|
|
6177
|
+
var TIMESTAMP_SUBMITTED = META_TS_SUBMIT_OTS;
|
|
6178
|
+
var TIMESTAMP_METADATA = META_TIMESTAMP_OTS;
|
|
6179
|
+
var TOKENIZED_METADATA = META_TOKEN_NZIP;
|
|
6180
|
+
var RESERVED_CANONICAL = [
|
|
6181
|
+
META_TOKEN_NZIP,
|
|
6182
|
+
META_TOKEN_NZIP_LEGACY,
|
|
6183
|
+
META_TS_SUBMIT_NZIP,
|
|
6184
|
+
META_TIMESTAMP_NZIP,
|
|
6185
|
+
META_TS_SUBMIT_OTS,
|
|
6186
|
+
META_TIMESTAMP_OTS,
|
|
6187
|
+
META_MANIFEST_JSON
|
|
6188
|
+
];
|
|
6189
|
+
function asciiPathEqualsIgnoreCase(a, b) {
|
|
6190
|
+
if (a.length !== b.length) {
|
|
6191
|
+
}
|
|
6192
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
6193
|
+
}
|
|
6194
|
+
function isMetaInfPath(filename) {
|
|
6195
|
+
return filename.toLowerCase().startsWith("meta-inf/");
|
|
6196
|
+
}
|
|
6197
|
+
function isReservedMetaPath(filename) {
|
|
6198
|
+
const lower = filename.toLowerCase();
|
|
6199
|
+
return RESERVED_CANONICAL.some((c) => c.toLowerCase() === lower);
|
|
6200
|
+
}
|
|
6201
|
+
function findReservedMetaEntry(entries, preferredCanonicalNames) {
|
|
6202
|
+
const preferred = Array.isArray(preferredCanonicalNames) ? preferredCanonicalNames : [preferredCanonicalNames];
|
|
6203
|
+
for (const name of preferred) {
|
|
6204
|
+
const exact = entries.find((e) => e.filename === name);
|
|
6205
|
+
if (exact) return exact;
|
|
6206
|
+
}
|
|
6207
|
+
for (const name of preferred) {
|
|
6208
|
+
const folded = entries.find(
|
|
6209
|
+
(e) => typeof e.filename === "string" && asciiPathEqualsIgnoreCase(e.filename, name)
|
|
6210
|
+
);
|
|
6211
|
+
if (folded) return folded;
|
|
6212
|
+
}
|
|
6213
|
+
return null;
|
|
6214
|
+
}
|
|
6215
|
+
|
|
6169
6216
|
// src/core/constants/Headers.ts
|
|
6170
6217
|
var LOCAL_FILE_HEADER = "PK";
|
|
6171
6218
|
var CENTRAL_FILE_HEADER = "PK";
|
|
@@ -6173,9 +6220,6 @@ var CENTRAL_DIRECTORY_END = "PK";
|
|
|
6173
6220
|
var ZIP64_CENTRAL_DIRECTORY_LOCATOR = "PK\x07";
|
|
6174
6221
|
var ZIP64_CENTRAL_DIRECTORY_END = "PK";
|
|
6175
6222
|
var DATA_DESCRIPTOR = "PK\x07\b";
|
|
6176
|
-
var TIMESTAMP_SUBMITTED = "META-INF/TS-SUBMIT.OTS";
|
|
6177
|
-
var TIMESTAMP_METADATA = "META-INF/TIMESTAMP.OTS";
|
|
6178
|
-
var TOKENIZED_METADATA = "META-INF/NZIP.TOKEN";
|
|
6179
6223
|
var LOCAL_HDR = {
|
|
6180
6224
|
SIZE: 30,
|
|
6181
6225
|
// LOC header size in bytes
|
|
@@ -6416,7 +6460,7 @@ var HDR_ID = {
|
|
|
6416
6460
|
IBM2: 102,
|
|
6417
6461
|
// IBM S/390 (Z390) - Attribute
|
|
6418
6462
|
SHA256: 334,
|
|
6419
|
-
//
|
|
6463
|
+
// NeoZip Extra Field: 32-byte SHA-256 of uncompressed payload (APPNOTE §5)
|
|
6420
6464
|
NEO_CRYPTO: 590,
|
|
6421
6465
|
// NeoEncrypt metadata (see docs/NEO_CRYPTO_FORMAT.md); LE on wire: 4E 02
|
|
6422
6466
|
POSZIP: 18064,
|
|
@@ -7568,7 +7612,13 @@ var ZipEntry = class {
|
|
|
7568
7612
|
this.gid = null;
|
|
7569
7613
|
// Group ID
|
|
7570
7614
|
this.sha256 = null;
|
|
7571
|
-
// SHA-256 hash of the file
|
|
7615
|
+
// SHA-256 hash of the file (Extra Field 0x014E, bare content digest)
|
|
7616
|
+
/**
|
|
7617
|
+
* APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload).
|
|
7618
|
+
* In-memory only (not written as a ZIP Extra today). Filled in the same streaming hash
|
|
7619
|
+
* pass as `sha256` during create/extract — never reconstruct by re-reading payload when set.
|
|
7620
|
+
*/
|
|
7621
|
+
this.merkleLeafV1 = null;
|
|
7572
7622
|
// Symbolic link data
|
|
7573
7623
|
this.isSymlink = false;
|
|
7574
7624
|
// Entry is a symbolic link
|
|
@@ -8492,24 +8542,30 @@ var HashCalculator = class _HashCalculator {
|
|
|
8492
8542
|
// Incremental hash calculation state (from StreamHashCalculator)
|
|
8493
8543
|
this.crc32State = ~0;
|
|
8494
8544
|
this.sha256Hash = null;
|
|
8545
|
+
/** Domain-separated leaf hasher (APPNOTE §6.3): SHA-256(0x00 ‖ payload). */
|
|
8546
|
+
this.merkleLeafHash = null;
|
|
8495
8547
|
this.useSHA256 = false;
|
|
8496
8548
|
// Hash accumulation state (from HashAccumulator)
|
|
8497
8549
|
this.hashes = [];
|
|
8498
8550
|
this.xorResult = Buffer.alloc(32, 0);
|
|
8499
8551
|
this.enableAccumulation = false;
|
|
8500
|
-
// Merkle tree state (from MerkleTree)
|
|
8552
|
+
// Merkle tree state (from MerkleTree) — generic digest folding; archive roots use merkle/MerkleRoot.ts
|
|
8501
8553
|
this.merkleLeaves = [];
|
|
8502
8554
|
this.merkleLayers = [];
|
|
8503
8555
|
this.merkleOptions = {
|
|
8504
8556
|
hashLeaves: false,
|
|
8505
|
-
|
|
8506
|
-
|
|
8557
|
+
// Archive content roots sort by path in MerkleRoot; generic accumulator keeps insertion order.
|
|
8558
|
+
sortLeaves: false,
|
|
8559
|
+
sortPairs: false,
|
|
8560
|
+
// match APPNOTE v0 odd-leaf rule when used as a generic Bitcoin-style tree
|
|
8507
8561
|
duplicateOdd: true
|
|
8508
8562
|
};
|
|
8509
8563
|
this.useSHA256 = options?.useSHA256 || false;
|
|
8510
8564
|
this.enableAccumulation = options?.enableAccumulation || false;
|
|
8511
8565
|
if (this.useSHA256) {
|
|
8512
8566
|
this.sha256Hash = createHash("sha256");
|
|
8567
|
+
this.merkleLeafHash = createHash("sha256");
|
|
8568
|
+
this.merkleLeafHash.update(Buffer.from([0]));
|
|
8513
8569
|
}
|
|
8514
8570
|
}
|
|
8515
8571
|
// ============================================================================
|
|
@@ -8527,6 +8583,9 @@ var HashCalculator = class _HashCalculator {
|
|
|
8527
8583
|
if (this.sha256Hash) {
|
|
8528
8584
|
this.sha256Hash.update(chunk);
|
|
8529
8585
|
}
|
|
8586
|
+
if (this.merkleLeafHash) {
|
|
8587
|
+
this.merkleLeafHash.update(chunk);
|
|
8588
|
+
}
|
|
8530
8589
|
}
|
|
8531
8590
|
/**
|
|
8532
8591
|
* Get final CRC-32 value
|
|
@@ -8536,7 +8595,7 @@ var HashCalculator = class _HashCalculator {
|
|
|
8536
8595
|
return ~this.crc32State >>> 0;
|
|
8537
8596
|
}
|
|
8538
8597
|
/**
|
|
8539
|
-
* Get final SHA-256 hash as hex string
|
|
8598
|
+
* Get final SHA-256 hash as hex string (Extra Field 0x014E — bare content digest)
|
|
8540
8599
|
* @returns SHA-256 hash as hex string, or null if SHA-256 not enabled
|
|
8541
8600
|
*/
|
|
8542
8601
|
finalizeSHA256() {
|
|
@@ -8545,13 +8604,46 @@ var HashCalculator = class _HashCalculator {
|
|
|
8545
8604
|
}
|
|
8546
8605
|
return null;
|
|
8547
8606
|
}
|
|
8607
|
+
/**
|
|
8608
|
+
* Get APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload) as hex.
|
|
8609
|
+
* Consumes the leaf hasher; call after or instead of finalizeSHA256 for dual streams.
|
|
8610
|
+
*/
|
|
8611
|
+
finalizeMerkleLeafV1() {
|
|
8612
|
+
if (this.merkleLeafHash) {
|
|
8613
|
+
return this.merkleLeafHash.digest("hex");
|
|
8614
|
+
}
|
|
8615
|
+
return null;
|
|
8616
|
+
}
|
|
8617
|
+
/**
|
|
8618
|
+
* Apply stream digests to a ZIP entry in one place.
|
|
8619
|
+
*
|
|
8620
|
+
* When SHA-256 was enabled, both Extra Field 0x014E (`sha256`) and the v1 Merkle leaf
|
|
8621
|
+
* (`merkleLeafV1`) are finalized from the **same** chunk stream — no second payload pass.
|
|
8622
|
+
*/
|
|
8623
|
+
applyToEntry(entry, options) {
|
|
8624
|
+
if (options.setCrc) {
|
|
8625
|
+
entry.crc = this.finalizeCRC32();
|
|
8626
|
+
}
|
|
8627
|
+
if (options.setSha256) {
|
|
8628
|
+
const digest = this.finalizeSHA256();
|
|
8629
|
+
const leaf = this.finalizeMerkleLeafV1();
|
|
8630
|
+
if (digest) {
|
|
8631
|
+
entry.sha256 = digest;
|
|
8632
|
+
}
|
|
8633
|
+
if (leaf) {
|
|
8634
|
+
entry.merkleLeafV1 = leaf;
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
8548
8638
|
/**
|
|
8549
8639
|
* Reset the incremental hash calculation state
|
|
8550
8640
|
*/
|
|
8551
8641
|
reset() {
|
|
8552
8642
|
this.crc32State = ~0;
|
|
8553
|
-
if (this.
|
|
8643
|
+
if (this.useSHA256) {
|
|
8554
8644
|
this.sha256Hash = createHash("sha256");
|
|
8645
|
+
this.merkleLeafHash = createHash("sha256");
|
|
8646
|
+
this.merkleLeafHash.update(Buffer.from([0]));
|
|
8555
8647
|
}
|
|
8556
8648
|
}
|
|
8557
8649
|
// ============================================================================
|
|
@@ -9159,18 +9251,16 @@ var _ZipCompress = class _ZipCompress {
|
|
|
9159
9251
|
const hashCalculator = needsHashCalculation ? new HashCalculator({ useSHA256: options?.useSHA256 && !entry.sha256 || false }) : null;
|
|
9160
9252
|
if (hashCalculator) {
|
|
9161
9253
|
hashCalculator.update(data);
|
|
9162
|
-
|
|
9163
|
-
entry.crc
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
9167
|
-
}
|
|
9254
|
+
hashCalculator.applyToEntry(entry, {
|
|
9255
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
9256
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256)
|
|
9257
|
+
});
|
|
9168
9258
|
this.log(`Final hashes: CRC32=0x${entry.crc.toString(16).padStart(8, "0")}, SHA256=${entry.sha256 || "N/A"}`);
|
|
9169
9259
|
}
|
|
9170
9260
|
const enc = options?.encryptionMethod;
|
|
9171
9261
|
const useZipCrypto = enc === "zipcrypto";
|
|
9172
|
-
const
|
|
9173
|
-
const
|
|
9262
|
+
const useAes = enc === "aes256";
|
|
9263
|
+
const useNeo = !!options?.password && !useZipCrypto && !useAes;
|
|
9174
9264
|
if (options?.password) {
|
|
9175
9265
|
const label = useZipCrypto ? "ZipCrypto" : useNeo ? "NeoEncrypt (AES-256)" : "WinZip AES-256";
|
|
9176
9266
|
this.log(`Encrypting compressed data for entry: ${entry.filename} (method: ${label})`);
|
|
@@ -9373,9 +9463,11 @@ var _ZipCompress = class _ZipCompress {
|
|
|
9373
9463
|
entry.uncompressedSize = fileData.length;
|
|
9374
9464
|
const hashCalculator = new HashCalculator({ useSHA256: cmpOptions?.useSHA256 || false });
|
|
9375
9465
|
hashCalculator.update(fileData);
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9466
|
+
hashCalculator.applyToEntry(entry, {
|
|
9467
|
+
setCrc: true,
|
|
9468
|
+
setSha256: !!cmpOptions?.useSHA256
|
|
9469
|
+
});
|
|
9470
|
+
if (entry.sha256) {
|
|
9379
9471
|
this.log(`SHA-256 calculated: ${entry.sha256}`);
|
|
9380
9472
|
}
|
|
9381
9473
|
return await this.compressData(entry, fileData, cmpOptions);
|
|
@@ -9522,17 +9614,21 @@ var _ZipDecompress = class _ZipDecompress {
|
|
|
9522
9614
|
} else {
|
|
9523
9615
|
throw new Error(`Unsupported compression method: ${method}`);
|
|
9524
9616
|
}
|
|
9617
|
+
const hashCalc = new HashCalculator({ useSHA256: true });
|
|
9618
|
+
hashCalc.update(outBuf);
|
|
9619
|
+
const calculatedCRC = hashCalc.finalizeCRC32();
|
|
9620
|
+
const calculatedHash = hashCalc.finalizeSHA256();
|
|
9621
|
+
const leaf = hashCalc.finalizeMerkleLeafV1();
|
|
9622
|
+
if (leaf) {
|
|
9623
|
+
entry.merkleLeafV1 = leaf;
|
|
9624
|
+
}
|
|
9525
9625
|
if (!skipHashCheck) {
|
|
9526
9626
|
if (entry.sha256) {
|
|
9527
|
-
|
|
9528
|
-
if (!isValid) {
|
|
9627
|
+
if (calculatedHash !== entry.sha256) {
|
|
9529
9628
|
throw new Error(Errors_default.INVALID_SHA256);
|
|
9530
9629
|
}
|
|
9531
|
-
} else {
|
|
9532
|
-
|
|
9533
|
-
if (!isValid) {
|
|
9534
|
-
throw new Error(Errors_default.INVALID_CRC);
|
|
9535
|
-
}
|
|
9630
|
+
} else if (calculatedCRC !== entry.crc) {
|
|
9631
|
+
throw new Error(Errors_default.INVALID_CRC);
|
|
9536
9632
|
}
|
|
9537
9633
|
}
|
|
9538
9634
|
return outBuf;
|
|
@@ -9543,6 +9639,176 @@ _ZipDecompress.loggingEnabled = false;
|
|
|
9543
9639
|
var ZipDecompress = _ZipDecompress;
|
|
9544
9640
|
var ZipDecompress_default = ZipDecompress;
|
|
9545
9641
|
|
|
9642
|
+
// src/core/merkle/MerkleRoot.ts
|
|
9643
|
+
var LEAF_DOMAIN = Buffer.from([0]);
|
|
9644
|
+
var NODE_DOMAIN = Buffer.from([1]);
|
|
9645
|
+
function sha2562(data) {
|
|
9646
|
+
const h = createHash("sha256");
|
|
9647
|
+
if (Array.isArray(data)) {
|
|
9648
|
+
for (const part of data) {
|
|
9649
|
+
h.update(part);
|
|
9650
|
+
}
|
|
9651
|
+
} else {
|
|
9652
|
+
h.update(data);
|
|
9653
|
+
}
|
|
9654
|
+
return h.digest();
|
|
9655
|
+
}
|
|
9656
|
+
function asBuffer(data, encoding) {
|
|
9657
|
+
if (typeof data === "string") {
|
|
9658
|
+
return Buffer.from(data, encoding ?? "hex");
|
|
9659
|
+
}
|
|
9660
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
9661
|
+
}
|
|
9662
|
+
function normalizeMerklePath(path) {
|
|
9663
|
+
let p = path.replace(/\\/g, "/");
|
|
9664
|
+
if (p.startsWith("./")) {
|
|
9665
|
+
p = p.slice(2);
|
|
9666
|
+
}
|
|
9667
|
+
while (p.startsWith("/")) {
|
|
9668
|
+
p = p.slice(1);
|
|
9669
|
+
}
|
|
9670
|
+
if (typeof p.normalize === "function") {
|
|
9671
|
+
p = p.normalize("NFC");
|
|
9672
|
+
}
|
|
9673
|
+
return p;
|
|
9674
|
+
}
|
|
9675
|
+
function pathSortKey(path, algorithm) {
|
|
9676
|
+
const normalized = algorithm === "v1" ? normalizeMerklePath(path) : path;
|
|
9677
|
+
return Buffer.from(normalized, "utf8");
|
|
9678
|
+
}
|
|
9679
|
+
function contentDigest(content) {
|
|
9680
|
+
return sha2562(asBuffer(content));
|
|
9681
|
+
}
|
|
9682
|
+
function leafHashV0(contentOrDigest, isDigest = false) {
|
|
9683
|
+
const buf = asBuffer(contentOrDigest);
|
|
9684
|
+
return isDigest ? buf : sha2562(buf);
|
|
9685
|
+
}
|
|
9686
|
+
function leafHashV1(content) {
|
|
9687
|
+
return sha2562([LEAF_DOMAIN, asBuffer(content)]);
|
|
9688
|
+
}
|
|
9689
|
+
function parentHash(left, right, algorithm) {
|
|
9690
|
+
if (algorithm === "v1") {
|
|
9691
|
+
return sha2562([NODE_DOMAIN, left, right]);
|
|
9692
|
+
}
|
|
9693
|
+
return sha2562([left, right]);
|
|
9694
|
+
}
|
|
9695
|
+
function merkleRootFromLeaves(leaves, algorithm) {
|
|
9696
|
+
if (leaves.length === 0) {
|
|
9697
|
+
return null;
|
|
9698
|
+
}
|
|
9699
|
+
let level = leaves.map((l) => Buffer.from(l));
|
|
9700
|
+
while (level.length > 1) {
|
|
9701
|
+
const next = [];
|
|
9702
|
+
for (let i = 0; i + 1 < level.length; i += 2) {
|
|
9703
|
+
next.push(parentHash(level[i], level[i + 1], algorithm));
|
|
9704
|
+
}
|
|
9705
|
+
if (level.length % 2 === 1) {
|
|
9706
|
+
const last = level[level.length - 1];
|
|
9707
|
+
if (algorithm === "v0") {
|
|
9708
|
+
next.push(parentHash(last, last, algorithm));
|
|
9709
|
+
} else {
|
|
9710
|
+
next.push(last);
|
|
9711
|
+
}
|
|
9712
|
+
}
|
|
9713
|
+
level = next;
|
|
9714
|
+
}
|
|
9715
|
+
return level[0].toString("hex");
|
|
9716
|
+
}
|
|
9717
|
+
function resolveLeaf(entry, algorithm) {
|
|
9718
|
+
if (algorithm === "v0") {
|
|
9719
|
+
if (entry.contentSha256) {
|
|
9720
|
+
const d = asBuffer(entry.contentSha256, typeof entry.contentSha256 === "string" ? "hex" : void 0);
|
|
9721
|
+
if (d.length !== 32) {
|
|
9722
|
+
return null;
|
|
9723
|
+
}
|
|
9724
|
+
return d;
|
|
9725
|
+
}
|
|
9726
|
+
if (entry.content) {
|
|
9727
|
+
return contentDigest(entry.content);
|
|
9728
|
+
}
|
|
9729
|
+
return null;
|
|
9730
|
+
}
|
|
9731
|
+
if (entry.merkleLeafV1) {
|
|
9732
|
+
const d = asBuffer(
|
|
9733
|
+
entry.merkleLeafV1,
|
|
9734
|
+
typeof entry.merkleLeafV1 === "string" ? "hex" : void 0
|
|
9735
|
+
);
|
|
9736
|
+
if (d.length !== 32) {
|
|
9737
|
+
return null;
|
|
9738
|
+
}
|
|
9739
|
+
return d;
|
|
9740
|
+
}
|
|
9741
|
+
if (entry.content) {
|
|
9742
|
+
return leafHashV1(entry.content);
|
|
9743
|
+
}
|
|
9744
|
+
return null;
|
|
9745
|
+
}
|
|
9746
|
+
function computeArchiveMerkleRoot(entries, algorithm = "v1") {
|
|
9747
|
+
const prepared = [];
|
|
9748
|
+
for (const entry of entries) {
|
|
9749
|
+
if (isMetaInfPath(entry.path || "")) {
|
|
9750
|
+
continue;
|
|
9751
|
+
}
|
|
9752
|
+
const leaf = resolveLeaf(entry, algorithm);
|
|
9753
|
+
if (!leaf) {
|
|
9754
|
+
continue;
|
|
9755
|
+
}
|
|
9756
|
+
prepared.push({
|
|
9757
|
+
key: pathSortKey(entry.path || "", algorithm),
|
|
9758
|
+
leaf
|
|
9759
|
+
});
|
|
9760
|
+
}
|
|
9761
|
+
if (prepared.length === 0) {
|
|
9762
|
+
return null;
|
|
9763
|
+
}
|
|
9764
|
+
prepared.sort((a, b) => Buffer.compare(a.key, b.key));
|
|
9765
|
+
return merkleRootFromLeaves(
|
|
9766
|
+
prepared.map((p) => p.leaf),
|
|
9767
|
+
algorithm
|
|
9768
|
+
);
|
|
9769
|
+
}
|
|
9770
|
+
function computeMerkleRootV0FromDigests(entries) {
|
|
9771
|
+
return computeArchiveMerkleRoot(
|
|
9772
|
+
entries.map((e) => ({ path: e.path, contentSha256: e.sha256 })),
|
|
9773
|
+
"v0"
|
|
9774
|
+
);
|
|
9775
|
+
}
|
|
9776
|
+
function computeMerkleRootV1FromContents(entries) {
|
|
9777
|
+
return computeArchiveMerkleRoot(
|
|
9778
|
+
entries.map((e) => ({ path: e.path, content: e.content })),
|
|
9779
|
+
"v1"
|
|
9780
|
+
);
|
|
9781
|
+
}
|
|
9782
|
+
function computeMerkleRootV1FromLeaves(entries) {
|
|
9783
|
+
return computeArchiveMerkleRoot(
|
|
9784
|
+
entries.map((e) => ({ path: e.path, merkleLeafV1: e.merkleLeafV1 })),
|
|
9785
|
+
"v1"
|
|
9786
|
+
);
|
|
9787
|
+
}
|
|
9788
|
+
function matchMerkleRoot(entries, declaredRoot) {
|
|
9789
|
+
const expected = declaredRoot.toLowerCase().replace(/^0x/, "");
|
|
9790
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
9791
|
+
return null;
|
|
9792
|
+
}
|
|
9793
|
+
const hasV1Material = entries.some(
|
|
9794
|
+
(e) => !isMetaInfPath(e.path || "") && (!!e.merkleLeafV1 || !!e.content)
|
|
9795
|
+
);
|
|
9796
|
+
if (hasV1Material) {
|
|
9797
|
+
const v1 = computeArchiveMerkleRoot(entries, "v1");
|
|
9798
|
+
if (v1 && v1 === expected) {
|
|
9799
|
+
return { algorithm: "v1", security: "high" };
|
|
9800
|
+
}
|
|
9801
|
+
}
|
|
9802
|
+
const v0 = computeArchiveMerkleRoot(entries, "v0");
|
|
9803
|
+
if (v0 && v0 === expected) {
|
|
9804
|
+
return { algorithm: "v0", security: "legacy" };
|
|
9805
|
+
}
|
|
9806
|
+
if (hasV1Material) {
|
|
9807
|
+
return null;
|
|
9808
|
+
}
|
|
9809
|
+
return null;
|
|
9810
|
+
}
|
|
9811
|
+
|
|
9546
9812
|
// src/core/Zipkit.ts
|
|
9547
9813
|
var Zipkit = class {
|
|
9548
9814
|
/**
|
|
@@ -9762,15 +10028,22 @@ var Zipkit = class {
|
|
|
9762
10028
|
return this.zipEntries;
|
|
9763
10029
|
}
|
|
9764
10030
|
/**
|
|
9765
|
-
* Get specific ZIP entry by filename from zipEntries[] array
|
|
9766
|
-
*
|
|
10031
|
+
* Get specific ZIP entry by filename from zipEntries[] array.
|
|
10032
|
+
* Reserved META-INF paths (APPNOTE §2.3) also match ASCII case-insensitively
|
|
10033
|
+
* when an exact spelling is not found.
|
|
10034
|
+
*
|
|
9767
10035
|
* @param filename - The name/path of the file/directory to find
|
|
9768
10036
|
* @returns ZipEntry object if found, null if not found
|
|
9769
10037
|
*/
|
|
9770
10038
|
getZipEntry(filename) {
|
|
9771
10039
|
try {
|
|
9772
10040
|
const centralDir = this.getDirectory();
|
|
9773
|
-
|
|
10041
|
+
const exact = centralDir.find((entry) => entry.filename === filename);
|
|
10042
|
+
if (exact) return exact;
|
|
10043
|
+
if (isReservedMetaPath(filename)) {
|
|
10044
|
+
return findReservedMetaEntry(centralDir, filename);
|
|
10045
|
+
}
|
|
10046
|
+
return null;
|
|
9774
10047
|
} catch (error) {
|
|
9775
10048
|
return null;
|
|
9776
10049
|
}
|
|
@@ -10014,14 +10287,23 @@ var Zipkit = class {
|
|
|
10014
10287
|
return ceBuf;
|
|
10015
10288
|
}
|
|
10016
10289
|
/**
|
|
10017
|
-
* Calculate Merkle
|
|
10018
|
-
*
|
|
10019
|
-
*
|
|
10020
|
-
*
|
|
10021
|
-
*
|
|
10022
|
-
*
|
|
10290
|
+
* Calculate Merkle root of archive content (non-META-INF entries).
|
|
10291
|
+
*
|
|
10292
|
+
* Per NEOZIP_APPNOTE.md §6:
|
|
10293
|
+
* - Default **v1**: domain-separated leaves SHA-256(0x00 ‖ uncompressed bytes) and
|
|
10294
|
+
* SHA-256(0x01 ‖ left ‖ right) parents; paths NFC-normalized before sort.
|
|
10295
|
+
* - Legacy **v0**: bare 0x014E digests as leaves, duplicate-odd pairing, path as stored.
|
|
10296
|
+
*
|
|
10297
|
+
* When only Extra Field digests are available (typical after open), **v1 requires
|
|
10298
|
+
* payload bytes**. This sync method therefore returns null for v1; use
|
|
10299
|
+
* {@link getMerkleRootAsync} (extracts content) or `{ algorithm: 'v0' }`.
|
|
10300
|
+
* When you already hold payloads, use {@link merkleRootFromEntries}.
|
|
10301
|
+
*
|
|
10302
|
+
* @param options.algorithm - `v1` (default) or legacy `v0`
|
|
10303
|
+
* @returns Lowercase hex Merkle root, or null if calculation fails
|
|
10023
10304
|
*/
|
|
10024
|
-
getMerkleRoot() {
|
|
10305
|
+
getMerkleRoot(options) {
|
|
10306
|
+
const algorithm = options?.algorithm ?? "v1";
|
|
10025
10307
|
let zipEntries;
|
|
10026
10308
|
try {
|
|
10027
10309
|
zipEntries = this.getDirectory();
|
|
@@ -10033,40 +10315,40 @@ var Zipkit = class {
|
|
|
10033
10315
|
if (!zipEntries || zipEntries.length === 0) {
|
|
10034
10316
|
return null;
|
|
10035
10317
|
}
|
|
10036
|
-
const
|
|
10037
|
-
|
|
10038
|
-
const
|
|
10039
|
-
return
|
|
10040
|
-
}
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
if (!merkleRoot) {
|
|
10049
|
-
return null;
|
|
10318
|
+
const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ""));
|
|
10319
|
+
if (algorithm === "v0") {
|
|
10320
|
+
const digests = contentEntries.filter((e) => e.sha256).map((e) => ({ path: e.filename || "", sha256: e.sha256 }));
|
|
10321
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
10322
|
+
}
|
|
10323
|
+
if (contentEntries.length > 0 && contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
10324
|
+
return computeMerkleRootV1FromLeaves(
|
|
10325
|
+
contentEntries.map((e) => ({
|
|
10326
|
+
path: e.filename || "",
|
|
10327
|
+
merkleLeafV1: e.merkleLeafV1
|
|
10328
|
+
}))
|
|
10329
|
+
);
|
|
10050
10330
|
}
|
|
10051
|
-
return
|
|
10331
|
+
return null;
|
|
10052
10332
|
}
|
|
10053
10333
|
/**
|
|
10054
|
-
* Calculate Merkle
|
|
10055
|
-
*
|
|
10056
|
-
*
|
|
10057
|
-
*
|
|
10058
|
-
*
|
|
10059
|
-
*
|
|
10334
|
+
* Calculate Merkle root (preferred for v1 / new archives).
|
|
10335
|
+
*
|
|
10336
|
+
* **v1 (default):** uses precomputed stream leaves when available; otherwise extracts
|
|
10337
|
+
* each content entry once and folds APPNOTE §6.3 root (no second content hash pass
|
|
10338
|
+
* when extract captures merkleLeafV1).
|
|
10339
|
+
* **v0:** uses Extra Field digests only (no extract), APPNOTE §6.2.
|
|
10340
|
+
*
|
|
10341
|
+
* META-INF/** is excluded. File-streaming mode: use ZipkitNode override (testEntry
|
|
10342
|
+
* pipeline) rather than buffering the whole archive.
|
|
10343
|
+
*
|
|
10344
|
+
* @param options.algorithm - `v1` (default) or legacy `v0`
|
|
10345
|
+
* @returns Promise resolving to lowercase hex root, or null
|
|
10060
10346
|
*/
|
|
10061
|
-
async getMerkleRootAsync() {
|
|
10347
|
+
async getMerkleRootAsync(options) {
|
|
10348
|
+
const algorithm = options?.algorithm ?? "v1";
|
|
10062
10349
|
let zipEntries = [];
|
|
10063
10350
|
try {
|
|
10064
|
-
|
|
10065
|
-
zipEntries = this.getDirectory();
|
|
10066
|
-
} else {
|
|
10067
|
-
Logger.error("getMerkleRootAsync() called on file-based ZIP. Use ZipkitServer.getMerkleRootAsync() instead.");
|
|
10068
|
-
return null;
|
|
10069
|
-
}
|
|
10351
|
+
zipEntries = this.getDirectory();
|
|
10070
10352
|
} catch (error) {
|
|
10071
10353
|
Logger.error(`Failed to get directory for merkle root calculation: ${error instanceof Error ? error.message : String(error)}`);
|
|
10072
10354
|
return null;
|
|
@@ -10074,22 +10356,60 @@ var Zipkit = class {
|
|
|
10074
10356
|
if (!zipEntries || zipEntries.length === 0) {
|
|
10075
10357
|
return null;
|
|
10076
10358
|
}
|
|
10077
|
-
const
|
|
10078
|
-
|
|
10079
|
-
const
|
|
10080
|
-
return
|
|
10081
|
-
}
|
|
10359
|
+
const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ""));
|
|
10360
|
+
if (algorithm === "v0") {
|
|
10361
|
+
const digests = contentEntries.filter((e) => e.sha256).map((e) => ({ path: e.filename || "", sha256: e.sha256 }));
|
|
10362
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
10363
|
+
}
|
|
10364
|
+
if (contentEntries.length > 0 && contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
10365
|
+
return computeMerkleRootV1FromLeaves(
|
|
10366
|
+
contentEntries.map((e) => ({
|
|
10367
|
+
path: e.filename || "",
|
|
10368
|
+
merkleLeafV1: e.merkleLeafV1
|
|
10369
|
+
}))
|
|
10370
|
+
);
|
|
10371
|
+
}
|
|
10372
|
+
if (!this.inBuffer) {
|
|
10373
|
+
Logger.error(
|
|
10374
|
+
"getMerkleRootAsync() called on file-based ZIP without precomputed merkle leaves. Use ZipkitNode (streaming testEntry) or ensure create captured merkleLeafV1."
|
|
10375
|
+
);
|
|
10376
|
+
return null;
|
|
10377
|
+
}
|
|
10082
10378
|
for (const entry of contentEntries) {
|
|
10083
|
-
|
|
10084
|
-
const
|
|
10085
|
-
|
|
10379
|
+
try {
|
|
10380
|
+
const data = await this.extract(entry, false);
|
|
10381
|
+
if (entry.merkleLeafV1) {
|
|
10382
|
+
continue;
|
|
10383
|
+
}
|
|
10384
|
+
if (data === null || data.length === 0) {
|
|
10385
|
+
entry.merkleLeafV1 = leafHashV1(Buffer.alloc(0)).toString("hex");
|
|
10386
|
+
} else {
|
|
10387
|
+
entry.merkleLeafV1 = leafHashV1(data).toString("hex");
|
|
10388
|
+
}
|
|
10389
|
+
} catch (error) {
|
|
10390
|
+
Logger.error(
|
|
10391
|
+
`getMerkleRootAsync(): failed to extract ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`
|
|
10392
|
+
);
|
|
10393
|
+
return null;
|
|
10086
10394
|
}
|
|
10087
10395
|
}
|
|
10088
|
-
|
|
10089
|
-
if (!merkleRoot) {
|
|
10396
|
+
if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
10090
10397
|
return null;
|
|
10091
10398
|
}
|
|
10092
|
-
return
|
|
10399
|
+
return computeMerkleRootV1FromLeaves(
|
|
10400
|
+
contentEntries.map((e) => ({
|
|
10401
|
+
path: e.filename || "",
|
|
10402
|
+
merkleLeafV1: e.merkleLeafV1
|
|
10403
|
+
}))
|
|
10404
|
+
);
|
|
10405
|
+
}
|
|
10406
|
+
/**
|
|
10407
|
+
* Compute Merkle root from known digests and/or payloads (APPNOTE §6).
|
|
10408
|
+
* Prefer this during create when uncompressed bytes are still in hand, or when
|
|
10409
|
+
* stream leaves (`merkleLeafV1`) were already produced.
|
|
10410
|
+
*/
|
|
10411
|
+
static merkleRootFromEntries(entries, algorithm = "v1") {
|
|
10412
|
+
return computeArchiveMerkleRoot(entries, algorithm);
|
|
10093
10413
|
}
|
|
10094
10414
|
};
|
|
10095
10415
|
Zipkit.version = NEOZIPKIT_INFO.version;
|
|
@@ -10443,7 +10763,7 @@ function getTypeOf(input) {
|
|
|
10443
10763
|
}
|
|
10444
10764
|
}
|
|
10445
10765
|
var isMetadataFile = (filename) => {
|
|
10446
|
-
return filename
|
|
10766
|
+
return isMetaInfPath(filename);
|
|
10447
10767
|
};
|
|
10448
10768
|
|
|
10449
10769
|
// src/core/components/ProgressTracker.ts
|
|
@@ -10662,7 +10982,7 @@ var EncryptionManager = class {
|
|
|
10662
10982
|
var currentDate = /* @__PURE__ */ new Date();
|
|
10663
10983
|
var releaseDate = `${String(currentDate.getMonth() + 1).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}-${currentDate.getFullYear()}`;
|
|
10664
10984
|
var VERSION = {
|
|
10665
|
-
number: "1.0.
|
|
10985
|
+
number: "1.0.5",
|
|
10666
10986
|
// packageJson.version
|
|
10667
10987
|
date: releaseDate
|
|
10668
10988
|
};
|
|
@@ -10780,7 +11100,7 @@ var ZipkitBrowser = class extends core_default {
|
|
|
10780
11100
|
const buffers = [];
|
|
10781
11101
|
for (const entry of zipEntries) {
|
|
10782
11102
|
if (!entry.isUpdated) {
|
|
10783
|
-
if (entry.filename
|
|
11103
|
+
if (isMetaInfPath(entry.filename || "")) {
|
|
10784
11104
|
entry.isUpdated = true;
|
|
10785
11105
|
} else {
|
|
10786
11106
|
const inData = await super.copyEntry(entry);
|
|
@@ -10805,7 +11125,7 @@ var ZipkitBrowser = class extends core_default {
|
|
|
10805
11125
|
_onEntryDone(entry, "Written (pre-compressed)");
|
|
10806
11126
|
continue;
|
|
10807
11127
|
}
|
|
10808
|
-
if (entry.filename
|
|
11128
|
+
if (isMetaInfPath(entry.filename || "")) {
|
|
10809
11129
|
const buffer = Buffer.from(fileBuffer);
|
|
10810
11130
|
entry.crc = crc32(buffer);
|
|
10811
11131
|
entry.cmpMethod = 0;
|
|
@@ -10846,7 +11166,7 @@ var ZipkitBrowser = class extends core_default {
|
|
|
10846
11166
|
* @returns boolean - True if the file is a metadata file
|
|
10847
11167
|
*/
|
|
10848
11168
|
isMetadataFile(filename) {
|
|
10849
|
-
return filename
|
|
11169
|
+
return isMetaInfPath(filename);
|
|
10850
11170
|
}
|
|
10851
11171
|
// --------------------------------------
|
|
10852
11172
|
// Helper method: Add a file to the ZIP with compression
|
|
@@ -10908,6 +11228,13 @@ export {
|
|
|
10908
11228
|
LOCAL_FILE_HEADER,
|
|
10909
11229
|
LOCAL_HDR,
|
|
10910
11230
|
Logger,
|
|
11231
|
+
META_MANIFEST_JSON,
|
|
11232
|
+
META_TIMESTAMP_NZIP,
|
|
11233
|
+
META_TIMESTAMP_OTS,
|
|
11234
|
+
META_TOKEN_NZIP,
|
|
11235
|
+
META_TOKEN_NZIP_LEGACY,
|
|
11236
|
+
META_TS_SUBMIT_NZIP,
|
|
11237
|
+
META_TS_SUBMIT_OTS,
|
|
10911
11238
|
NEOZIPKIT_INFO,
|
|
10912
11239
|
NEO_CRYPTO_EXTRA_FIELD_SIZE,
|
|
10913
11240
|
NEO_CRYPTO_MAGIC_BYTES,
|
|
@@ -10927,12 +11254,27 @@ export {
|
|
|
10927
11254
|
ZipCrypto,
|
|
10928
11255
|
ZipEntry,
|
|
10929
11256
|
ZipkitBrowser,
|
|
11257
|
+
asciiPathEqualsIgnoreCase,
|
|
11258
|
+
computeArchiveMerkleRoot,
|
|
11259
|
+
computeMerkleRootV0FromDigests,
|
|
11260
|
+
computeMerkleRootV1FromContents,
|
|
11261
|
+
computeMerkleRootV1FromLeaves,
|
|
10930
11262
|
configureLoggerFromEnvironment,
|
|
11263
|
+
contentDigest,
|
|
10931
11264
|
crc32,
|
|
10932
11265
|
crc32update,
|
|
11266
|
+
findReservedMetaEntry,
|
|
10933
11267
|
getTypeOf,
|
|
11268
|
+
isMetaInfPath,
|
|
10934
11269
|
isMetadataFile,
|
|
11270
|
+
isReservedMetaPath,
|
|
10935
11271
|
isStream,
|
|
11272
|
+
leafHashV0,
|
|
11273
|
+
leafHashV1,
|
|
11274
|
+
matchMerkleRoot,
|
|
11275
|
+
merkleRootFromLeaves,
|
|
11276
|
+
normalizeMerklePath,
|
|
11277
|
+
parentHash,
|
|
10936
11278
|
readBigUInt64LE,
|
|
10937
11279
|
sha256,
|
|
10938
11280
|
uMul
|