neozipkit 1.0.2 → 1.5.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/NEOZIP_APPNOTE.md +625 -0
- package/README.md +44 -11
- package/WHATS_NEW.md +153 -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/zstd-browser-shim.d.ts +17 -9
- package/dist/browser/zstd-browser-shim.d.ts.map +1 -1
- package/dist/browser/zstd-browser-shim.js +99 -21
- package/dist/browser/zstd-browser-shim.js.map +1 -1
- package/dist/browser-esm/index.js +6534 -1275
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +6534 -1275
- package/dist/browser-umd/index.js.map +4 -4
- package/dist/browser-umd/index.min.js +6 -3
- package/dist/core/ZipCompress.d.ts +6 -0
- 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/ZipCopy.d.ts.map +1 -1
- package/dist/core/ZipCopy.js +33 -25
- package/dist/core/ZipCopy.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 +24 -0
- package/dist/core/ZipEntry.d.ts.map +1 -1
- package/dist/core/ZipEntry.js +146 -14
- 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 +132 -82
- 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 +2 -3
- package/dist/core/constants/Headers.d.ts.map +1 -1
- package/dist/core/constants/Headers.js +15 -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 +5 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +18 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/inflateZipPayload.d.ts +6 -0
- package/dist/core/inflateZipPayload.d.ts.map +1 -0
- package/dist/core/inflateZipPayload.js +28 -0
- package/dist/core/inflateZipPayload.js.map +1 -0
- 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/origin/OriginExtra.d.ts +43 -0
- package/dist/core/origin/OriginExtra.d.ts.map +1 -0
- package/dist/core/origin/OriginExtra.js +187 -0
- package/dist/core/origin/OriginExtra.js.map +1 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zip64/Zip64.d.ts +138 -0
- package/dist/core/zip64/Zip64.d.ts.map +1 -0
- package/dist/core/zip64/Zip64.js +353 -0
- package/dist/core/zip64/Zip64.js.map +1 -0
- 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/ZipCopyNode.d.ts.map +1 -1
- package/dist/node/ZipCopyNode.js +26 -26
- package/dist/node/ZipCopyNode.js.map +1 -1
- package/dist/node/ZipDecompressNode.d.ts.map +1 -1
- package/dist/node/ZipDecompressNode.js +21 -3
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +23 -1
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +158 -57
- package/dist/node/ZipkitNode.js.map +1 -1
- package/dist/node/ZstdNode.d.ts +14 -4
- package/dist/node/ZstdNode.d.ts.map +1 -1
- package/dist/node/ZstdNode.js +28 -4
- package/dist/node/ZstdNode.js.map +1 -1
- package/dist/node/buildZipBuffer.d.ts +49 -0
- package/dist/node/buildZipBuffer.d.ts.map +1 -0
- package/dist/node/buildZipBuffer.js +146 -0
- package/dist/node/buildZipBuffer.js.map +1 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +6 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/inflateZipPayload.d.ts +5 -0
- package/dist/node/inflateZipPayload.d.ts.map +1 -0
- package/dist/node/inflateZipPayload.js +21 -0
- package/dist/node/inflateZipPayload.js.map +1 -0
- package/dist/types/index.js +2 -2
- package/node-esm.mjs +32 -0
- package/package.json +13 -6
- package/src/browser/ZipkitBrowser.ts +4 -6
- package/src/browser/zstd-browser-shim.ts +110 -28
- package/src/core/ZipCompress.ts +20 -13
- package/src/core/ZipCopy.ts +56 -27
- package/src/core/ZipDecompress.ts +14 -8
- package/src/core/ZipEntry.ts +200 -14
- package/src/core/Zipkit.ts +189 -98
- package/src/core/components/HashCalculator.ts +58 -6
- package/src/core/components/Util.ts +16 -7
- package/src/core/constants/Headers.ts +15 -4
- package/src/core/constants/MetaPaths.ts +86 -0
- package/src/core/index.ts +17 -0
- package/src/core/inflateZipPayload.ts +27 -0
- package/src/core/merkle/MerkleRoot.ts +312 -0
- package/src/core/origin/OriginExtra.ts +209 -0
- package/src/core/version.ts +1 -1
- package/src/core/zip64/Zip64.ts +473 -0
- package/src/node/ZipCompressNode.ts +40 -49
- package/src/node/ZipCopyNode.ts +31 -29
- package/src/node/ZipDecompressNode.ts +16 -3
- package/src/node/ZipkitNode.ts +203 -72
- package/src/node/ZstdNode.ts +33 -8
- package/src/node/buildZipBuffer.ts +221 -0
- package/src/node/index.ts +7 -0
- package/src/node/inflateZipPayload.ts +19 -0
- package/src/types/index.ts +2 -2
package/src/core/Zipkit.ts
CHANGED
|
@@ -10,7 +10,14 @@ import { NEOZIPKIT_INFO } from '../types';
|
|
|
10
10
|
import { ZipCompress, CompressOptions } from './ZipCompress';
|
|
11
11
|
import ZipDecompress from './ZipDecompress';
|
|
12
12
|
import { sha256, crc32 } from './encryption/ZipCrypto';
|
|
13
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
computeArchiveMerkleRoot,
|
|
15
|
+
computeMerkleRootV0FromDigests,
|
|
16
|
+
computeMerkleRootV1FromContents,
|
|
17
|
+
computeMerkleRootV1FromLeaves,
|
|
18
|
+
leafHashV1,
|
|
19
|
+
type MerkleAlgorithm,
|
|
20
|
+
} from './merkle/MerkleRoot';
|
|
14
21
|
import {
|
|
15
22
|
LOCAL_HDR,
|
|
16
23
|
ENCRYPT_HDR_SIZE,
|
|
@@ -18,7 +25,7 @@ import {
|
|
|
18
25
|
CENTRAL_END,
|
|
19
26
|
CENTRAL_DIR,
|
|
20
27
|
ZIP64_CENTRAL_END,
|
|
21
|
-
ZIP64_CENTRAL_DIR
|
|
28
|
+
ZIP64_CENTRAL_DIR,
|
|
22
29
|
GP_FLAG,
|
|
23
30
|
TIMESTAMP_SUBMITTED,
|
|
24
31
|
TIMESTAMP_METADATA,
|
|
@@ -28,6 +35,19 @@ import {
|
|
|
28
35
|
CENTRAL_DIRECTORY_END,
|
|
29
36
|
HDR_ID
|
|
30
37
|
} from './constants/Headers';
|
|
38
|
+
import {
|
|
39
|
+
buildEndRecords,
|
|
40
|
+
classicEocdNeedsZip64,
|
|
41
|
+
parseZip64Eocd,
|
|
42
|
+
parseZip64Locator,
|
|
43
|
+
ZIP64_EOCD_RECORD_SIZE,
|
|
44
|
+
ZIP64_LOCATOR_SIZE,
|
|
45
|
+
} from './zip64/Zip64';
|
|
46
|
+
import {
|
|
47
|
+
findReservedMetaEntry,
|
|
48
|
+
isMetaInfPath,
|
|
49
|
+
isReservedMetaPath,
|
|
50
|
+
} from './constants/MetaPaths';
|
|
31
51
|
|
|
32
52
|
/**
|
|
33
53
|
* Streaming file handle interface
|
|
@@ -371,15 +391,22 @@ export default class Zipkit {
|
|
|
371
391
|
|
|
372
392
|
|
|
373
393
|
/**
|
|
374
|
-
* Get specific ZIP entry by filename from zipEntries[] array
|
|
375
|
-
*
|
|
394
|
+
* Get specific ZIP entry by filename from zipEntries[] array.
|
|
395
|
+
* Reserved META-INF paths (APPNOTE §2.3) also match ASCII case-insensitively
|
|
396
|
+
* when an exact spelling is not found.
|
|
397
|
+
*
|
|
376
398
|
* @param filename - The name/path of the file/directory to find
|
|
377
399
|
* @returns ZipEntry object if found, null if not found
|
|
378
400
|
*/
|
|
379
401
|
getZipEntry(filename: string): ZipEntry | null {
|
|
380
402
|
try {
|
|
381
403
|
const centralDir = this.getDirectory();
|
|
382
|
-
|
|
404
|
+
const exact = centralDir.find((entry: ZipEntry) => entry.filename === filename);
|
|
405
|
+
if (exact) return exact;
|
|
406
|
+
if (isReservedMetaPath(filename)) {
|
|
407
|
+
return findReservedMetaEntry(centralDir, filename);
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
383
410
|
} catch (error) {
|
|
384
411
|
return null;
|
|
385
412
|
}
|
|
@@ -586,17 +613,30 @@ export default class Zipkit {
|
|
|
586
613
|
if (eocdBuffer.readUInt32LE(0) === CENTRAL_END.SIGNATURE) {
|
|
587
614
|
this.centralDirSize = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
|
|
588
615
|
this.centralDirOffset = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
this.
|
|
598
|
-
|
|
616
|
+
const volEntries = eocdBuffer.readUInt16LE(CENTRAL_END.VOL_ENTRIES);
|
|
617
|
+
const totalEntries = eocdBuffer.readUInt16LE(CENTRAL_END.TOTAL_ENTRIES);
|
|
618
|
+
|
|
619
|
+
if (
|
|
620
|
+
classicEocdNeedsZip64({
|
|
621
|
+
volEntries,
|
|
622
|
+
totalEntries,
|
|
623
|
+
centralDirSize: this.centralDirSize,
|
|
624
|
+
centralDirOffset: this.centralDirOffset,
|
|
625
|
+
})
|
|
626
|
+
) {
|
|
627
|
+
const locatorOffset = eocdOffset - ZIP64_LOCATOR_SIZE;
|
|
628
|
+
if (locatorOffset < 0) {
|
|
629
|
+
throw new Error(Errors.INVALID_FORMAT);
|
|
599
630
|
}
|
|
631
|
+
const locatorBuffer = buffer.subarray(locatorOffset, locatorOffset + ZIP64_LOCATOR_SIZE);
|
|
632
|
+
const zip64Offset = parseZip64Locator(locatorBuffer);
|
|
633
|
+
const zip64Buffer = buffer.subarray(
|
|
634
|
+
zip64Offset,
|
|
635
|
+
zip64Offset + ZIP64_EOCD_RECORD_SIZE
|
|
636
|
+
);
|
|
637
|
+
const z64 = parseZip64Eocd(zip64Buffer);
|
|
638
|
+
this.centralDirSize = z64.centralDirSize;
|
|
639
|
+
this.centralDirOffset = z64.centralDirOffset;
|
|
600
640
|
}
|
|
601
641
|
} else {
|
|
602
642
|
throw new Error(Errors.INVALID_FORMAT);
|
|
@@ -672,126 +712,177 @@ export default class Zipkit {
|
|
|
672
712
|
* Note: totalEntries must be passed as parameter since zipEntries[] in Zipkit is the cache
|
|
673
713
|
*/
|
|
674
714
|
private centralEndHdrMethod(centralDirSize: number, centralDirOffset: number, totalEntries: number): Buffer {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
ceBuf.writeUInt16LE(0, 20); // ZIP file comment length
|
|
684
|
-
|
|
685
|
-
return ceBuf;
|
|
715
|
+
// Buffer path: allow entry-count Zip64; refuse size/offset Zip64 (multi-GiB in memory).
|
|
716
|
+
return buildEndRecords({
|
|
717
|
+
totalEntries,
|
|
718
|
+
centralDirSize,
|
|
719
|
+
centralDirOffset,
|
|
720
|
+
zip64EocdOffset: centralDirOffset + centralDirSize,
|
|
721
|
+
allowSizeOffsetZip64: false,
|
|
722
|
+
});
|
|
686
723
|
}
|
|
687
724
|
|
|
688
725
|
/**
|
|
689
|
-
* Calculate Merkle
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
726
|
+
* Calculate Merkle root of archive content (non-META-INF entries).
|
|
727
|
+
*
|
|
728
|
+
* Per NEOZIP_APPNOTE.md §6:
|
|
729
|
+
* - Default **v1**: domain-separated leaves SHA-256(0x00 ‖ uncompressed bytes) and
|
|
730
|
+
* SHA-256(0x01 ‖ left ‖ right) parents; paths NFC-normalized before sort.
|
|
731
|
+
* - Legacy **v0**: bare 0x014E digests as leaves, duplicate-odd pairing, path as stored.
|
|
732
|
+
*
|
|
733
|
+
* When only Extra Field digests are available (typical after open), **v1 requires
|
|
734
|
+
* payload bytes**. This sync method therefore returns null for v1; use
|
|
735
|
+
* {@link getMerkleRootAsync} (extracts content) or `{ algorithm: 'v0' }`.
|
|
736
|
+
* When you already hold payloads, use {@link merkleRootFromEntries}.
|
|
737
|
+
*
|
|
738
|
+
* @param options.algorithm - `v1` (default) or legacy `v0`
|
|
739
|
+
* @returns Lowercase hex Merkle root, or null if calculation fails
|
|
695
740
|
*/
|
|
696
|
-
getMerkleRoot(): string | null {
|
|
697
|
-
|
|
741
|
+
getMerkleRoot(options?: { algorithm?: MerkleAlgorithm }): string | null {
|
|
742
|
+
const algorithm: MerkleAlgorithm = options?.algorithm ?? 'v1';
|
|
743
|
+
|
|
698
744
|
let zipEntries: ZipEntry[];
|
|
699
745
|
try {
|
|
700
746
|
zipEntries = this.getDirectory();
|
|
701
747
|
} catch (error) {
|
|
702
|
-
// Catch any errors from getDirectory() - this should never happen
|
|
703
|
-
// but if it does, log it and return null
|
|
704
748
|
Logger.error(`getMerkleRoot(): Error calling getDirectory(): ${error instanceof Error ? error.message : String(error)}`);
|
|
705
749
|
Logger.error(`getMerkleRoot(): Stack trace: ${error instanceof Error ? error.stack : 'No stack trace'}`);
|
|
706
750
|
return null;
|
|
707
751
|
}
|
|
708
|
-
|
|
752
|
+
|
|
709
753
|
if (!zipEntries || zipEntries.length === 0) {
|
|
710
754
|
return null;
|
|
711
755
|
}
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return
|
|
720
|
-
filename !== TIMESTAMP_METADATA &&
|
|
721
|
-
filename !== TOKENIZED_METADATA;
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
for (const entry of contentEntries) {
|
|
725
|
-
if (entry.sha256) {
|
|
726
|
-
// Convert hex string to Buffer
|
|
727
|
-
const hashBuffer = Buffer.from(entry.sha256, 'hex');
|
|
728
|
-
hashAccumulator.addHash(hashBuffer);
|
|
729
|
-
}
|
|
756
|
+
|
|
757
|
+
const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ''));
|
|
758
|
+
|
|
759
|
+
if (algorithm === 'v0') {
|
|
760
|
+
const digests = contentEntries
|
|
761
|
+
.filter((e) => e.sha256)
|
|
762
|
+
.map((e) => ({ path: e.filename || '', sha256: e.sha256 as string }));
|
|
763
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
730
764
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if (
|
|
734
|
-
|
|
765
|
+
|
|
766
|
+
// v1: prefer leaves captured in the same create/extract hash stream (no re-hash)
|
|
767
|
+
if (
|
|
768
|
+
contentEntries.length > 0 &&
|
|
769
|
+
contentEntries.every((e) => !!e.merkleLeafV1)
|
|
770
|
+
) {
|
|
771
|
+
return computeMerkleRootV1FromLeaves(
|
|
772
|
+
contentEntries.map((e) => ({
|
|
773
|
+
path: e.filename || '',
|
|
774
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
775
|
+
}))
|
|
776
|
+
);
|
|
735
777
|
}
|
|
736
778
|
|
|
737
|
-
|
|
779
|
+
// v1: bare digests are not leaves — need getMerkleRootAsync() or stream leaves
|
|
780
|
+
return null;
|
|
738
781
|
}
|
|
739
782
|
|
|
740
783
|
/**
|
|
741
|
-
* Calculate Merkle
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
784
|
+
* Calculate Merkle root (preferred for v1 / new archives).
|
|
785
|
+
*
|
|
786
|
+
* **v1 (default):** uses precomputed stream leaves when available; otherwise extracts
|
|
787
|
+
* each content entry once and folds APPNOTE §6.3 root (no second content hash pass
|
|
788
|
+
* when extract captures merkleLeafV1).
|
|
789
|
+
* **v0:** uses Extra Field digests only (no extract), APPNOTE §6.2.
|
|
790
|
+
*
|
|
791
|
+
* META-INF/** is excluded. File-streaming mode: use ZipkitNode override (testEntry
|
|
792
|
+
* pipeline) rather than buffering the whole archive.
|
|
793
|
+
*
|
|
794
|
+
* @param options.algorithm - `v1` (default) or legacy `v0`
|
|
795
|
+
* @returns Promise resolving to lowercase hex root, or null
|
|
747
796
|
*/
|
|
748
|
-
async getMerkleRootAsync(): Promise<string | null> {
|
|
797
|
+
async getMerkleRootAsync(options?: { algorithm?: MerkleAlgorithm }): Promise<string | null> {
|
|
798
|
+
const algorithm: MerkleAlgorithm = options?.algorithm ?? 'v1';
|
|
799
|
+
|
|
749
800
|
let zipEntries: ZipEntry[] = [];
|
|
750
|
-
|
|
801
|
+
|
|
751
802
|
try {
|
|
752
|
-
|
|
753
|
-
if (this.inBuffer) {
|
|
754
|
-
zipEntries = this.getDirectory();
|
|
755
|
-
} else {
|
|
756
|
-
// File-based: cannot get entries synchronously - return null
|
|
757
|
-
// Caller should use ZipkitServer.getMerkleRootAsync() for file-based ZIPs
|
|
758
|
-
Logger.error('getMerkleRootAsync() called on file-based ZIP. Use ZipkitServer.getMerkleRootAsync() instead.');
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
803
|
+
zipEntries = this.getDirectory();
|
|
761
804
|
} catch (error) {
|
|
762
805
|
Logger.error(`Failed to get directory for merkle root calculation: ${error instanceof Error ? error.message : String(error)}`);
|
|
763
806
|
return null;
|
|
764
807
|
}
|
|
765
|
-
|
|
808
|
+
|
|
766
809
|
if (!zipEntries || zipEntries.length === 0) {
|
|
767
810
|
return null;
|
|
768
811
|
}
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
812
|
+
|
|
813
|
+
const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ''));
|
|
814
|
+
|
|
815
|
+
if (algorithm === 'v0') {
|
|
816
|
+
const digests = contentEntries
|
|
817
|
+
.filter((e) => e.sha256)
|
|
818
|
+
.map((e) => ({ path: e.filename || '', sha256: e.sha256 as string }));
|
|
819
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Reuse leaves already produced during write/extract (one stream hash pass)
|
|
823
|
+
if (
|
|
824
|
+
contentEntries.length > 0 &&
|
|
825
|
+
contentEntries.every((e) => !!e.merkleLeafV1)
|
|
826
|
+
) {
|
|
827
|
+
return computeMerkleRootV1FromLeaves(
|
|
828
|
+
contentEntries.map((e) => ({
|
|
829
|
+
path: e.filename || '',
|
|
830
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
831
|
+
}))
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Buffer path: one decompress + dual hash inside extract (ZipDecompress)
|
|
836
|
+
if (!this.inBuffer) {
|
|
837
|
+
Logger.error(
|
|
838
|
+
'getMerkleRootAsync() called on file-based ZIP without precomputed merkle leaves. ' +
|
|
839
|
+
'Use ZipkitNode (streaming testEntry) or ensure create captured merkleLeafV1.'
|
|
840
|
+
);
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
|
|
781
844
|
for (const entry of contentEntries) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
845
|
+
try {
|
|
846
|
+
const data = await this.extract(entry, false);
|
|
847
|
+
if (entry.merkleLeafV1) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
// Legacy decompress without dual leaf: hash payload once, cache leaf
|
|
851
|
+
if (data === null || data.length === 0) {
|
|
852
|
+
entry.merkleLeafV1 = leafHashV1(Buffer.alloc(0)).toString('hex');
|
|
853
|
+
} else {
|
|
854
|
+
entry.merkleLeafV1 = leafHashV1(data).toString('hex');
|
|
855
|
+
}
|
|
856
|
+
} catch (error) {
|
|
857
|
+
Logger.error(
|
|
858
|
+
`getMerkleRootAsync(): failed to extract ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`
|
|
859
|
+
);
|
|
860
|
+
return null;
|
|
786
861
|
}
|
|
787
862
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (!merkleRoot) {
|
|
863
|
+
|
|
864
|
+
if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
791
865
|
return null;
|
|
792
866
|
}
|
|
793
867
|
|
|
794
|
-
return
|
|
868
|
+
return computeMerkleRootV1FromLeaves(
|
|
869
|
+
contentEntries.map((e) => ({
|
|
870
|
+
path: e.filename || '',
|
|
871
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
872
|
+
}))
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Compute Merkle root from known digests and/or payloads (APPNOTE §6).
|
|
878
|
+
* Prefer this during create when uncompressed bytes are still in hand, or when
|
|
879
|
+
* stream leaves (`merkleLeafV1`) were already produced.
|
|
880
|
+
*/
|
|
881
|
+
static merkleRootFromEntries(
|
|
882
|
+
entries: Array<{ path: string; content?: Buffer; contentSha256?: string; merkleLeafV1?: string }>,
|
|
883
|
+
algorithm: MerkleAlgorithm = 'v1'
|
|
884
|
+
): string | null {
|
|
885
|
+
return computeArchiveMerkleRoot(entries, algorithm);
|
|
795
886
|
}
|
|
796
887
|
|
|
797
888
|
}
|
|
@@ -45,6 +45,8 @@ export class HashCalculator {
|
|
|
45
45
|
// Incremental hash calculation state (from StreamHashCalculator)
|
|
46
46
|
private crc32State: number = ~0;
|
|
47
47
|
private sha256Hash: crypto.Hash | null = null;
|
|
48
|
+
/** Domain-separated leaf hasher (APPNOTE §6.3): SHA-256(0x00 ‖ payload). */
|
|
49
|
+
private merkleLeafHash: crypto.Hash | null = null;
|
|
48
50
|
private useSHA256: boolean = false;
|
|
49
51
|
|
|
50
52
|
// Hash accumulation state (from HashAccumulator)
|
|
@@ -52,13 +54,15 @@ export class HashCalculator {
|
|
|
52
54
|
private xorResult: Buffer = Buffer.alloc(32, 0);
|
|
53
55
|
private enableAccumulation: boolean = false;
|
|
54
56
|
|
|
55
|
-
// Merkle tree state (from MerkleTree)
|
|
57
|
+
// Merkle tree state (from MerkleTree) — generic digest folding; archive roots use merkle/MerkleRoot.ts
|
|
56
58
|
private merkleLeaves: Buffer[] = [];
|
|
57
59
|
private merkleLayers: Buffer[][] = [];
|
|
58
60
|
private merkleOptions: MerkleTreeOptions = {
|
|
59
61
|
hashLeaves: false,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
// Archive content roots sort by path in MerkleRoot; generic accumulator keeps insertion order.
|
|
63
|
+
sortLeaves: false,
|
|
64
|
+
sortPairs: false,
|
|
65
|
+
// match APPNOTE v0 odd-leaf rule when used as a generic Bitcoin-style tree
|
|
62
66
|
duplicateOdd: true
|
|
63
67
|
};
|
|
64
68
|
|
|
@@ -77,6 +81,8 @@ export class HashCalculator {
|
|
|
77
81
|
|
|
78
82
|
if (this.useSHA256) {
|
|
79
83
|
this.sha256Hash = crypto.createHash('sha256');
|
|
84
|
+
this.merkleLeafHash = crypto.createHash('sha256');
|
|
85
|
+
this.merkleLeafHash.update(Buffer.from([0x00]));
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -95,10 +101,13 @@ export class HashCalculator {
|
|
|
95
101
|
this.crc32State = crc32update(this.crc32State, chunk[i]);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
// Update SHA-256 incrementally
|
|
104
|
+
// Update SHA-256 and v1 Merkle leaf hash incrementally
|
|
99
105
|
if (this.sha256Hash) {
|
|
100
106
|
this.sha256Hash.update(chunk);
|
|
101
107
|
}
|
|
108
|
+
if (this.merkleLeafHash) {
|
|
109
|
+
this.merkleLeafHash.update(chunk);
|
|
110
|
+
}
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
/**
|
|
@@ -110,7 +119,7 @@ export class HashCalculator {
|
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/**
|
|
113
|
-
* Get final SHA-256 hash as hex string
|
|
122
|
+
* Get final SHA-256 hash as hex string (Extra Field 0x014E — bare content digest)
|
|
114
123
|
* @returns SHA-256 hash as hex string, or null if SHA-256 not enabled
|
|
115
124
|
*/
|
|
116
125
|
finalizeSHA256(): string | null {
|
|
@@ -120,13 +129,55 @@ export class HashCalculator {
|
|
|
120
129
|
return null;
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Get APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload) as hex.
|
|
134
|
+
* Consumes the leaf hasher; call after or instead of finalizeSHA256 for dual streams.
|
|
135
|
+
*/
|
|
136
|
+
finalizeMerkleLeafV1(): string | null {
|
|
137
|
+
if (this.merkleLeafHash) {
|
|
138
|
+
return this.merkleLeafHash.digest('hex');
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Apply stream digests to a ZIP entry in one place.
|
|
145
|
+
*
|
|
146
|
+
* When SHA-256 was enabled, both Extra Field 0x014E (`sha256`) and the v1 Merkle leaf
|
|
147
|
+
* (`merkleLeafV1`) are finalized from the **same** chunk stream — no second payload pass.
|
|
148
|
+
*/
|
|
149
|
+
applyToEntry(
|
|
150
|
+
entry: {
|
|
151
|
+
crc: number;
|
|
152
|
+
sha256: string | null;
|
|
153
|
+
merkleLeafV1?: string | null;
|
|
154
|
+
},
|
|
155
|
+
options: { setCrc?: boolean; setSha256?: boolean }
|
|
156
|
+
): void {
|
|
157
|
+
if (options.setCrc) {
|
|
158
|
+
entry.crc = this.finalizeCRC32();
|
|
159
|
+
}
|
|
160
|
+
if (options.setSha256) {
|
|
161
|
+
const digest = this.finalizeSHA256();
|
|
162
|
+
const leaf = this.finalizeMerkleLeafV1();
|
|
163
|
+
if (digest) {
|
|
164
|
+
entry.sha256 = digest;
|
|
165
|
+
}
|
|
166
|
+
if (leaf) {
|
|
167
|
+
entry.merkleLeafV1 = leaf;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
123
172
|
/**
|
|
124
173
|
* Reset the incremental hash calculation state
|
|
125
174
|
*/
|
|
126
175
|
reset(): void {
|
|
127
176
|
this.crc32State = ~0;
|
|
128
|
-
if (this.
|
|
177
|
+
if (this.useSHA256) {
|
|
129
178
|
this.sha256Hash = crypto.createHash('sha256');
|
|
179
|
+
this.merkleLeafHash = crypto.createHash('sha256');
|
|
180
|
+
this.merkleLeafHash.update(Buffer.from([0x00]));
|
|
130
181
|
}
|
|
131
182
|
}
|
|
132
183
|
|
|
@@ -264,6 +315,7 @@ export class HashCalculator {
|
|
|
264
315
|
if (this.merkleOptions.sortPairs && Buffer.compare(left, right) > 0) {
|
|
265
316
|
[left, right] = [right, left];
|
|
266
317
|
}
|
|
318
|
+
// Generic accumulator tree (not APPNOTE archive root — see merkle/MerkleRoot.ts)
|
|
267
319
|
return this.hash(Buffer.concat([left, right]));
|
|
268
320
|
}
|
|
269
321
|
|
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import Support from "./Support";
|
|
8
8
|
import Errors from "../constants/Errors";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isMetaInfPath,
|
|
11
|
+
findReservedMetaEntry,
|
|
12
|
+
isReservedMetaPath,
|
|
13
|
+
asciiPathEqualsIgnoreCase,
|
|
14
|
+
} from "../constants/MetaPaths";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
findReservedMetaEntry,
|
|
18
|
+
isReservedMetaPath,
|
|
19
|
+
isMetaInfPath,
|
|
20
|
+
asciiPathEqualsIgnoreCase,
|
|
21
|
+
} from "../constants/MetaPaths";
|
|
10
22
|
|
|
11
23
|
export const DATATYPE = {
|
|
12
24
|
STRING: "string",
|
|
@@ -80,12 +92,9 @@ export function getTypeOf(input: string|Buffer|Blob|Array<any>|ArrayBuffer|Uint8
|
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
/**
|
|
83
|
-
* Check if a filename is
|
|
84
|
-
*
|
|
85
|
-
* @returns True if the file is a metadata file (META-INF)
|
|
95
|
+
* Check if a filename is NeoZip metadata (any META-INF/** path).
|
|
96
|
+
* Case-insensitive on the META-INF/ prefix (NEOZIP_APPNOTE.md §2.3 / §6.2).
|
|
86
97
|
*/
|
|
87
98
|
export const isMetadataFile = (filename: string): boolean => {
|
|
88
|
-
return filename
|
|
89
|
-
filename === TIMESTAMP_METADATA ||
|
|
90
|
-
filename === TOKENIZED_METADATA;
|
|
99
|
+
return isMetaInfPath(filename);
|
|
91
100
|
};
|
|
@@ -11,9 +11,19 @@ export const ZIP64_CENTRAL_DIRECTORY_LOCATOR = 'PK\x06\x07';
|
|
|
11
11
|
export const ZIP64_CENTRAL_DIRECTORY_END = 'PK\x06\x06';
|
|
12
12
|
export const DATA_DESCRIPTOR = 'PK\x07\x08';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
|
|
14
|
+
// Reserved META-INF paths — see constants/MetaPaths.ts and NEOZIP_APPNOTE.md §2
|
|
15
|
+
export {
|
|
16
|
+
META_TOKEN_NZIP,
|
|
17
|
+
META_TOKEN_NZIP_LEGACY,
|
|
18
|
+
META_TS_SUBMIT_NZIP,
|
|
19
|
+
META_TIMESTAMP_NZIP,
|
|
20
|
+
META_TS_SUBMIT_OTS,
|
|
21
|
+
META_TIMESTAMP_OTS,
|
|
22
|
+
META_MANIFEST_JSON,
|
|
23
|
+
TIMESTAMP_SUBMITTED,
|
|
24
|
+
TIMESTAMP_METADATA,
|
|
25
|
+
TOKENIZED_METADATA,
|
|
26
|
+
} from './MetaPaths';
|
|
17
27
|
|
|
18
28
|
// Local file header
|
|
19
29
|
export const LOCAL_HDR = {
|
|
@@ -176,7 +186,8 @@ export const HDR_ID = {
|
|
|
176
186
|
x509PKCS7RL: 0x0019, // X.509 Certificate Revocation List (PKCS#7)
|
|
177
187
|
IBM1: 0x0065, // IBM S/390 (Z390) - Attribute
|
|
178
188
|
IBM2: 0x0066, // IBM S/390 (Z390) - Attribute
|
|
179
|
-
SHA256: 0x014E, //
|
|
189
|
+
SHA256: 0x014E, // NeoZip Extra Field: 32-byte SHA-256 of uncompressed payload (APPNOTE §5)
|
|
190
|
+
ORIGIN: 0x014F, // ZipWiki origin locator (URI, size, mtime, CRC-32 or SHA-256)
|
|
180
191
|
NEO_CRYPTO: 0x024E, // NeoEncrypt metadata (see docs/NEO_CRYPTO_FORMAT.md); LE on wire: 4E 02
|
|
181
192
|
POSZIP: 0x4690, // POSZIP 4690
|
|
182
193
|
UNV_TIME: 0x5455, // Universal Time (UT) (32-bit) 5 data bytes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NeoZip reserved META-INF paths and ASCII case-insensitive discovery.
|
|
3
|
+
* Matches NEOZIP_APPNOTE.md §2.3: writers emit canonical spellings; readers fold case.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Canonical writers MUST emit these spellings. */
|
|
7
|
+
export const META_TOKEN_NZIP = 'META-INF/TOKEN.NZIP';
|
|
8
|
+
export const META_TOKEN_NZIP_LEGACY = 'META-INF/NZIP.TOKEN';
|
|
9
|
+
export const META_TS_SUBMIT_NZIP = 'META-INF/TS-SUBMIT.NZIP';
|
|
10
|
+
export const META_TIMESTAMP_NZIP = 'META-INF/TIMESTAMP.NZIP';
|
|
11
|
+
export const META_TS_SUBMIT_OTS = 'META-INF/TS-SUBMIT.OTS';
|
|
12
|
+
export const META_TIMESTAMP_OTS = 'META-INF/TIMESTAMP.OTS';
|
|
13
|
+
export const META_MANIFEST_JSON = 'META-INF/manifest.json';
|
|
14
|
+
|
|
15
|
+
/** @deprecated Prefer META_TS_SUBMIT_OTS — kept for existing imports. */
|
|
16
|
+
export const TIMESTAMP_SUBMITTED = META_TS_SUBMIT_OTS;
|
|
17
|
+
/** @deprecated Prefer META_TIMESTAMP_OTS — kept for existing imports. */
|
|
18
|
+
export const TIMESTAMP_METADATA = META_TIMESTAMP_OTS;
|
|
19
|
+
/** @deprecated Prefer META_TOKEN_NZIP — kept for existing imports (canonical TOKEN.NZIP). */
|
|
20
|
+
export const TOKENIZED_METADATA = META_TOKEN_NZIP;
|
|
21
|
+
|
|
22
|
+
const RESERVED_CANONICAL = [
|
|
23
|
+
META_TOKEN_NZIP,
|
|
24
|
+
META_TOKEN_NZIP_LEGACY,
|
|
25
|
+
META_TS_SUBMIT_NZIP,
|
|
26
|
+
META_TIMESTAMP_NZIP,
|
|
27
|
+
META_TS_SUBMIT_OTS,
|
|
28
|
+
META_TIMESTAMP_OTS,
|
|
29
|
+
META_MANIFEST_JSON,
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type ZipEntryLikeFilename = { filename?: string };
|
|
33
|
+
|
|
34
|
+
/** ASCII case-insensitive path compare (APPNOTE §2.3). */
|
|
35
|
+
export function asciiPathEqualsIgnoreCase(a: string, b: string): boolean {
|
|
36
|
+
if (a.length !== b.length) {
|
|
37
|
+
// Still allow length mismatch only via toLowerCase for UTF-8 safety on ASCII paths
|
|
38
|
+
}
|
|
39
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when path is under META-INF/ (ASCII case-insensitive). */
|
|
43
|
+
export function isMetaInfPath(filename: string): boolean {
|
|
44
|
+
return filename.toLowerCase().startsWith('meta-inf/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when filename is a reserved NeoZip meta path (any casing). */
|
|
48
|
+
export function isReservedMetaPath(filename: string): boolean {
|
|
49
|
+
const lower = filename.toLowerCase();
|
|
50
|
+
return RESERVED_CANONICAL.some((c) => c.toLowerCase() === lower);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* True for any META-INF/** entry (integrity merkle leaves exclude these per APPNOTE §6.2).
|
|
55
|
+
*/
|
|
56
|
+
export function isMetadataFile(filename: string): boolean {
|
|
57
|
+
return isMetaInfPath(filename);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Find a reserved meta entry among ZIP directory entries.
|
|
62
|
+
* Prefers an exact canonical match, then ASCII case-insensitive match.
|
|
63
|
+
* When multiple preferred names are given, earlier names win.
|
|
64
|
+
*/
|
|
65
|
+
export function findReservedMetaEntry<T extends ZipEntryLikeFilename>(
|
|
66
|
+
entries: T[],
|
|
67
|
+
preferredCanonicalNames: string | readonly string[]
|
|
68
|
+
): T | null {
|
|
69
|
+
const preferred = Array.isArray(preferredCanonicalNames)
|
|
70
|
+
? preferredCanonicalNames
|
|
71
|
+
: [preferredCanonicalNames];
|
|
72
|
+
|
|
73
|
+
for (const name of preferred) {
|
|
74
|
+
const exact = entries.find((e) => e.filename === name);
|
|
75
|
+
if (exact) return exact;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const name of preferred) {
|
|
79
|
+
const folded = entries.find(
|
|
80
|
+
(e) => typeof e.filename === 'string' && asciiPathEqualsIgnoreCase(e.filename, name)
|
|
81
|
+
);
|
|
82
|
+
if (folded) return folded;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|