neozipkit 0.7.2 → 1.0.2
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/README.md +57 -24
- package/WHATS_NEW.md +114 -0
- package/dist/browser/zstd-browser-shim.d.ts +28 -0
- package/dist/browser/zstd-browser-shim.d.ts.map +1 -0
- package/dist/browser/zstd-browser-shim.js +66 -0
- package/dist/browser/zstd-browser-shim.js.map +1 -0
- package/dist/browser-esm/index.js +1786 -41737
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +1786 -41737
- package/dist/browser-umd/index.js.map +4 -4
- package/dist/browser-umd/index.min.js +3 -21
- package/dist/core/ZipCompress.d.ts +2 -7
- package/dist/core/ZipCompress.d.ts.map +1 -1
- package/dist/core/ZipCompress.js +45 -22
- package/dist/core/ZipCompress.js.map +1 -1
- package/dist/core/ZipDecompress.d.ts +1 -3
- package/dist/core/ZipDecompress.d.ts.map +1 -1
- package/dist/core/ZipDecompress.js +14 -9
- package/dist/core/ZipDecompress.js.map +1 -1
- package/dist/core/encryption/AesCrypto.d.ts +55 -11
- package/dist/core/encryption/AesCrypto.d.ts.map +1 -1
- package/dist/core/encryption/AesCrypto.js +223 -68
- package/dist/core/encryption/AesCrypto.js.map +1 -1
- package/dist/core/encryption/index.d.ts +1 -1
- package/dist/core/encryption/index.d.ts.map +1 -1
- package/dist/core/encryption/index.js +3 -1
- package/dist/core/encryption/index.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/node/LegacyZstd.d.ts +67 -0
- package/dist/node/LegacyZstd.d.ts.map +1 -0
- package/dist/node/LegacyZstd.js +209 -0
- package/dist/node/LegacyZstd.js.map +1 -0
- package/dist/node/ZipCompressNode.d.ts +9 -7
- package/dist/node/ZipCompressNode.d.ts.map +1 -1
- package/dist/node/ZipCompressNode.js +85 -46
- package/dist/node/ZipCompressNode.js.map +1 -1
- package/dist/node/ZipDecompressNode.d.ts +7 -3
- package/dist/node/ZipDecompressNode.d.ts.map +1 -1
- package/dist/node/ZipDecompressNode.js +51 -55
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +14 -0
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +130 -39
- package/dist/node/ZipkitNode.js.map +1 -1
- package/dist/node/ZstdNode.d.ts +39 -0
- package/dist/node/ZstdNode.d.ts.map +1 -0
- package/dist/node/ZstdNode.js +171 -0
- package/dist/node/ZstdNode.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 +11 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/zstd-browser-shim.ts +76 -0
- package/src/core/ZipCompress.ts +57 -29
- package/src/core/ZipDecompress.ts +21 -10
- package/src/core/encryption/AesCrypto.ts +265 -86
- package/src/core/encryption/index.ts +1 -1
- package/src/core/version.ts +1 -1
- package/src/node/LegacyZstd.ts +270 -0
- package/src/node/ZipCompressNode.ts +107 -55
- package/src/node/ZipDecompressNode.ts +80 -66
- package/src/node/ZipkitNode.ts +150 -40
- package/src/node/ZstdNode.ts +155 -0
- package/src/node/index.ts +10 -0
- package/dist/core/ZstdManager.d.ts +0 -56
- package/dist/core/ZstdManager.d.ts.map +0 -1
- package/dist/core/ZstdManager.js +0 -144
- package/dist/core/ZstdManager.js.map +0 -1
- package/src/core/ZstdManager.ts +0 -165
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect legacy NeoZipKit WASM zstd (`@oneidentity/zstd-js`).
|
|
3
|
+
*
|
|
4
|
+
* That codec wrote valid-looking zstd frames but declared Frame_Content_Size as
|
|
5
|
+
* `uncompressed_size + 18`. Standard libzstd / Node zlib decode to the real
|
|
6
|
+
* payload plus 18 trailing zero bytes. CRC of the truncated payload matches
|
|
7
|
+
* the ZIP entry CRC.
|
|
8
|
+
*
|
|
9
|
+
* Header-only checks do not require decompressing. Verified checks decompress
|
|
10
|
+
* with native zlib and confirm trailing zeros + optional CRC.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ZstdNode } from './ZstdNode';
|
|
14
|
+
import { HashCalculator } from '../core/components/HashCalculator';
|
|
15
|
+
|
|
16
|
+
/** Fixed extra bytes declared/emitted by the broken WASM codec. */
|
|
17
|
+
export const LEGACY_WASM_ZSTD_PADDING = 18;
|
|
18
|
+
|
|
19
|
+
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
20
|
+
|
|
21
|
+
export type LegacyZstdConfidence = 'none' | 'header' | 'verified';
|
|
22
|
+
|
|
23
|
+
export interface LegacyZstdDetectResult {
|
|
24
|
+
/** True when this looks like (or verifies as) legacy WASM zstd. */
|
|
25
|
+
isLegacyWasm: boolean;
|
|
26
|
+
/** How the decision was made. */
|
|
27
|
+
confidence: LegacyZstdConfidence;
|
|
28
|
+
/** Parsed Frame_Content_Size, if present in the frame header. */
|
|
29
|
+
frameContentSize: number | null;
|
|
30
|
+
/** Expected plaintext size from the ZIP entry (or caller). */
|
|
31
|
+
expectedUncompressedSize: number;
|
|
32
|
+
/** Declared/observed padding past expected size (typically 18). */
|
|
33
|
+
paddingBytes: number;
|
|
34
|
+
/** Human-readable reason. */
|
|
35
|
+
reason: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DetectLegacyZstdOptions {
|
|
39
|
+
/**
|
|
40
|
+
* When true (default), decompress with native zlib and confirm
|
|
41
|
+
* trailing zeros (+ optional CRC) instead of trusting the header alone.
|
|
42
|
+
*/
|
|
43
|
+
verify?: boolean;
|
|
44
|
+
/** ZIP CRC-32 of the original uncompressed data (strengthens verified check). */
|
|
45
|
+
crc?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read Frame_Content_Size from a zstd frame header, or null if absent / unparseable.
|
|
50
|
+
* @see https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md
|
|
51
|
+
*/
|
|
52
|
+
export function parseZstdFrameContentSize(compressed: Buffer): number | null {
|
|
53
|
+
if (!compressed || compressed.length < 6) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
compressed[0] !== ZSTD_MAGIC[0] ||
|
|
58
|
+
compressed[1] !== ZSTD_MAGIC[1] ||
|
|
59
|
+
compressed[2] !== ZSTD_MAGIC[2] ||
|
|
60
|
+
compressed[3] !== ZSTD_MAGIC[3]
|
|
61
|
+
) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const descriptor = compressed[4];
|
|
66
|
+
const fcsFlag = (descriptor >> 6) & 0x3;
|
|
67
|
+
const singleSegment = ((descriptor >> 5) & 0x1) === 1;
|
|
68
|
+
const dictIdFlag = descriptor & 0x3;
|
|
69
|
+
const dictIdSizes = [0, 1, 2, 4];
|
|
70
|
+
let pos = 5 + dictIdSizes[dictIdFlag];
|
|
71
|
+
|
|
72
|
+
if (fcsFlag === 0) {
|
|
73
|
+
if (!singleSegment) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (pos >= compressed.length) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return compressed[pos];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fcsSizes = [0, 2, 4, 8];
|
|
83
|
+
const n = fcsSizes[fcsFlag];
|
|
84
|
+
if (pos + n > compressed.length) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let fcs: number;
|
|
89
|
+
if (n === 2) {
|
|
90
|
+
fcs = compressed.readUInt16LE(pos);
|
|
91
|
+
} else if (n === 4) {
|
|
92
|
+
fcs = compressed.readUInt32LE(pos);
|
|
93
|
+
} else {
|
|
94
|
+
const lo = compressed.readUInt32LE(pos);
|
|
95
|
+
const hi = compressed.readUInt32LE(pos + 4);
|
|
96
|
+
// Content sizes that do not fit in JS safe integers are not our legacy case.
|
|
97
|
+
if (hi !== 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
fcs = lo;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Spec: when FCS_Flag == 1, stored value is (size - 256).
|
|
104
|
+
if (fcsFlag === 1) {
|
|
105
|
+
fcs += 256;
|
|
106
|
+
}
|
|
107
|
+
return fcs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fast header heuristic: FCS === expectedUncompressedSize + 18.
|
|
112
|
+
*/
|
|
113
|
+
export function looksLikeLegacyWasmZstd(
|
|
114
|
+
compressed: Buffer,
|
|
115
|
+
expectedUncompressedSize: number
|
|
116
|
+
): boolean {
|
|
117
|
+
if (expectedUncompressedSize < 0 || !Number.isFinite(expectedUncompressedSize)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const fcs = parseZstdFrameContentSize(compressed);
|
|
121
|
+
if (fcs === null) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return fcs === expectedUncompressedSize + LEGACY_WASM_ZSTD_PADDING;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function crc32Of(data: Buffer): number {
|
|
128
|
+
const h = new HashCalculator();
|
|
129
|
+
h.update(data);
|
|
130
|
+
return h.finalizeCRC32();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Detect legacy WASM zstd frames for a single compressed payload.
|
|
135
|
+
*/
|
|
136
|
+
export async function detectLegacyWasmZstd(
|
|
137
|
+
compressed: Buffer,
|
|
138
|
+
expectedUncompressedSize: number,
|
|
139
|
+
options?: DetectLegacyZstdOptions
|
|
140
|
+
): Promise<LegacyZstdDetectResult> {
|
|
141
|
+
const expected = expectedUncompressedSize >>> 0;
|
|
142
|
+
const fcs = parseZstdFrameContentSize(compressed);
|
|
143
|
+
const headerMatch =
|
|
144
|
+
fcs !== null && fcs === expected + LEGACY_WASM_ZSTD_PADDING;
|
|
145
|
+
|
|
146
|
+
const base = {
|
|
147
|
+
frameContentSize: fcs,
|
|
148
|
+
expectedUncompressedSize: expected,
|
|
149
|
+
paddingBytes: LEGACY_WASM_ZSTD_PADDING,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (!headerMatch && options?.verify === false) {
|
|
153
|
+
return {
|
|
154
|
+
...base,
|
|
155
|
+
isLegacyWasm: false,
|
|
156
|
+
confidence: 'none',
|
|
157
|
+
reason:
|
|
158
|
+
fcs === null
|
|
159
|
+
? 'No Frame_Content_Size in zstd header (or not a zstd frame)'
|
|
160
|
+
: `Frame_Content_Size ${fcs} does not equal expected ${expected} + ${LEGACY_WASM_ZSTD_PADDING}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (options?.verify === false) {
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
isLegacyWasm: true,
|
|
168
|
+
confidence: 'header',
|
|
169
|
+
reason: `Frame_Content_Size is expected+${LEGACY_WASM_ZSTD_PADDING} (legacy WASM heuristic)`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Verified path (default): decompress and check padding / CRC.
|
|
174
|
+
let decoded: Buffer;
|
|
175
|
+
try {
|
|
176
|
+
decoded = await ZstdNode.decompress(compressed);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
isLegacyWasm: false,
|
|
181
|
+
confidence: 'none',
|
|
182
|
+
reason: `Native decompress failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pad = decoded.length - expected;
|
|
187
|
+
const trailingZeros =
|
|
188
|
+
pad === LEGACY_WASM_ZSTD_PADDING &&
|
|
189
|
+
decoded.subarray(expected).equals(Buffer.alloc(LEGACY_WASM_ZSTD_PADDING));
|
|
190
|
+
const truncated = decoded.subarray(0, Math.min(expected, decoded.length));
|
|
191
|
+
const crcOk =
|
|
192
|
+
options?.crc === undefined ||
|
|
193
|
+
options.crc === 0 ||
|
|
194
|
+
crc32Of(truncated) === (options.crc >>> 0);
|
|
195
|
+
|
|
196
|
+
if (trailingZeros && crcOk && decoded.length === expected + LEGACY_WASM_ZSTD_PADDING) {
|
|
197
|
+
return {
|
|
198
|
+
...base,
|
|
199
|
+
isLegacyWasm: true,
|
|
200
|
+
confidence: 'verified',
|
|
201
|
+
reason:
|
|
202
|
+
options?.crc && options.crc !== 0
|
|
203
|
+
? `Decoded length is +${LEGACY_WASM_ZSTD_PADDING} with trailing zeros; truncated CRC matches`
|
|
204
|
+
: `Decoded length is +${LEGACY_WASM_ZSTD_PADDING} with trailing zeros`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (headerMatch && !trailingZeros) {
|
|
209
|
+
return {
|
|
210
|
+
...base,
|
|
211
|
+
isLegacyWasm: false,
|
|
212
|
+
confidence: 'none',
|
|
213
|
+
reason:
|
|
214
|
+
'Header suggested legacy WASM but decoded payload did not have 18 trailing zeros',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
isLegacyWasm: false,
|
|
221
|
+
confidence: 'none',
|
|
222
|
+
paddingBytes: Math.max(0, pad),
|
|
223
|
+
reason:
|
|
224
|
+
decoded.length === expected
|
|
225
|
+
? 'Standard zstd frame (decoded size matches expected)'
|
|
226
|
+
: `Decoded size ${decoded.length} does not match legacy pattern (expected ${expected} or ${expected + LEGACY_WASM_ZSTD_PADDING})`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Truncate plaintext if it matches the legacy +18 zero padding pattern.
|
|
232
|
+
* Returns the original buffer when not legacy-shaped.
|
|
233
|
+
*/
|
|
234
|
+
export function repairLegacyWasmZstdPlaintext(
|
|
235
|
+
decoded: Buffer,
|
|
236
|
+
expectedUncompressedSize: number,
|
|
237
|
+
crc?: number
|
|
238
|
+
): { repaired: Buffer; wasLegacy: boolean } {
|
|
239
|
+
const expected = expectedUncompressedSize >>> 0;
|
|
240
|
+
if (
|
|
241
|
+
decoded.length === expected + LEGACY_WASM_ZSTD_PADDING &&
|
|
242
|
+
decoded.subarray(expected).equals(Buffer.alloc(LEGACY_WASM_ZSTD_PADDING))
|
|
243
|
+
) {
|
|
244
|
+
const repaired = decoded.subarray(0, expected);
|
|
245
|
+
if (crc !== undefined && crc !== 0) {
|
|
246
|
+
if (crc32Of(repaired) !== (crc >>> 0)) {
|
|
247
|
+
return { repaired: decoded, wasLegacy: false };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { repaired, wasLegacy: true };
|
|
251
|
+
}
|
|
252
|
+
if (decoded.length > expected && expected > 0) {
|
|
253
|
+
// Match Rust extract behavior: bound to declared uncompressed size for zstd.
|
|
254
|
+
const repaired = decoded.subarray(0, expected);
|
|
255
|
+
if (crc !== undefined && crc !== 0 && crc32Of(repaired) === (crc >>> 0)) {
|
|
256
|
+
return { repaired, wasLegacy: true };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { repaired: decoded, wasLegacy: false };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const LegacyZstd = {
|
|
263
|
+
LEGACY_WASM_ZSTD_PADDING,
|
|
264
|
+
parseZstdFrameContentSize,
|
|
265
|
+
looksLikeLegacyWasmZstd,
|
|
266
|
+
detectLegacyWasmZstd,
|
|
267
|
+
repairLegacyWasmZstdPlaintext,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export default LegacyZstd;
|
|
@@ -22,10 +22,10 @@ import { HashCalculator } from '../core/components/HashCalculator';
|
|
|
22
22
|
import { ZipCrypto } from '../core/encryption/ZipCrypto';
|
|
23
23
|
import { AesCrypto } from '../core/encryption/AesCrypto';
|
|
24
24
|
import { NeoCrypto, NEO_CRYPTO_ALGORITHM_AES256_V1 } from '../core/encryption/NeoCrypto';
|
|
25
|
-
import { ZstdManager } from '../core/ZstdManager';
|
|
26
25
|
import Errors from '../core/constants/Errors';
|
|
27
26
|
import * as fs from 'fs';
|
|
28
27
|
import * as path from 'path';
|
|
28
|
+
import { ZstdNode } from './ZstdNode';
|
|
29
29
|
|
|
30
30
|
const pako = require('pako');
|
|
31
31
|
|
|
@@ -339,16 +339,23 @@ export class ZipCompressNode {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
|
-
* Compresses data using Zstandard
|
|
343
|
-
*
|
|
342
|
+
* Compresses data using Zstandard via Node native zlib chunked streaming.
|
|
343
|
+
*
|
|
344
|
+
* @param input - Buffer OR chunked reader (`onReadChunk` preferred; `readChunk` accepted)
|
|
344
345
|
* @param options - Compression options
|
|
345
|
-
* @param bufferSize -
|
|
346
|
-
* @param entry - Optional ZIP entry for hash
|
|
347
|
-
* @param onOutputBuffer - Optional callback for streaming output
|
|
348
|
-
* @returns Compressed
|
|
346
|
+
* @param bufferSize - Chunk size for streaming reads (default: 512KB)
|
|
347
|
+
* @param entry - Optional ZIP entry for hash / compressedSize
|
|
348
|
+
* @param onOutputBuffer - Optional callback for streaming compressed output
|
|
349
|
+
* @returns Compressed buffer, or empty buffer when streamed via onOutputBuffer
|
|
349
350
|
*/
|
|
350
351
|
async zstdCompress(
|
|
351
|
-
input:
|
|
352
|
+
input:
|
|
353
|
+
| Buffer
|
|
354
|
+
| {
|
|
355
|
+
totalSize: number;
|
|
356
|
+
onReadChunk?: (position: number, size: number) => Buffer;
|
|
357
|
+
readChunk?: (position: number, size: number) => Buffer;
|
|
358
|
+
},
|
|
352
359
|
options?: CompressOptions,
|
|
353
360
|
bufferSize?: number,
|
|
354
361
|
entry?: ZipEntry,
|
|
@@ -357,66 +364,112 @@ export class ZipCompressNode {
|
|
|
357
364
|
const effectiveBufferSize = bufferSize || options?.bufferSize || this.zipkitNode.getBufferSize();
|
|
358
365
|
const isBuffer = Buffer.isBuffer(input);
|
|
359
366
|
const totalSize = isBuffer ? input.length : input.totalSize;
|
|
360
|
-
|
|
367
|
+
|
|
368
|
+
const needsHashCalculation =
|
|
369
|
+
entry && ((!entry.crc || entry.crc === 0) || (options?.useSHA256 && !entry.sha256));
|
|
370
|
+
const hashCalculator = needsHashCalculation
|
|
371
|
+
? new HashCalculator({ useSHA256: !!(options?.useSHA256 && entry && !entry.sha256) })
|
|
372
|
+
: null;
|
|
373
|
+
|
|
361
374
|
if (options?.level == 0) {
|
|
362
|
-
// For store mode, return as-is
|
|
363
375
|
if (isBuffer) {
|
|
376
|
+
if (hashCalculator && entry) {
|
|
377
|
+
hashCalculator.update(input);
|
|
378
|
+
if (!entry.crc || entry.crc === 0) {
|
|
379
|
+
entry.crc = hashCalculator.finalizeCRC32();
|
|
380
|
+
}
|
|
381
|
+
if (options?.useSHA256 && !entry.sha256) {
|
|
382
|
+
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
364
385
|
return input;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const chunks: Buffer[] = [];
|
|
389
|
+
const read = input.onReadChunk ?? input.readChunk;
|
|
390
|
+
if (!read) {
|
|
391
|
+
throw new Error('ZSTD store mode: chunked reader missing read callback');
|
|
392
|
+
}
|
|
393
|
+
let position = 0;
|
|
394
|
+
while (position < totalSize) {
|
|
395
|
+
const size = Math.min(effectiveBufferSize, totalSize - position);
|
|
396
|
+
const chunk = read(position, size);
|
|
397
|
+
if (hashCalculator) {
|
|
398
|
+
hashCalculator.update(chunk);
|
|
399
|
+
}
|
|
400
|
+
if (onOutputBuffer) {
|
|
401
|
+
await onOutputBuffer(chunk);
|
|
402
|
+
} else {
|
|
372
403
|
chunks.push(chunk);
|
|
373
|
-
position += size;
|
|
374
404
|
}
|
|
375
|
-
|
|
405
|
+
position += chunk.length;
|
|
376
406
|
}
|
|
407
|
+
if (hashCalculator && entry) {
|
|
408
|
+
if (!entry.crc || entry.crc === 0) {
|
|
409
|
+
entry.crc = hashCalculator.finalizeCRC32();
|
|
410
|
+
}
|
|
411
|
+
if (options?.useSHA256 && !entry.sha256) {
|
|
412
|
+
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return onOutputBuffer ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
377
416
|
}
|
|
378
|
-
|
|
417
|
+
|
|
379
418
|
try {
|
|
380
|
-
// Zstd compression levels range from 1 (fastest) to 22 (highest compression)
|
|
381
|
-
// Map our 1-9 level to a reasonable zstd range (1-19)
|
|
382
419
|
const level = options?.level ?? 6;
|
|
383
|
-
const zstdLevel = Math.min(Math.max(1, Math.floor(level * 2.1)), 19);
|
|
384
420
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
chunks.push(chunk);
|
|
393
|
-
position += size;
|
|
394
|
-
}
|
|
395
|
-
return Buffer.concat(chunks);
|
|
396
|
-
})();
|
|
397
|
-
|
|
398
|
-
// Validate input
|
|
399
|
-
if (!inbuf || inbuf.length === 0) {
|
|
421
|
+
const read = isBuffer
|
|
422
|
+
? (position: number, size: number) => input.subarray(position, position + size)
|
|
423
|
+
: (input.onReadChunk ?? input.readChunk);
|
|
424
|
+
if (!read) {
|
|
425
|
+
throw new Error('ZSTD compression: chunked reader missing read callback');
|
|
426
|
+
}
|
|
427
|
+
if (!totalSize) {
|
|
400
428
|
throw new Error('ZSTD compression: empty input buffer');
|
|
401
429
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
430
|
+
|
|
431
|
+
if (isBuffer && hashCalculator && entry) {
|
|
432
|
+
hashCalculator.update(input);
|
|
433
|
+
entry.crc = hashCalculator.finalizeCRC32();
|
|
434
|
+
if (options?.useSHA256 && !entry.sha256) {
|
|
435
|
+
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const collected: Buffer[] = [];
|
|
440
|
+
const compressedSize = await ZstdNode.compressChunks(
|
|
441
|
+
read,
|
|
442
|
+
totalSize,
|
|
443
|
+
effectiveBufferSize,
|
|
444
|
+
level,
|
|
445
|
+
isBuffer
|
|
446
|
+
? undefined
|
|
447
|
+
: (plain) => {
|
|
448
|
+
hashCalculator?.update(plain);
|
|
449
|
+
},
|
|
450
|
+
async (compressed) => {
|
|
451
|
+
if (onOutputBuffer) {
|
|
452
|
+
await onOutputBuffer(compressed);
|
|
453
|
+
} else {
|
|
454
|
+
collected.push(compressed);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (!isBuffer && hashCalculator && entry) {
|
|
460
|
+
entry.crc = hashCalculator.finalizeCRC32();
|
|
461
|
+
if (options?.useSHA256 && !entry.sha256) {
|
|
462
|
+
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
411
465
|
if (entry) {
|
|
412
|
-
entry.compressedSize =
|
|
466
|
+
entry.compressedSize = compressedSize;
|
|
413
467
|
}
|
|
414
|
-
|
|
468
|
+
|
|
415
469
|
if (onOutputBuffer) {
|
|
416
|
-
|
|
470
|
+
return Buffer.alloc(0);
|
|
417
471
|
}
|
|
418
|
-
|
|
419
|
-
return compressedBuffer;
|
|
472
|
+
return Buffer.concat(collected);
|
|
420
473
|
} catch (e) {
|
|
421
474
|
Logger.error('Error during zstd compression:', e);
|
|
422
475
|
throw new Error(Errors.COMPRESSION_ERROR);
|
|
@@ -591,9 +644,8 @@ export class ZipCompressNode {
|
|
|
591
644
|
const fileData = fs.readFileSync(filePath);
|
|
592
645
|
return await this.compressData(entry, fileData, options, onOutputBuffer);
|
|
593
646
|
} else if (compressionMethod === 'ZSTD') {
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
return await this.compressData(entry, fileData, options, onOutputBuffer);
|
|
647
|
+
// Native Node zstd chunked streaming
|
|
648
|
+
return await this.zstdCompress(chunkedReader, options, bufferSize, entry, onOutputBuffer);
|
|
597
649
|
} else {
|
|
598
650
|
// DEFLATED: Use deflateCompress with chunked reader
|
|
599
651
|
return await this.deflateCompress(chunkedReader, options, bufferSize, entry, onOutputBuffer);
|