nitro-patcher 0.2.0 → 0.3.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.
package/src/cli.ts CHANGED
@@ -1,24 +1,36 @@
1
1
  #!/usr/bin/env node
2
- import { mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
3
- import { basename, dirname, join, resolve } from 'node:path';
4
- import { parseArgs } from 'node:util';
5
- import { render } from 'markdansi';
6
- import { patchBuffer, readPatchInfo, type PatchMetadata } from './nitro-patch-helper';
2
+ import { render } from "markdansi";
3
+ import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, resolve, sep } from "node:path";
5
+ import { parseArgs } from "node:util";
7
6
 
8
- const help = `nitro-patcher — 原生 Node.js NDS ROM 补丁工具
7
+ import { NDSFile } from "./nitro/nds-file";
8
+ import { type PatchMetadata, patchBuffer, readPatchInfo } from "./nitro-patch-helper";
9
+
10
+ const help = `nitro-patcher — NDS ROM 补丁工具
9
11
 
10
12
  用法:
11
13
  nitro-patcher [--json] [--dry-run] <原始ROM> <补丁ZIP> [输出ROM]
12
- nitro-patcher [--json] [--dry-run] --input <原始ROM> --patch <补丁ZIP> [--output <输出ROM>]
14
+ nitro-patcher patch [--json] [--dry-run] --input <原始ROM> --patch <补丁ZIP> [--output <输出ROM>]
15
+ nitro-patcher extract [--json] --input <ROM> --output <目录>
16
+ nitro-patcher inspect [--json] --input <ROM>
13
17
 
14
- --json 输出 JSON 状态和 MD5
18
+ --json 结果输出为 JSON 格式
15
19
  --dry-run 完整执行补丁流程,但不写入输出 ROM
16
- -i, --input 原始 ROM 路径
20
+ -i, --input 输入 ROM 路径
17
21
  -p, --patch 补丁 ZIP 路径
18
- -o, --output 输出 ROM 路径;试运行时可省略
22
+ -o, --output 输出 ROM 或提取目录;试运行时可省略
19
23
  -h, --help 显示帮助
20
24
  -v, --version 显示版本`;
21
25
 
26
+ interface CommandOptions {
27
+ json: boolean;
28
+ dryRun: boolean;
29
+ input: string | undefined;
30
+ patch: string | undefined;
31
+ output: string | undefined;
32
+ }
33
+
22
34
  const sameFile = async (input: string, output: string): Promise<boolean> => {
23
35
  if (resolve(input) === resolve(output)) return true;
24
36
  const original = await stat(input);
@@ -26,14 +38,14 @@ const sameFile = async (input: string, output: string): Promise<boolean> => {
26
38
  const destination = await stat(output);
27
39
  return original.dev === destination.dev && original.ino === destination.ino;
28
40
  } catch (error) {
29
- if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
41
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
30
42
  return false;
31
43
  }
32
44
  };
33
45
 
34
46
  const safeTerminalText = (value: string): string => {
35
47
  const characters: string[] = [];
36
- for (const character of value.replaceAll('\r\n', '\n').replaceAll('\r', '\n')) {
48
+ for (const character of value.replaceAll("\r\n", "\n").replaceAll("\r", "\n")) {
37
49
  const code = character.codePointAt(0)!;
38
50
  if (
39
51
  (code < 0x20 && code !== 0x09 && code !== 0x0a) ||
@@ -44,37 +56,97 @@ const safeTerminalText = (value: string): string => {
44
56
  continue;
45
57
  characters.push(character);
46
58
  }
47
- return characters.join('');
59
+ return characters.join("");
48
60
  };
49
61
 
50
62
  const formatMetadata = (metadata: PatchMetadata): string => {
51
63
  const labels = {
52
- author: '作者',
53
- name: '名称',
54
- homepage: '主页',
55
- version: '版本',
64
+ author: "作者",
65
+ name: "名称",
66
+ homepage: "主页",
67
+ version: "版本",
56
68
  } as const;
57
69
  const lines: string[] = [];
58
- for (const field of ['author', 'name', 'homepage', 'version'] as const) {
70
+ for (const field of ["author", "name", "homepage", "version"] as const) {
59
71
  const value = metadata[field];
60
72
  if (value === undefined) continue;
61
- const suffix = field === 'version' && metadata.isBeta ? '(测试版)' : '';
62
- lines.push(`- ${labels[field]}:${safeTerminalText(value).replaceAll('\n', ' ')}${suffix}`);
73
+ const suffix = field === "version" && metadata.isBeta ? "(测试版)" : "";
74
+ lines.push(`- ${labels[field]}:${safeTerminalText(value).replaceAll("\n", " ")}${suffix}`);
75
+ }
76
+ return lines.join("\n");
77
+ };
78
+
79
+ const extractRom = async (options: CommandOptions): Promise<void> => {
80
+ if (!options.input || !options.output || options.patch !== undefined || options.dryRun) throw new Error(help);
81
+ const destination = join(await realpath(dirname(resolve(options.output))), basename(resolve(options.output)));
82
+ const rom = new NDSFile(await readFile(options.input));
83
+ const files = rom.listFiles();
84
+ await mkdir(destination, { recursive: true });
85
+ for (const { path } of files) {
86
+ const output = resolve(destination, path);
87
+ if (!output.startsWith(`${destination}${sep}`)) throw new Error(`ROM 文件路径超出输出目录:${path}`);
88
+ await mkdir(dirname(output), { recursive: true });
89
+ await writeFile(output, rom.getFile(path));
90
+ }
91
+
92
+ if (options.json) {
93
+ console.log(
94
+ JSON.stringify({
95
+ output: destination,
96
+ fileCount: files.length,
97
+ files: files.map(({ path }) => path),
98
+ }),
99
+ );
100
+ } else {
101
+ console.log(`已提取 ${files.length} 个文件到:${destination}`);
63
102
  }
64
- return lines.join('\n');
103
+ };
104
+
105
+ const inspectRom = async (options: CommandOptions): Promise<void> => {
106
+ if (!options.input || options.patch !== undefined || options.output !== undefined || options.dryRun)
107
+ throw new Error(help);
108
+ const input = await readFile(options.input);
109
+ const rom = new NDSFile(input);
110
+ const info = {
111
+ gameTitle: rom.header.gameTitle.replace(/\0+$/, ""),
112
+ gameCode: rom.header.gameCode,
113
+ size: input.length,
114
+ fileCount: rom.listFiles().length,
115
+ checksums: {
116
+ header: rom.header.headerCRC,
117
+ logo: rom.header.logoCRC,
118
+ secureArea: rom.header.secureCRC,
119
+ banner: rom.banner.bannerCRC,
120
+ },
121
+ };
122
+ if (options.json) {
123
+ console.log(JSON.stringify(info));
124
+ return;
125
+ }
126
+ console.log(`ROM 信息:
127
+
128
+ 标题:${safeTerminalText(info.gameTitle).replaceAll("\n", " ")}
129
+ 游戏代码:${safeTerminalText(info.gameCode).replaceAll("\n", " ")}
130
+ 大小:${info.size} 字节
131
+ 文件数量:${info.fileCount}
132
+ 校验:
133
+ - Header:${info.checksums.header ? "通过" : "失败"}
134
+ - Logo:${info.checksums.logo ? "通过" : "失败"}
135
+ - Secure Area:${info.checksums.secureArea ? "通过" : "失败"}
136
+ - Banner:${info.checksums.banner ? "通过" : "失败"}`);
65
137
  };
66
138
 
67
139
  const main = async (): Promise<void> => {
68
140
  const { values, positionals } = parseArgs({
69
141
  allowPositionals: true,
70
142
  options: {
71
- help: { type: 'boolean', short: 'h' },
72
- version: { type: 'boolean', short: 'v' },
73
- json: { type: 'boolean' },
74
- 'dry-run': { type: 'boolean' },
75
- input: { type: 'string', short: 'i' },
76
- patch: { type: 'string', short: 'p' },
77
- output: { type: 'string', short: 'o' },
143
+ help: { type: "boolean", short: "h" },
144
+ version: { type: "boolean", short: "v" },
145
+ json: { type: "boolean" },
146
+ "dry-run": { type: "boolean" },
147
+ input: { type: "string", short: "i" },
148
+ patch: { type: "string", short: "p" },
149
+ output: { type: "string", short: "o" },
78
150
  },
79
151
  });
80
152
  if (values.help) {
@@ -82,38 +154,43 @@ const main = async (): Promise<void> => {
82
154
  return;
83
155
  }
84
156
  if (values.version) {
85
- const metadata: unknown = JSON.parse(
86
- await readFile(new URL('../package.json', import.meta.url), 'utf8'),
87
- );
88
- if (
89
- !metadata ||
90
- typeof metadata !== 'object' ||
91
- !('version' in metadata) ||
92
- typeof metadata.version !== 'string'
93
- )
94
- throw new Error('Invalid package version metadata');
157
+ const metadata: unknown = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
158
+ if (!metadata || typeof metadata !== "object" || !("version" in metadata) || typeof metadata.version !== "string")
159
+ throw new Error("Invalid package version metadata");
95
160
  console.log(metadata.version);
96
161
  return;
97
162
  }
98
- const dryRun = values['dry-run'] ?? false;
99
- const usesNamedPaths =
100
- values.input !== undefined || values.patch !== undefined || values.output !== undefined;
101
- if (usesNamedPaths && positionals.length > 0) throw new Error(help);
102
- if (
103
- (usesNamedPaths && (!values.input || !values.patch || (!dryRun && !values.output))) ||
104
- (!usesNamedPaths &&
105
- ((!dryRun && positionals.length !== 3) ||
106
- (dryRun && (positionals.length < 2 || positionals.length > 3))))
107
- )
163
+ const options: CommandOptions = {
164
+ json: values.json ?? false,
165
+ dryRun: values["dry-run"] ?? false,
166
+ input: values.input,
167
+ patch: values.patch,
168
+ output: values.output,
169
+ };
170
+ const command = positionals.length === 1 ? positionals[0] : undefined;
171
+ if (positionals.length !== 0 && positionals.length !== 1 && positionals.length !== 3) throw new Error(help);
172
+ if (command !== undefined && command !== "patch" && command !== "extract" && command !== "inspect")
173
+ throw new Error(help);
174
+ if (command === "extract") {
175
+ await extractRom(options);
176
+ return;
177
+ }
178
+ if (command === "inspect") {
179
+ await inspectRom(options);
180
+ return;
181
+ }
182
+
183
+ const dryRun = options.dryRun;
184
+ const usesLegacyPaths = positionals.length === 3;
185
+ if (usesLegacyPaths && (values.input !== undefined || values.patch !== undefined || values.output !== undefined))
108
186
  throw new Error(help);
109
- const [original, patch, output] = usesNamedPaths
110
- ? [values.input, values.patch, values.output]
111
- : positionals;
187
+ if (!usesLegacyPaths && (!values.input || !values.patch || (!dryRun && !values.output))) throw new Error(help);
188
+ const [original, patch, output] = usesLegacyPaths ? positionals : [values.input, values.patch, values.output];
112
189
  if (!original || !patch) throw new Error(help);
113
190
  if (!dryRun) {
114
191
  if (!output) throw new Error(help);
115
192
  if ((await sameFile(original, output)) || (await sameFile(patch, output)))
116
- throw new Error('输出路径不能覆盖原始 ROM 或补丁包。');
193
+ throw new Error("输出路径不能覆盖原始 ROM 或补丁包。");
117
194
  }
118
195
  const [originalBytes, patchBytes] = await Promise.all([readFile(original), readFile(patch)]);
119
196
  const info = readPatchInfo(patchBytes);
@@ -121,9 +198,9 @@ const main = async (): Promise<void> => {
121
198
  if (!dryRun) {
122
199
  if (!output) throw new Error(help);
123
200
  const destination = join(await realpath(dirname(resolve(output))), basename(output));
124
- const temporary = await mkdtemp(join(dirname(destination), '.nitro-patcher-'));
201
+ const temporary = await mkdtemp(join(dirname(destination), ".nitro-patcher-"));
125
202
  try {
126
- const file = join(temporary, 'output.nds');
203
+ const file = join(temporary, "output.nds");
127
204
  await writeFile(file, result.buffer);
128
205
  await rename(file, destination);
129
206
  } finally {
@@ -131,7 +208,7 @@ const main = async (): Promise<void> => {
131
208
  }
132
209
  }
133
210
  const { returnValue, inputMd5, outputMd5 } = result;
134
- if (values.json) {
211
+ if (options.json) {
135
212
  console.log(
136
213
  JSON.stringify({
137
214
  returnValue,
@@ -147,23 +224,23 @@ const main = async (): Promise<void> => {
147
224
  const sections: string[] = [];
148
225
  if (info.metadata) {
149
226
  const metadata = formatMetadata(info.metadata);
150
- if (metadata) sections.push(`补丁元数据:\n\n${metadata}`);
227
+ if (metadata) sections.push(`补丁信息:\n\n${metadata}`);
151
228
  }
152
229
  if (info.readme) {
153
230
  const content = safeTerminalText(info.readme.content);
154
231
  const rendered =
155
- info.readme.format === 'markdown'
232
+ info.readme.format === "markdown"
156
233
  ? render(content, {
157
234
  width: process.stdout.columns ?? 80,
158
235
  color: Boolean(process.stdout.isTTY),
159
236
  hyperlinks: false,
160
237
  })
161
238
  : content;
162
- sections.push(`补丁说明:\n\n${rendered.trimEnd()}`);
239
+ sections.push(`补丁说明:\n\n${rendered.trim()}`);
163
240
  }
164
- if (dryRun) sections.push('试运行完成,未写入输出 ROM。');
241
+ if (dryRun) sections.push("试运行完成,未写入输出 ROM。");
165
242
  sections.push(`${returnValue}\n\n原始 ROM 的 MD5:${inputMd5}\n生成 ROM 的 MD5:${outputMd5}`);
166
- console.log(sections.join('\n\n'));
243
+ console.log(sections.join("\n\n"));
167
244
  };
168
245
 
169
246
  main().catch((error: unknown) => {
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './nitro-helper';
2
- export * from './nitro-patch-helper';
1
+ export * from "./nitro-helper";
2
+ export * from "./nitro-patch-helper";
@@ -1,16 +1,16 @@
1
1
  // Port of NitroHelper/Banner.cs. Copyright Xzonn and upstream contributors, GPL-3.0.
2
- import { bytes, range } from './binary';
3
- import { crc16 } from './crc';
2
+ import { bytes, range } from "./binary";
3
+ import { crc16 } from "./crc";
4
4
 
5
5
  const titles = [
6
- 'japaneseTitle',
7
- 'englishTitle',
8
- 'frenchTitle',
9
- 'germanTitle',
10
- 'italianTitle',
11
- 'spanishTitle',
12
- 'chineseTitle',
13
- 'koreanTitle',
6
+ "japaneseTitle",
7
+ "englishTitle",
8
+ "frenchTitle",
9
+ "germanTitle",
10
+ "italianTitle",
11
+ "spanishTitle",
12
+ "chineseTitle",
13
+ "koreanTitle",
14
14
  ] as const;
15
15
  export class Banner {
16
16
  version: number;
@@ -18,21 +18,20 @@ export class Banner {
18
18
  bannerCRC: boolean;
19
19
  tileData: Buffer;
20
20
  palette: Buffer;
21
- japaneseTitle = '';
22
- englishTitle = '';
23
- frenchTitle = '';
24
- germanTitle = '';
25
- italianTitle = '';
26
- spanishTitle = '';
27
- chineseTitle = '';
28
- koreanTitle = '';
21
+ japaneseTitle = "";
22
+ englishTitle = "";
23
+ frenchTitle = "";
24
+ germanTitle = "";
25
+ italianTitle = "";
26
+ spanishTitle = "";
27
+ chineseTitle = "";
28
+ koreanTitle = "";
29
29
  constructor(input: Uint8Array, offset = 0, hardSize = 0) {
30
- const data = bytes(input, 'banner');
31
- this.version = range(data, offset, 2, 'banner').readUInt16LE();
30
+ const data = bytes(input, "banner");
31
+ this.version = range(data, offset, 2, "banner").readUInt16LE();
32
32
  let size = this.getDefSize(hardSize);
33
- if (this.version >>> 8 === 1)
34
- size = hardSize > 0 && hardSize !== 0xffffffff ? Math.min(hardSize, 0x23c0) : 0x23c0;
35
- this.raw = Buffer.from(range(data, offset, size, 'banner'));
33
+ if (this.version >>> 8 === 1) size = hardSize > 0 && hardSize !== 0xffffffff ? Math.min(hardSize, 0x23c0) : 0x23c0;
34
+ this.raw = Buffer.from(range(data, offset, size, "banner"));
36
35
  this.bannerCRC = crc16(this.raw.subarray(0x20, 0x840)) === this.raw.readUInt16LE(2);
37
36
  this.tileData = Buffer.from(this.raw.subarray(0x20, 0x220));
38
37
  this.palette = Buffer.from(this.raw.subarray(0x220, 0x240));
@@ -41,9 +40,9 @@ export class Banner {
41
40
  i < this.titleCount
42
41
  ? this.raw
43
42
  .subarray(0x240 + i * 0x100, 0x340 + i * 0x100)
44
- .toString('utf16le')
45
- .split('\0')[0]
46
- : '';
43
+ .toString("utf16le")
44
+ .split("\0")[0]
45
+ : "";
47
46
  }
48
47
  get titleCount() {
49
48
  return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
@@ -57,14 +56,12 @@ export class Banner {
57
56
  toBuffer() {
58
57
  const output = Buffer.from(this.raw);
59
58
  output.writeUInt16LE(this.version);
60
- if (this.tileData.length !== 512 || this.palette.length !== 32)
61
- throw new RangeError('Invalid banner icon size');
59
+ if (this.tileData.length !== 512 || this.palette.length !== 32) throw new RangeError("Invalid banner icon size");
62
60
  this.tileData.copy(output, 0x20);
63
61
  this.palette.copy(output, 0x220);
64
62
  for (let i = 0; i < this.titleCount; i++) {
65
- const text = Buffer.from(this[titles[i]], 'utf16le');
66
- if (text.length > 256)
67
- throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
63
+ const text = Buffer.from(this[titles[i]], "utf16le");
64
+ if (text.length > 256) throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
68
65
  output.fill(0, 0x240 + i * 256, 0x340 + i * 256);
69
66
  text.copy(output, 0x240 + i * 256);
70
67
  }
@@ -1,16 +1,11 @@
1
1
  export const DEFAULT_MAX_SIZE = 0x40000000;
2
2
 
3
- export const bytes = (value: Uint8Array, name = 'input'): Buffer => {
3
+ export const bytes = (value: Uint8Array, name = "input"): Buffer => {
4
4
  if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Buffer or Uint8Array`);
5
5
  return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
6
6
  };
7
7
 
8
- export const range = (
9
- buffer: Buffer,
10
- offset: number,
11
- size: number,
12
- name = 'binary data',
13
- ): Buffer => {
8
+ export const range = (buffer: Buffer, offset: number, size: number, name = "binary data"): Buffer => {
14
9
  if (
15
10
  !Number.isSafeInteger(offset) ||
16
11
  !Number.isSafeInteger(size) ||
@@ -18,16 +13,12 @@ export const range = (
18
13
  size < 0 ||
19
14
  offset + size > buffer.length
20
15
  ) {
21
- throw new RangeError(
22
- `Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`,
23
- );
16
+ throw new RangeError(`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`);
24
17
  }
25
18
  return buffer.subarray(offset, offset + size);
26
19
  };
27
20
 
28
- export const align = (value: number, alignment = 0x200): number => {
29
- return Math.ceil(value / alignment) * alignment;
30
- };
21
+ export const align = (value: number, alignment = 0x200): number => Math.ceil(value / alignment) * alignment;
31
22
 
32
23
  /** Seekable, bounded in-memory writer; unwritten gaps have MemoryStream's zero fill. */
33
24
  export class BinaryWriter {
@@ -37,7 +28,7 @@ export class BinaryWriter {
37
28
  length: number;
38
29
  constructor(initialSize = 65536, maxSize = DEFAULT_MAX_SIZE) {
39
30
  if (!Number.isSafeInteger(maxSize) || maxSize < 1 || maxSize > 0xffffffff)
40
- throw new RangeError('Invalid output size limit');
31
+ throw new RangeError("Invalid output size limit");
41
32
  this.maxSize = maxSize;
42
33
  this.buffer = Buffer.alloc(Math.min(initialSize, maxSize));
43
34
  this.position = 0;
@@ -55,8 +46,7 @@ export class BinaryWriter {
55
46
 
56
47
  write(data: Uint8Array) {
57
48
  data = bytes(data);
58
- if (!Number.isSafeInteger(this.position) || this.position < 0)
59
- throw new RangeError('Invalid writer position');
49
+ if (!Number.isSafeInteger(this.position) || this.position < 0) throw new RangeError("Invalid writer position");
60
50
  this.ensure(this.position + data.length);
61
51
  bytes(data).copy(this.buffer, this.position);
62
52
  this.position += data.length;
package/src/nitro/blz.ts CHANGED
@@ -7,7 +7,7 @@ export interface Arm9Header {
7
7
  // Native port of Xzonn/BlzHelper BLZ.cs (MIT), including overwrite-safe prefix.
8
8
  // Compression returns an empty Buffer when no smaller representation exists.
9
9
  export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
10
- if (input.length < 8) throw new Error('BLZ footer is truncated');
10
+ if (input.length < 8) throw new Error("BLZ footer is truncated");
11
11
  const diff = input.readUInt32LE(input.length - 4);
12
12
  const packed = input.readUInt32LE(input.length - 8),
13
13
  header = packed >>> 24,
@@ -15,7 +15,7 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
15
15
  const size = input.length + diff,
16
16
  prefix = input.length - length;
17
17
  if (header < 8 || header > length || length > input.length || size > maxSize)
18
- throw new Error('Invalid BLZ size or footer');
18
+ throw new Error("Invalid BLZ size or footer");
19
19
  const out = Buffer.alloc(size);
20
20
  input.copy(out, 0, 0, prefix);
21
21
  let src = input.length - header,
@@ -24,16 +24,15 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
24
24
  const flags = input[--src];
25
25
  for (let bit = 128; bit && src > prefix; bit >>= 1) {
26
26
  if (!(flags & bit)) {
27
- if (dst <= prefix) throw new Error('BLZ output overflow');
27
+ if (dst <= prefix) throw new Error("BLZ output overflow");
28
28
  out[--dst] = input[--src];
29
29
  } else {
30
- if (src - 2 < prefix) throw new Error('Truncated BLZ reference');
30
+ if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
31
31
  const high = input[--src],
32
32
  low = input[--src];
33
33
  const count = (high >>> 4) + 3,
34
34
  offset = (((high & 15) << 8) | low) + 3;
35
- if (dst - count < prefix || dst - 1 + offset >= size)
36
- throw new Error('Invalid BLZ reference');
35
+ if (dst - count < prefix || dst - 1 + offset >= size) throw new Error("Invalid BLZ reference");
37
36
  for (let j = 0; j < count; j++) {
38
37
  --dst;
39
38
  out[dst] = out[dst + offset];
@@ -41,12 +40,12 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
41
40
  }
42
41
  }
43
42
  }
44
- if (dst !== prefix) throw new Error('BLZ decompressed size mismatch');
43
+ if (dst !== prefix) throw new Error("BLZ decompressed size mismatch");
45
44
  return out;
46
45
  };
47
46
 
48
47
  export const compressBLZ = (input: Buffer): Buffer => {
49
- if (input.length > 0xffffff) throw new Error('BLZ input exceeds 24-bit format limit');
48
+ if (input.length > 0xffffff) throw new Error("BLZ input exceeds 24-bit format limit");
50
49
  const temp = Buffer.alloc(input.length);
51
50
  let src = input.length,
52
51
  dst = input.length;
@@ -131,31 +130,24 @@ export const compressBLZ = (input: Buffer): Buffer => {
131
130
  return out;
132
131
  };
133
132
 
134
- export const decompressArm9 = (
135
- data: Buffer,
136
- header: Arm9Header,
137
- ): { compressed: boolean; data: Buffer } => {
138
- if (data.length < 0x18) throw new Error('ARM9 data too short');
133
+ export const decompressArm9 = (data: Buffer, header: Arm9Header): { compressed: boolean; data: Buffer } => {
134
+ if (data.length < 0x18) throw new Error("ARM9 data too short");
139
135
  const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
140
136
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
141
137
  const ptr = init ? data.readUInt32LE(init + 0x14) : header.ARM9ramAddress + header.ARM9size;
142
- const compressed =
143
- ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
138
+ const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
144
139
  return {
145
140
  compressed,
146
141
  data: compressed
147
- ? Buffer.concat([
148
- decompressBLZ(data.subarray(0, data.length - nitro)),
149
- data.subarray(data.length - nitro),
150
- ])
142
+ ? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)])
151
143
  : Buffer.from(data),
152
144
  };
153
145
  };
154
146
  export const compressArm9 = (data: Buffer, header: Arm9Header, postSize = 0): Buffer => {
155
- if (data.length < 0x4000) throw new Error('ARM9 secure area is truncated');
147
+ if (data.length < 0x4000) throw new Error("ARM9 secure area is truncated");
156
148
  const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
157
149
  const packed = compressBLZ(data.subarray(0x4000, data.length - nitro));
158
- if (!packed.length) throw new Error('ARM9 BLZ compression produced no smaller representation');
150
+ if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
159
151
  const out = Buffer.concat([data.subarray(0, 0x4000), packed, data.subarray(data.length - nitro)]);
160
152
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
161
153
  if (init) out.writeUInt32LE(out.length - postSize + header.ARM9ramAddress, init + 0x14);