nitro-patcher 0.2.0 → 0.2.1

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.
@@ -11,7 +11,7 @@ class Reader {
11
11
  pos = 0;
12
12
  constructor(
13
13
  readonly bytes: Buffer,
14
- readonly label = 'patch',
14
+ readonly label = "patch",
15
15
  ) {}
16
16
  get remaining() {
17
17
  return this.bytes.length - this.pos;
@@ -31,8 +31,7 @@ class Reader {
31
31
  return fail(`invalid variable-length integer in ${this.label}`);
32
32
  }
33
33
  take(size: number): Buffer {
34
- if (size > this.remaining)
35
- fail(`truncated ${this.label}: requested ${size} bytes, ${this.remaining} remain`);
34
+ if (size > this.remaining) fail(`truncated ${this.label}: requested ${size} bytes, ${this.remaining} remain`);
36
35
  const result = this.bytes.subarray(this.pos, this.pos + size);
37
36
  this.pos += size;
38
37
  return result;
@@ -40,24 +39,28 @@ class Reader {
40
39
  }
41
40
 
42
41
  // RFC 3284 section 5.6: (operation, size, COPY address mode), up to two per opcode.
43
- type Operation = { type: 'ADD' | 'RUN' | 'COPY'; size: number; mode: number };
42
+ interface Operation {
43
+ type: "ADD" | "RUN" | "COPY";
44
+ size: number;
45
+ mode: number;
46
+ }
44
47
  const table: Operation[][] = [];
45
- const op = (type: Operation['type'], size: number, mode = 0): Operation => ({ type, size, mode });
46
- table.push([op('RUN', 0)]);
47
- for (let size = 0; size <= 17; size++) table.push([op('ADD', size)]);
48
+ const op = (type: Operation["type"], size: number, mode = 0): Operation => ({ type, size, mode });
49
+ table.push([op("RUN", 0)]);
50
+ for (let size = 0; size <= 17; size++) table.push([op("ADD", size)]);
48
51
  for (let mode = 0; mode <= 8; mode++) {
49
- table.push([op('COPY', 0, mode)]);
50
- for (let size = 4; size <= 18; size++) table.push([op('COPY', size, mode)]);
52
+ table.push([op("COPY", 0, mode)]);
53
+ for (let size = 4; size <= 18; size++) table.push([op("COPY", size, mode)]);
51
54
  }
52
55
  for (let mode = 0; mode <= 5; mode++) {
53
56
  for (let add = 1; add <= 4; add++) {
54
- for (let copy = 4; copy <= 6; copy++) table.push([op('ADD', add), op('COPY', copy, mode)]);
57
+ for (let copy = 4; copy <= 6; copy++) table.push([op("ADD", add), op("COPY", copy, mode)]);
55
58
  }
56
59
  }
57
60
  for (let mode = 6; mode <= 8; mode++) {
58
- for (let add = 1; add <= 4; add++) table.push([op('ADD', add), op('COPY', 4, mode)]);
61
+ for (let add = 1; add <= 4; add++) table.push([op("ADD", add), op("COPY", 4, mode)]);
59
62
  }
60
- for (let mode = 0; mode <= 8; mode++) table.push([op('COPY', 4, mode), op('ADD', 1)]);
63
+ for (let mode = 0; mode <= 8; mode++) table.push([op("COPY", 4, mode), op("ADD", 1)]);
61
64
 
62
65
  function adler32(bytes: Uint8Array): number {
63
66
  let a = 1,
@@ -96,22 +99,17 @@ export interface XdeltaOptions {
96
99
  maxWindowSize?: number;
97
100
  }
98
101
 
99
- export const decodeXdelta = (
100
- source: Uint8Array,
101
- patch: Uint8Array,
102
- options: XdeltaOptions = {},
103
- ): Buffer => {
104
- const sourceBytes = asBuffer(source, 'source');
105
- const reader = new Reader(asBuffer(patch, 'patch'));
106
- const maxOutput = limit(options.maxOutputSize, 1024 * 1024 * 1024, 'maxOutputSize');
107
- const maxWindow = limit(options.maxWindowSize, 16 * 1024 * 1024, 'maxWindowSize');
108
- if (!reader.take(3).equals(Buffer.from([0xd6, 0xc3, 0xc4]))) fail('invalid file signature');
109
- if (reader.byte() !== 0) fail('unsupported version (expected 0)');
102
+ export const decodeXdelta = (source: Uint8Array, patch: Uint8Array, options: XdeltaOptions = {}): Buffer => {
103
+ const sourceBytes = asBuffer(source, "source");
104
+ const reader = new Reader(asBuffer(patch, "patch"));
105
+ const maxOutput = limit(options.maxOutputSize, 1024 * 1024 * 1024, "maxOutputSize");
106
+ const maxWindow = limit(options.maxWindowSize, 16 * 1024 * 1024, "maxWindowSize");
107
+ if (!reader.take(3).equals(Buffer.from([0xd6, 0xc3, 0xc4]))) fail("invalid file signature");
108
+ if (reader.byte() !== 0) fail("unsupported version (expected 0)");
110
109
  const header = reader.byte();
111
- if (header & ~7) fail('unrecognized header indicator bits');
112
- if (header & 1)
113
- fail('secondary compression is unsupported; recreate the patch using xdelta3 -S none');
114
- if (header & 2) fail('custom code table is unsupported');
110
+ if (header & ~7) fail("unrecognized header indicator bits");
111
+ if (header & 1) fail("secondary compression is unsupported; recreate the patch using xdelta3 -S none");
112
+ if (header & 2) fail("custom code table is unsupported");
115
113
  if (header & 4) reader.take(reader.integer());
116
114
  const windows: Buffer[] = [];
117
115
  let total = 0;
@@ -119,14 +117,14 @@ export const decodeXdelta = (
119
117
  while (reader.remaining) {
120
118
  const indicator = reader.byte();
121
119
  if (indicator & ~7 || (indicator & 3) === 3)
122
- fail('invalid window indicator: source and target are mutually exclusive');
120
+ fail("invalid window indicator: source and target are mutually exclusive");
123
121
  let dictionary: Buffer = Buffer.alloc(0);
124
122
  if (indicator & 3) {
125
123
  const length = reader.integer();
126
124
  const offset = reader.integer();
127
125
  const available = indicator & 1 ? sourceBytes.length : total;
128
126
  if (offset > available || length > available - offset)
129
- fail('source segment exceeds available source/target bounds');
127
+ fail("source segment exceeds available source/target bounds");
130
128
  if (indicator & 1) dictionary = sourceBytes.subarray(offset, offset + length);
131
129
  else {
132
130
  dictionary = Buffer.allocUnsafe(length);
@@ -141,24 +139,22 @@ export const decodeXdelta = (
141
139
  }
142
140
  }
143
141
  const deltaLength = reader.integer();
144
- const delta = new Reader(reader.take(deltaLength), 'delta window');
142
+ const delta = new Reader(reader.take(deltaLength), "delta window");
145
143
  const targetLength = delta.integer();
146
- if (targetLength > maxWindow)
147
- fail(`window size ${targetLength} exceeds maximum limit ${maxWindow}`);
144
+ if (targetLength > maxWindow) fail(`window size ${targetLength} exceeds maximum limit ${maxWindow}`);
148
145
  if (targetLength > maxOutput - total) fail(`output size exceeds maximum limit ${maxOutput}`);
149
- if (dictionary.length + targetLength > UINT32_MAX)
150
- fail('window address space exceeds 32-bit limit');
146
+ if (dictionary.length + targetLength > UINT32_MAX) fail("window address space exceeds 32-bit limit");
151
147
  const compressed = delta.byte();
152
- if (compressed & ~7) fail('unrecognized delta indicator bits');
153
- if (compressed) fail('secondary compression in delta sections is unsupported');
148
+ if (compressed & ~7) fail("unrecognized delta indicator bits");
149
+ if (compressed) fail("secondary compression in delta sections is unsupported");
154
150
  const dataLength = delta.integer(),
155
151
  instructionLength = delta.integer(),
156
152
  addressLength = delta.integer();
157
153
  const checksum = indicator & 4 ? delta.take(4).readUInt32BE() : null;
158
- const data = new Reader(delta.take(dataLength), 'data section');
159
- const instructions = new Reader(delta.take(instructionLength), 'instruction section');
160
- const addresses = new Reader(delta.take(addressLength), 'address section');
161
- if (delta.remaining) fail('delta encoding length does not match section lengths');
154
+ const data = new Reader(delta.take(dataLength), "data section");
155
+ const instructions = new Reader(delta.take(instructionLength), "instruction section");
156
+ const addresses = new Reader(delta.take(addressLength), "address section");
157
+ if (delta.remaining) fail("delta encoding length does not match section lengths");
162
158
  const target = Buffer.allocUnsafe(targetLength);
163
159
  const near = new Uint32Array(4),
164
160
  same = new Uint32Array(768);
@@ -169,9 +165,9 @@ export const decodeXdelta = (
169
165
  for (const { type, size: fixedSize, mode } of operations) {
170
166
  const size = fixedSize || instructions.integer();
171
167
  if (size > targetLength - position) fail(`${type} size exceeds target window length`);
172
- if (type === 'ADD') {
168
+ if (type === "ADD") {
173
169
  data.take(size).copy(target, position);
174
- } else if (type === 'RUN') {
170
+ } else if (type === "RUN") {
175
171
  target.fill(data.byte(), position, position + size);
176
172
  } else {
177
173
  const here = dictionary.length + position;
@@ -180,8 +176,7 @@ export const decodeXdelta = (
180
176
  else if (mode === 1) address = here - addresses.integer();
181
177
  else if (mode < 6) address = near[mode - 2]! + addresses.integer();
182
178
  else address = same[(mode - 6) * 256 + addresses.byte()]!;
183
- if (address < 0 || address >= here)
184
- fail(`COPY address ${address} is outside decoded address space ${here}`);
179
+ if (address < 0 || address >= here) fail(`COPY address ${address} is outside decoded address space ${here}`);
185
180
  near[nearIndex] = address;
186
181
  nearIndex = (nearIndex + 1) % near.length;
187
182
  same[address % same.length] = address;
@@ -195,7 +190,7 @@ export const decodeXdelta = (
195
190
  const from = Math.max(0, address - dictionary.length);
196
191
  while (copied < size) {
197
192
  const count = Math.min(size - copied, position + copied - from);
198
- if (count <= 0) fail('COPY references unavailable target bytes');
193
+ if (count <= 0) fail("COPY references unavailable target bytes");
199
194
  target.copy(target, position + copied, from, from + count);
200
195
  copied += count;
201
196
  }
@@ -203,10 +198,9 @@ export const decodeXdelta = (
203
198
  position += size;
204
199
  }
205
200
  }
206
- if (position !== targetLength)
207
- fail(`target window length mismatch: decoded ${position}, expected ${targetLength}`);
208
- if (data.remaining || addresses.remaining) fail('unused bytes in data/address section');
209
- if (checksum !== null && adler32(target) !== checksum) fail('Adler32 checksum mismatch');
201
+ if (position !== targetLength) fail(`target window length mismatch: decoded ${position}, expected ${targetLength}`);
202
+ if (data.remaining || addresses.remaining) fail("unused bytes in data/address section");
203
+ if (checksum !== null && adler32(target) !== checksum) fail("Adler32 checksum mismatch");
210
204
  windows.push(target);
211
205
  total += targetLength;
212
206
  }
package/src/patch/zip.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { inflateRawSync } from '#nitro-runtime';
2
- import { crc32 } from '../nitro/crc';
1
+ import { inflateRawSync } from "#nitro-runtime";
2
+
3
+ import { crc32 } from "../nitro/crc";
3
4
 
4
5
  /** Resource limits apply to every central-directory entry, including duplicates. */
5
6
  export interface ZipOptions {
@@ -10,22 +11,21 @@ export interface ZipOptions {
10
11
  const fail = (message: string): never => {
11
12
  throw new Error(`ZIP: ${message}`);
12
13
  };
13
- const utf8 = new TextDecoder('utf-8', { fatal: true });
14
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
14
15
  // IBM code page 437, byte values 0x80..0xff. ASCII bytes retain their literal values.
15
16
  const cp437 =
16
- 'ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ';
17
+ "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ";
17
18
 
18
19
  function decodeUtf8(bytes: Uint8Array): string {
19
20
  try {
20
21
  return utf8.decode(bytes);
21
22
  } catch {
22
- return fail('invalid UTF-8 filename');
23
+ return fail("invalid UTF-8 filename");
23
24
  }
24
25
  }
25
26
  function boundOption(value: number | undefined, fallback: number, label: string): number {
26
27
  value ??= fallback;
27
- if (!Number.isSafeInteger(value) || value < 0)
28
- throw new RangeError(`${label} must be a non-negative safe integer`);
28
+ if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${label} must be a non-negative safe integer`);
29
29
  return value;
30
30
  }
31
31
  function checkRange(bytes: Buffer, offset: number, length: number, end = bytes.length): void {
@@ -41,7 +41,7 @@ function extras(bytes: Buffer): Map<number, Buffer> {
41
41
  length = bytes.readUInt16LE(offset + 2);
42
42
  offset += 4;
43
43
  checkRange(bytes, offset, length);
44
- if (id === 1) fail('ZIP64 archives are unsupported');
44
+ if (id === 1) fail("ZIP64 archives are unsupported");
45
45
  fields.set(id, bytes.subarray(offset, offset + length));
46
46
  offset += length;
47
47
  }
@@ -52,7 +52,7 @@ function filename(raw: Buffer, flags: number, fields: Map<number, Buffer>): stri
52
52
  const unicode = fields.get(0x7075);
53
53
  if (unicode && unicode.length >= 5 && unicode[0] === 1 && unicode.readUInt32LE(1) === crc32(raw))
54
54
  return decodeUtf8(unicode.subarray(5));
55
- let result = '';
55
+ let result = "";
56
56
  for (const byte of raw) result += byte < 128 ? String.fromCharCode(byte) : cp437[byte - 128]!;
57
57
  return result;
58
58
  }
@@ -65,26 +65,18 @@ function filename(raw: Buffer, flags: number, fields: Map<number, Buffer>): stri
65
65
  * Format reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
66
66
  */
67
67
  export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string, Buffer> => {
68
- if (!(input instanceof Uint8Array))
69
- throw new TypeError('ZIP input must be a Buffer or Uint8Array');
68
+ if (!(input instanceof Uint8Array)) throw new TypeError("ZIP input must be a Buffer or Uint8Array");
70
69
  const bytes = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
71
- const maxSize = boundOption(
72
- options.maxUncompressedSize,
73
- 1024 * 1024 * 1024,
74
- 'maxUncompressedSize',
75
- );
76
- const maxEntries = boundOption(options.maxEntries, 100000, 'maxEntries');
70
+ const maxSize = boundOption(options.maxUncompressedSize, 1024 * 1024 * 1024, "maxUncompressedSize");
71
+ const maxEntries = boundOption(options.maxEntries, 100000, "maxEntries");
77
72
  let eocd = -1;
78
73
  for (let offset = bytes.length - 22; offset >= Math.max(0, bytes.length - 65557); offset--) {
79
- if (
80
- bytes.readUInt32LE(offset) === 0x06054b50 &&
81
- offset + 22 + bytes.readUInt16LE(offset + 20) === bytes.length
82
- ) {
74
+ if (bytes.readUInt32LE(offset) === 0x06054b50 && offset + 22 + bytes.readUInt16LE(offset + 20) === bytes.length) {
83
75
  eocd = offset;
84
76
  break;
85
77
  }
86
78
  }
87
- if (eocd < 0) fail('end of central directory not found or truncated');
79
+ if (eocd < 0) fail("end of central directory not found or truncated");
88
80
  const disk = bytes.readUInt16LE(eocd + 4),
89
81
  centralDisk = bytes.readUInt16LE(eocd + 6);
90
82
  const diskEntries = bytes.readUInt16LE(eocd + 8),
@@ -98,20 +90,19 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
98
90
  centralOffset === 0xffffffff ||
99
91
  (eocd >= 20 && bytes.readUInt32LE(eocd - 20) === 0x07064b50)
100
92
  )
101
- fail('ZIP64 archives are unsupported');
102
- if (disk || centralDisk || diskEntries !== count)
103
- fail('split or multiple-disk archives are unsupported');
93
+ fail("ZIP64 archives are unsupported");
94
+ if (disk || centralDisk || diskEntries !== count) fail("split or multiple-disk archives are unsupported");
104
95
  if (count > maxEntries) fail(`entry count exceeds limit ${maxEntries}`);
105
96
  checkRange(bytes, centralOffset, centralSize, eocd);
106
- if (centralOffset + centralSize !== eocd) fail('central directory size/offset mismatch');
107
- if (count > Math.floor(centralSize / 46)) fail('entry count exceeds central directory size');
97
+ if (centralOffset + centralSize !== eocd) fail("central directory size/offset mismatch");
98
+ if (count > Math.floor(centralSize / 46)) fail("entry count exceeds central directory size");
108
99
  const result = new Map<string, Buffer>();
109
- const ranges: Array<{ start: number; end: number }> = [];
100
+ const ranges: { start: number; end: number }[] = [];
110
101
  let offset = centralOffset,
111
102
  totalSize = 0;
112
103
  for (let index = 0; index < count; index++) {
113
104
  checkRange(bytes, offset, 46, eocd);
114
- if (bytes.readUInt32LE(offset) !== 0x02014b50) fail('invalid central directory signature');
105
+ if (bytes.readUInt32LE(offset) !== 0x02014b50) fail("invalid central directory signature");
115
106
  const flags = bytes.readUInt16LE(offset + 8),
116
107
  method = bytes.readUInt16LE(offset + 10);
117
108
  const checksum = bytes.readUInt32LE(offset + 16),
@@ -122,40 +113,30 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
122
113
  commentLength = bytes.readUInt16LE(offset + 32);
123
114
  const startDisk = bytes.readUInt16LE(offset + 34),
124
115
  localOffset = bytes.readUInt32LE(offset + 42);
125
- if (
126
- size === 0xffffffff ||
127
- compressedSize === 0xffffffff ||
128
- localOffset === 0xffffffff ||
129
- startDisk === 0xffff
130
- )
131
- fail('ZIP64 entry is unsupported');
132
- if (startDisk) fail('split or multiple-disk entry is unsupported');
133
- if (flags & 0x2041) fail('encrypted entries are unsupported');
134
- if (flags & ~0x80e) fail('unsupported general purpose flags');
116
+ if (size === 0xffffffff || compressedSize === 0xffffffff || localOffset === 0xffffffff || startDisk === 0xffff)
117
+ fail("ZIP64 entry is unsupported");
118
+ if (startDisk) fail("split or multiple-disk entry is unsupported");
119
+ if (flags & 0x2041) fail("encrypted entries are unsupported");
120
+ if (flags & ~0x80e) fail("unsupported general purpose flags");
135
121
  if (method !== 0 && method !== 8) fail(`unsupported compression method ${method}`);
136
122
  if (size > maxSize - totalSize) fail(`uncompressed size exceeds limit ${maxSize}`);
137
123
  totalSize += size;
138
124
  checkRange(bytes, offset + 46, nameLength + extraLength + commentLength, eocd);
139
125
  const rawName = bytes.subarray(offset + 46, offset + 46 + nameLength);
140
- const fields = extras(
141
- bytes.subarray(offset + 46 + nameLength, offset + 46 + nameLength + extraLength),
142
- );
143
- const name = filename(rawName, flags, fields).replaceAll('\\', '/').toLowerCase();
126
+ const fields = extras(bytes.subarray(offset + 46 + nameLength, offset + 46 + nameLength + extraLength));
127
+ const name = filename(rawName, flags, fields).replaceAll("\\", "/").toLowerCase();
144
128
  offset += 46 + nameLength + extraLength + commentLength;
145
129
 
146
130
  checkRange(bytes, localOffset, 30, centralOffset);
147
- if (bytes.readUInt32LE(localOffset) !== 0x04034b50) fail('invalid local header signature');
148
- if (
149
- bytes.readUInt16LE(localOffset + 6) !== flags ||
150
- bytes.readUInt16LE(localOffset + 8) !== method
151
- )
152
- fail('local header flags/compression method mismatch');
131
+ if (bytes.readUInt32LE(localOffset) !== 0x04034b50) fail("invalid local header signature");
132
+ if (bytes.readUInt16LE(localOffset + 6) !== flags || bytes.readUInt16LE(localOffset + 8) !== method)
133
+ fail("local header flags/compression method mismatch");
153
134
  const localNameLength = bytes.readUInt16LE(localOffset + 26),
154
135
  localExtraLength = bytes.readUInt16LE(localOffset + 28);
155
136
  const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
156
137
  checkRange(bytes, localOffset + 30, localNameLength + localExtraLength, centralOffset);
157
138
  if (!rawName.equals(bytes.subarray(localOffset + 30, localOffset + 30 + localNameLength)))
158
- fail('local filename differs from central directory');
139
+ fail("local filename differs from central directory");
159
140
  extras(bytes.subarray(localOffset + 30 + localNameLength, dataOffset));
160
141
  if (
161
142
  !(flags & 8) &&
@@ -163,7 +144,7 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
163
144
  bytes.readUInt32LE(localOffset + 18) !== compressedSize ||
164
145
  bytes.readUInt32LE(localOffset + 22) !== size)
165
146
  )
166
- fail('local header CRC/size mismatch');
147
+ fail("local header CRC/size mismatch");
167
148
  checkRange(bytes, dataOffset, compressedSize, centralOffset);
168
149
  let dataEnd = dataOffset + compressedSize;
169
150
  if (flags & 8) {
@@ -177,13 +158,13 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
177
158
  bytes.readUInt32LE(at + 8) === size;
178
159
  if (matches(dataEnd)) dataEnd += 12;
179
160
  else if (bytes.readUInt32LE(dataEnd) === 0x08074b50 && matches(dataEnd + 4)) dataEnd += 16;
180
- else fail('data descriptor CRC/size mismatch');
161
+ else fail("data descriptor CRC/size mismatch");
181
162
  }
182
163
  ranges.push({ start: localOffset, end: dataEnd });
183
164
  const compressed = bytes.subarray(dataOffset, dataOffset + compressedSize);
184
165
  let data: Buffer;
185
166
  if (method === 0) {
186
- if (compressedSize !== size) fail('stored entry compressed/uncompressed size mismatch');
167
+ if (compressedSize !== size) fail("stored entry compressed/uncompressed size mismatch");
187
168
  data = Buffer.from(compressed);
188
169
  } else {
189
170
  try {
@@ -196,11 +177,21 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
196
177
  }
197
178
  if (data.length !== size) fail(`uncompressed size mismatch for ${JSON.stringify(name)}`);
198
179
  if (crc32(data) !== checksum) fail(`CRC32 mismatch for ${JSON.stringify(name)}`);
199
- if (name && !name.endsWith('/')) result.set(name, data);
180
+ if (name && !name.endsWith("/")) result.set(name, data);
200
181
  }
201
- if (offset !== eocd) fail('central directory entry count/length mismatch');
182
+ if (offset !== eocd) fail("central directory entry count/length mismatch");
202
183
  ranges.sort((a, b) => a.start - b.start);
203
184
  for (let i = 1; i < ranges.length; i++)
204
- if (ranges[i]!.start < ranges[i - 1]!.end) fail('overlapping local entry ranges');
185
+ if (ranges[i]!.start < ranges[i - 1]!.end) fail("overlapping local entry ranges");
205
186
  return result;
206
187
  };
188
+
189
+ /** Extract one file using the same normalized, case-insensitive paths as readZip. */
190
+ export const extractZipEntry = (input: Uint8Array, entryName: string, options: ZipOptions = {}): Buffer => {
191
+ if (typeof entryName !== "string" || !entryName) throw new TypeError("ZIP entry name must be a non-empty string");
192
+ const normalized = entryName.replaceAll("\\", "/").toLowerCase();
193
+ if (normalized.endsWith("/")) fail(`entry ${JSON.stringify(entryName)} is a directory`);
194
+ const entry = readZip(input, options).get(normalized);
195
+ if (entry === undefined) return fail(`entry ${JSON.stringify(entryName)} not found`);
196
+ return entry;
197
+ };
@@ -1,12 +1,11 @@
1
- import { md5, sha1 } from '#browser-hashes';
2
- import { hmac } from '#browser-hmac';
3
- import { ecb } from '#browser-aes';
4
- import { Inflate } from 'fflate';
1
+ import { ecb } from "#browser-aes";
2
+ import { md5, sha1 } from "#browser-hashes";
3
+ import { hmac } from "#browser-hmac";
4
+ import { Inflate } from "fflate";
5
5
 
6
- export const hashDigest = (algorithm: 'md5' | 'sha1', data: Uint8Array): Buffer =>
7
- Buffer.from((algorithm === 'md5' ? md5 : sha1)(data));
8
- export const hmacSha1 = (key: Uint8Array, data: Uint8Array): Buffer =>
9
- Buffer.from(hmac(sha1, key, data));
6
+ export const hashDigest = (algorithm: "md5" | "sha1", data: Uint8Array): Buffer =>
7
+ Buffer.from((algorithm === "md5" ? md5 : sha1)(data));
8
+ export const hmacSha1 = (key: Uint8Array, data: Uint8Array): Buffer => Buffer.from(hmac(sha1, key, data));
10
9
  export const createAesBlockCipher =
11
10
  (key: Uint8Array): ((block: Uint8Array) => Buffer) =>
12
11
  (block) =>
@@ -17,8 +16,7 @@ export const inflateRawSync = (data: Uint8Array, options: { maxOutputLength: num
17
16
  const output = Buffer.alloc(options.maxOutputLength);
18
17
  let length = 0;
19
18
  const stream = new Inflate((chunk) => {
20
- if (chunk.length > output.length - length)
21
- throw new RangeError('Inflated data exceeds declared size');
19
+ if (chunk.length > output.length - length) throw new RangeError("Inflated data exceeds declared size");
22
20
  output.set(chunk, length);
23
21
  length += chunk.length;
24
22
  });
@@ -32,7 +30,7 @@ export const inflateRawSync = (data: Uint8Array, options: { maxOutputLength: num
32
30
  // These Node-only methods are not exposed by the browser public API. Keep a
33
31
  // clear error if a future internal call accidentally crosses that boundary.
34
32
  const nodeOnly = (): never => {
35
- throw new Error('Filesystem and RSA APIs are only available in the Node entry');
33
+ throw new Error("Filesystem and RSA APIs are only available in the Node entry");
36
34
  };
37
35
  export const readFile = nodeOnly;
38
36
  export const writeFile = nodeOnly;
@@ -1,28 +1,27 @@
1
- import { createHash, createHmac, createCipheriv, createPublicKey, verify } from 'node:crypto';
2
- export { readFile, writeFile } from 'node:fs/promises';
3
- export { readFileSync } from 'node:fs';
4
- export { inflateRawSync } from 'node:zlib';
1
+ import { createCipheriv, createHash, createHmac, createPublicKey, verify } from "node:crypto";
2
+ export { readFile, writeFile } from "node:fs/promises";
3
+ export { readFileSync } from "node:fs";
4
+ export { inflateRawSync } from "node:zlib";
5
5
 
6
- export const hashDigest = (algorithm: 'md5' | 'sha1', data: Uint8Array): Buffer =>
6
+ export const hashDigest = (algorithm: "md5" | "sha1", data: Uint8Array): Buffer =>
7
7
  createHash(algorithm).update(data).digest();
8
- export const hmacSha1 = (key: Uint8Array, data: Uint8Array): Buffer =>
9
- createHmac('sha1', key).update(data).digest();
8
+ export const hmacSha1 = (key: Uint8Array, data: Uint8Array): Buffer => createHmac("sha1", key).update(data).digest();
10
9
  export const createAesBlockCipher = (key: Uint8Array): ((block: Uint8Array) => Buffer) => {
11
- const cipher = createCipheriv('aes-128-ecb', key, null);
10
+ const cipher = createCipheriv("aes-128-ecb", key, null);
12
11
  cipher.setAutoPadding(false);
13
12
  return (block) => cipher.update(block);
14
13
  };
15
14
  export const verifyRsaSha1 = (data: Uint8Array, signature: Uint8Array): boolean => {
16
15
  const key = createPublicKey({
17
16
  key: {
18
- kty: 'RSA',
17
+ kty: "RSA",
19
18
  n: Buffer.from(
20
- '956F790DF08BB85A76AAEFA27FE874758BED9EDF9E9A670CD818BEB9B2885203B3FA11AEAA186513B5D6BB85A384D0D0EFB366CBC6051AAA86827AB74311F59C9BFC6C7079D5F17BD0819F522056738C721F40CF2361932590A3C5DC94CFD17A8CBC954A918AA858F4D804BAF7D3C1C4D7B8F077012FA170260B2C049056F3A5',
21
- 'hex',
22
- ).toString('base64url'),
23
- e: 'AQAB',
19
+ "956F790DF08BB85A76AAEFA27FE874758BED9EDF9E9A670CD818BEB9B2885203B3FA11AEAA186513B5D6BB85A384D0D0EFB366CBC6051AAA86827AB74311F59C9BFC6C7079D5F17BD0819F522056738C721F40CF2361932590A3C5DC94CFD17A8CBC954A918AA858F4D804BAF7D3C1C4D7B8F077012FA170260B2C049056F3A5",
20
+ "hex",
21
+ ).toString("base64url"),
22
+ e: "AQAB",
24
23
  },
25
- format: 'jwk',
24
+ format: "jwk",
26
25
  });
27
- return verify('RSA-SHA1', data, key, signature);
26
+ return verify("RSA-SHA1", data, key, signature);
28
27
  };