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.
package/src/nitro/twl.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { hashDigest, hmacSha1, verifyRsaSha1, readFileSync } from '#nitro-runtime';
2
- import type { BinaryWriter } from './binary';
3
- import type { ModcryptHeader } from './crypto';
4
- import type { Arm9Header } from './blz';
5
- import { aes128CtrCrypt, encryptSecureArea, modcryptKey } from './crypto';
6
- import { compressArm9, decompressArm9 } from './blz';
1
+ import { hashDigest, hmacSha1, readFileSync, verifyRsaSha1 } from "#nitro-runtime";
2
+
3
+ import type { BinaryWriter } from "./binary";
4
+ import type { Arm9Header } from "./blz";
5
+ import { compressArm9, decompressArm9 } from "./blz";
6
+ import type { ModcryptHeader } from "./crypto";
7
+ import { aes128CtrCrypt, encryptSecureArea, modcryptKey } from "./crypto";
7
8
 
8
9
  export interface TwlHeader extends ModcryptHeader {
9
10
  digest_sector_size: number;
@@ -43,8 +44,8 @@ export interface OverlayRange {
43
44
  }
44
45
 
45
46
  export const HMAC_SHA1_KEY = Buffer.from(
46
- '2106c0deba98ce3fa692e39d46f2ed0176e3cc08562363facad4ecdf9a6278348f6d633cfe22ca9220889723d2cfaec232678dfeca836498acfd3e3787465824',
47
- 'hex',
47
+ "2106c0deba98ce3fa692e39d46f2ed0176e3cc08562363facad4ecdf9a6278348f6d633cfe22ca9220889723d2cfaec232678dfeca836498acfd3e3787465824",
48
+ "hex",
48
49
  );
49
50
  const hash = (data: Buffer): Buffer => hmacSha1(HMAC_SHA1_KEY, data);
50
51
  function slice(data: Buffer, start: number, size: number, label: string): Buffer {
@@ -60,46 +61,44 @@ function slice(data: Buffer, start: number, size: number, label: string): Buffer
60
61
  }
61
62
  function validate(h: TwlHeader): void {
62
63
  for (const field of [
63
- 'digest_ntr_start',
64
- 'digest_ntr_size',
65
- 'digest_twl_start',
66
- 'digest_twl_size',
67
- 'sector_hashtable_start',
68
- 'sector_hashtable_size',
69
- 'block_hashtable_start',
70
- 'block_hashtable_size',
71
- 'dsi9_rom_offset',
72
- 'dsi9_size',
73
- 'dsi7_rom_offset',
74
- 'dsi7_size',
75
- 'modcrypt1_start',
76
- 'modcrypt1_size',
77
- 'modcrypt2_start',
78
- 'modcrypt2_size',
79
- 'total_rom_size',
64
+ "digest_ntr_start",
65
+ "digest_ntr_size",
66
+ "digest_twl_start",
67
+ "digest_twl_size",
68
+ "sector_hashtable_start",
69
+ "sector_hashtable_size",
70
+ "block_hashtable_start",
71
+ "block_hashtable_size",
72
+ "dsi9_rom_offset",
73
+ "dsi9_size",
74
+ "dsi7_rom_offset",
75
+ "dsi7_size",
76
+ "modcrypt1_start",
77
+ "modcrypt1_size",
78
+ "modcrypt2_start",
79
+ "modcrypt2_size",
80
+ "total_rom_size",
80
81
  ] as const) {
81
82
  if (!Number.isSafeInteger(h[field]) || h[field] < 0 || h[field] > 0xffffffff) {
82
83
  throw new Error(`Invalid TWL ${field}`);
83
84
  }
84
85
  }
85
86
  const s = h.digest_sector_size;
86
- if (!Number.isSafeInteger(s) || s <= 0 || s > 0x4000 || 0x4000 % s)
87
- throw new Error('Invalid TWL digest sector size');
87
+ if (!Number.isSafeInteger(s) || s <= 0 || s > 0x4000 || 0x4000 % s) throw new Error("Invalid TWL digest sector size");
88
88
  if (
89
89
  !Number.isSafeInteger(h.digest_block_sectorcount) ||
90
90
  h.digest_block_sectorcount <= 0 ||
91
91
  h.digest_block_sectorcount > 0x100000
92
92
  )
93
- throw new Error('Invalid TWL digest block sector count');
93
+ throw new Error("Invalid TWL digest block sector count");
94
94
  if (h.digest_ntr_size < 0x4000 || h.digest_ntr_size % s || h.digest_twl_size % s)
95
- throw new Error('Invalid TWL digest region size');
95
+ throw new Error("Invalid TWL digest region size");
96
96
  const hashes = (h.digest_ntr_size + h.digest_twl_size) / s;
97
97
  if (
98
98
  hashes * 20 > h.sector_hashtable_size ||
99
- Math.ceil(h.sector_hashtable_size / (h.digest_block_sectorcount * 20)) * 20 >
100
- h.block_hashtable_size
99
+ Math.ceil(h.sector_hashtable_size / (h.digest_block_sectorcount * 20)) * 20 > h.block_hashtable_size
101
100
  )
102
- throw new Error('TWL hash tables are too small');
101
+ throw new Error("TWL hash tables are too small");
103
102
  }
104
103
  export class TWL {
105
104
  readonly firstDSiHashOffset: number;
@@ -117,51 +116,31 @@ export class TWL {
117
116
  this.firstDSiHashOffset = (h.digest_ntr_size / h.digest_sector_size) * 20;
118
117
  this.dsiHashSize = (h.digest_twl_size / h.digest_sector_size) * 20;
119
118
  this.Hashtable1Data = Buffer.from(
120
- slice(original, h.sector_hashtable_start, h.sector_hashtable_size, 'Sector hash table'),
119
+ slice(original, h.sector_hashtable_start, h.sector_hashtable_size, "Sector hash table"),
121
120
  );
122
121
  this.Hashtable2Data = Buffer.from(
123
- slice(original, h.block_hashtable_start, h.block_hashtable_size, 'Block hash table'),
124
- );
125
- this.DSi9Data = Buffer.from(
126
- slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), 'ARM9i'),
127
- );
128
- this.DSi7Data = Buffer.from(
129
- slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), 'ARM7i'),
122
+ slice(original, h.block_hashtable_start, h.block_hashtable_size, "Block hash table"),
130
123
  );
124
+ this.DSi9Data = Buffer.from(slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), "ARM9i"));
125
+ this.DSi7Data = Buffer.from(slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), "ARM7i"));
131
126
  this.Header2Data = h.trimmedRom
132
127
  ? null
133
128
  : Array.from({ length: 3 }, (_, i) =>
134
- Buffer.from(
135
- slice(original, h.digest_twl_start - 0x3000 + 0x1000 * i, 0x1000, 'Secondary header'),
136
- ),
129
+ Buffer.from(slice(original, h.digest_twl_start - 0x3000 + 0x1000 * i, 0x1000, "Secondary header")),
137
130
  );
138
- this.Overlays9Sha1Hmac = overlays.map((o) =>
139
- hash(slice(original, o.offset, o.size, 'ARM9 overlay')),
140
- );
131
+ this.Overlays9Sha1Hmac = overlays.map((o) => hash(slice(original, o.offset, o.size, "ARM9 overlay")));
141
132
  let encrypted = false;
142
133
  const modcryptStart = [
143
134
  { start: h.modcrypt1_start, size: h.modcrypt1_size },
144
135
  { start: h.modcrypt2_start, size: h.modcrypt2_size },
145
- ].find(
146
- ({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 0xffffffff,
147
- )?.start;
136
+ ].find(({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 0xffffffff)?.start;
148
137
  if (modcryptStart !== undefined) {
149
138
  const index = Math.floor((modcryptStart - h.digest_twl_start) / h.digest_sector_size);
150
139
  const actual = hash(
151
- slice(
152
- original,
153
- h.digest_twl_start + index * h.digest_sector_size,
154
- h.digest_sector_size,
155
- 'Modcrypt sector',
156
- ),
140
+ slice(original, h.digest_twl_start + index * h.digest_sector_size, h.digest_sector_size, "Modcrypt sector"),
157
141
  );
158
142
  encrypted = !actual.equals(
159
- slice(
160
- this.Hashtable1Data,
161
- this.firstDSiHashOffset + index * 20,
162
- 20,
163
- 'Modcrypt sector hash',
164
- ),
143
+ slice(this.Hashtable1Data, this.firstDSiHashOffset + index * 20, 20, "Modcrypt sector hash"),
165
144
  );
166
145
  }
167
146
  if (encrypted) this.cryptSections(h);
@@ -178,8 +157,8 @@ export class TWL {
178
157
  const off = h[`modcrypt${id}_start`] - start;
179
158
  const result = aes128CtrCrypt(
180
159
  key,
181
- slice(counter, 0, 16, 'Modcrypt counter'),
182
- slice(data, off, size, 'Modcrypt payload'),
160
+ slice(counter, 0, 16, "Modcrypt counter"),
161
+ slice(data, off, size, "Modcrypt payload"),
183
162
  );
184
163
  if (writer) {
185
164
  writer.position = start + off;
@@ -194,20 +173,16 @@ export class TWL {
194
173
  this.DSi7Data = this.importData(data, offset, size);
195
174
  }
196
175
  private importData(data: Buffer | string, offset: number, size?: number): Buffer {
197
- if (typeof data === 'string') data = readFileSync(data);
176
+ if (typeof data === "string") data = readFileSync(data);
198
177
  size ??= data.length - offset;
199
- const src = slice(data, offset, size, 'DSi import');
178
+ const src = slice(data, offset, size, "DSi import");
200
179
  const out = Buffer.alloc(Math.ceil(size / 16) * 16, 255);
201
180
  src.copy(out);
202
181
  return out;
203
182
  }
204
- updateOverlays9Sha1Hmac(
205
- arm9Data: Buffer,
206
- header: Arm9Header,
207
- overlays: readonly Buffer[],
208
- ): Buffer {
183
+ updateOverlays9Sha1Hmac(arm9Data: Buffer, header: Arm9Header, overlays: readonly Buffer[]): Buffer {
209
184
  if (overlays.length !== this.Overlays9Sha1Hmac.length)
210
- throw new Error('Changing ARM9 overlay count is unsupported');
185
+ throw new Error("Changing ARM9 overlay count is unsupported");
211
186
  const hashes = overlays.map(hash);
212
187
  if (hashes.every((h, i) => h.equals(this.Overlays9Sha1Hmac[i]))) return Buffer.from(arm9Data);
213
188
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
@@ -215,30 +190,23 @@ export class TWL {
215
190
  const unpacked = decompressArm9(arm9Data, header);
216
191
  const end = unpacked.data.readUInt32LE(init + 8) - header.ARM9ramAddress;
217
192
  let found = -1;
218
- for (
219
- let p = Math.min(end - hashes.length * 20, unpacked.data.length - hashes.length * 20);
220
- p >= 0;
221
- p--
222
- ) {
193
+ for (let p = Math.min(end - hashes.length * 20, unpacked.data.length - hashes.length * 20); p >= 0; p--) {
223
194
  if (unpacked.data.subarray(p, p + 20).equals(this.Overlays9Sha1Hmac[0])) {
224
195
  found = p;
225
196
  break;
226
197
  }
227
198
  }
228
- if (found < 0)
229
- throw new Error('ARM9 overlay hashes changed but original hash table was not found');
199
+ if (found < 0) throw new Error("ARM9 overlay hashes changed but original hash table was not found");
230
200
  hashes.forEach((h, i) => h.copy(unpacked.data, found + i * 20));
231
201
  // Original C# inverted this check and tried to compress uncompressed data.
232
202
  // Preserve the original storage format: recompress only a compressed ARM9.
233
- return unpacked.compressed
234
- ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr)
235
- : unpacked.data;
203
+ return unpacked.compressed ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr) : unpacked.data;
236
204
  }
237
205
  writeTo(writer: BinaryWriter, h: TwlHeader): Buffer {
238
206
  validate(h);
239
207
  // Fixed DSi regions cannot be shifted safely without rewriting the header.
240
208
  if (writer.position > h.sector_hashtable_start) {
241
- throw new Error('NTR output overlaps the fixed DSi sector hash table');
209
+ throw new Error("NTR output overlaps the fixed DSi sector hash table");
242
210
  }
243
211
  if (
244
212
  h.sector_hashtable_start + h.sector_hashtable_size > h.block_hashtable_start ||
@@ -246,7 +214,7 @@ export class TWL {
246
214
  h.dsi9_rom_offset + this.DSi9Data.length > h.dsi7_rom_offset ||
247
215
  h.dsi7_rom_offset + this.DSi7Data.length > h.total_rom_size
248
216
  ) {
249
- throw new Error('DSi sections overlap or exceed the ROM size');
217
+ throw new Error("DSi sections overlap or exceed the ROM size");
250
218
  }
251
219
  const fill = (end: number): void => {
252
220
  if (writer.position < end) writer.fillTo(end, 255);
@@ -268,7 +236,7 @@ export class TWL {
268
236
  fill(h.total_rom_size);
269
237
  const finalPosition = writer.position;
270
238
  const read = (p: number, n: number): Buffer =>
271
- slice(writer.buffer.subarray(0, writer.length), p, n, 'TWL output digest');
239
+ slice(writer.buffer.subarray(0, writer.length), p, n, "TWL output digest");
272
240
  let secure: Buffer = Buffer.from(read(h.digest_ntr_start, 0x4000));
273
241
  if (h.decrypted) secure = encryptSecureArea(h.gameCode, secure);
274
242
  let index = 0;
@@ -276,19 +244,10 @@ export class TWL {
276
244
  writer.position = h.sector_hashtable_start + index++ * 20;
277
245
  writer.write(hash(b));
278
246
  };
279
- for (let p = 0; p < 0x4000; p += h.digest_sector_size)
280
- put(secure.subarray(p, p + h.digest_sector_size));
281
- for (
282
- let p = h.digest_ntr_start + 0x4000;
283
- p < h.digest_ntr_start + h.digest_ntr_size;
284
- p += h.digest_sector_size
285
- )
247
+ for (let p = 0; p < 0x4000; p += h.digest_sector_size) put(secure.subarray(p, p + h.digest_sector_size));
248
+ for (let p = h.digest_ntr_start + 0x4000; p < h.digest_ntr_start + h.digest_ntr_size; p += h.digest_sector_size)
286
249
  put(read(p, h.digest_sector_size));
287
- for (
288
- let p = h.digest_twl_start;
289
- p < h.digest_twl_start + h.digest_twl_size;
290
- p += h.digest_sector_size
291
- )
250
+ for (let p = h.digest_twl_start; p < h.digest_twl_start + h.digest_twl_size; p += h.digest_sector_size)
292
251
  put(read(p, h.digest_sector_size));
293
252
  index = 0;
294
253
  for (
@@ -314,9 +273,9 @@ export const updateHeaderSignatures = (
314
273
  originalHeader: Buffer,
315
274
  keepOriginal: boolean,
316
275
  ): void => {
317
- const signedData = Buffer.from(slice(originalHeader, 0, 0xe00, 'Signed header'));
318
- const digest = slice(header.hmac_digest_master, 0, 20, 'Master digest');
319
- const signature = slice(header.rsa_signature, 0, 128, 'RSA signature');
276
+ const signedData = Buffer.from(slice(originalHeader, 0, 0xe00, "Signed header"));
277
+ const digest = slice(header.hmac_digest_master, 0, 20, "Master digest");
278
+ const signature = slice(header.rsa_signature, 0, 128, "RSA signature");
320
279
  digest.copy(signedData, 0x328);
321
280
  const valid = verifyRsaSha1(signedData, signature);
322
281
  const position = writer.position;
@@ -328,7 +287,7 @@ export const updateHeaderSignatures = (
328
287
  mask[0] = 0;
329
288
  mask[1] = 1;
330
289
  mask[107] = 0;
331
- hashDigest('sha1', signedData).copy(mask, 108);
290
+ hashDigest("sha1", signedData).copy(mask, 108);
332
291
  header.rsa_signature = mask;
333
292
  }
334
293
  writer.position = 0xf80;
@@ -1,14 +1,14 @@
1
- export { NDSFile } from './nitro/nds-file';
2
- export type { RomOptions, RomFileInfo } from './nitro/nds-file';
3
- export { Header, UnitCode } from './nitro/header';
4
- export { Banner } from './nitro/banner';
5
- export { FileAllocationTable, FileNameTable, OverlayTable } from './nitro/filesystem';
6
- export type { NitroFile, NitroFolder, FatEntry, OverlayItem } from './nitro/filesystem';
7
- export { BinaryWriter } from './nitro/binary';
8
- export { crc16, crc32 } from './nitro/crc';
9
- export { encryptSecureArea, aes128CtrCrypt, modcryptKey } from './nitro/crypto';
10
- export type { ModcryptHeader } from './nitro/crypto';
11
- export { compressBLZ, decompressBLZ, compressArm9, decompressArm9 } from './nitro/blz';
12
- export type { Arm9Header } from './nitro/blz';
13
- export { TWL, updateHeaderSignatures } from './nitro/twl';
14
- export type { TwlHeader, SignedTwlHeader, OverlayRange } from './nitro/twl';
1
+ export { NDSFile } from "./nitro/nds-file";
2
+ export type { RomFileInfo, RomOptions } from "./nitro/nds-file";
3
+ export { Header, UnitCode } from "./nitro/header";
4
+ export { Banner } from "./nitro/banner";
5
+ export { FileAllocationTable, FileNameTable, OverlayTable } from "./nitro/filesystem";
6
+ export type { FatEntry, NitroFile, NitroFolder, OverlayItem } from "./nitro/filesystem";
7
+ export { BinaryWriter } from "./nitro/binary";
8
+ export { crc16, crc32 } from "./nitro/crc";
9
+ export { aes128CtrCrypt, encryptSecureArea, modcryptKey } from "./nitro/crypto";
10
+ export type { ModcryptHeader } from "./nitro/crypto";
11
+ export { compressArm9, compressBLZ, decompressArm9, decompressBLZ } from "./nitro/blz";
12
+ export type { Arm9Header } from "./nitro/blz";
13
+ export { TWL, updateHeaderSignatures } from "./nitro/twl";
14
+ export type { OverlayRange, SignedTwlHeader, TwlHeader } from "./nitro/twl";
@@ -1,13 +1,14 @@
1
1
  // Native TypeScript port of NitroPatchHelper/PatchHelper.cs, GPL-3.0.
2
- import { hashDigest, readFile } from '#nitro-runtime';
3
- import { bytes } from './nitro/binary';
4
- import { NDSFile } from './nitro/nds-file';
5
- import { readZip } from './patch/zip';
6
- import { decodeXdelta } from './patch/xdelta';
2
+ import { hashDigest, readFile } from "#nitro-runtime";
3
+
4
+ import { bytes } from "./nitro/binary";
5
+ import { NDSFile } from "./nitro/nds-file";
6
+ import { decodeXdelta } from "./patch/xdelta";
7
+ import { extractZipEntry as extractEntry, readZip } from "./patch/zip";
7
8
 
8
9
  export const PatchReturnValue = Object.freeze({
9
- SUCCESS: 'SUCCESS',
10
- MD5_MISMATCH: 'MD5_MISMATCH',
10
+ SUCCESS: "SUCCESS",
11
+ MD5_MISMATCH: "MD5_MISMATCH",
11
12
  } as const);
12
13
  export type PatchReturnValue = (typeof PatchReturnValue)[keyof typeof PatchReturnValue];
13
14
 
@@ -41,7 +42,7 @@ export interface PatchMetadata {
41
42
  }
42
43
 
43
44
  export interface PatchReadme {
44
- format: 'markdown' | 'plaintext';
45
+ format: "markdown" | "plaintext";
45
46
  content: string;
46
47
  }
47
48
 
@@ -50,7 +51,7 @@ export interface PatchInfo {
50
51
  readme: PatchReadme | null;
51
52
  }
52
53
 
53
- const md5 = (data: Uint8Array): string => hashDigest('md5', data).toString('hex');
54
+ const md5 = (data: Uint8Array): string => hashDigest("md5", data).toString("hex");
54
55
  const invalidMetadata = (message: string): null => {
55
56
  console.warn(`忽略 metadata.json:${message}`);
56
57
  return null;
@@ -60,43 +61,46 @@ const invalidMetadataField = (field: keyof PatchMetadata, message: string): void
60
61
  };
61
62
 
62
63
  const readMetadata = (files: Map<string, Buffer>): PatchMetadata | null => {
63
- const file = files.get('metadata.json');
64
+ const file = files.get("metadata.json");
64
65
  if (!file) return null;
65
66
 
66
67
  let metadata: unknown;
67
68
  try {
68
- metadata = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(file));
69
+ metadata = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(file));
69
70
  } catch (error) {
70
71
  return invalidMetadata(
71
72
  `格式错误,可能是因为补丁包已损坏:${error instanceof Error ? error.message : String(error)}`,
72
73
  );
73
74
  }
74
- if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
75
- return invalidMetadata('根节点必须是对象。');
75
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
76
+ return invalidMetadata("根节点必须是对象。");
76
77
 
77
78
  const value = metadata as Record<string, unknown>;
78
79
  const result: PatchMetadata = {};
79
- for (const field of ['id', 'author', 'name', 'homepage', 'version'] as const) {
80
- if (typeof value[field] === 'string') result[field] = value[field];
81
- else invalidMetadataField(field, '必须是字符串。');
80
+ for (const field of ["id", "author", "name", "homepage", "version"] as const) {
81
+ const fieldValue = value[field];
82
+ if (fieldValue === undefined) continue;
83
+ if (typeof fieldValue === "string") result[field] = fieldValue;
84
+ else invalidMetadataField(field, "必须是字符串。");
82
85
  }
83
- if (typeof value.isBeta === 'boolean') result.isBeta = value.isBeta;
84
- else invalidMetadataField('isBeta', '必须是布尔值。');
86
+ if (value.isBeta === undefined) return result;
87
+ if (typeof value.isBeta === "boolean") result.isBeta = value.isBeta;
88
+ else invalidMetadataField("isBeta", "必须是布尔值。");
85
89
 
86
90
  return result;
87
91
  };
88
92
 
89
93
  const readReadme = (files: Map<string, Buffer>): PatchReadme | null => {
90
94
  for (const [filename, format] of [
91
- ['README.md', 'markdown'],
92
- ['README.txt', 'plaintext'],
95
+ ["README.md", "markdown"],
96
+ ["README.txt", "plaintext"],
93
97
  ] as const) {
94
98
  const file = files.get(filename.toLowerCase());
95
99
  if (!file) continue;
96
100
  try {
97
101
  return {
98
102
  format,
99
- content: new TextDecoder('utf-8', { fatal: true }).decode(file),
103
+ content: new TextDecoder("utf-8", { fatal: true }).decode(file),
100
104
  };
101
105
  } catch (error) {
102
106
  console.warn(
@@ -108,16 +112,12 @@ const readReadme = (files: Map<string, Buffer>): PatchReadme | null => {
108
112
  };
109
113
 
110
114
  /** Read and validate metadata.json from a NitroPatcher ZIP package. */
111
- export const readPatchMetadata = (
112
- patch: Uint8Array,
113
- options: PatchOptions = {},
114
- ): PatchMetadata | null => readMetadata(readZip(patch, options));
115
+ export const readPatchMetadata = (patch: Uint8Array, options: PatchOptions = {}): PatchMetadata | null =>
116
+ readMetadata(readZip(patch, options));
115
117
 
116
118
  /** Read a root README.md or README.txt from a NitroPatcher ZIP package. */
117
- export const readPatchReadme = (
118
- patch: Uint8Array,
119
- options: PatchOptions = {},
120
- ): PatchReadme | null => readReadme(readZip(patch, options));
119
+ export const readPatchReadme = (patch: Uint8Array, options: PatchOptions = {}): PatchReadme | null =>
120
+ readReadme(readZip(patch, options));
121
121
 
122
122
  /** Read metadata and README content while parsing the patch package only once. */
123
123
  export const readPatchInfo = (patch: Uint8Array, options: PatchOptions = {}): PatchInfo => {
@@ -125,6 +125,10 @@ export const readPatchInfo = (patch: Uint8Array, options: PatchOptions = {}): Pa
125
125
  return { metadata: readMetadata(files), readme: readReadme(files) };
126
126
  };
127
127
 
128
+ /** Extract one file from a ZIP archive entirely in memory. */
129
+ export const extractZipEntry = (archive: Uint8Array, entryName: string, options: PatchOptions = {}): Buffer =>
130
+ extractEntry(archive, entryName, options);
131
+
128
132
  /** Read metadata.json from a NitroPatcher ZIP package on disk. */
129
133
  export const readPatchMetadataFile = async (
130
134
  patchPath: string,
@@ -132,40 +136,29 @@ export const readPatchMetadataFile = async (
132
136
  ): Promise<PatchMetadata | null> => readPatchMetadata(await readFile(patchPath), options);
133
137
 
134
138
  /** Read a patch README from disk. */
135
- export const readPatchReadmeFile = async (
136
- patchPath: string,
137
- options: PatchOptions = {},
138
- ): Promise<PatchReadme | null> => readPatchReadme(await readFile(patchPath), options);
139
+ export const readPatchReadmeFile = async (patchPath: string, options: PatchOptions = {}): Promise<PatchReadme | null> =>
140
+ readPatchReadme(await readFile(patchPath), options);
139
141
 
140
142
  /** Read patch metadata and README content from disk. */
141
- export const readPatchInfoFile = async (
142
- patchPath: string,
143
- options: PatchOptions = {},
144
- ): Promise<PatchInfo> => readPatchInfo(await readFile(patchPath), options);
143
+ export const readPatchInfoFile = async (patchPath: string, options: PatchOptions = {}): Promise<PatchInfo> =>
144
+ readPatchInfo(await readFile(patchPath), options);
145
145
 
146
146
  /** Apply an existing NitroPatcher ZIP package entirely in memory. */
147
- export const patchBuffer = (
148
- original: Uint8Array,
149
- patch: Uint8Array,
150
- options: PatchOptions = {},
151
- ): PatchResult => {
152
- const input = bytes(original, 'original ROM');
147
+ export const patchBuffer = (original: Uint8Array, patch: Uint8Array, options: PatchOptions = {}): PatchResult => {
148
+ const input = bytes(original, "original ROM");
153
149
  const patches = readZip(patch, options);
154
150
  const inputMd5 = md5(input);
155
151
  const preprocessing = patches.get(`preprocessing/${inputMd5}.xdelta`);
156
- const rom = new NDSFile(
157
- preprocessing ? decodeXdelta(input, preprocessing, options) : input,
158
- options,
159
- );
152
+ const rom = new NDSFile(preprocessing ? decodeXdelta(input, preprocessing, options) : input, options);
160
153
  let matched = true;
161
- const md5File = patches.get('md5.txt');
154
+ const md5File = patches.get("md5.txt");
162
155
  if (md5File) {
163
156
  matched = false;
164
- for (const line of md5File.toString('utf8').split('\n')) {
157
+ for (const line of md5File.toString("utf8").split("\n")) {
165
158
  const expected = line.trim();
166
- if (!expected || expected.startsWith('#')) continue;
159
+ if (!expected || expected.startsWith("#")) continue;
167
160
  // Keep the original format contract: exactly 32 characters, case-insensitive comparison.
168
- if (expected.length !== 32) throw new Error('md5.txt 格式错误,可能是因为补丁包已损坏。');
161
+ if (expected.length !== 32) throw new Error("md5.txt 格式错误,可能是因为补丁包已损坏。");
169
162
  if (expected.toLowerCase() === inputMd5) {
170
163
  matched = true;
171
164
  break;
@@ -173,11 +166,9 @@ export const patchBuffer = (
173
166
  }
174
167
  }
175
168
  for (const { path } of rom.listFiles()) {
176
- const normalized = path.replaceAll('\\', '/').toLowerCase();
169
+ const normalized = path.replaceAll("\\", "/").toLowerCase();
177
170
  const delta = patches.get(`xdelta/${normalized}`);
178
- const replacement = delta
179
- ? decodeXdelta(rom.getFile(path), delta, options)
180
- : patches.get(normalized);
171
+ const replacement = delta ? decodeXdelta(rom.getFile(path), delta, options) : patches.get(normalized);
181
172
  if (replacement) rom.replaceFile(path, replacement);
182
173
  }
183
174
  const buffer = rom.toBuffer();
@@ -201,6 +192,7 @@ export const patchIt = async (
201
192
 
202
193
  /** Namespaced counterpart of the original C# helper, sharing the same typed functions. */
203
194
  export const PatchHelper = Object.freeze({
195
+ extractZipEntry,
204
196
  patchBuffer,
205
197
  patchIt,
206
198
  readPatchInfo,
@@ -210,5 +202,5 @@ export const PatchHelper = Object.freeze({
210
202
  readPatchReadme,
211
203
  readPatchReadmeFile,
212
204
  });
213
- export { decodeXdelta } from './patch/xdelta';
214
- export type { XdeltaOptions } from './patch/xdelta';
205
+ export { decodeXdelta } from "./patch/xdelta";
206
+ export type { XdeltaOptions } from "./patch/xdelta";