neozipkit 0.8.0 → 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.
Files changed (115) hide show
  1. package/NEOZIP_APPNOTE.md +527 -0
  2. package/README.md +57 -21
  3. package/WHATS_NEW.md +146 -0
  4. package/dist/browser/ZipkitBrowser.d.ts.map +1 -1
  5. package/dist/browser/ZipkitBrowser.js +4 -6
  6. package/dist/browser/ZipkitBrowser.js.map +1 -1
  7. package/dist/browser/zstd-browser-shim.d.ts +28 -0
  8. package/dist/browser/zstd-browser-shim.d.ts.map +1 -0
  9. package/dist/browser/zstd-browser-shim.js +66 -0
  10. package/dist/browser/zstd-browser-shim.js.map +1 -0
  11. package/dist/browser-esm/index.js +2110 -41755
  12. package/dist/browser-esm/index.js.map +4 -4
  13. package/dist/browser-umd/index.js +2110 -41755
  14. package/dist/browser-umd/index.js.map +4 -4
  15. package/dist/browser-umd/index.min.js +3 -21
  16. package/dist/core/ZipCompress.d.ts +2 -7
  17. package/dist/core/ZipCompress.d.ts.map +1 -1
  18. package/dist/core/ZipCompress.js +57 -33
  19. package/dist/core/ZipCompress.js.map +1 -1
  20. package/dist/core/ZipDecompress.d.ts +1 -3
  21. package/dist/core/ZipDecompress.d.ts.map +1 -1
  22. package/dist/core/ZipDecompress.js +27 -17
  23. package/dist/core/ZipDecompress.js.map +1 -1
  24. package/dist/core/ZipEntry.d.ts +6 -0
  25. package/dist/core/ZipEntry.d.ts.map +1 -1
  26. package/dist/core/ZipEntry.js +7 -1
  27. package/dist/core/ZipEntry.js.map +1 -1
  28. package/dist/core/Zipkit.d.ts +43 -11
  29. package/dist/core/Zipkit.d.ts.map +1 -1
  30. package/dist/core/Zipkit.js +106 -63
  31. package/dist/core/Zipkit.js.map +1 -1
  32. package/dist/core/components/HashCalculator.d.ts +22 -1
  33. package/dist/core/components/HashCalculator.d.ts.map +1 -1
  34. package/dist/core/components/HashCalculator.js +49 -6
  35. package/dist/core/components/HashCalculator.js.map +1 -1
  36. package/dist/core/components/Util.d.ts +3 -4
  37. package/dist/core/components/Util.d.ts.map +1 -1
  38. package/dist/core/components/Util.js +10 -8
  39. package/dist/core/components/Util.js.map +1 -1
  40. package/dist/core/constants/Headers.d.ts +1 -3
  41. package/dist/core/constants/Headers.d.ts.map +1 -1
  42. package/dist/core/constants/Headers.js +14 -5
  43. package/dist/core/constants/Headers.js.map +1 -1
  44. package/dist/core/constants/MetaPaths.d.ts +38 -0
  45. package/dist/core/constants/MetaPaths.d.ts.map +1 -0
  46. package/dist/core/constants/MetaPaths.js +79 -0
  47. package/dist/core/constants/MetaPaths.js.map +1 -0
  48. package/dist/core/encryption/AesCrypto.d.ts +55 -11
  49. package/dist/core/encryption/AesCrypto.d.ts.map +1 -1
  50. package/dist/core/encryption/AesCrypto.js +223 -68
  51. package/dist/core/encryption/AesCrypto.js.map +1 -1
  52. package/dist/core/encryption/index.d.ts +1 -1
  53. package/dist/core/encryption/index.d.ts.map +1 -1
  54. package/dist/core/encryption/index.js +3 -1
  55. package/dist/core/encryption/index.js.map +1 -1
  56. package/dist/core/index.d.ts +2 -0
  57. package/dist/core/index.d.ts.map +1 -1
  58. package/dist/core/index.js +14 -1
  59. package/dist/core/index.js.map +1 -1
  60. package/dist/core/merkle/MerkleRoot.d.ts +109 -0
  61. package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
  62. package/dist/core/merkle/MerkleRoot.js +275 -0
  63. package/dist/core/merkle/MerkleRoot.js.map +1 -0
  64. package/dist/core/version.js +1 -1
  65. package/dist/node/LegacyZstd.d.ts +67 -0
  66. package/dist/node/LegacyZstd.d.ts.map +1 -0
  67. package/dist/node/LegacyZstd.js +209 -0
  68. package/dist/node/LegacyZstd.js.map +1 -0
  69. package/dist/node/ZipCompressNode.d.ts +9 -7
  70. package/dist/node/ZipCompressNode.d.ts.map +1 -1
  71. package/dist/node/ZipCompressNode.js +105 -75
  72. package/dist/node/ZipCompressNode.js.map +1 -1
  73. package/dist/node/ZipDecompressNode.d.ts +7 -3
  74. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  75. package/dist/node/ZipDecompressNode.js +68 -56
  76. package/dist/node/ZipDecompressNode.js.map +1 -1
  77. package/dist/node/ZipkitNode.d.ts +23 -0
  78. package/dist/node/ZipkitNode.d.ts.map +1 -1
  79. package/dist/node/ZipkitNode.js +195 -41
  80. package/dist/node/ZipkitNode.js.map +1 -1
  81. package/dist/node/ZstdNode.d.ts +39 -0
  82. package/dist/node/ZstdNode.d.ts.map +1 -0
  83. package/dist/node/ZstdNode.js +171 -0
  84. package/dist/node/ZstdNode.js.map +1 -0
  85. package/dist/node/index.d.ts +3 -0
  86. package/dist/node/index.d.ts.map +1 -1
  87. package/dist/node/index.js +11 -1
  88. package/dist/node/index.js.map +1 -1
  89. package/package.json +4 -3
  90. package/src/browser/ZipkitBrowser.ts +4 -6
  91. package/src/browser/zstd-browser-shim.ts +76 -0
  92. package/src/core/ZipCompress.ts +71 -42
  93. package/src/core/ZipDecompress.ts +35 -18
  94. package/src/core/ZipEntry.ts +8 -1
  95. package/src/core/Zipkit.ts +149 -76
  96. package/src/core/components/HashCalculator.ts +58 -6
  97. package/src/core/components/Util.ts +16 -7
  98. package/src/core/constants/Headers.ts +14 -4
  99. package/src/core/constants/MetaPaths.ts +86 -0
  100. package/src/core/encryption/AesCrypto.ts +265 -86
  101. package/src/core/encryption/index.ts +1 -1
  102. package/src/core/index.ts +14 -0
  103. package/src/core/merkle/MerkleRoot.ts +312 -0
  104. package/src/core/version.ts +1 -1
  105. package/src/node/LegacyZstd.ts +270 -0
  106. package/src/node/ZipCompressNode.ts +127 -84
  107. package/src/node/ZipDecompressNode.ts +93 -67
  108. package/src/node/ZipkitNode.ts +230 -42
  109. package/src/node/ZstdNode.ts +155 -0
  110. package/src/node/index.ts +10 -0
  111. package/dist/core/ZstdManager.d.ts +0 -56
  112. package/dist/core/ZstdManager.d.ts.map +0 -1
  113. package/dist/core/ZstdManager.js +0 -144
  114. package/dist/core/ZstdManager.js.map +0 -1
  115. package/src/core/ZstdManager.ts +0 -165
@@ -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
- sortLeaves: true,
61
- sortPairs: true,
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.sha256Hash) {
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 { TIMESTAMP_SUBMITTED, TIMESTAMP_METADATA, TOKENIZED_METADATA } from "../constants/Headers";
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 a metadata file
84
- * @param filename The filename to check
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 === TIMESTAMP_SUBMITTED ||
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
- export const TIMESTAMP_SUBMITTED = 'META-INF/TS-SUBMIT.OTS';
15
- export const TIMESTAMP_METADATA = 'META-INF/TIMESTAMP.OTS';
16
- export const TOKENIZED_METADATA = 'META-INF/NZIP.TOKEN';
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, // SHA256 "N\01" with 64 bytes (256 bit) of SHA256 hash data
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
+ }