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
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,
|
|
@@ -28,6 +35,11 @@ import {
|
|
|
28
35
|
CENTRAL_DIRECTORY_END,
|
|
29
36
|
HDR_ID
|
|
30
37
|
} from './constants/Headers';
|
|
38
|
+
import {
|
|
39
|
+
findReservedMetaEntry,
|
|
40
|
+
isMetaInfPath,
|
|
41
|
+
isReservedMetaPath,
|
|
42
|
+
} from './constants/MetaPaths';
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
45
|
* Streaming file handle interface
|
|
@@ -371,15 +383,22 @@ export default class Zipkit {
|
|
|
371
383
|
|
|
372
384
|
|
|
373
385
|
/**
|
|
374
|
-
* Get specific ZIP entry by filename from zipEntries[] array
|
|
375
|
-
*
|
|
386
|
+
* Get specific ZIP entry by filename from zipEntries[] array.
|
|
387
|
+
* Reserved META-INF paths (APPNOTE §2.3) also match ASCII case-insensitively
|
|
388
|
+
* when an exact spelling is not found.
|
|
389
|
+
*
|
|
376
390
|
* @param filename - The name/path of the file/directory to find
|
|
377
391
|
* @returns ZipEntry object if found, null if not found
|
|
378
392
|
*/
|
|
379
393
|
getZipEntry(filename: string): ZipEntry | null {
|
|
380
394
|
try {
|
|
381
395
|
const centralDir = this.getDirectory();
|
|
382
|
-
|
|
396
|
+
const exact = centralDir.find((entry: ZipEntry) => entry.filename === filename);
|
|
397
|
+
if (exact) return exact;
|
|
398
|
+
if (isReservedMetaPath(filename)) {
|
|
399
|
+
return findReservedMetaEntry(centralDir, filename);
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
383
402
|
} catch (error) {
|
|
384
403
|
return null;
|
|
385
404
|
}
|
|
@@ -686,112 +705,166 @@ export default class Zipkit {
|
|
|
686
705
|
}
|
|
687
706
|
|
|
688
707
|
/**
|
|
689
|
-
* Calculate Merkle
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
708
|
+
* Calculate Merkle root of archive content (non-META-INF entries).
|
|
709
|
+
*
|
|
710
|
+
* Per NEOZIP_APPNOTE.md §6:
|
|
711
|
+
* - Default **v1**: domain-separated leaves SHA-256(0x00 ‖ uncompressed bytes) and
|
|
712
|
+
* SHA-256(0x01 ‖ left ‖ right) parents; paths NFC-normalized before sort.
|
|
713
|
+
* - Legacy **v0**: bare 0x014E digests as leaves, duplicate-odd pairing, path as stored.
|
|
714
|
+
*
|
|
715
|
+
* When only Extra Field digests are available (typical after open), **v1 requires
|
|
716
|
+
* payload bytes**. This sync method therefore returns null for v1; use
|
|
717
|
+
* {@link getMerkleRootAsync} (extracts content) or `{ algorithm: 'v0' }`.
|
|
718
|
+
* When you already hold payloads, use {@link merkleRootFromEntries}.
|
|
719
|
+
*
|
|
720
|
+
* @param options.algorithm - `v1` (default) or legacy `v0`
|
|
721
|
+
* @returns Lowercase hex Merkle root, or null if calculation fails
|
|
695
722
|
*/
|
|
696
|
-
getMerkleRoot(): string | null {
|
|
697
|
-
|
|
723
|
+
getMerkleRoot(options?: { algorithm?: MerkleAlgorithm }): string | null {
|
|
724
|
+
const algorithm: MerkleAlgorithm = options?.algorithm ?? 'v1';
|
|
725
|
+
|
|
698
726
|
let zipEntries: ZipEntry[];
|
|
699
727
|
try {
|
|
700
728
|
zipEntries = this.getDirectory();
|
|
701
729
|
} catch (error) {
|
|
702
|
-
// Catch any errors from getDirectory() - this should never happen
|
|
703
|
-
// but if it does, log it and return null
|
|
704
730
|
Logger.error(`getMerkleRoot(): Error calling getDirectory(): ${error instanceof Error ? error.message : String(error)}`);
|
|
705
731
|
Logger.error(`getMerkleRoot(): Stack trace: ${error instanceof Error ? error.stack : 'No stack trace'}`);
|
|
706
732
|
return null;
|
|
707
733
|
}
|
|
708
|
-
|
|
734
|
+
|
|
709
735
|
if (!zipEntries || zipEntries.length === 0) {
|
|
710
736
|
return null;
|
|
711
737
|
}
|
|
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
|
-
}
|
|
738
|
+
|
|
739
|
+
const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ''));
|
|
740
|
+
|
|
741
|
+
if (algorithm === 'v0') {
|
|
742
|
+
const digests = contentEntries
|
|
743
|
+
.filter((e) => e.sha256)
|
|
744
|
+
.map((e) => ({ path: e.filename || '', sha256: e.sha256 as string }));
|
|
745
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
730
746
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if (
|
|
734
|
-
|
|
747
|
+
|
|
748
|
+
// v1: prefer leaves captured in the same create/extract hash stream (no re-hash)
|
|
749
|
+
if (
|
|
750
|
+
contentEntries.length > 0 &&
|
|
751
|
+
contentEntries.every((e) => !!e.merkleLeafV1)
|
|
752
|
+
) {
|
|
753
|
+
return computeMerkleRootV1FromLeaves(
|
|
754
|
+
contentEntries.map((e) => ({
|
|
755
|
+
path: e.filename || '',
|
|
756
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
757
|
+
}))
|
|
758
|
+
);
|
|
735
759
|
}
|
|
736
760
|
|
|
737
|
-
|
|
761
|
+
// v1: bare digests are not leaves — need getMerkleRootAsync() or stream leaves
|
|
762
|
+
return null;
|
|
738
763
|
}
|
|
739
764
|
|
|
740
765
|
/**
|
|
741
|
-
* Calculate Merkle
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
766
|
+
* Calculate Merkle root (preferred for v1 / new archives).
|
|
767
|
+
*
|
|
768
|
+
* **v1 (default):** uses precomputed stream leaves when available; otherwise extracts
|
|
769
|
+
* each content entry once and folds APPNOTE §6.3 root (no second content hash pass
|
|
770
|
+
* when extract captures merkleLeafV1).
|
|
771
|
+
* **v0:** uses Extra Field digests only (no extract), APPNOTE §6.2.
|
|
772
|
+
*
|
|
773
|
+
* META-INF/** is excluded. File-streaming mode: use ZipkitNode override (testEntry
|
|
774
|
+
* pipeline) rather than buffering the whole archive.
|
|
775
|
+
*
|
|
776
|
+
* @param options.algorithm - `v1` (default) or legacy `v0`
|
|
777
|
+
* @returns Promise resolving to lowercase hex root, or null
|
|
747
778
|
*/
|
|
748
|
-
async getMerkleRootAsync(): Promise<string | null> {
|
|
779
|
+
async getMerkleRootAsync(options?: { algorithm?: MerkleAlgorithm }): Promise<string | null> {
|
|
780
|
+
const algorithm: MerkleAlgorithm = options?.algorithm ?? 'v1';
|
|
781
|
+
|
|
749
782
|
let zipEntries: ZipEntry[] = [];
|
|
750
|
-
|
|
783
|
+
|
|
751
784
|
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
|
-
}
|
|
785
|
+
zipEntries = this.getDirectory();
|
|
761
786
|
} catch (error) {
|
|
762
787
|
Logger.error(`Failed to get directory for merkle root calculation: ${error instanceof Error ? error.message : String(error)}`);
|
|
763
788
|
return null;
|
|
764
789
|
}
|
|
765
|
-
|
|
790
|
+
|
|
766
791
|
if (!zipEntries || zipEntries.length === 0) {
|
|
767
792
|
return null;
|
|
768
793
|
}
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
794
|
+
|
|
795
|
+
const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ''));
|
|
796
|
+
|
|
797
|
+
if (algorithm === 'v0') {
|
|
798
|
+
const digests = contentEntries
|
|
799
|
+
.filter((e) => e.sha256)
|
|
800
|
+
.map((e) => ({ path: e.filename || '', sha256: e.sha256 as string }));
|
|
801
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Reuse leaves already produced during write/extract (one stream hash pass)
|
|
805
|
+
if (
|
|
806
|
+
contentEntries.length > 0 &&
|
|
807
|
+
contentEntries.every((e) => !!e.merkleLeafV1)
|
|
808
|
+
) {
|
|
809
|
+
return computeMerkleRootV1FromLeaves(
|
|
810
|
+
contentEntries.map((e) => ({
|
|
811
|
+
path: e.filename || '',
|
|
812
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
813
|
+
}))
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Buffer path: one decompress + dual hash inside extract (ZipDecompress)
|
|
818
|
+
if (!this.inBuffer) {
|
|
819
|
+
Logger.error(
|
|
820
|
+
'getMerkleRootAsync() called on file-based ZIP without precomputed merkle leaves. ' +
|
|
821
|
+
'Use ZipkitNode (streaming testEntry) or ensure create captured merkleLeafV1.'
|
|
822
|
+
);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
781
826
|
for (const entry of contentEntries) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
827
|
+
try {
|
|
828
|
+
const data = await this.extract(entry, false);
|
|
829
|
+
if (entry.merkleLeafV1) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
// Legacy decompress without dual leaf: hash payload once, cache leaf
|
|
833
|
+
if (data === null || data.length === 0) {
|
|
834
|
+
entry.merkleLeafV1 = leafHashV1(Buffer.alloc(0)).toString('hex');
|
|
835
|
+
} else {
|
|
836
|
+
entry.merkleLeafV1 = leafHashV1(data).toString('hex');
|
|
837
|
+
}
|
|
838
|
+
} catch (error) {
|
|
839
|
+
Logger.error(
|
|
840
|
+
`getMerkleRootAsync(): failed to extract ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`
|
|
841
|
+
);
|
|
842
|
+
return null;
|
|
786
843
|
}
|
|
787
844
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (!merkleRoot) {
|
|
845
|
+
|
|
846
|
+
if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
791
847
|
return null;
|
|
792
848
|
}
|
|
793
849
|
|
|
794
|
-
return
|
|
850
|
+
return computeMerkleRootV1FromLeaves(
|
|
851
|
+
contentEntries.map((e) => ({
|
|
852
|
+
path: e.filename || '',
|
|
853
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
854
|
+
}))
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Compute Merkle root from known digests and/or payloads (APPNOTE §6).
|
|
860
|
+
* Prefer this during create when uncompressed bytes are still in hand, or when
|
|
861
|
+
* stream leaves (`merkleLeafV1`) were already produced.
|
|
862
|
+
*/
|
|
863
|
+
static merkleRootFromEntries(
|
|
864
|
+
entries: Array<{ path: string; content?: Buffer; contentSha256?: string; merkleLeafV1?: string }>,
|
|
865
|
+
algorithm: MerkleAlgorithm = 'v1'
|
|
866
|
+
): string | null {
|
|
867
|
+
return computeArchiveMerkleRoot(entries, algorithm);
|
|
795
868
|
}
|
|
796
869
|
|
|
797
870
|
}
|
|
@@ -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,7 @@ 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)
|
|
180
190
|
NEO_CRYPTO: 0x024E, // NeoEncrypt metadata (see docs/NEO_CRYPTO_FORMAT.md); LE on wire: 4E 02
|
|
181
191
|
POSZIP: 0x4690, // POSZIP 4690
|
|
182
192
|
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
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { default as HashCalculator, HashCalculator as HashCalculatorClass } from
|
|
|
24
24
|
export * from './components/Util';
|
|
25
25
|
export * from './components/Support';
|
|
26
26
|
export { ProgressTracker } from './components/ProgressTracker';
|
|
27
|
+
export * from './merkle/MerkleRoot';
|
|
27
28
|
|
|
28
29
|
// Encryption functionality
|
|
29
30
|
export { EncryptionManager } from './encryption/Manager';
|
|
@@ -33,6 +34,19 @@ export type { EncryptionMethod, EncryptionOptions, EncryptionResult, DecryptionR
|
|
|
33
34
|
// Types and constants
|
|
34
35
|
export * from '../types';
|
|
35
36
|
export * from './constants/Headers';
|
|
37
|
+
export {
|
|
38
|
+
META_TOKEN_NZIP,
|
|
39
|
+
META_TOKEN_NZIP_LEGACY,
|
|
40
|
+
META_TS_SUBMIT_NZIP,
|
|
41
|
+
META_TIMESTAMP_NZIP,
|
|
42
|
+
META_TS_SUBMIT_OTS,
|
|
43
|
+
META_TIMESTAMP_OTS,
|
|
44
|
+
META_MANIFEST_JSON,
|
|
45
|
+
asciiPathEqualsIgnoreCase,
|
|
46
|
+
isMetaInfPath,
|
|
47
|
+
isReservedMetaPath,
|
|
48
|
+
findReservedMetaEntry,
|
|
49
|
+
} from './constants/MetaPaths';
|
|
36
50
|
export * from './constants/Errors';
|
|
37
51
|
export * from './version';
|
|
38
52
|
|