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
@@ -25,7 +25,7 @@ import {
25
25
  CENTRAL_END,
26
26
  CENTRAL_DIR,
27
27
  ZIP64_CENTRAL_END,
28
- ZIP64_CENTRAL_DIR ,
28
+ ZIP64_CENTRAL_DIR,
29
29
  GP_FLAG,
30
30
  TIMESTAMP_SUBMITTED,
31
31
  TIMESTAMP_METADATA,
@@ -35,6 +35,14 @@ import {
35
35
  CENTRAL_DIRECTORY_END,
36
36
  HDR_ID
37
37
  } from './constants/Headers';
38
+ import {
39
+ buildEndRecords,
40
+ classicEocdNeedsZip64,
41
+ parseZip64Eocd,
42
+ parseZip64Locator,
43
+ ZIP64_EOCD_RECORD_SIZE,
44
+ ZIP64_LOCATOR_SIZE,
45
+ } from './zip64/Zip64';
38
46
  import {
39
47
  findReservedMetaEntry,
40
48
  isMetaInfPath,
@@ -605,17 +613,30 @@ export default class Zipkit {
605
613
  if (eocdBuffer.readUInt32LE(0) === CENTRAL_END.SIGNATURE) {
606
614
  this.centralDirSize = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
607
615
  this.centralDirOffset = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
608
-
609
- if (this.centralDirOffset === 0xFFFFFFFF) {
610
- // ZIP64: locate locator (20 bytes before EOCD) and then ZIP64 EOCD (56 bytes)
611
- const locatorOffset = eocdOffset - 20;
612
- const locatorBuffer = buffer.subarray(locatorOffset, locatorOffset + 20);
613
- if (locatorBuffer.readUInt32LE(0) === ZIP64_CENTRAL_END.SIGNATURE) {
614
- const zip64Offset = Number(locatorBuffer.readBigUInt64LE(8));
615
- const zip64Buffer = buffer.subarray(zip64Offset, zip64Offset + 56);
616
- this.centralDirSize = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE));
617
- this.centralDirOffset = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET));
616
+ const volEntries = eocdBuffer.readUInt16LE(CENTRAL_END.VOL_ENTRIES);
617
+ const totalEntries = eocdBuffer.readUInt16LE(CENTRAL_END.TOTAL_ENTRIES);
618
+
619
+ if (
620
+ classicEocdNeedsZip64({
621
+ volEntries,
622
+ totalEntries,
623
+ centralDirSize: this.centralDirSize,
624
+ centralDirOffset: this.centralDirOffset,
625
+ })
626
+ ) {
627
+ const locatorOffset = eocdOffset - ZIP64_LOCATOR_SIZE;
628
+ if (locatorOffset < 0) {
629
+ throw new Error(Errors.INVALID_FORMAT);
618
630
  }
631
+ const locatorBuffer = buffer.subarray(locatorOffset, locatorOffset + ZIP64_LOCATOR_SIZE);
632
+ const zip64Offset = parseZip64Locator(locatorBuffer);
633
+ const zip64Buffer = buffer.subarray(
634
+ zip64Offset,
635
+ zip64Offset + ZIP64_EOCD_RECORD_SIZE
636
+ );
637
+ const z64 = parseZip64Eocd(zip64Buffer);
638
+ this.centralDirSize = z64.centralDirSize;
639
+ this.centralDirOffset = z64.centralDirOffset;
619
640
  }
620
641
  } else {
621
642
  throw new Error(Errors.INVALID_FORMAT);
@@ -691,17 +712,14 @@ export default class Zipkit {
691
712
  * Note: totalEntries must be passed as parameter since zipEntries[] in Zipkit is the cache
692
713
  */
693
714
  private centralEndHdrMethod(centralDirSize: number, centralDirOffset: number, totalEntries: number): Buffer {
694
- const ceBuf = Buffer.alloc(CENTRAL_END.SIZE);
695
- ceBuf.writeUInt32LE(CENTRAL_END.SIGNATURE, 0);
696
- ceBuf.writeUInt16LE(0, 4); // Number of this disk
697
- ceBuf.writeUInt16LE(0, 6); // Number of the disk with the start of the central directory
698
- ceBuf.writeUInt16LE(totalEntries, 8); // Total number of entries
699
- ceBuf.writeUInt16LE(totalEntries, 10); // Total number of entries on this disk
700
- ceBuf.writeUInt32LE(centralDirSize, 12); // Size of central directory
701
- ceBuf.writeUInt32LE(centralDirOffset, 16); // Offset of start of central directory
702
- ceBuf.writeUInt16LE(0, 20); // ZIP file comment length
703
-
704
- return ceBuf;
715
+ // Buffer path: allow entry-count Zip64; refuse size/offset Zip64 (multi-GiB in memory).
716
+ return buildEndRecords({
717
+ totalEntries,
718
+ centralDirSize,
719
+ centralDirOffset,
720
+ zip64EocdOffset: centralDirOffset + centralDirSize,
721
+ allowSizeOffsetZip64: false,
722
+ });
705
723
  }
706
724
 
707
725
  /**
@@ -187,6 +187,7 @@ export const HDR_ID = {
187
187
  IBM1: 0x0065, // IBM S/390 (Z390) - Attribute
188
188
  IBM2: 0x0066, // IBM S/390 (Z390) - Attribute
189
189
  SHA256: 0x014E, // NeoZip Extra Field: 32-byte SHA-256 of uncompressed payload (APPNOTE §5)
190
+ ORIGIN: 0x014F, // ZipWiki origin locator (URI, size, mtime, CRC-32 or SHA-256)
190
191
  NEO_CRYPTO: 0x024E, // NeoEncrypt metadata (see docs/NEO_CRYPTO_FORMAT.md); LE on wire: 4E 02
191
192
  POSZIP: 0x4690, // POSZIP 4690
192
193
  UNV_TIME: 0x5455, // Universal Time (UT) (32-bit) 5 data bytes
package/src/core/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './ZipEntry';
10
10
  export { ZipCompress } from './ZipCompress';
11
11
  export type { CompressOptions, CreateZipOptions } from './ZipCompress';
12
12
  export type { DecompressionResult, DecompressionOptions } from './ZipDecompress';
13
+ export { inflateZipPayload } from './inflateZipPayload';
13
14
  export { ZipCopy } from './ZipCopy';
14
15
  export type {
15
16
  CopyOptions,
@@ -25,6 +26,8 @@ export * from './components/Util';
25
26
  export * from './components/Support';
26
27
  export { ProgressTracker } from './components/ProgressTracker';
27
28
  export * from './merkle/MerkleRoot';
29
+ export * from './zip64/Zip64';
30
+ export * from './origin/OriginExtra';
28
31
 
29
32
  // Encryption functionality
30
33
  export { EncryptionManager } from './encryption/Manager';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Inflate one ZIP member payload (store, deflate, zstd) from an in-memory buffer.
3
+ * Browser bundles alias ZstdNode to the CompressionStream codec.
4
+ */
5
+
6
+ import pako from 'pako';
7
+ import { decompress } from '../node/ZstdNode';
8
+
9
+ export async function inflateZipPayload(
10
+ method: number,
11
+ compressed: Uint8Array | ArrayBuffer,
12
+ ): Promise<Uint8Array> {
13
+ const bytes =
14
+ compressed instanceof ArrayBuffer ? new Uint8Array(compressed) : compressed;
15
+ if (method === 0) {
16
+ return bytes.slice();
17
+ }
18
+ if (method === 8) {
19
+ const out = pako.inflateRaw(bytes);
20
+ return out instanceof Uint8Array ? out : new Uint8Array(out);
21
+ }
22
+ if (method === 93) {
23
+ const out = await decompress(bytes);
24
+ return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
25
+ }
26
+ throw new Error(`unsupported compression method ${method}`);
27
+ }
@@ -0,0 +1,209 @@
1
+ // ======================================
2
+ // OriginExtra.ts — NeoZip Extra Field 0x014F (original locator)
3
+ // Copyright (c) 2026 NeoWare, Inc. All rights reserved.
4
+ // ======================================
5
+ // APPNOTE §7.1.1 / ZipWiki §6.7 — v1 TLV (URI, CRC-32, size, mtime, SHA-256).
6
+ // The kit stores opaque caller extras on ZipEntry.additionalExtra; these helpers
7
+ // build and parse the 0x014F wire form.
8
+
9
+ import { HDR_ID } from '../constants/Headers';
10
+ import { crc32, sha256 } from '../encryption/ZipCrypto';
11
+
12
+ /** Payload version byte. Stays 0x01 until first release. */
13
+ export const ORIGIN_EXTRA_VERSION = 0x01;
14
+
15
+ /** Soft cap on URI byte length (well under uint16 extra size). */
16
+ export const ORIGIN_URI_MAX_BYTES = 2048;
17
+
18
+ /** Registered v1 TLV tags. Unknown tags are skipped (forward compatible). */
19
+ export const ORIGIN_TAG = {
20
+ URI: 0x01,
21
+ CRC32: 0x02,
22
+ SIZE: 0x03,
23
+ MTIME: 0x04,
24
+ SHA256: 0x05,
25
+ } as const;
26
+
27
+ export type OriginLocator = {
28
+ /** Absolute RFC 3986 URI (`https:` / `http:` / `file:`). */
29
+ uri?: string;
30
+ /** ZIP/IEEE CRC-32 of original uncompressed bytes. */
31
+ crc32?: number;
32
+ /** Original uncompressed byte length. */
33
+ size?: number;
34
+ /** Original modification time: Unix seconds (UTC, signed). */
35
+ mtime?: number;
36
+ /** SHA-256 of original uncompressed bytes (32 bytes or hex). */
37
+ sha256?: Buffer | string;
38
+ };
39
+
40
+ function originSha256Bytes(value: Buffer | string): Buffer {
41
+ const buf = typeof value === 'string' ? Buffer.from(value, 'hex') : value;
42
+ if (buf.length !== 32) {
43
+ throw new Error('0x014F sha256 must be 32 bytes');
44
+ }
45
+ return buf;
46
+ }
47
+
48
+ function tlv(tag: number, value: Buffer): Buffer {
49
+ const rec = Buffer.alloc(3 + value.length);
50
+ rec.writeUInt8(tag, 0);
51
+ rec.writeUInt16LE(value.length, 1);
52
+ value.copy(rec, 3);
53
+ return rec;
54
+ }
55
+
56
+ function assertInteger(name: string, value: number): void {
57
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
58
+ throw new Error(`0x014F ${name} must be an integer`);
59
+ }
60
+ }
61
+
62
+ export function originLocatorPresent(
63
+ loc: OriginLocator | null | undefined
64
+ ): loc is OriginLocator {
65
+ if (!loc) return false;
66
+ return (
67
+ (typeof loc.uri === 'string' && loc.uri.length > 0) ||
68
+ loc.crc32 !== undefined ||
69
+ loc.size !== undefined ||
70
+ loc.mtime !== undefined ||
71
+ loc.sha256 !== undefined
72
+ );
73
+ }
74
+
75
+ /** ZIP/IEEE CRC-32 of uncompressed original bytes. */
76
+ export function originCrc32Of(buf: Buffer): number {
77
+ return crc32(buf) >>> 0;
78
+ }
79
+
80
+ /** SHA-256 of uncompressed original bytes (32-byte Buffer). */
81
+ export function originSha256Of(buf: Buffer): Buffer {
82
+ return Buffer.from(sha256(buf), 'hex');
83
+ }
84
+
85
+ /** Truncate a Date to Unix seconds (UTC). */
86
+ export function unixTimeSeconds(date: Date): number {
87
+ return Math.floor(date.getTime() / 1000);
88
+ }
89
+
90
+ /**
91
+ * Build Extra Field 0x014F (v1 TLV). Throws if no attribute is present,
92
+ * if URI is empty when provided, or if URI exceeds max bytes.
93
+ */
94
+ export function makeOriginExtra(loc: OriginLocator): Buffer {
95
+ const records: Buffer[] = [];
96
+
97
+ const uri = loc.uri?.trim();
98
+ if (uri !== undefined && uri.length === 0) {
99
+ throw new Error('0x014F URI must be non-empty when present');
100
+ }
101
+ if (uri) {
102
+ const uriBuf = Buffer.from(uri, 'utf8');
103
+ if (uriBuf.length > ORIGIN_URI_MAX_BYTES) {
104
+ throw new Error(
105
+ `0x014F URI exceeds ${ORIGIN_URI_MAX_BYTES} bytes (${uriBuf.length})`
106
+ );
107
+ }
108
+ records.push(tlv(ORIGIN_TAG.URI, uriBuf));
109
+ }
110
+
111
+ if (loc.crc32 !== undefined) {
112
+ assertInteger('crc32', loc.crc32);
113
+ const b = Buffer.alloc(4);
114
+ b.writeUInt32LE(loc.crc32 >>> 0, 0);
115
+ records.push(tlv(ORIGIN_TAG.CRC32, b));
116
+ }
117
+
118
+ if (loc.size !== undefined) {
119
+ assertInteger('size', loc.size);
120
+ if (loc.size < 0) {
121
+ throw new Error('0x014F size must be >= 0');
122
+ }
123
+ const b = Buffer.alloc(8);
124
+ b.writeBigUInt64LE(BigInt(loc.size), 0);
125
+ records.push(tlv(ORIGIN_TAG.SIZE, b));
126
+ }
127
+
128
+ if (loc.mtime !== undefined) {
129
+ assertInteger('mtime', loc.mtime);
130
+ const b = Buffer.alloc(8);
131
+ b.writeBigInt64LE(BigInt(loc.mtime), 0);
132
+ records.push(tlv(ORIGIN_TAG.MTIME, b));
133
+ }
134
+
135
+ if (loc.sha256 !== undefined) {
136
+ records.push(tlv(ORIGIN_TAG.SHA256, originSha256Bytes(loc.sha256)));
137
+ }
138
+
139
+ if (records.length === 0) {
140
+ throw new Error('0x014F requires at least one attribute');
141
+ }
142
+
143
+ const dataSize = 1 + records.reduce((n, r) => n + r.length, 0);
144
+ const extra = Buffer.alloc(4 + dataSize);
145
+ extra.writeUInt16LE(HDR_ID.ORIGIN, 0);
146
+ extra.writeUInt16LE(dataSize, 2);
147
+ extra.writeUInt8(ORIGIN_EXTRA_VERSION, 4);
148
+ let off = 5;
149
+ for (const rec of records) {
150
+ rec.copy(extra, off);
151
+ off += rec.length;
152
+ }
153
+ return extra;
154
+ }
155
+
156
+ function parseOriginTlv(data: Buffer): OriginLocator | null {
157
+ const loc: OriginLocator = {};
158
+ let i = 0;
159
+ while (i + 3 <= data.length) {
160
+ const tag = data.readUInt8(i);
161
+ const len = data.readUInt16LE(i + 1);
162
+ i += 3;
163
+ if (i + len > data.length) break;
164
+ const value = data.subarray(i, i + len);
165
+ i += len;
166
+ if (tag === ORIGIN_TAG.URI && len > 0) {
167
+ const u = value.toString('utf8');
168
+ if (u.length > 0) loc.uri = u;
169
+ } else if (tag === ORIGIN_TAG.CRC32 && len === 4) {
170
+ loc.crc32 = value.readUInt32LE(0) >>> 0;
171
+ } else if (tag === ORIGIN_TAG.SIZE && len === 8) {
172
+ loc.size = Number(value.readBigUInt64LE(0));
173
+ } else if (tag === ORIGIN_TAG.MTIME && len === 8) {
174
+ loc.mtime = Number(value.readBigInt64LE(0));
175
+ } else if (tag === ORIGIN_TAG.SHA256 && len === 32) {
176
+ loc.sha256 = Buffer.from(value);
177
+ }
178
+ }
179
+ return originLocatorPresent(loc) ? loc : null;
180
+ }
181
+
182
+ /** Walk a ZIP Extra Field blob for NeoZip 0x014F (v1 TLV). */
183
+ export function parseOriginFromExtra(extra: Buffer | null | undefined): OriginLocator | null {
184
+ if (!extra || extra.length < 4) return null;
185
+ let i = 0;
186
+ while (i + 4 <= extra.length) {
187
+ const id = extra.readUInt16LE(i);
188
+ const size = extra.readUInt16LE(i + 2);
189
+ i += 4;
190
+ if (i + size > extra.length) break;
191
+ if (id === HDR_ID.ORIGIN && size >= 1) {
192
+ const version = extra.readUInt8(i);
193
+ const payload = extra.subarray(i + 1, i + size);
194
+ if (version === ORIGIN_EXTRA_VERSION) {
195
+ const loc = parseOriginTlv(payload);
196
+ if (loc) return loc;
197
+ }
198
+ }
199
+ i += size;
200
+ }
201
+ return null;
202
+ }
203
+
204
+ /** Read origin locator from a ZipEntry's preserved caller extras. */
205
+ export function parseOriginFromEntry(entry: {
206
+ additionalExtra?: Buffer | null;
207
+ }): OriginLocator | null {
208
+ return parseOriginFromExtra(entry.additionalExtra ?? null);
209
+ }
@@ -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.5', // packageJson.version
31
+ number: '1.5.0', // packageJson.version
32
32
  date: releaseDate
33
33
  };