nitro-patcher 0.2.1 → 1.0.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.
@@ -0,0 +1,201 @@
1
+ declare const UnitCode: Readonly<{
2
+ NDS: 0;
3
+ NDS_DSi: 2;
4
+ DSi: 3;
5
+ }>;
6
+ declare class Header {
7
+ raw: Buffer;
8
+ trimmedRom: boolean;
9
+ doublePadding: boolean;
10
+ nitrocode: boolean;
11
+ decrypted: boolean;
12
+ headerCRC: boolean;
13
+ logoCRC: boolean;
14
+ secureCRC: boolean;
15
+ dlp_signature: Buffer;
16
+ ARM9romOffset: number;
17
+ ARM9entryAddress: number;
18
+ ARM9ramAddress: number;
19
+ ARM9size: number;
20
+ ARM7romOffset: number;
21
+ ARM7entryAddress: number;
22
+ ARM7ramAddress: number;
23
+ ARM7size: number;
24
+ FNToffset: number;
25
+ FNTsize: number;
26
+ FAToffset: number;
27
+ FATsize: number;
28
+ ARM9overlayOffset: number;
29
+ ARM9overlaySize: number;
30
+ ARM7overlayOffset: number;
31
+ ARM7overlaySize: number;
32
+ flagsRead: number;
33
+ flagsInit: number;
34
+ bannerOffset: number;
35
+ dsi9_rom_offset: number;
36
+ offset_0x1C4: number;
37
+ dsi9_ram_address: number;
38
+ dsi9_size: number;
39
+ dsi7_rom_offset: number;
40
+ offset_0x1D4: number;
41
+ dsi7_ram_address: number;
42
+ dsi7_size: number;
43
+ digest_ntr_start: number;
44
+ digest_ntr_size: number;
45
+ digest_twl_start: number;
46
+ digest_twl_size: number;
47
+ sector_hashtable_start: number;
48
+ sector_hashtable_size: number;
49
+ block_hashtable_start: number;
50
+ block_hashtable_size: number;
51
+ digest_sector_size: number;
52
+ digest_block_sectorcount: number;
53
+ banner_size: number;
54
+ offset_0x20C: number;
55
+ total_rom_size: number;
56
+ offset_0x214: number;
57
+ offset_0x218: number;
58
+ offset_0x21C: number;
59
+ modcrypt1_start: number;
60
+ modcrypt1_size: number;
61
+ modcrypt2_start: number;
62
+ modcrypt2_size: number;
63
+ tid_low: number;
64
+ tid_high: number;
65
+ public_sav_size: number;
66
+ private_sav_size: number;
67
+ unitCode: number;
68
+ encryptionSeed: number;
69
+ twlInternalFlags: number;
70
+ permitsFlags: number;
71
+ ROMversion: number;
72
+ internalFlags: number;
73
+ secureCRC16: number;
74
+ ROMtimeout: number;
75
+ ARM9autoload: number;
76
+ ARM7autoload: number;
77
+ ROMsize: number;
78
+ ntr_region_end: number;
79
+ twl_region_start: number;
80
+ headerSize: number;
81
+ logoCRC16: number;
82
+ headerCRC16: number;
83
+ debug_romOffset: number;
84
+ debug_size: number;
85
+ debug_ramAddress: number;
86
+ reserved3: number;
87
+ mbk9_wramcnt_setting: number;
88
+ region_flags: number;
89
+ access_control: number;
90
+ scfg_ext_mask: number;
91
+ gameTitle: string;
92
+ gameCode: string;
93
+ makerCode: string;
94
+ reserved: Buffer;
95
+ reserved2: Buffer;
96
+ logo: Buffer;
97
+ appflags: Buffer;
98
+ hmac_arm9: Buffer;
99
+ hmac_arm7: Buffer;
100
+ hmac_digest_master: Buffer;
101
+ hmac_icon_title: Buffer;
102
+ hmac_arm9i: Buffer;
103
+ hmac_arm7i: Buffer;
104
+ hmac_arm9_no_secure: Buffer;
105
+ rsa_signature: Buffer;
106
+ secureDisable: bigint;
107
+ constructor(input: Uint8Array);
108
+ get size(): number;
109
+ set size(value: number);
110
+ toBuffer(): Buffer<ArrayBuffer>;
111
+ toString(): string;
112
+ }
113
+
114
+ interface Arm9Header {
115
+ nitrocode?: boolean;
116
+ reserved2: Buffer;
117
+ ARM9ramAddress: number;
118
+ ARM9size: number;
119
+ }
120
+ declare const decompressBLZ: (input: Buffer, maxSize?: number) => Buffer;
121
+ declare const compressBLZ: (input: Buffer) => Buffer;
122
+ declare const decompressArm9: (data: Buffer, header: Arm9Header, maxSize?: number) => {
123
+ compressed: boolean;
124
+ data: Buffer;
125
+ };
126
+ declare const compressArm9: (data: Buffer, header: Arm9Header, postSize?: number) => Buffer;
127
+
128
+ interface DsiCheck {
129
+ status: "valid" | "invalid" | "not-applicable";
130
+ reason?: string;
131
+ }
132
+ interface DsiVerification {
133
+ arm9: DsiCheck;
134
+ arm7: DsiCheck;
135
+ arm9i: DsiCheck;
136
+ arm7i: DsiCheck;
137
+ arm9NoSecure: DsiCheck;
138
+ banner: DsiCheck;
139
+ sectors: DsiCheck;
140
+ blocks: DsiCheck;
141
+ master: DsiCheck;
142
+ signature: DsiCheck;
143
+ }
144
+ type ProgramName = "arm9" | "arm7" | "arm9i" | "arm7i";
145
+ interface Program {
146
+ name: ProgramName;
147
+ start: number;
148
+ length: number;
149
+ data: Buffer;
150
+ original: Buffer;
151
+ replaced: boolean;
152
+ }
153
+ interface CryptRegion {
154
+ id: 1 | 2;
155
+ program: ProgramName;
156
+ offset: number;
157
+ length: number;
158
+ kind: "whole" | "secure" | "fixed";
159
+ }
160
+ interface DsiSection {
161
+ label: string;
162
+ start: number;
163
+ originalSize: number;
164
+ data: Buffer;
165
+ place: (start: number) => void;
166
+ }
167
+ /** Complete cartridge processing; legacy TWL serialization stays independent. */
168
+ declare class CompleteTWL {
169
+ readonly header: Header;
170
+ readonly programs: Map<ProgramName, Program>;
171
+ readonly regions: CryptRegion[];
172
+ readonly hasDigests: boolean;
173
+ readonly Hashtable1Data: Buffer;
174
+ readonly Hashtable2Data: Buffer;
175
+ readonly Header2Data: Buffer[];
176
+ readonly Overlays9Sha1Hmac: Buffer[];
177
+ readonly twlEncrypted: boolean;
178
+ readonly firstDSiHashOffset: number;
179
+ readonly dsiHashSize: number;
180
+ private readonly original;
181
+ private readonly limit;
182
+ constructor(header: Header, overlays: readonly Buffer[], original: Buffer, limit: number);
183
+ private validateDigests;
184
+ private decryptPrograms;
185
+ get DSi9Data(): Buffer;
186
+ set DSi9Data(data: Buffer);
187
+ get DSi7Data(): Buffer;
188
+ set DSi7Data(data: Buffer);
189
+ getProgram(name: ProgramName): Buffer;
190
+ setProgram(name: ProgramName, data: Buffer): void;
191
+ importArm9iData(data: Buffer | string, offset?: number, size?: number): void;
192
+ importArm7iData(data: Buffer | string, offset?: number, size?: number): void;
193
+ unchanged(): boolean;
194
+ updateOverlays9Sha1Hmac(arm9: Buffer, h: Arm9Header, overlays: readonly Buffer[]): Buffer;
195
+ private view;
196
+ verify(data?: Buffer<ArrayBufferLike>, h?: Header): DsiVerification;
197
+ serialize(h: Header, sections: DsiSection[], forceLayout: boolean): Buffer;
198
+ private checkMovableTwl;
199
+ }
200
+
201
+ export { type Arm9Header as A, CompleteTWL as C, type DsiCheck as D, Header as H, UnitCode as U, type DsiVerification as a, compressBLZ as b, compressArm9 as c, decompressArm9 as d, decompressBLZ as e };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-patcher",
3
- "version": "0.2.1",
3
+ "version": "1.0.0",
4
4
  "description": "Native Node.js Nintendo DS ROM reader, repacker and ZIP/Xdelta patcher",
5
5
  "type": "module",
6
6
  "engines": {
@@ -49,6 +49,7 @@
49
49
  "@noble/ciphers": "2.4.0",
50
50
  "@noble/hashes": "2.4.0",
51
51
  "@types/node": "22.20.2",
52
+ "@types/node-forge": "1.3.14",
52
53
  "buffer": "6.0.3",
53
54
  "esbuild": "0.28.1",
54
55
  "eslint": "10.9.1",
@@ -59,7 +60,9 @@
59
60
  "fflate": "0.8.3",
60
61
  "globals": "16.5.0",
61
62
  "jiti": "2.7.0",
63
+ "playwright-core": "1.64.1",
62
64
  "prettier": "3.9.6",
65
+ "string_decoder": "1.3.0",
63
66
  "tsup": "8.5.1",
64
67
  "tsx": "4.23.13",
65
68
  "type-fest": "4.41.0",
@@ -73,7 +76,9 @@
73
76
  "#browser-aes": "@noble/ciphers/aes.js"
74
77
  },
75
78
  "dependencies": {
76
- "markdansi": "0.3.3"
79
+ "iconv-lite": "0.7.3",
80
+ "markdansi": "0.3.3",
81
+ "node-forge": "1.4.0"
77
82
  },
78
83
  "scripts": {
79
84
  "clean": "tsx scripts/clean.ts",
@@ -81,6 +86,7 @@
81
86
  "typecheck": "tsc -p tsconfig.test.json",
82
87
  "test": "tsx --test test/*.test.ts",
83
88
  "test:reference": "tsx scripts/verify-reference.ts",
89
+ "test:dsi": "tsx scripts/verify-dsi.ts",
84
90
  "check": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm test && pnpm run build && pnpm run test:browser",
85
91
  "test:package": "tsx scripts/verify-package.ts",
86
92
  "lint": "eslint . --max-warnings 0",
@@ -1 +1,2 @@
1
1
  export { Buffer } from "buffer";
2
+ export const window = globalThis;
package/src/browser.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./nitro-patch-helper";
9
9
 
10
10
  export interface PatchMetadata {
11
+ formatVersion?: number;
11
12
  id?: string;
12
13
  author?: string;
13
14
  name?: string;
@@ -30,11 +31,28 @@ export interface BrowserPatchOptions {
30
31
  maxUncompressedSize?: number;
31
32
  maxEntries?: number;
32
33
  }
34
+ export interface DsiCheck {
35
+ status: "valid" | "invalid" | "not-applicable";
36
+ reason?: string;
37
+ }
38
+ export interface DsiVerification {
39
+ arm9: DsiCheck;
40
+ arm7: DsiCheck;
41
+ arm9i: DsiCheck;
42
+ arm7i: DsiCheck;
43
+ arm9NoSecure: DsiCheck;
44
+ banner: DsiCheck;
45
+ sectors: DsiCheck;
46
+ blocks: DsiCheck;
47
+ master: DsiCheck;
48
+ signature: DsiCheck;
49
+ }
33
50
  export interface BrowserPatchResult {
34
51
  buffer: Uint8Array<ArrayBuffer>;
35
52
  returnValue: "SUCCESS" | "MD5_MISMATCH";
36
53
  inputMd5: string;
37
54
  outputMd5: string;
55
+ dsi?: DsiVerification;
38
56
  }
39
57
  export interface RomInfo {
40
58
  gameTitle: string;
@@ -42,10 +60,11 @@ export interface RomInfo {
42
60
  size: number;
43
61
  fileCount: number;
44
62
  checksums: { header: boolean; logo: boolean; secureArea: boolean; banner: boolean };
63
+ dsi?: DsiVerification;
45
64
  }
46
65
  /** Parse ROM metadata entirely in memory. No filesystem or Node types required. */
47
- export const inspectRom = (bytes: Uint8Array): RomInfo => {
48
- const rom = new NDSFile(bytes);
66
+ export const inspectRom = (bytes: Uint8Array, options: { dsiMode?: "legacy" | "complete" } = {}): RomInfo => {
67
+ const rom = new NDSFile(bytes, options);
49
68
  return {
50
69
  gameTitle: rom.header.gameTitle,
51
70
  gameCode: rom.header.gameCode,
@@ -57,6 +76,7 @@ export const inspectRom = (bytes: Uint8Array): RomInfo => {
57
76
  secureArea: rom.header.secureCRC,
58
77
  banner: rom.banner.bannerCRC,
59
78
  },
79
+ ...(options.dsiMode === "complete" && { dsi: rom.verifyDsi() }),
60
80
  };
61
81
  };
62
82
  /** Synchronous CPU work; call in a Web Worker for responsive browser UIs. */
@@ -77,6 +97,7 @@ export const patchBuffer = (
77
97
  returnValue: result.returnValue,
78
98
  inputMd5: result.inputMd5,
79
99
  outputMd5: result.outputMd5,
100
+ ...(result.dsi && { dsi: result.dsi }),
80
101
  };
81
102
  };
82
103
  /** Read and validate metadata.json from a patch package entirely in memory. */
package/src/cli.ts CHANGED
@@ -1,25 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import { render } from "markdansi";
3
- import { mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join, resolve } from "node:path";
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
5
  import { parseArgs } from "node:util";
6
6
 
7
+ import { NDSFile } from "./nitro/nds-file";
7
8
  import { type PatchMetadata, patchBuffer, readPatchInfo } from "./nitro-patch-helper";
8
9
 
9
- const help = `nitro-patcher — 原生 Node.js NDS ROM 补丁工具
10
+ const help = `nitro-patcher — NDS ROM 补丁工具
10
11
 
11
12
  用法:
12
13
  nitro-patcher [--json] [--dry-run] <原始ROM> <补丁ZIP> [输出ROM]
13
- 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>
14
17
 
15
- --json 输出 JSON 状态和 MD5
18
+ --json 结果输出为 JSON 格式
16
19
  --dry-run 完整执行补丁流程,但不写入输出 ROM
17
- -i, --input 原始 ROM 路径
20
+ --dsi-mode extract/inspect 的 DSi 处理模式:legacy 或 complete
21
+ -i, --input 输入 ROM 路径
18
22
  -p, --patch 补丁 ZIP 路径
19
- -o, --output 输出 ROM 路径;试运行时可省略
23
+ -o, --output 输出 ROM 或提取目录;试运行时可省略
20
24
  -h, --help 显示帮助
21
25
  -v, --version 显示版本`;
22
26
 
27
+ interface CommandOptions {
28
+ json: boolean;
29
+ dryRun: boolean;
30
+ input: string | undefined;
31
+ patch: string | undefined;
32
+ output: string | undefined;
33
+ dsiMode: "legacy" | "complete" | undefined;
34
+ }
35
+
23
36
  const sameFile = async (input: string, output: string): Promise<boolean> => {
24
37
  if (resolve(input) === resolve(output)) return true;
25
38
  const original = await stat(input);
@@ -65,6 +78,70 @@ const formatMetadata = (metadata: PatchMetadata): string => {
65
78
  return lines.join("\n");
66
79
  };
67
80
 
81
+ const extractRom = async (options: CommandOptions): Promise<void> => {
82
+ if (!options.input || !options.output || options.patch !== undefined || options.dryRun) throw new Error(help);
83
+ const destination = join(await realpath(dirname(resolve(options.output))), basename(resolve(options.output)));
84
+ const rom = new NDSFile(await readFile(options.input), { dsiMode: options.dsiMode ?? "legacy" });
85
+ const files = rom.listFiles();
86
+ await mkdir(destination, { recursive: true });
87
+ for (const { path } of files) {
88
+ const output = resolve(destination, path);
89
+ if (!output.startsWith(`${destination}${sep}`)) throw new Error(`ROM 文件路径超出输出目录:${path}`);
90
+ await mkdir(dirname(output), { recursive: true });
91
+ await writeFile(output, rom.getFile(path));
92
+ }
93
+
94
+ if (options.json) {
95
+ console.log(
96
+ JSON.stringify({
97
+ output: destination,
98
+ fileCount: files.length,
99
+ files: files.map(({ path }) => path),
100
+ }),
101
+ );
102
+ } else {
103
+ console.log(`已提取 ${files.length} 个文件到:${destination}`);
104
+ }
105
+ };
106
+
107
+ const inspectRom = async (options: CommandOptions): Promise<void> => {
108
+ if (!options.input || options.patch !== undefined || options.output !== undefined || options.dryRun)
109
+ throw new Error(help);
110
+ const input = await readFile(options.input);
111
+ const rom = new NDSFile(input, { dsiMode: options.dsiMode ?? "legacy" });
112
+ const info = {
113
+ gameTitle: rom.header.gameTitle.replace(/\0+$/, ""),
114
+ gameCode: rom.header.gameCode,
115
+ size: input.length,
116
+ fileCount: rom.listFiles().length,
117
+ checksums: {
118
+ header: rom.header.headerCRC,
119
+ logo: rom.header.logoCRC,
120
+ secureArea: rom.header.secureCRC,
121
+ banner: rom.banner.bannerCRC,
122
+ },
123
+ ...(options.dsiMode === "complete" && { dsi: rom.verifyDsi() }),
124
+ };
125
+ if (options.json) {
126
+ console.log(JSON.stringify(info));
127
+ return;
128
+ }
129
+ console.log(`ROM 信息:
130
+
131
+ 标题:${safeTerminalText(info.gameTitle).replaceAll("\n", " ")}
132
+ 游戏代码:${safeTerminalText(info.gameCode).replaceAll("\n", " ")}
133
+ 大小:${info.size} 字节
134
+ 文件数量:${info.fileCount}
135
+ 校验:
136
+ - Header:${info.checksums.header ? "通过" : "失败"}
137
+ - Logo:${info.checksums.logo ? "通过" : "失败"}
138
+ - Secure Area:${info.checksums.secureArea ? "通过" : "失败"}
139
+ - Banner:${info.checksums.banner ? "通过" : "失败"}`);
140
+ if (info.dsi)
141
+ for (const [name, result] of Object.entries(info.dsi))
142
+ console.log(`- DSi ${name}:${result.status}${result.reason ? ` (${result.reason})` : ""}`);
143
+ };
144
+
68
145
  const main = async (): Promise<void> => {
69
146
  const { values, positionals } = parseArgs({
70
147
  allowPositionals: true,
@@ -73,6 +150,7 @@ const main = async (): Promise<void> => {
73
150
  version: { type: "boolean", short: "v" },
74
151
  json: { type: "boolean" },
75
152
  "dry-run": { type: "boolean" },
153
+ "dsi-mode": { type: "string" },
76
154
  input: { type: "string", short: "i" },
77
155
  patch: { type: "string", short: "p" },
78
156
  output: { type: "string", short: "o" },
@@ -89,16 +167,37 @@ const main = async (): Promise<void> => {
89
167
  console.log(metadata.version);
90
168
  return;
91
169
  }
92
- const dryRun = values["dry-run"] ?? false;
93
- const usesNamedPaths = values.input !== undefined || values.patch !== undefined || values.output !== undefined;
94
- if (usesNamedPaths && positionals.length > 0) throw new Error(help);
95
- if (
96
- (usesNamedPaths && (!values.input || !values.patch || (!dryRun && !values.output))) ||
97
- (!usesNamedPaths &&
98
- ((!dryRun && positionals.length !== 3) || (dryRun && (positionals.length < 2 || positionals.length > 3))))
99
- )
170
+ const dsiMode = values["dsi-mode"];
171
+ if (dsiMode !== undefined && dsiMode !== "legacy" && dsiMode !== "complete")
172
+ throw new Error("Invalid --dsi-mode; expected legacy or complete");
173
+ const options: CommandOptions = {
174
+ json: values.json ?? false,
175
+ dryRun: values["dry-run"] ?? false,
176
+ input: values.input,
177
+ patch: values.patch,
178
+ output: values.output,
179
+ dsiMode,
180
+ };
181
+ const command = positionals.length === 1 ? positionals[0] : undefined;
182
+ if (positionals.length !== 0 && positionals.length !== 1 && positionals.length !== 3) throw new Error(help);
183
+ if (command !== undefined && command !== "patch" && command !== "extract" && command !== "inspect")
184
+ throw new Error(help);
185
+ if (command === "extract") {
186
+ await extractRom(options);
187
+ return;
188
+ }
189
+ if (command === "inspect") {
190
+ await inspectRom(options);
191
+ return;
192
+ }
193
+
194
+ const dryRun = options.dryRun;
195
+ if (options.dsiMode !== undefined) throw new Error("patch selects DSi processing from metadata.json formatVersion");
196
+ const usesLegacyPaths = positionals.length === 3;
197
+ if (usesLegacyPaths && (values.input !== undefined || values.patch !== undefined || values.output !== undefined))
100
198
  throw new Error(help);
101
- const [original, patch, output] = usesNamedPaths ? [values.input, values.patch, values.output] : positionals;
199
+ if (!usesLegacyPaths && (!values.input || !values.patch || (!dryRun && !values.output))) throw new Error(help);
200
+ const [original, patch, output] = usesLegacyPaths ? positionals : [values.input, values.patch, values.output];
102
201
  if (!original || !patch) throw new Error(help);
103
202
  if (!dryRun) {
104
203
  if (!output) throw new Error(help);
@@ -121,7 +220,7 @@ const main = async (): Promise<void> => {
121
220
  }
122
221
  }
123
222
  const { returnValue, inputMd5, outputMd5 } = result;
124
- if (values.json) {
223
+ if (options.json) {
125
224
  console.log(
126
225
  JSON.stringify({
127
226
  returnValue,
@@ -129,6 +228,7 @@ const main = async (): Promise<void> => {
129
228
  outputMd5,
130
229
  ...info,
131
230
  ...(dryRun && { dryRun: true }),
231
+ ...(result.dsi && { dsi: result.dsi }),
132
232
  }),
133
233
  );
134
234
  return;
@@ -137,7 +237,7 @@ const main = async (): Promise<void> => {
137
237
  const sections: string[] = [];
138
238
  if (info.metadata) {
139
239
  const metadata = formatMetadata(info.metadata);
140
- if (metadata) sections.push(`补丁元数据:\n\n${metadata}`);
240
+ if (metadata) sections.push(`补丁信息:\n\n${metadata}`);
141
241
  }
142
242
  if (info.readme) {
143
243
  const content = safeTerminalText(info.readme.content);
package/src/nitro/blz.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export interface Arm9Header {
2
+ nitrocode?: boolean;
2
3
  reserved2: Buffer;
3
4
  ARM9ramAddress: number;
4
5
  ARM9size: number;
@@ -130,22 +131,30 @@ export const compressBLZ = (input: Buffer): Buffer => {
130
131
  return out;
131
132
  };
132
133
 
133
- export const decompressArm9 = (data: Buffer, header: Arm9Header): { compressed: boolean; data: Buffer } => {
134
+ export const decompressArm9 = (
135
+ data: Buffer,
136
+ header: Arm9Header,
137
+ maxSize = 0x10000000,
138
+ ): { compressed: boolean; data: Buffer } => {
134
139
  if (data.length < 0x18) throw new Error("ARM9 data too short");
135
- const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
140
+ if (data.length > maxSize) throw new RangeError("ARM9 exceeds output size limit");
141
+ const nitro = (header.nitrocode ?? data.readUInt32LE(0xc) === 0xdec00621) ? 12 : 0;
136
142
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
137
143
  const ptr = init ? data.readUInt32LE(init + 0x14) : header.ARM9ramAddress + header.ARM9size;
138
144
  const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
139
145
  return {
140
146
  compressed,
141
147
  data: compressed
142
- ? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)])
148
+ ? Buffer.concat([
149
+ decompressBLZ(data.subarray(0, data.length - nitro), maxSize - nitro),
150
+ data.subarray(data.length - nitro),
151
+ ])
143
152
  : Buffer.from(data),
144
153
  };
145
154
  };
146
155
  export const compressArm9 = (data: Buffer, header: Arm9Header, postSize = 0): Buffer => {
147
156
  if (data.length < 0x4000) throw new Error("ARM9 secure area is truncated");
148
- const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
157
+ const nitro = (header.nitrocode ?? data.readUInt32LE(0xc) === 0xdec00621) ? 12 : 0;
149
158
  const packed = compressBLZ(data.subarray(0x4000, data.length - nitro));
150
159
  if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
151
160
  const out = Buffer.concat([data.subarray(0, 0x4000), packed, data.subarray(data.length - nitro)]);
@@ -1,5 +1,7 @@
1
1
  import { createAesBlockCipher } from "#nitro-runtime";
2
2
 
3
+ import { crc16 } from "./crc";
4
+
3
5
  export interface ModcryptHeader {
4
6
  gameCode: string;
5
7
  gameTitle: string;
@@ -117,32 +119,37 @@ export const modcryptKey = (header: ModcryptHeader): Buffer => {
117
119
  return out;
118
120
  };
119
121
 
120
- export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
122
+ export const encryptSecureArea = (gameCode: string, input: Buffer, refreshCrc = false): Buffer =>
123
+ secureAreaCrypt(gameCode, input, false, refreshCrc);
124
+ export const decryptSecureArea = (gameCode: string, input: Buffer): Buffer => secureAreaCrypt(gameCode, input, true);
125
+
126
+ const secureAreaCrypt = (gameCode: string, input: Buffer, decrypting: boolean, refreshCrc = false): Buffer => {
121
127
  if (typeof gameCode !== "string" || gameCode.length !== 4) throw new Error("Expected four-character game code");
122
128
  if (input.length < 0x800) throw new RangeError("Secure area must contain at least 0x800 bytes");
123
129
  const output = Buffer.from(input);
130
+ if (refreshCrc) output.writeUInt16LE(crc16(output.subarray(0x10, 0x800)), 0xe);
124
131
  let magic = new Uint32Array(0x412);
125
132
  let args: [number, number, number] = [0, 0, 0];
126
- function encrypt(a: number, b: number): [number, number] {
133
+ function crypt(a: number, b: number, reverse = false): [number, number] {
127
134
  for (let i = 0; i < 16; i++) {
128
- const c = (magic[i] ^ a) >>> 0;
135
+ const c = (magic[reverse ? 17 - i : i] ^ a) >>> 0;
129
136
  const sum = (magic[18 + (c >>> 24)] + magic[274 + ((c >>> 16) & 255)]) >>> 0;
130
137
  const f = (magic[786 + (c & 255)] + (magic[530 + ((c >>> 8) & 255)] ^ sum)) >>> 0;
131
138
  a = (b ^ f) >>> 0;
132
139
  b = c;
133
140
  }
134
- return [(b ^ magic[17]) >>> 0, (a ^ magic[16]) >>> 0];
141
+ return [(b ^ magic[reverse ? 0 : 17]) >>> 0, (a ^ magic[reverse ? 1 : 16]) >>> 0];
135
142
  }
136
143
  function init2() {
137
- [args[2], args[1]] = encrypt(args[2], args[1]);
138
- [args[1], args[0]] = encrypt(args[1], args[0]);
144
+ [args[2], args[1]] = crypt(args[2], args[1]);
145
+ [args[1], args[0]] = crypt(args[1], args[0]);
139
146
  const key = Buffer.alloc(12);
140
147
  args.forEach((v, i) => key.writeUInt32LE(v, i * 4));
141
148
  for (let j = 0; j < 18; j++) magic[j] ^= key.readUInt32BE((j * 4) & 7);
142
149
  let a = 0,
143
150
  b = 0;
144
151
  for (let i = 0; i < 0x412; i += 2) {
145
- [a, b] = encrypt(a, b);
152
+ [a, b] = crypt(a, b);
146
153
  magic[i] = a;
147
154
  magic[i + 1] = b;
148
155
  }
@@ -155,17 +162,28 @@ export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
155
162
  init2();
156
163
  }
157
164
  init1();
165
+ if (decrypting) {
166
+ const [b, a] = crypt(output.readUInt32LE(4), output.readUInt32LE(0), true);
167
+ output.writeUInt32LE(a, 0);
168
+ output.writeUInt32LE(b, 4);
169
+ }
158
170
  args[1] = (args[1] << 1) >>> 0;
159
171
  args[2] >>>= 1;
160
172
  init2();
161
- for (let p = 8; p < 0x800; p += 8) {
162
- const [b, a] = encrypt(output.readUInt32LE(p + 4), output.readUInt32LE(p));
173
+ for (let p = decrypting ? 0 : 8; p < 0x800; p += 8) {
174
+ const [b, a] = crypt(output.readUInt32LE(p + 4), output.readUInt32LE(p), decrypting);
163
175
  output.writeUInt32LE(a, p);
164
176
  output.writeUInt32LE(b, p + 4);
165
177
  }
166
- let [b, a] = encrypt(0x6a624f79, 0x72636e65);
178
+ if (decrypting) {
179
+ if (output.readUInt32LE(0) !== 0x72636e65 || output.readUInt32LE(4) !== 0x6a624f79)
180
+ throw new Error("Invalid KEY1 secure-area marker");
181
+ output.writeBigUInt64LE(0xe7ffdeffe7ffdeffn);
182
+ return output;
183
+ }
184
+ let [b, a] = crypt(0x6a624f79, 0x72636e65);
167
185
  init1();
168
- [b, a] = encrypt(b, a);
186
+ [b, a] = crypt(b, a);
169
187
  output.writeUInt32LE(a, 0);
170
188
  output.writeUInt32LE(b, 4);
171
189
  return output;