neozipkit 1.0.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/NEOZIP_APPNOTE.md +116 -18
  2. package/README.md +38 -8
  3. package/WHATS_NEW.md +111 -0
  4. package/dist/browser/zstd-browser-shim.d.ts +17 -9
  5. package/dist/browser/zstd-browser-shim.d.ts.map +1 -1
  6. package/dist/browser/zstd-browser-shim.js +99 -21
  7. package/dist/browser/zstd-browser-shim.js.map +1 -1
  8. package/dist/browser-esm/index.js +6178 -1261
  9. package/dist/browser-esm/index.js.map +4 -4
  10. package/dist/browser-umd/index.js +6178 -1261
  11. package/dist/browser-umd/index.js.map +4 -4
  12. package/dist/browser-umd/index.min.js +6 -3
  13. package/dist/core/ZipCompress.d.ts +6 -0
  14. package/dist/core/ZipCompress.d.ts.map +1 -1
  15. package/dist/core/ZipCompress.js.map +1 -1
  16. package/dist/core/ZipCopy.d.ts.map +1 -1
  17. package/dist/core/ZipCopy.js +33 -25
  18. package/dist/core/ZipCopy.js.map +1 -1
  19. package/dist/core/ZipEntry.d.ts +18 -0
  20. package/dist/core/ZipEntry.d.ts.map +1 -1
  21. package/dist/core/ZipEntry.js +139 -13
  22. package/dist/core/ZipEntry.js.map +1 -1
  23. package/dist/core/Zipkit.d.ts.map +1 -1
  24. package/dist/core/Zipkit.js +26 -19
  25. package/dist/core/Zipkit.js.map +1 -1
  26. package/dist/core/constants/Headers.d.ts +1 -0
  27. package/dist/core/constants/Headers.d.ts.map +1 -1
  28. package/dist/core/constants/Headers.js +1 -0
  29. package/dist/core/constants/Headers.js.map +1 -1
  30. package/dist/core/index.d.ts +3 -0
  31. package/dist/core/index.d.ts.map +1 -1
  32. package/dist/core/index.js +5 -1
  33. package/dist/core/index.js.map +1 -1
  34. package/dist/core/inflateZipPayload.d.ts +6 -0
  35. package/dist/core/inflateZipPayload.d.ts.map +1 -0
  36. package/dist/core/inflateZipPayload.js +28 -0
  37. package/dist/core/inflateZipPayload.js.map +1 -0
  38. package/dist/core/origin/OriginExtra.d.ts +43 -0
  39. package/dist/core/origin/OriginExtra.d.ts.map +1 -0
  40. package/dist/core/origin/OriginExtra.js +187 -0
  41. package/dist/core/origin/OriginExtra.js.map +1 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/zip64/Zip64.d.ts +138 -0
  44. package/dist/core/zip64/Zip64.d.ts.map +1 -0
  45. package/dist/core/zip64/Zip64.js +353 -0
  46. package/dist/core/zip64/Zip64.js.map +1 -0
  47. package/dist/node/ZipCopyNode.d.ts.map +1 -1
  48. package/dist/node/ZipCopyNode.js +26 -26
  49. package/dist/node/ZipCopyNode.js.map +1 -1
  50. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  51. package/dist/node/ZipDecompressNode.js +8 -6
  52. package/dist/node/ZipDecompressNode.js.map +1 -1
  53. package/dist/node/ZipkitNode.d.ts +14 -1
  54. package/dist/node/ZipkitNode.d.ts.map +1 -1
  55. package/dist/node/ZipkitNode.js +91 -53
  56. package/dist/node/ZipkitNode.js.map +1 -1
  57. package/dist/node/ZstdNode.d.ts +14 -4
  58. package/dist/node/ZstdNode.d.ts.map +1 -1
  59. package/dist/node/ZstdNode.js +28 -4
  60. package/dist/node/ZstdNode.js.map +1 -1
  61. package/dist/node/buildZipBuffer.d.ts +49 -0
  62. package/dist/node/buildZipBuffer.d.ts.map +1 -0
  63. package/dist/node/buildZipBuffer.js +146 -0
  64. package/dist/node/buildZipBuffer.js.map +1 -0
  65. package/dist/node/index.d.ts +3 -0
  66. package/dist/node/index.d.ts.map +1 -1
  67. package/dist/node/index.js +6 -1
  68. package/dist/node/index.js.map +1 -1
  69. package/dist/node/inflateZipPayload.d.ts +5 -0
  70. package/dist/node/inflateZipPayload.d.ts.map +1 -0
  71. package/dist/node/inflateZipPayload.js +21 -0
  72. package/dist/node/inflateZipPayload.js.map +1 -0
  73. package/dist/types/index.js +2 -2
  74. package/node-esm.mjs +32 -0
  75. package/package.json +11 -5
  76. package/src/browser/zstd-browser-shim.ts +110 -28
  77. package/src/core/ZipCompress.ts +6 -0
  78. package/src/core/ZipCopy.ts +56 -27
  79. package/src/core/ZipEntry.ts +192 -13
  80. package/src/core/Zipkit.ts +40 -22
  81. package/src/core/constants/Headers.ts +1 -0
  82. package/src/core/index.ts +3 -0
  83. package/src/core/inflateZipPayload.ts +27 -0
  84. package/src/core/origin/OriginExtra.ts +209 -0
  85. package/src/core/version.ts +1 -1
  86. package/src/core/zip64/Zip64.ts +473 -0
  87. package/src/node/ZipCopyNode.ts +31 -29
  88. package/src/node/ZipDecompressNode.ts +6 -5
  89. package/src/node/ZipkitNode.ts +121 -68
  90. package/src/node/ZstdNode.ts +33 -8
  91. package/src/node/buildZipBuffer.ts +221 -0
  92. package/src/node/index.ts +7 -0
  93. package/src/node/inflateZipPayload.ts +19 -0
  94. package/src/types/index.ts +2 -2
@@ -11,19 +11,31 @@ import { ZipDecompressNode } from './ZipDecompressNode';
11
11
  import {
12
12
  CENTRAL_END,
13
13
  CENTRAL_DIR,
14
- ZIP64_CENTRAL_END,
15
- ZIP64_CENTRAL_DIR,
16
14
  LOCAL_HDR,
17
15
  GP_FLAG,
18
16
  CMP_METHOD,
19
- ENCRYPT_HDR_SIZE
17
+ ENCRYPT_HDR_SIZE,
18
+ HDR_ID,
20
19
  } from '../core/constants/Headers';
20
+ import {
21
+ buildEndRecords,
22
+ classicEocdNeedsZip64,
23
+ exceedsU32,
24
+ parseZip64Eocd,
25
+ parseZip64Locator,
26
+ writeU64,
27
+ ZIP64_EOCD_RECORD_SIZE,
28
+ ZIP64_LOCATOR_SIZE,
29
+ ZIP64_U32,
30
+ } from '../core/zip64/Zip64';
21
31
  import { NEO_CRYPTO_ALGORITHM_AES256_V1 } from '../core/encryption/NeoCrypto';
22
32
  import { AesCrypto } from '../core/encryption/AesCrypto';
23
33
  import {
24
34
  detectLegacyWasmZstd,
25
35
  type LegacyZstdDetectResult,
26
36
  } from './LegacyZstd';
37
+ import { writeZipFileSync, type ZipBufferMember, type BuildZipBufferOptions } from './buildZipBuffer';
38
+ export type { ZipBufferMember };
27
39
  import * as fs from 'fs';
28
40
  import * as path from 'path';
29
41
  import { minimatch } from 'minimatch';
@@ -717,6 +729,10 @@ export default class ZipkitNode extends Zipkit {
717
729
  onHashCalculated?: (entry: ZipEntry, hash: Buffer) => void;
718
730
  }
719
731
  ): Promise<void> {
732
+ if (options?.forceZip64) {
733
+ entry.forceZip64 = true;
734
+ }
735
+
720
736
  // Set compression method based on options
721
737
  const level = options?.level ?? 6;
722
738
  let realCmpMethod: number;
@@ -859,9 +875,55 @@ export default class ZipkitNode extends Zipkit {
859
875
  throw new Error(`Compressed size not set for entry: ${entry.filename}`);
860
876
  }
861
877
 
862
- const sizeBuffer = Buffer.alloc(4);
863
- sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
864
- fs.writeSync(writer.outputFd, sizeBuffer, 0, 4, entry.localHdrOffset + 18);
878
+ const classicCmp = entry.localHdrOffset + LOCAL_HDR.CMP_SIZE;
879
+ const classicUncmp = entry.localHdrOffset + LOCAL_HDR.UNCMP_SIZE;
880
+ const useZip64Sizes =
881
+ entry.forceZip64 ||
882
+ exceedsU32(entry.uncompressedSize) ||
883
+ exceedsU32(entry.compressedSize);
884
+
885
+ if (useZip64Sizes) {
886
+ // Classic fields stay at 0xFFFFFFFF; update u64 sizes inside local extra 0x0001
887
+ const sentinel = Buffer.alloc(4);
888
+ sentinel.writeUInt32LE(ZIP64_U32, 0);
889
+ fs.writeSync(writer.outputFd, sentinel, 0, 4, classicCmp);
890
+ fs.writeSync(writer.outputFd, sentinel, 0, 4, classicUncmp);
891
+
892
+ const fnameLenBuf = Buffer.alloc(2);
893
+ fs.readSync(writer.outputFd, fnameLenBuf, 0, 2, entry.localHdrOffset + LOCAL_HDR.FNAME_LEN);
894
+ const fnameLen = fnameLenBuf.readUInt16LE(0);
895
+ const extraLenBuf = Buffer.alloc(2);
896
+ fs.readSync(writer.outputFd, extraLenBuf, 0, 2, entry.localHdrOffset + LOCAL_HDR.EXTRA_LEN);
897
+ const extraLen = extraLenBuf.readUInt16LE(0);
898
+ const extraStart = entry.localHdrOffset + LOCAL_HDR.SIZE + fnameLen;
899
+ const extraBuf = Buffer.alloc(extraLen);
900
+ if (extraLen > 0) {
901
+ fs.readSync(writer.outputFd, extraBuf, 0, extraLen, extraStart);
902
+ }
903
+ let patched = false;
904
+ for (let i = 0; i + 4 <= extraBuf.length; ) {
905
+ const id = extraBuf.readUInt16LE(i);
906
+ const len = extraBuf.readUInt16LE(i + 2);
907
+ if (id === HDR_ID.ZIP64 && len >= 16) {
908
+ // Local Zip64 always has uncompressed (8) then compressed (8)
909
+ writeU64(extraBuf, i + 4, entry.uncompressedSize);
910
+ writeU64(extraBuf, i + 12, entry.compressedSize);
911
+ fs.writeSync(writer.outputFd, extraBuf, i + 4, 16, extraStart + i + 4);
912
+ patched = true;
913
+ break;
914
+ }
915
+ i += 4 + len;
916
+ }
917
+ if (!patched) {
918
+ throw new Error(
919
+ `Zip64 local extra (0x0001) missing for large entry: ${entry.filename}`
920
+ );
921
+ }
922
+ } else {
923
+ const sizeBuffer = Buffer.alloc(4);
924
+ sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
925
+ fs.writeSync(writer.outputFd, sizeBuffer, 0, 4, classicCmp);
926
+ }
865
927
 
866
928
  if (entry.crc !== undefined) {
867
929
  const crcBuffer = Buffer.alloc(4);
@@ -938,6 +1000,7 @@ export default class ZipkitNode extends Zipkit {
938
1000
  * @param centralDirSize - Size of central directory in bytes
939
1001
  * @param centralDirOffset - Offset to start of central directory
940
1002
  * @param archiveComment - Optional archive comment (max 65535 bytes)
1003
+ * @param options - Optional Zip64 force and related EOCD flags
941
1004
  * @returns Promise that resolves when EOCD is written
942
1005
  */
943
1006
  async writeEndOfCentralDirectory(
@@ -945,53 +1008,20 @@ export default class ZipkitNode extends Zipkit {
945
1008
  totalEntries: number,
946
1009
  centralDirSize: number,
947
1010
  centralDirOffset: number,
948
- archiveComment?: string
1011
+ archiveComment?: string,
1012
+ options?: { forceZip64?: boolean }
949
1013
  ): Promise<void> {
950
- const comment = archiveComment || '';
951
- const commentBytes = Buffer.from(comment, 'utf8');
952
- const commentLength = Math.min(commentBytes.length, 0xFFFF); // Max 65535 bytes
953
-
954
- const buffer = Buffer.alloc(22 + commentLength);
955
- let offset = 0;
956
-
957
- // End of central directory signature (4 bytes)
958
- buffer.writeUInt32LE(0x06054b50, offset);
959
- offset += 4;
960
-
961
- // Number of this disk (2 bytes)
962
- buffer.writeUInt16LE(0, offset);
963
- offset += 2;
964
-
965
- // Number of the disk with the start of the central directory (2 bytes)
966
- buffer.writeUInt16LE(0, offset);
967
- offset += 2;
968
-
969
- // Total number of entries in the central directory on this disk (2 bytes)
970
- buffer.writeUInt16LE(totalEntries, offset);
971
- offset += 2;
972
-
973
- // Total number of entries in the central directory (2 bytes)
974
- buffer.writeUInt16LE(totalEntries, offset);
975
- offset += 2;
976
-
977
- // Size of the central directory (4 bytes)
978
- buffer.writeUInt32LE(centralDirSize, offset);
979
- offset += 4;
980
-
981
- // Offset of start of central directory with respect to the starting disk number (4 bytes)
982
- buffer.writeUInt32LE(centralDirOffset, offset);
983
- offset += 4;
984
-
985
- // ZIP file comment length (2 bytes)
986
- buffer.writeUInt16LE(commentLength, offset);
987
- offset += 2;
988
-
989
- // ZIP file comment (variable length)
990
- if (commentLength > 0) {
991
- commentBytes.copy(buffer, offset, 0, commentLength);
992
- }
1014
+ const buffer = buildEndRecords({
1015
+ totalEntries,
1016
+ centralDirSize,
1017
+ centralDirOffset,
1018
+ zip64EocdOffset: centralDirOffset + centralDirSize,
1019
+ archiveComment,
1020
+ allowSizeOffsetZip64: true,
1021
+ forceZip64: options?.forceZip64 === true,
1022
+ });
993
1023
 
994
- // Write EOCD to file
1024
+ // Write EOCD (and Zip64 EOCD + locator when needed) to file
995
1025
  await new Promise<void>((resolve, reject) => {
996
1026
  writer.outputStream.write(buffer, (error) => {
997
1027
  if (error) {
@@ -1067,7 +1097,9 @@ export default class ZipkitNode extends Zipkit {
1067
1097
  writer,
1068
1098
  entries.length,
1069
1099
  centralDirSize,
1070
- centralDirOffset
1100
+ centralDirOffset,
1101
+ undefined,
1102
+ { forceZip64: options?.forceZip64 === true }
1071
1103
  );
1072
1104
  } finally {
1073
1105
  await this.finalizeZipFile(writer);
@@ -1210,6 +1242,21 @@ export default class ZipkitNode extends Zipkit {
1210
1242
  throw new Error('updateZipFile() - Full implementation pending. Use neozip CLI for now.');
1211
1243
  }
1212
1244
 
1245
+ /**
1246
+ * Stream members to `filePath` (local headers, payloads, central directory, EOCD).
1247
+ * Pass `useSHA256` for Extra Field 0x014E and `additionalExtra` for caller blocks
1248
+ * such as ZipWiki 0x014F. `method` 93 uses zlib zstd at `level` (default 7).
1249
+ * NeoEncrypt is not enabled. Precompressed members are copied without recompression.
1250
+ * Pass `{ forceZip64: true }` to emit Zip64 even for small members (testing).
1251
+ */
1252
+ writeMembersSync(
1253
+ filePath: string,
1254
+ members: ZipBufferMember[],
1255
+ options?: BuildZipBufferOptions
1256
+ ): void {
1257
+ writeZipFileSync(filePath, members, options);
1258
+ }
1259
+
1213
1260
  // ============================================================================
1214
1261
  // File-based ZIP Loading Methods (merged from ZipLoadEntriesServer)
1215
1262
  // ============================================================================
@@ -1275,9 +1322,17 @@ export default class ZipkitNode extends Zipkit {
1275
1322
  const zipkit = this as any;
1276
1323
  zipkit.centralDirSize = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
1277
1324
  zipkit.centralDirOffset = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
1278
-
1279
- // Handle ZIP64
1280
- if (zipkit.centralDirOffset === 0xFFFFFFFF) {
1325
+ const volEntries = eocdBuffer.readUInt16LE(CENTRAL_END.VOL_ENTRIES);
1326
+ const totalEntries = eocdBuffer.readUInt16LE(CENTRAL_END.TOTAL_ENTRIES);
1327
+
1328
+ if (
1329
+ classicEocdNeedsZip64({
1330
+ volEntries,
1331
+ totalEntries,
1332
+ centralDirSize: zipkit.centralDirSize,
1333
+ centralDirOffset: zipkit.centralDirOffset,
1334
+ })
1335
+ ) {
1281
1336
  await this.loadZIP64EOCD(eocdOffset);
1282
1337
  }
1283
1338
  } else {
@@ -1307,20 +1362,18 @@ export default class ZipkitNode extends Zipkit {
1307
1362
  }
1308
1363
 
1309
1364
  // Look for ZIP64 locator
1310
- const locatorOffset = eocdOffset - 20;
1311
- const locatorBuffer = Buffer.alloc(20);
1312
- await this.fileHandle.read(locatorBuffer, 0, 20, locatorOffset);
1365
+ const locatorOffset = eocdOffset - ZIP64_LOCATOR_SIZE;
1366
+ const locatorBuffer = Buffer.alloc(ZIP64_LOCATOR_SIZE);
1367
+ await this.fileHandle.read(locatorBuffer, 0, ZIP64_LOCATOR_SIZE, locatorOffset);
1313
1368
 
1314
- if (locatorBuffer.readUInt32LE(0) === ZIP64_CENTRAL_END.SIGNATURE) {
1315
- // Read ZIP64 EOCD
1316
- const zip64Offset = locatorBuffer.readBigUInt64LE(8);
1317
- const zip64Buffer = Buffer.alloc(56);
1318
- await this.fileHandle.read(zip64Buffer, 0, 56, Number(zip64Offset));
1319
-
1320
- const zipkit = this as any;
1321
- zipkit.centralDirSize = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE));
1322
- zipkit.centralDirOffset = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET));
1323
- }
1369
+ const zip64Offset = parseZip64Locator(locatorBuffer);
1370
+ const zip64Buffer = Buffer.alloc(ZIP64_EOCD_RECORD_SIZE);
1371
+ await this.fileHandle.read(zip64Buffer, 0, ZIP64_EOCD_RECORD_SIZE, zip64Offset);
1372
+
1373
+ const z64 = parseZip64Eocd(zip64Buffer);
1374
+ const zipkit = this as any;
1375
+ zipkit.centralDirSize = z64.centralDirSize;
1376
+ zipkit.centralDirOffset = z64.centralDirOffset;
1324
1377
  }
1325
1378
 
1326
1379
  /**
@@ -54,26 +54,49 @@ export function createDecompressTransform(): zlib.ZstdDecompress {
54
54
  return zlib.createZstdDecompress();
55
55
  }
56
56
 
57
- export async function compress(data: Buffer, level: number = 6): Promise<Buffer> {
57
+ /** Bytes accepted by the buffer zstd codec (Node Buffer or browser ArrayBuffer). */
58
+ export type ZstdInput = Buffer | Uint8Array | ArrayBuffer;
59
+
60
+ export function asZstdBuffer(data: ZstdInput): Buffer {
61
+ if (Buffer.isBuffer(data)) return data;
62
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
63
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
64
+ }
65
+
66
+ export async function compress(data: ZstdInput, level: number = 6): Promise<Buffer> {
58
67
  assertNativeZstd();
59
- const result = await zstdCompressAsync(data, compressOptions(level));
68
+ const result = await zstdCompressAsync(asZstdBuffer(data), compressOptions(level));
60
69
  return Buffer.from(result);
61
70
  }
62
71
 
63
- export async function decompress(data: Buffer): Promise<Buffer> {
72
+ export async function decompress(data: ZstdInput): Promise<Buffer> {
64
73
  assertNativeZstd();
65
- const result = await zstdDecompressAsync(data);
74
+ const result = await zstdDecompressAsync(asZstdBuffer(data));
66
75
  return Buffer.from(result);
67
76
  }
68
77
 
69
- export function compressSync(data: Buffer, level: number = 6): Buffer {
78
+ export function compressSync(data: ZstdInput, level: number = 6): Buffer {
79
+ assertNativeZstd();
80
+ return zlib.zstdCompressSync(asZstdBuffer(data), compressOptions(level));
81
+ }
82
+
83
+ /**
84
+ * Compress with a raw zlib zstd level (1–19), without the 1–9 kit scale.
85
+ * ZipWiki pack uses level 7 on this scale.
86
+ */
87
+ export function compressSyncAtLevel(data: ZstdInput, zstdLevel: number): Buffer {
70
88
  assertNativeZstd();
71
- return zlib.zstdCompressSync(data, compressOptions(level));
89
+ const level = Math.min(19, Math.max(1, Math.trunc(zstdLevel)));
90
+ return zlib.zstdCompressSync(asZstdBuffer(data), {
91
+ params: {
92
+ [zlib.constants.ZSTD_c_compressionLevel]: level,
93
+ },
94
+ });
72
95
  }
73
96
 
74
- export function decompressSync(data: Buffer): Buffer {
97
+ export function decompressSync(data: ZstdInput): Buffer {
75
98
  assertNativeZstd();
76
- return zlib.zstdDecompressSync(data);
99
+ return zlib.zstdDecompressSync(asZstdBuffer(data));
77
100
  }
78
101
 
79
102
  /**
@@ -147,7 +170,9 @@ export const ZstdNode = {
147
170
  compress,
148
171
  decompress,
149
172
  compressSync,
173
+ compressSyncAtLevel,
150
174
  decompressSync,
175
+ asZstdBuffer,
151
176
  compressChunks,
152
177
  decompressStream,
153
178
  };
@@ -0,0 +1,221 @@
1
+ /**
2
+ * In-memory and streaming ZIP writer.
3
+ * Zstd level is the raw zlib level (ZipWiki default 7). NeoEncrypt stays off.
4
+ * Caller extra blocks (ZipWiki 0x014F) are written beside optional 0x014E.
5
+ */
6
+
7
+ import { closeSync, openSync, writeSync } from 'fs';
8
+ import { deflateRawSync } from 'zlib';
9
+ import ZipEntry from '../core/ZipEntry';
10
+ import { crc32, sha256 } from '../core/encryption/ZipCrypto';
11
+ import { compressSyncAtLevel } from './ZstdNode';
12
+ import {
13
+ assertBufferAllowsEntryZip64,
14
+ buildEndRecords,
15
+ } from '../core/zip64/Zip64';
16
+
17
+ export type ZipPrecompressedMember = {
18
+ method: number;
19
+ data: Buffer;
20
+ crc32: number;
21
+ uncompressedSize: number;
22
+ /** Complete extra-field bytes copied onto the local and central headers. */
23
+ extra?: Buffer;
24
+ };
25
+
26
+ export type ZipBufferMember = {
27
+ name: string;
28
+ /** Uncompressed payload. Unused as stored bytes when `precompressed` is set. */
29
+ data: Buffer;
30
+ mtime?: Date;
31
+ /** DOS time word. Wins over `mtime` when set together with `dosDate`. */
32
+ dosTime?: number;
33
+ dosDate?: number;
34
+ /**
35
+ * Complete extra-field records (id, size, payload), e.g. ZipWiki 0x014F.
36
+ * Ignored when `precompressed.extra` is set — that blob is copied as-is.
37
+ */
38
+ additionalExtra?: Buffer;
39
+ /** Write Extra Field 0x014E (SHA-256 of uncompressed `data`). */
40
+ useSHA256?: boolean;
41
+ /** 0 store, 8 deflate, 93 zstd. Default 93. Ignored when `precompressed` is set. */
42
+ method?: 0 | 8 | 93;
43
+ /** zlib zstd level or raw deflate level. Default 7. */
44
+ level?: number;
45
+ /**
46
+ * Force Zip64 Extra `0x0001` on this member even when sizes fit classic fields.
47
+ * Pair with `forceZip64` on `buildZipBufferSync` / `writeZipFileSync` options
48
+ * (or set on every member) so the EOCD is Zip64 as well.
49
+ */
50
+ forceZip64?: boolean;
51
+ precompressed?: ZipPrecompressedMember;
52
+ };
53
+
54
+ type FramedMember = {
55
+ local: Buffer;
56
+ payload: Buffer;
57
+ central: Buffer;
58
+ };
59
+
60
+ function dosFromDate(date: Date): { time: number; date: number } {
61
+ const year = date.getFullYear();
62
+ const month = date.getMonth() + 1;
63
+ const day = date.getDate();
64
+ const hours = date.getHours();
65
+ const minutes = date.getMinutes();
66
+ const seconds = Math.floor(date.getSeconds() / 2);
67
+ const y = Math.max(0, Math.min(127, year - 1980));
68
+ return {
69
+ time: ((hours & 0x1f) << 11) | ((minutes & 0x3f) << 5) | (seconds & 0x1f),
70
+ date: ((y & 0x7f) << 9) | ((month & 0x0f) << 5) | (day & 0x1f),
71
+ };
72
+ }
73
+
74
+ function compressPayload(
75
+ data: Buffer,
76
+ method: 0 | 8 | 93,
77
+ level: number,
78
+ ): { method: number; data: Buffer } {
79
+ if (method === 0 || data.length === 0) {
80
+ return { method: 0, data };
81
+ }
82
+ const compressed =
83
+ method === 8
84
+ ? deflateRawSync(data, { level: Math.max(1, Math.min(9, level)) })
85
+ : compressSyncAtLevel(data, Math.max(1, level));
86
+ if (compressed.length >= data.length) {
87
+ return { method: 0, data };
88
+ }
89
+ return { method, data: compressed };
90
+ }
91
+
92
+ function frameMembers(
93
+ members: ZipBufferMember[],
94
+ opts?: { forceZip64?: boolean }
95
+ ): FramedMember[] {
96
+ const packTime = new Date();
97
+ const framed: FramedMember[] = [];
98
+ let offset = 0;
99
+ const forceAll = opts?.forceZip64 === true;
100
+ for (const member of members) {
101
+ const copied = member.precompressed;
102
+ const uncompressed = member.data ?? Buffer.alloc(0);
103
+ const level = member.level ?? 7;
104
+ const requested = member.method ?? 93;
105
+ const payload = copied
106
+ ? {
107
+ method: copied.method,
108
+ data: copied.data,
109
+ uncompressedSize: copied.uncompressedSize,
110
+ }
111
+ : {
112
+ ...compressPayload(uncompressed, requested, level),
113
+ uncompressedSize: uncompressed.length,
114
+ };
115
+ const crc = copied ? copied.crc32 >>> 0 : crc32(uncompressed) >>> 0;
116
+ const extra = copied?.extra ?? member.additionalExtra;
117
+ const dos =
118
+ member.dosTime !== undefined && member.dosDate !== undefined
119
+ ? { time: member.dosTime, date: member.dosDate }
120
+ : dosFromDate(member.mtime ?? packTime);
121
+
122
+ const entry = new ZipEntry(member.name, null, false);
123
+ entry.bitFlags = 0;
124
+ entry.emitUnicodePath = false;
125
+ entry.isUpdated = true;
126
+ entry.filename = member.name;
127
+ entry.cmpMethod = payload.method;
128
+ entry.crc = crc;
129
+ entry.compressedSize = payload.data.length;
130
+ entry.uncompressedSize = payload.uncompressedSize;
131
+ entry.timeDateDOS = ((dos.date & 0xffff) << 16) | (dos.time & 0xffff);
132
+ entry.localHdrOffset = offset;
133
+ entry.forceZip64 = forceAll || member.forceZip64 === true;
134
+ assertBufferAllowsEntryZip64(
135
+ entry.uncompressedSize,
136
+ entry.compressedSize,
137
+ entry.localHdrOffset
138
+ );
139
+ if (!copied && member.useSHA256 === true) {
140
+ entry.sha256 = sha256(uncompressed);
141
+ }
142
+ if (extra && extra.length > 0) {
143
+ entry.additionalExtra = extra;
144
+ }
145
+ const local = entry.createLocalHdr();
146
+ const central = entry.centralDirEntry();
147
+ framed.push({ local, payload: payload.data, central });
148
+ offset += local.length + payload.data.length;
149
+ }
150
+ return framed;
151
+ }
152
+
153
+ function eocd(
154
+ entryCount: number,
155
+ centralSize: number,
156
+ centralOffset: number,
157
+ forceZip64?: boolean
158
+ ): Buffer {
159
+ return buildEndRecords({
160
+ totalEntries: entryCount,
161
+ centralDirSize: centralSize,
162
+ centralDirOffset: centralOffset,
163
+ zip64EocdOffset: centralOffset + centralSize,
164
+ allowSizeOffsetZip64: false,
165
+ forceZip64: forceZip64 === true,
166
+ });
167
+ }
168
+
169
+ export type BuildZipBufferOptions = {
170
+ /** Force Zip64 EOCD + per-member Extra when sizes fit classic fields. */
171
+ forceZip64?: boolean;
172
+ };
173
+
174
+ /** Build a ZIP in one buffer. Kept for tests and atomic in-memory rewrites. */
175
+ export function buildZipBufferSync(
176
+ members: ZipBufferMember[],
177
+ options?: BuildZipBufferOptions
178
+ ): Buffer {
179
+ const forceZip64 = options?.forceZip64 === true;
180
+ const framed = frameMembers(members, { forceZip64 });
181
+ const parts: Buffer[] = [];
182
+ let centralSize = 0;
183
+ let payloadBytes = 0;
184
+ for (const part of framed) {
185
+ parts.push(part.local, part.payload);
186
+ payloadBytes += part.local.length + part.payload.length;
187
+ }
188
+ for (const part of framed) {
189
+ parts.push(part.central);
190
+ centralSize += part.central.length;
191
+ }
192
+ parts.push(eocd(framed.length, centralSize, payloadBytes, forceZip64));
193
+ return Buffer.concat(parts);
194
+ }
195
+
196
+ /** Stream local headers, payloads, the central directory, and the EOCD to `filePath`. */
197
+ export function writeZipFileSync(
198
+ filePath: string,
199
+ members: ZipBufferMember[],
200
+ options?: BuildZipBufferOptions
201
+ ): void {
202
+ const forceZip64 = options?.forceZip64 === true;
203
+ const framed = frameMembers(members, { forceZip64 });
204
+ const fd = openSync(filePath, 'w');
205
+ try {
206
+ let payloadBytes = 0;
207
+ let centralSize = 0;
208
+ for (const part of framed) {
209
+ writeSync(fd, part.local);
210
+ writeSync(fd, part.payload);
211
+ payloadBytes += part.local.length + part.payload.length;
212
+ }
213
+ for (const part of framed) {
214
+ writeSync(fd, part.central);
215
+ centralSize += part.central.length;
216
+ }
217
+ writeSync(fd, eocd(framed.length, centralSize, payloadBytes, forceZip64));
218
+ } finally {
219
+ closeSync(fd);
220
+ }
221
+ }
package/src/node/index.ts CHANGED
@@ -34,6 +34,13 @@ export type { LegacyZstdDetectResult, LegacyZstdConfidence, DetectLegacyZstdOpti
34
34
 
35
35
  // Efficient ZIP copying using ZipEntry directly
36
36
  export { ZipCopyNode } from './ZipCopyNode';
37
+ export { buildZipBufferSync, writeZipFileSync } from './buildZipBuffer';
38
+ export type {
39
+ ZipBufferMember,
40
+ ZipPrecompressedMember,
41
+ BuildZipBufferOptions,
42
+ } from './buildZipBuffer';
43
+ export { inflateZipPayloadSync } from './inflateZipPayload';
37
44
  export type {
38
45
  CopyOptions,
39
46
  CopyResult,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Synchronous inflate for Node readers (store, raw deflate, native zstd).
3
+ */
4
+
5
+ import { inflateRawSync } from 'zlib';
6
+ import { decompressSync } from './ZstdNode';
7
+
8
+ export function inflateZipPayloadSync(method: number, compressed: Buffer): Buffer {
9
+ if (method === 0) {
10
+ return Buffer.from(compressed);
11
+ }
12
+ if (method === 8) {
13
+ return inflateRawSync(compressed);
14
+ }
15
+ if (method === 93) {
16
+ return decompressSync(compressed);
17
+ }
18
+ throw new Error(`unsupported compression method ${method}`);
19
+ }
@@ -8,8 +8,8 @@ export interface NeoZipKitInfo {
8
8
  }
9
9
 
10
10
  export const NEOZIPKIT_INFO: NeoZipKitInfo = {
11
- version: '0.7.0',
12
- releaseDate: '2026-04-06'
11
+ version: '1.5.0',
12
+ releaseDate: '2026-10-25'
13
13
  };
14
14
 
15
15
  // ============================================================================