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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// ======================================
|
|
2
|
+
// MerkleRoot.ts — NeoZip APPNOTE §6
|
|
3
|
+
// v0 (legacy Bitcoin-style) and v1 (RFC 6962–style domain separation)
|
|
4
|
+
// ======================================
|
|
5
|
+
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
import { isMetaInfPath } from '../constants/MetaPaths';
|
|
8
|
+
|
|
9
|
+
/** Merkle algorithms defined in NEOZIP_APPNOTE.md §6. */
|
|
10
|
+
export type MerkleAlgorithm = 'v0' | 'v1';
|
|
11
|
+
|
|
12
|
+
export interface MerkleContentEntry {
|
|
13
|
+
/** Central-directory path (UTF-8 string as stored or as known to the writer). */
|
|
14
|
+
path: string;
|
|
15
|
+
/**
|
|
16
|
+
* Uncompressed payload bytes (same bytes hashed into Extra Field 0x014E).
|
|
17
|
+
* Required for **v1** leaves when `merkleLeafV1` is absent. Optional for **v0** when `contentSha256` is set.
|
|
18
|
+
*/
|
|
19
|
+
content?: Buffer | Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Bare SHA-256 of uncompressed payload (= Extra Field 0x014E), hex or 32-byte Buffer.
|
|
22
|
+
* Used as v0 leaf; for v1 this alone is **not** a leaf (see APPNOTE §6.3).
|
|
23
|
+
*/
|
|
24
|
+
contentSha256?: string | Buffer;
|
|
25
|
+
/**
|
|
26
|
+
* Precomputed APPNOTE §6.3 v1 leaf: SHA-256(0x00 ‖ uncompressed_bytes), hex or 32-byte Buffer.
|
|
27
|
+
* Produced in the same streaming pass as `contentSha256` (create/extract via HashCalculator).
|
|
28
|
+
* Preferred for v1 when present — no second content read.
|
|
29
|
+
*/
|
|
30
|
+
merkleLeafV1?: string | Buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LEAF_DOMAIN = Buffer.from([0x00]);
|
|
34
|
+
const NODE_DOMAIN = Buffer.from([0x01]);
|
|
35
|
+
|
|
36
|
+
function sha256(data: Buffer | Uint8Array | Buffer[]): Buffer {
|
|
37
|
+
const h = crypto.createHash('sha256');
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
for (const part of data) {
|
|
40
|
+
h.update(part);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
h.update(data);
|
|
44
|
+
}
|
|
45
|
+
return h.digest();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function asBuffer(data: Buffer | Uint8Array | string, encoding?: BufferEncoding): Buffer {
|
|
49
|
+
if (typeof data === 'string') {
|
|
50
|
+
return Buffer.from(data, encoding ?? 'hex');
|
|
51
|
+
}
|
|
52
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalize a ZIP entry path for v1 leaf ordering (APPNOTE §6.3):
|
|
57
|
+
* POSIX separators, strip leading `./` and `/`, Unicode NFC, UTF-8 byte order when compared.
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeMerklePath(path: string): string {
|
|
60
|
+
let p = path.replace(/\\/g, '/');
|
|
61
|
+
if (p.startsWith('./')) {
|
|
62
|
+
p = p.slice(2);
|
|
63
|
+
}
|
|
64
|
+
while (p.startsWith('/')) {
|
|
65
|
+
p = p.slice(1);
|
|
66
|
+
}
|
|
67
|
+
if (typeof p.normalize === 'function') {
|
|
68
|
+
p = p.normalize('NFC');
|
|
69
|
+
}
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Sort key for path comparison (UTF-8 byte order). */
|
|
74
|
+
function pathSortKey(path: string, algorithm: MerkleAlgorithm): Buffer {
|
|
75
|
+
const normalized = algorithm === 'v1' ? normalizeMerklePath(path) : path;
|
|
76
|
+
return Buffer.from(normalized, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Bare content digest (= 0x014E value). */
|
|
80
|
+
export function contentDigest(content: Buffer | Uint8Array): Buffer {
|
|
81
|
+
return sha256(asBuffer(content));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* v0 leaf: SHA-256(uncompressed bytes) — same as Extra Field 0x014E.
|
|
86
|
+
*/
|
|
87
|
+
export function leafHashV0(contentOrDigest: Buffer | Uint8Array, isDigest = false): Buffer {
|
|
88
|
+
const buf = asBuffer(contentOrDigest);
|
|
89
|
+
return isDigest ? buf : sha256(buf);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* v1 leaf: SHA-256(0x00 ‖ uncompressed_bytes).
|
|
94
|
+
* Must not be confused with the bare 0x014E digest (APPNOTE §6.3).
|
|
95
|
+
*/
|
|
96
|
+
export function leafHashV1(content: Buffer | Uint8Array): Buffer {
|
|
97
|
+
return sha256([LEAF_DOMAIN, asBuffer(content)]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parent node hashes.
|
|
102
|
+
* v0: SHA-256(H_left ‖ H_right) — order preserved, no pair sorting.
|
|
103
|
+
* v1: SHA-256(0x01 ‖ H_left ‖ H_right).
|
|
104
|
+
*/
|
|
105
|
+
export function parentHash(left: Buffer, right: Buffer, algorithm: MerkleAlgorithm): Buffer {
|
|
106
|
+
if (algorithm === 'v1') {
|
|
107
|
+
return sha256([NODE_DOMAIN, left, right]);
|
|
108
|
+
}
|
|
109
|
+
return sha256([left, right]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fold an ordered list of leaf hashes into a single root (hex lowercase).
|
|
114
|
+
* Odd-count rule:
|
|
115
|
+
* - v0: duplicate last leaf (Bitcoin-style)
|
|
116
|
+
* - v1: promote last node unhashed (RFC 6962–style)
|
|
117
|
+
*/
|
|
118
|
+
export function merkleRootFromLeaves(leaves: Buffer[], algorithm: MerkleAlgorithm): string | null {
|
|
119
|
+
if (leaves.length === 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let level: Buffer[] = leaves.map((l) => Buffer.from(l));
|
|
124
|
+
|
|
125
|
+
while (level.length > 1) {
|
|
126
|
+
const next: Buffer[] = [];
|
|
127
|
+
for (let i = 0; i + 1 < level.length; i += 2) {
|
|
128
|
+
next.push(parentHash(level[i], level[i + 1], algorithm));
|
|
129
|
+
}
|
|
130
|
+
if (level.length % 2 === 1) {
|
|
131
|
+
const last = level[level.length - 1];
|
|
132
|
+
if (algorithm === 'v0') {
|
|
133
|
+
// Duplicate last leaf, then hash the pair
|
|
134
|
+
next.push(parentHash(last, last, algorithm));
|
|
135
|
+
} else {
|
|
136
|
+
// Promote unhashed
|
|
137
|
+
next.push(last);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
level = next;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return level[0].toString('hex');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveLeaf(
|
|
147
|
+
entry: MerkleContentEntry,
|
|
148
|
+
algorithm: MerkleAlgorithm
|
|
149
|
+
): Buffer | null {
|
|
150
|
+
if (algorithm === 'v0') {
|
|
151
|
+
if (entry.contentSha256) {
|
|
152
|
+
const d = asBuffer(entry.contentSha256, typeof entry.contentSha256 === 'string' ? 'hex' : undefined);
|
|
153
|
+
if (d.length !== 32) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return d;
|
|
157
|
+
}
|
|
158
|
+
if (entry.content) {
|
|
159
|
+
return contentDigest(entry.content);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// v1: prefer precomputed domain-separated leaf from the same hashing pass as 0x014E
|
|
165
|
+
if (entry.merkleLeafV1) {
|
|
166
|
+
const d = asBuffer(
|
|
167
|
+
entry.merkleLeafV1,
|
|
168
|
+
typeof entry.merkleLeafV1 === 'string' ? 'hex' : undefined
|
|
169
|
+
);
|
|
170
|
+
if (d.length !== 32) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return d;
|
|
174
|
+
}
|
|
175
|
+
// Domain-separated hash of uncompressed payload (re-hash only if leaf not available)
|
|
176
|
+
if (entry.content) {
|
|
177
|
+
return leafHashV1(entry.content);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Compute archive Merkle root per APPNOTE §6 from content entries
|
|
184
|
+
* (caller already excluded META-INF/** if desired).
|
|
185
|
+
*
|
|
186
|
+
* @returns lowercase hex root, or null if no usable leaves
|
|
187
|
+
*/
|
|
188
|
+
export function computeArchiveMerkleRoot(
|
|
189
|
+
entries: MerkleContentEntry[],
|
|
190
|
+
algorithm: MerkleAlgorithm = 'v1'
|
|
191
|
+
): string | null {
|
|
192
|
+
const prepared: { key: Buffer; leaf: Buffer }[] = [];
|
|
193
|
+
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (isMetaInfPath(entry.path || '')) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const leaf = resolveLeaf(entry, algorithm);
|
|
199
|
+
if (!leaf) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
prepared.push({
|
|
203
|
+
key: pathSortKey(entry.path || '', algorithm),
|
|
204
|
+
leaf,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (prepared.length === 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
prepared.sort((a, b) => Buffer.compare(a.key, b.key));
|
|
213
|
+
return merkleRootFromLeaves(
|
|
214
|
+
prepared.map((p) => p.leaf),
|
|
215
|
+
algorithm
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build v0 root from path + bare content digests only (0x014E extras).
|
|
221
|
+
* Path order: central-directory bytes as stored (no NFC / strip).
|
|
222
|
+
*/
|
|
223
|
+
export function computeMerkleRootV0FromDigests(
|
|
224
|
+
entries: Array<{ path: string; sha256: string | Buffer }>
|
|
225
|
+
): string | null {
|
|
226
|
+
return computeArchiveMerkleRoot(
|
|
227
|
+
entries.map((e) => ({ path: e.path, contentSha256: e.sha256 })),
|
|
228
|
+
'v0'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build v1 root from path + uncompressed payloads (APPNOTE §6.3).
|
|
234
|
+
* Paths are normalized before sorting.
|
|
235
|
+
*/
|
|
236
|
+
export function computeMerkleRootV1FromContents(
|
|
237
|
+
entries: Array<{ path: string; content: Buffer | Uint8Array }>
|
|
238
|
+
): string | null {
|
|
239
|
+
return computeArchiveMerkleRoot(
|
|
240
|
+
entries.map((e) => ({ path: e.path, content: e.content })),
|
|
241
|
+
'v1'
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Build v1 root from path + precomputed domain-separated leaves (APPNOTE §6.3).
|
|
247
|
+
* Prefer this after create/extract when HashCalculator produced merkleLeafV1 in one stream pass.
|
|
248
|
+
*/
|
|
249
|
+
export function computeMerkleRootV1FromLeaves(
|
|
250
|
+
entries: Array<{ path: string; merkleLeafV1: string | Buffer }>
|
|
251
|
+
): string | null {
|
|
252
|
+
return computeArchiveMerkleRoot(
|
|
253
|
+
entries.map((e) => ({ path: e.path, merkleLeafV1: e.merkleLeafV1 })),
|
|
254
|
+
'v1'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Verify a declared root (APPNOTE §6.4): try v1 first (when content or leaves available),
|
|
260
|
+
* then v0 digests. Returns which algorithm matched, or null on failure.
|
|
261
|
+
*
|
|
262
|
+
* When only bare 0x014E digests are available, only v0 can match APPNOTE correctly.
|
|
263
|
+
* Pass `merkleLeafV1` or content for full v1 verification without re-hashing from scratch
|
|
264
|
+
* when leaves were computed in the decompress stream.
|
|
265
|
+
*/
|
|
266
|
+
export function matchMerkleRoot(
|
|
267
|
+
entries: MerkleContentEntry[],
|
|
268
|
+
declaredRoot: string
|
|
269
|
+
): { algorithm: MerkleAlgorithm; security: 'high' | 'legacy' } | null {
|
|
270
|
+
const expected = declaredRoot.toLowerCase().replace(/^0x/, '');
|
|
271
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const hasV1Material = entries.some(
|
|
276
|
+
(e) =>
|
|
277
|
+
!isMetaInfPath(e.path || '') &&
|
|
278
|
+
(!!e.merkleLeafV1 || !!e.content)
|
|
279
|
+
);
|
|
280
|
+
if (hasV1Material) {
|
|
281
|
+
const v1 = computeArchiveMerkleRoot(entries, 'v1');
|
|
282
|
+
if (v1 && v1 === expected) {
|
|
283
|
+
return { algorithm: 'v1', security: 'high' };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const v0 = computeArchiveMerkleRoot(entries, 'v0');
|
|
288
|
+
if (v0 && v0 === expected) {
|
|
289
|
+
return { algorithm: 'v0', security: 'legacy' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// If v1 was not tried (no leaves/content), v0 was the only attempt.
|
|
293
|
+
if (hasV1Material) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export default {
|
|
301
|
+
normalizeMerklePath,
|
|
302
|
+
contentDigest,
|
|
303
|
+
leafHashV0,
|
|
304
|
+
leafHashV1,
|
|
305
|
+
parentHash,
|
|
306
|
+
merkleRootFromLeaves,
|
|
307
|
+
computeArchiveMerkleRoot,
|
|
308
|
+
computeMerkleRootV0FromDigests,
|
|
309
|
+
computeMerkleRootV1FromContents,
|
|
310
|
+
computeMerkleRootV1FromLeaves,
|
|
311
|
+
matchMerkleRoot,
|
|
312
|
+
};
|
package/src/core/version.ts
CHANGED
|
@@ -28,6 +28,6 @@ const currentDate = new Date();
|
|
|
28
28
|
const releaseDate = `${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}-${currentDate.getFullYear()}`;
|
|
29
29
|
|
|
30
30
|
export const VERSION = {
|
|
31
|
-
number: '1.0.
|
|
31
|
+
number: '1.0.5', // packageJson.version
|
|
32
32
|
date: releaseDate
|
|
33
33
|
};
|
|
@@ -119,12 +119,10 @@ export class ZipCompressNode {
|
|
|
119
119
|
|
|
120
120
|
if (hashCalculator) {
|
|
121
121
|
hashCalculator.update(data);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
127
|
-
}
|
|
122
|
+
hashCalculator.applyToEntry(entry, {
|
|
123
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
124
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
125
|
+
});
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
// Compress based on method
|
|
@@ -148,10 +146,11 @@ export class ZipCompressNode {
|
|
|
148
146
|
const enc = options.encryptionMethod;
|
|
149
147
|
if (enc === 'zipcrypto') {
|
|
150
148
|
buffer = this.encryptCompressedData(buffer, entry, options.password);
|
|
151
|
-
} else if (enc === '
|
|
152
|
-
buffer = this.encryptCompressedDataNeo(buffer, entry, options.password);
|
|
153
|
-
} else {
|
|
149
|
+
} else if (enc === 'aes256') {
|
|
154
150
|
buffer = this.encryptCompressedDataAes(buffer, entry, options.password);
|
|
151
|
+
} else {
|
|
152
|
+
// Default / 'neo-aes256': NeoEncrypt (0x024E)
|
|
153
|
+
buffer = this.encryptCompressedDataNeo(buffer, entry, options.password);
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
156
|
|
|
@@ -187,12 +186,10 @@ export class ZipCompressNode {
|
|
|
187
186
|
// For buffer, calculate hashes if needed
|
|
188
187
|
if (hashCalculator && entry) {
|
|
189
188
|
hashCalculator.update(data);
|
|
190
|
-
|
|
191
|
-
entry.crc
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
195
|
-
}
|
|
189
|
+
hashCalculator.applyToEntry(entry, {
|
|
190
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
191
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
192
|
+
});
|
|
196
193
|
}
|
|
197
194
|
return data;
|
|
198
195
|
} else {
|
|
@@ -219,12 +216,10 @@ export class ZipCompressNode {
|
|
|
219
216
|
|
|
220
217
|
// Finalize hashes
|
|
221
218
|
if (hashCalculator && entry) {
|
|
222
|
-
|
|
223
|
-
entry.crc
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
227
|
-
}
|
|
219
|
+
hashCalculator.applyToEntry(entry, {
|
|
220
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
221
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
222
|
+
});
|
|
228
223
|
}
|
|
229
224
|
|
|
230
225
|
return Buffer.alloc(0);
|
|
@@ -245,10 +240,10 @@ export class ZipCompressNode {
|
|
|
245
240
|
if (hashCalculator && entry) {
|
|
246
241
|
hashCalculator.update(data);
|
|
247
242
|
// Always set CRC if hash calculator was used (it calculated the correct CRC)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
hashCalculator.applyToEntry(entry, {
|
|
244
|
+
setCrc: true,
|
|
245
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
246
|
+
});
|
|
252
247
|
}
|
|
253
248
|
|
|
254
249
|
const result = pako.deflateRaw(data, { level: level ?? 6 });
|
|
@@ -298,10 +293,10 @@ export class ZipCompressNode {
|
|
|
298
293
|
// Finalize hashes after all chunks processed and compressed
|
|
299
294
|
if (hashCalculator && entry) {
|
|
300
295
|
// Always set CRC if hash calculator was used (it calculated the correct CRC)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
hashCalculator.applyToEntry(entry, {
|
|
297
|
+
setCrc: true,
|
|
298
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
299
|
+
});
|
|
305
300
|
}
|
|
306
301
|
|
|
307
302
|
// For chunked processing, return empty buffer (data already written via onOutputBuffer)
|
|
@@ -375,12 +370,10 @@ export class ZipCompressNode {
|
|
|
375
370
|
if (isBuffer) {
|
|
376
371
|
if (hashCalculator && entry) {
|
|
377
372
|
hashCalculator.update(input);
|
|
378
|
-
|
|
379
|
-
entry.crc
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
383
|
-
}
|
|
373
|
+
hashCalculator.applyToEntry(entry, {
|
|
374
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
375
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
376
|
+
});
|
|
384
377
|
}
|
|
385
378
|
return input;
|
|
386
379
|
}
|
|
@@ -405,12 +398,10 @@ export class ZipCompressNode {
|
|
|
405
398
|
position += chunk.length;
|
|
406
399
|
}
|
|
407
400
|
if (hashCalculator && entry) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
413
|
-
}
|
|
401
|
+
hashCalculator.applyToEntry(entry, {
|
|
402
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
403
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
404
|
+
});
|
|
414
405
|
}
|
|
415
406
|
return onOutputBuffer ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
416
407
|
}
|
|
@@ -430,10 +421,10 @@ export class ZipCompressNode {
|
|
|
430
421
|
|
|
431
422
|
if (isBuffer && hashCalculator && entry) {
|
|
432
423
|
hashCalculator.update(input);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
entry.sha256
|
|
436
|
-
}
|
|
424
|
+
hashCalculator.applyToEntry(entry, {
|
|
425
|
+
setCrc: true,
|
|
426
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
427
|
+
});
|
|
437
428
|
}
|
|
438
429
|
|
|
439
430
|
const collected: Buffer[] = [];
|
|
@@ -457,10 +448,10 @@ export class ZipCompressNode {
|
|
|
457
448
|
);
|
|
458
449
|
|
|
459
450
|
if (!isBuffer && hashCalculator && entry) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
entry.sha256
|
|
463
|
-
}
|
|
451
|
+
hashCalculator.applyToEntry(entry, {
|
|
452
|
+
setCrc: true,
|
|
453
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
454
|
+
});
|
|
464
455
|
}
|
|
465
456
|
if (entry) {
|
|
466
457
|
entry.compressedSize = compressedSize;
|
|
@@ -451,6 +451,8 @@ export class ZipDecompressNode {
|
|
|
451
451
|
if (!options?.skipHashCheck) {
|
|
452
452
|
if (entry.sha256) {
|
|
453
453
|
const calculatedHash = hashCalc.finalizeSHA256();
|
|
454
|
+
const leaf = hashCalc.finalizeMerkleLeafV1();
|
|
455
|
+
if (leaf) entry.merkleLeafV1 = leaf;
|
|
454
456
|
this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
|
|
455
457
|
if (calculatedHash !== entry.sha256) {
|
|
456
458
|
if (options?.outputPath && fs) {
|
|
@@ -540,6 +542,8 @@ export class ZipDecompressNode {
|
|
|
540
542
|
if (!options?.skipHashCheck) {
|
|
541
543
|
if (entry.sha256) {
|
|
542
544
|
const calculatedHash = hashCalc.finalizeSHA256();
|
|
545
|
+
const leaf = hashCalc.finalizeMerkleLeafV1();
|
|
546
|
+
if (leaf) entry.merkleLeafV1 = leaf;
|
|
543
547
|
this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
|
|
544
548
|
if (calculatedHash !== entry.sha256) {
|
|
545
549
|
throw new Error(Errors.INVALID_SHA256);
|
|
@@ -607,6 +611,8 @@ export class ZipDecompressNode {
|
|
|
607
611
|
if (!options?.skipHashCheck) {
|
|
608
612
|
if (entry.sha256) {
|
|
609
613
|
const calculatedHash = hashCalc.finalizeSHA256();
|
|
614
|
+
const leaf = hashCalc.finalizeMerkleLeafV1();
|
|
615
|
+
if (leaf) entry.merkleLeafV1 = leaf;
|
|
610
616
|
this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
|
|
611
617
|
if (calculatedHash !== entry.sha256) {
|
|
612
618
|
throw new Error(Errors.INVALID_SHA256);
|
|
@@ -625,7 +631,13 @@ export class ZipDecompressNode {
|
|
|
625
631
|
return { verifiedHash: undefined };
|
|
626
632
|
}
|
|
627
633
|
} else {
|
|
628
|
-
// Hash check skipped
|
|
634
|
+
// Hash check skipped — still finalize leaf/digest once from this stream when SHA was enabled
|
|
635
|
+
if (entry.sha256 && hashCalc) {
|
|
636
|
+
const calculatedHash = hashCalc.finalizeSHA256();
|
|
637
|
+
const leaf = hashCalc.finalizeMerkleLeafV1();
|
|
638
|
+
if (leaf) entry.merkleLeafV1 = leaf;
|
|
639
|
+
return { verifiedHash: calculatedHash || undefined };
|
|
640
|
+
}
|
|
629
641
|
return { verifiedHash: undefined };
|
|
630
642
|
}
|
|
631
643
|
} catch (error) {
|
package/src/node/ZipkitNode.ts
CHANGED
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
import * as fs from 'fs';
|
|
28
28
|
import * as path from 'path';
|
|
29
29
|
import { minimatch } from 'minimatch';
|
|
30
|
+
import {
|
|
31
|
+
computeMerkleRootV0FromDigests,
|
|
32
|
+
computeMerkleRootV1FromLeaves,
|
|
33
|
+
type MerkleAlgorithm,
|
|
34
|
+
} from '../core/merkle/MerkleRoot';
|
|
35
|
+
import { isMetaInfPath } from '../core/constants/MetaPaths';
|
|
30
36
|
|
|
31
37
|
// Re-export everything from core Zipkit
|
|
32
38
|
export * from '../core';
|
|
@@ -111,6 +117,79 @@ export default class ZipkitNode extends Zipkit {
|
|
|
111
117
|
return this._zipkitDeCmpNode;
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Streaming Merkle root for file-based archives.
|
|
122
|
+
* V1 leaves come from the same decompress→hash pass as integrity (testEntry / extract),
|
|
123
|
+
* chunked — never re-reads uncompressed sources or materializes whole entries for the root alone.
|
|
124
|
+
*/
|
|
125
|
+
async getMerkleRootAsync(options?: { algorithm?: MerkleAlgorithm }): Promise<string | null> {
|
|
126
|
+
const algorithm: MerkleAlgorithm = options?.algorithm ?? 'v1';
|
|
127
|
+
let zipEntries: ZipEntry[];
|
|
128
|
+
try {
|
|
129
|
+
zipEntries = this.getDirectory();
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!zipEntries?.length) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const contentEntries = zipEntries.filter((e) => !isMetaInfPath(e.filename || ''));
|
|
138
|
+
if (contentEntries.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (algorithm === 'v0') {
|
|
143
|
+
const digests = contentEntries
|
|
144
|
+
.filter((e) => e.sha256)
|
|
145
|
+
.map((e) => ({ path: e.filename || '', sha256: e.sha256 as string }));
|
|
146
|
+
return computeMerkleRootV0FromDigests(digests);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
150
|
+
return computeMerkleRootV1FromLeaves(
|
|
151
|
+
contentEntries.map((e) => ({
|
|
152
|
+
path: e.filename || '',
|
|
153
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
154
|
+
}))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Buffer-loaded: base class extract path
|
|
159
|
+
if ((this as any).inBuffer) {
|
|
160
|
+
return super.getMerkleRootAsync(options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// File-based: stream hash each content entry once via testEntry (no write, dual hash)
|
|
164
|
+
if (!this.fileHandle && !this.filePath) {
|
|
165
|
+
// Create-session only: may only have live ZipEntry[] with leaves from write
|
|
166
|
+
return super.getMerkleRootAsync(options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
for (const entry of contentEntries) {
|
|
171
|
+
if (entry.merkleLeafV1) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
await this.getZipDecompressNode().testEntry(entry, { skipHashCheck: false });
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('[ZipkitNode.getMerkleRootAsync] stream hash failed:', err);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return computeMerkleRootV1FromLeaves(
|
|
186
|
+
contentEntries.map((e) => ({
|
|
187
|
+
path: e.filename || '',
|
|
188
|
+
merkleLeafV1: e.merkleLeafV1 as string,
|
|
189
|
+
}))
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
114
193
|
// ============================================================================
|
|
115
194
|
// File Loading Methods
|
|
116
195
|
// ============================================================================
|
|
@@ -650,11 +729,10 @@ export default class ZipkitNode extends Zipkit {
|
|
|
650
729
|
}
|
|
651
730
|
|
|
652
731
|
// For AES encryption, pre-configure the entry so the initial local header
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
// Default password encryption is WinZip AES-256 (matches compressData).
|
|
732
|
+
// matches the chosen scheme (WinZip method 99 + 0x9901, or NeoEncrypt + 0x024E).
|
|
733
|
+
// Default password encryption is NeoEncrypt (matches NeoZip product AES-256).
|
|
656
734
|
const encMethod = options?.password
|
|
657
|
-
? (options.encryptionMethod || 'aes256')
|
|
735
|
+
? (options.encryptionMethod || 'neo-aes256')
|
|
658
736
|
: undefined;
|
|
659
737
|
const isAesRequested = encMethod === 'aes256';
|
|
660
738
|
const isNeoRequested = encMethod === 'neo-aes256';
|