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/dist/cli.js CHANGED
@@ -2,25 +2,29 @@
2
2
  import {
3
3
  patchBuffer,
4
4
  readPatchInfo
5
- } from "./chunk-ZY3OSZCA.js";
6
- import "./chunk-GFF67HX4.js";
5
+ } from "./chunk-IGURDH5L.js";
6
+ import {
7
+ NDSFile
8
+ } from "./chunk-GLK3K2HY.js";
7
9
 
8
10
  // src/cli.ts
9
- import { mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "fs/promises";
10
- import { basename, dirname, join, resolve } from "path";
11
- import { parseArgs } from "util";
12
11
  import { render } from "markdansi";
13
- var help = `nitro-patcher \u2014 \u539F\u751F Node.js NDS ROM \u8865\u4E01\u5DE5\u5177
12
+ import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "fs/promises";
13
+ import { basename, dirname, join, resolve, sep } from "path";
14
+ import { parseArgs } from "util";
15
+ var help = `nitro-patcher \u2014 NDS ROM \u8865\u4E01\u5DE5\u5177
14
16
 
15
17
  \u7528\u6CD5\uFF1A
16
18
  nitro-patcher [--json] [--dry-run] <\u539F\u59CBROM> <\u8865\u4E01ZIP> [\u8F93\u51FAROM]
17
- nitro-patcher [--json] [--dry-run] --input <\u539F\u59CBROM> --patch <\u8865\u4E01ZIP> [--output <\u8F93\u51FAROM>]
19
+ nitro-patcher patch [--json] [--dry-run] --input <\u539F\u59CBROM> --patch <\u8865\u4E01ZIP> [--output <\u8F93\u51FAROM>]
20
+ nitro-patcher extract [--json] --input <ROM> --output <\u76EE\u5F55>
21
+ nitro-patcher inspect [--json] --input <ROM>
18
22
 
19
- --json \u8F93\u51FA JSON \u72B6\u6001\u548C MD5
23
+ --json \u7ED3\u679C\u8F93\u51FA\u4E3A JSON \u683C\u5F0F
20
24
  --dry-run \u5B8C\u6574\u6267\u884C\u8865\u4E01\u6D41\u7A0B\uFF0C\u4F46\u4E0D\u5199\u5165\u8F93\u51FA ROM
21
- -i, --input \u539F\u59CB ROM \u8DEF\u5F84
25
+ -i, --input \u8F93\u5165 ROM \u8DEF\u5F84
22
26
  -p, --patch \u8865\u4E01 ZIP \u8DEF\u5F84
23
- -o, --output \u8F93\u51FA ROM \u8DEF\u5F84\uFF1B\u8BD5\u8FD0\u884C\u65F6\u53EF\u7701\u7565
27
+ -o, --output \u8F93\u51FA ROM \u6216\u63D0\u53D6\u76EE\u5F55\uFF1B\u8BD5\u8FD0\u884C\u65F6\u53EF\u7701\u7565
24
28
  -h, --help \u663E\u793A\u5E2E\u52A9
25
29
  -v, --version \u663E\u793A\u7248\u672C`;
26
30
  var sameFile = async (input, output) => {
@@ -60,6 +64,63 @@ var formatMetadata = (metadata) => {
60
64
  }
61
65
  return lines.join("\n");
62
66
  };
67
+ var extractRom = async (options) => {
68
+ if (!options.input || !options.output || options.patch !== void 0 || options.dryRun) throw new Error(help);
69
+ const destination = join(await realpath(dirname(resolve(options.output))), basename(resolve(options.output)));
70
+ const rom = new NDSFile(await readFile(options.input));
71
+ const files = rom.listFiles();
72
+ await mkdir(destination, { recursive: true });
73
+ for (const { path } of files) {
74
+ const output = resolve(destination, path);
75
+ if (!output.startsWith(`${destination}${sep}`)) throw new Error(`ROM \u6587\u4EF6\u8DEF\u5F84\u8D85\u51FA\u8F93\u51FA\u76EE\u5F55\uFF1A${path}`);
76
+ await mkdir(dirname(output), { recursive: true });
77
+ await writeFile(output, rom.getFile(path));
78
+ }
79
+ if (options.json) {
80
+ console.log(
81
+ JSON.stringify({
82
+ output: destination,
83
+ fileCount: files.length,
84
+ files: files.map(({ path }) => path)
85
+ })
86
+ );
87
+ } else {
88
+ console.log(`\u5DF2\u63D0\u53D6 ${files.length} \u4E2A\u6587\u4EF6\u5230\uFF1A${destination}`);
89
+ }
90
+ };
91
+ var inspectRom = async (options) => {
92
+ if (!options.input || options.patch !== void 0 || options.output !== void 0 || options.dryRun)
93
+ throw new Error(help);
94
+ const input = await readFile(options.input);
95
+ const rom = new NDSFile(input);
96
+ const info = {
97
+ gameTitle: rom.header.gameTitle.replace(/\0+$/, ""),
98
+ gameCode: rom.header.gameCode,
99
+ size: input.length,
100
+ fileCount: rom.listFiles().length,
101
+ checksums: {
102
+ header: rom.header.headerCRC,
103
+ logo: rom.header.logoCRC,
104
+ secureArea: rom.header.secureCRC,
105
+ banner: rom.banner.bannerCRC
106
+ }
107
+ };
108
+ if (options.json) {
109
+ console.log(JSON.stringify(info));
110
+ return;
111
+ }
112
+ console.log(`ROM \u4FE1\u606F\uFF1A
113
+
114
+ \u6807\u9898\uFF1A${safeTerminalText(info.gameTitle).replaceAll("\n", " ")}
115
+ \u6E38\u620F\u4EE3\u7801\uFF1A${safeTerminalText(info.gameCode).replaceAll("\n", " ")}
116
+ \u5927\u5C0F\uFF1A${info.size} \u5B57\u8282
117
+ \u6587\u4EF6\u6570\u91CF\uFF1A${info.fileCount}
118
+ \u6821\u9A8C\uFF1A
119
+ - Header\uFF1A${info.checksums.header ? "\u901A\u8FC7" : "\u5931\u8D25"}
120
+ - Logo\uFF1A${info.checksums.logo ? "\u901A\u8FC7" : "\u5931\u8D25"}
121
+ - Secure Area\uFF1A${info.checksums.secureArea ? "\u901A\u8FC7" : "\u5931\u8D25"}
122
+ - Banner\uFF1A${info.checksums.banner ? "\u901A\u8FC7" : "\u5931\u8D25"}`);
123
+ };
63
124
  var main = async () => {
64
125
  const { values, positionals } = parseArgs({
65
126
  allowPositionals: true,
@@ -78,20 +139,37 @@ var main = async () => {
78
139
  return;
79
140
  }
80
141
  if (values.version) {
81
- const metadata = JSON.parse(
82
- await readFile(new URL("../package.json", import.meta.url), "utf8")
83
- );
142
+ const metadata = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
84
143
  if (!metadata || typeof metadata !== "object" || !("version" in metadata) || typeof metadata.version !== "string")
85
144
  throw new Error("Invalid package version metadata");
86
145
  console.log(metadata.version);
87
146
  return;
88
147
  }
89
- const dryRun = values["dry-run"] ?? false;
90
- const usesNamedPaths = values.input !== void 0 || values.patch !== void 0 || values.output !== void 0;
91
- if (usesNamedPaths && positionals.length > 0) throw new Error(help);
92
- if (usesNamedPaths && (!values.input || !values.patch || !dryRun && !values.output) || !usesNamedPaths && (!dryRun && positionals.length !== 3 || dryRun && (positionals.length < 2 || positionals.length > 3)))
148
+ const options = {
149
+ json: values.json ?? false,
150
+ dryRun: values["dry-run"] ?? false,
151
+ input: values.input,
152
+ patch: values.patch,
153
+ output: values.output
154
+ };
155
+ const command = positionals.length === 1 ? positionals[0] : void 0;
156
+ if (positionals.length !== 0 && positionals.length !== 1 && positionals.length !== 3) throw new Error(help);
157
+ if (command !== void 0 && command !== "patch" && command !== "extract" && command !== "inspect")
158
+ throw new Error(help);
159
+ if (command === "extract") {
160
+ await extractRom(options);
161
+ return;
162
+ }
163
+ if (command === "inspect") {
164
+ await inspectRom(options);
165
+ return;
166
+ }
167
+ const dryRun = options.dryRun;
168
+ const usesLegacyPaths = positionals.length === 3;
169
+ if (usesLegacyPaths && (values.input !== void 0 || values.patch !== void 0 || values.output !== void 0))
93
170
  throw new Error(help);
94
- const [original, patch, output] = usesNamedPaths ? [values.input, values.patch, values.output] : positionals;
171
+ if (!usesLegacyPaths && (!values.input || !values.patch || !dryRun && !values.output)) throw new Error(help);
172
+ const [original, patch, output] = usesLegacyPaths ? positionals : [values.input, values.patch, values.output];
95
173
  if (!original || !patch) throw new Error(help);
96
174
  if (!dryRun) {
97
175
  if (!output) throw new Error(help);
@@ -114,7 +192,7 @@ var main = async () => {
114
192
  }
115
193
  }
116
194
  const { returnValue, inputMd5, outputMd5 } = result;
117
- if (values.json) {
195
+ if (options.json) {
118
196
  console.log(
119
197
  JSON.stringify({
120
198
  returnValue,
@@ -129,7 +207,7 @@ var main = async () => {
129
207
  const sections = [];
130
208
  if (info.metadata) {
131
209
  const metadata = formatMetadata(info.metadata);
132
- if (metadata) sections.push(`\u8865\u4E01\u5143\u6570\u636E\uFF1A
210
+ if (metadata) sections.push(`\u8865\u4E01\u4FE1\u606F\uFF1A
133
211
 
134
212
  ${metadata}`);
135
213
  }
@@ -142,7 +220,7 @@ ${metadata}`);
142
220
  }) : content;
143
221
  sections.push(`\u8865\u4E01\u8BF4\u660E\uFF1A
144
222
 
145
- ${rendered.trimEnd()}`);
223
+ ${rendered.trim()}`);
146
224
  }
147
225
  if (dryRun) sections.push("\u8BD5\u8FD0\u884C\u5B8C\u6210\uFF0C\u672A\u5199\u5165\u8F93\u51FA ROM\u3002");
148
226
  sections.push(`${returnValue}
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { parseArgs } from 'node:util';\nimport { render } from 'markdansi';\nimport { patchBuffer, readPatchInfo, type PatchMetadata } from './nitro-patch-helper';\n\nconst help = `nitro-patcher — 原生 Node.js NDS ROM 补丁工具\n\n用法:\n nitro-patcher [--json] [--dry-run] <原始ROM> <补丁ZIP> [输出ROM]\n nitro-patcher [--json] [--dry-run] --input <原始ROM> --patch <补丁ZIP> [--output <输出ROM>]\n\n --json 输出 JSON 状态和 MD5\n --dry-run 完整执行补丁流程,但不写入输出 ROM\n -i, --input 原始 ROM 路径\n -p, --patch 补丁 ZIP 路径\n -o, --output 输出 ROM 路径;试运行时可省略\n -h, --help 显示帮助\n -v, --version 显示版本`;\n\nconst sameFile = async (input: string, output: string): Promise<boolean> => {\n if (resolve(input) === resolve(output)) return true;\n const original = await stat(input);\n try {\n const destination = await stat(output);\n return original.dev === destination.dev && original.ino === destination.ino;\n } catch (error) {\n if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;\n return false;\n }\n};\n\nconst safeTerminalText = (value: string): string => {\n const characters: string[] = [];\n for (const character of value.replaceAll('\\r\\n', '\\n').replaceAll('\\r', '\\n')) {\n const code = character.codePointAt(0)!;\n if (\n (code < 0x20 && code !== 0x09 && code !== 0x0a) ||\n (code >= 0x7f && code <= 0x9f) ||\n (code >= 0x202a && code <= 0x202e) ||\n (code >= 0x2066 && code <= 0x2069)\n )\n continue;\n characters.push(character);\n }\n return characters.join('');\n};\n\nconst formatMetadata = (metadata: PatchMetadata): string => {\n const labels = {\n author: '作者',\n name: '名称',\n homepage: '主页',\n version: '版本',\n } as const;\n const lines: string[] = [];\n for (const field of ['author', 'name', 'homepage', 'version'] as const) {\n const value = metadata[field];\n if (value === undefined) continue;\n const suffix = field === 'version' && metadata.isBeta ? '(测试版)' : '';\n lines.push(`- ${labels[field]}:${safeTerminalText(value).replaceAll('\\n', ' ')}${suffix}`);\n }\n return lines.join('\\n');\n};\n\nconst main = async (): Promise<void> => {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n json: { type: 'boolean' },\n 'dry-run': { type: 'boolean' },\n input: { type: 'string', short: 'i' },\n patch: { type: 'string', short: 'p' },\n output: { type: 'string', short: 'o' },\n },\n });\n if (values.help) {\n process.stdout.write(help);\n return;\n }\n if (values.version) {\n const metadata: unknown = JSON.parse(\n await readFile(new URL('../package.json', import.meta.url), 'utf8'),\n );\n if (\n !metadata ||\n typeof metadata !== 'object' ||\n !('version' in metadata) ||\n typeof metadata.version !== 'string'\n )\n throw new Error('Invalid package version metadata');\n console.log(metadata.version);\n return;\n }\n const dryRun = values['dry-run'] ?? false;\n const usesNamedPaths =\n values.input !== undefined || values.patch !== undefined || values.output !== undefined;\n if (usesNamedPaths && positionals.length > 0) throw new Error(help);\n if (\n (usesNamedPaths && (!values.input || !values.patch || (!dryRun && !values.output))) ||\n (!usesNamedPaths &&\n ((!dryRun && positionals.length !== 3) ||\n (dryRun && (positionals.length < 2 || positionals.length > 3))))\n )\n throw new Error(help);\n const [original, patch, output] = usesNamedPaths\n ? [values.input, values.patch, values.output]\n : positionals;\n if (!original || !patch) throw new Error(help);\n if (!dryRun) {\n if (!output) throw new Error(help);\n if ((await sameFile(original, output)) || (await sameFile(patch, output)))\n throw new Error('输出路径不能覆盖原始 ROM 或补丁包。');\n }\n const [originalBytes, patchBytes] = await Promise.all([readFile(original), readFile(patch)]);\n const info = readPatchInfo(patchBytes);\n const result = patchBuffer(originalBytes, patchBytes);\n if (!dryRun) {\n if (!output) throw new Error(help);\n const destination = join(await realpath(dirname(resolve(output))), basename(output));\n const temporary = await mkdtemp(join(dirname(destination), '.nitro-patcher-'));\n try {\n const file = join(temporary, 'output.nds');\n await writeFile(file, result.buffer);\n await rename(file, destination);\n } finally {\n await rm(temporary, { recursive: true, force: true });\n }\n }\n const { returnValue, inputMd5, outputMd5 } = result;\n if (values.json) {\n console.log(\n JSON.stringify({\n returnValue,\n inputMd5,\n outputMd5,\n ...info,\n ...(dryRun && { dryRun: true }),\n }),\n );\n return;\n }\n\n const sections: string[] = [];\n if (info.metadata) {\n const metadata = formatMetadata(info.metadata);\n if (metadata) sections.push(`补丁元数据:\\n\\n${metadata}`);\n }\n if (info.readme) {\n const content = safeTerminalText(info.readme.content);\n const rendered =\n info.readme.format === 'markdown'\n ? render(content, {\n width: process.stdout.columns ?? 80,\n color: Boolean(process.stdout.isTTY),\n hyperlinks: false,\n })\n : content;\n sections.push(`补丁说明:\\n\\n${rendered.trimEnd()}`);\n }\n if (dryRun) sections.push('试运行完成,未写入输出 ROM。');\n sections.push(`${returnValue}\\n\\n原始 ROM 的 MD5:${inputMd5}\\n生成 ROM 的 MD5:${outputMd5}`);\n console.log(sections.join('\\n\\n'));\n};\n\nmain().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { render } from \"markdansi\";\nimport { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join, resolve, sep } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { NDSFile } from \"./nitro/nds-file\";\nimport { type PatchMetadata, patchBuffer, readPatchInfo } from \"./nitro-patch-helper\";\n\nconst help = `nitro-patcher — NDS ROM 补丁工具\n\n用法:\n nitro-patcher [--json] [--dry-run] <原始ROM> <补丁ZIP> [输出ROM]\n nitro-patcher patch [--json] [--dry-run] --input <原始ROM> --patch <补丁ZIP> [--output <输出ROM>]\n nitro-patcher extract [--json] --input <ROM> --output <目录>\n nitro-patcher inspect [--json] --input <ROM>\n\n --json 结果输出为 JSON 格式\n --dry-run 完整执行补丁流程,但不写入输出 ROM\n -i, --input 输入 ROM 路径\n -p, --patch 补丁 ZIP 路径\n -o, --output 输出 ROM 或提取目录;试运行时可省略\n -h, --help 显示帮助\n -v, --version 显示版本`;\n\ninterface CommandOptions {\n json: boolean;\n dryRun: boolean;\n input: string | undefined;\n patch: string | undefined;\n output: string | undefined;\n}\n\nconst sameFile = async (input: string, output: string): Promise<boolean> => {\n if (resolve(input) === resolve(output)) return true;\n const original = await stat(input);\n try {\n const destination = await stat(output);\n return original.dev === destination.dev && original.ino === destination.ino;\n } catch (error) {\n if (!(error instanceof Error && \"code\" in error && error.code === \"ENOENT\")) throw error;\n return false;\n }\n};\n\nconst safeTerminalText = (value: string): string => {\n const characters: string[] = [];\n for (const character of value.replaceAll(\"\\r\\n\", \"\\n\").replaceAll(\"\\r\", \"\\n\")) {\n const code = character.codePointAt(0)!;\n if (\n (code < 0x20 && code !== 0x09 && code !== 0x0a) ||\n (code >= 0x7f && code <= 0x9f) ||\n (code >= 0x202a && code <= 0x202e) ||\n (code >= 0x2066 && code <= 0x2069)\n )\n continue;\n characters.push(character);\n }\n return characters.join(\"\");\n};\n\nconst formatMetadata = (metadata: PatchMetadata): string => {\n const labels = {\n author: \"作者\",\n name: \"名称\",\n homepage: \"主页\",\n version: \"版本\",\n } as const;\n const lines: string[] = [];\n for (const field of [\"author\", \"name\", \"homepage\", \"version\"] as const) {\n const value = metadata[field];\n if (value === undefined) continue;\n const suffix = field === \"version\" && metadata.isBeta ? \"(测试版)\" : \"\";\n lines.push(`- ${labels[field]}:${safeTerminalText(value).replaceAll(\"\\n\", \" \")}${suffix}`);\n }\n return lines.join(\"\\n\");\n};\n\nconst extractRom = async (options: CommandOptions): Promise<void> => {\n if (!options.input || !options.output || options.patch !== undefined || options.dryRun) throw new Error(help);\n const destination = join(await realpath(dirname(resolve(options.output))), basename(resolve(options.output)));\n const rom = new NDSFile(await readFile(options.input));\n const files = rom.listFiles();\n await mkdir(destination, { recursive: true });\n for (const { path } of files) {\n const output = resolve(destination, path);\n if (!output.startsWith(`${destination}${sep}`)) throw new Error(`ROM 文件路径超出输出目录:${path}`);\n await mkdir(dirname(output), { recursive: true });\n await writeFile(output, rom.getFile(path));\n }\n\n if (options.json) {\n console.log(\n JSON.stringify({\n output: destination,\n fileCount: files.length,\n files: files.map(({ path }) => path),\n }),\n );\n } else {\n console.log(`已提取 ${files.length} 个文件到:${destination}`);\n }\n};\n\nconst inspectRom = async (options: CommandOptions): Promise<void> => {\n if (!options.input || options.patch !== undefined || options.output !== undefined || options.dryRun)\n throw new Error(help);\n const input = await readFile(options.input);\n const rom = new NDSFile(input);\n const info = {\n gameTitle: rom.header.gameTitle.replace(/\\0+$/, \"\"),\n gameCode: rom.header.gameCode,\n size: input.length,\n fileCount: rom.listFiles().length,\n checksums: {\n header: rom.header.headerCRC,\n logo: rom.header.logoCRC,\n secureArea: rom.header.secureCRC,\n banner: rom.banner.bannerCRC,\n },\n };\n if (options.json) {\n console.log(JSON.stringify(info));\n return;\n }\n console.log(`ROM 信息:\n\n标题:${safeTerminalText(info.gameTitle).replaceAll(\"\\n\", \" \")}\n游戏代码:${safeTerminalText(info.gameCode).replaceAll(\"\\n\", \" \")}\n大小:${info.size} 字节\n文件数量:${info.fileCount}\n校验:\n- Header:${info.checksums.header ? \"通过\" : \"失败\"}\n- Logo:${info.checksums.logo ? \"通过\" : \"失败\"}\n- Secure Area:${info.checksums.secureArea ? \"通过\" : \"失败\"}\n- Banner:${info.checksums.banner ? \"通过\" : \"失败\"}`);\n};\n\nconst main = async (): Promise<void> => {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n json: { type: \"boolean\" },\n \"dry-run\": { type: \"boolean\" },\n input: { type: \"string\", short: \"i\" },\n patch: { type: \"string\", short: \"p\" },\n output: { type: \"string\", short: \"o\" },\n },\n });\n if (values.help) {\n process.stdout.write(help);\n return;\n }\n if (values.version) {\n const metadata: unknown = JSON.parse(await readFile(new URL(\"../package.json\", import.meta.url), \"utf8\"));\n if (!metadata || typeof metadata !== \"object\" || !(\"version\" in metadata) || typeof metadata.version !== \"string\")\n throw new Error(\"Invalid package version metadata\");\n console.log(metadata.version);\n return;\n }\n const options: CommandOptions = {\n json: values.json ?? false,\n dryRun: values[\"dry-run\"] ?? false,\n input: values.input,\n patch: values.patch,\n output: values.output,\n };\n const command = positionals.length === 1 ? positionals[0] : undefined;\n if (positionals.length !== 0 && positionals.length !== 1 && positionals.length !== 3) throw new Error(help);\n if (command !== undefined && command !== \"patch\" && command !== \"extract\" && command !== \"inspect\")\n throw new Error(help);\n if (command === \"extract\") {\n await extractRom(options);\n return;\n }\n if (command === \"inspect\") {\n await inspectRom(options);\n return;\n }\n\n const dryRun = options.dryRun;\n const usesLegacyPaths = positionals.length === 3;\n if (usesLegacyPaths && (values.input !== undefined || values.patch !== undefined || values.output !== undefined))\n throw new Error(help);\n if (!usesLegacyPaths && (!values.input || !values.patch || (!dryRun && !values.output))) throw new Error(help);\n const [original, patch, output] = usesLegacyPaths ? positionals : [values.input, values.patch, values.output];\n if (!original || !patch) throw new Error(help);\n if (!dryRun) {\n if (!output) throw new Error(help);\n if ((await sameFile(original, output)) || (await sameFile(patch, output)))\n throw new Error(\"输出路径不能覆盖原始 ROM 或补丁包。\");\n }\n const [originalBytes, patchBytes] = await Promise.all([readFile(original), readFile(patch)]);\n const info = readPatchInfo(patchBytes);\n const result = patchBuffer(originalBytes, patchBytes);\n if (!dryRun) {\n if (!output) throw new Error(help);\n const destination = join(await realpath(dirname(resolve(output))), basename(output));\n const temporary = await mkdtemp(join(dirname(destination), \".nitro-patcher-\"));\n try {\n const file = join(temporary, \"output.nds\");\n await writeFile(file, result.buffer);\n await rename(file, destination);\n } finally {\n await rm(temporary, { recursive: true, force: true });\n }\n }\n const { returnValue, inputMd5, outputMd5 } = result;\n if (options.json) {\n console.log(\n JSON.stringify({\n returnValue,\n inputMd5,\n outputMd5,\n ...info,\n ...(dryRun && { dryRun: true }),\n }),\n );\n return;\n }\n\n const sections: string[] = [];\n if (info.metadata) {\n const metadata = formatMetadata(info.metadata);\n if (metadata) sections.push(`补丁信息:\\n\\n${metadata}`);\n }\n if (info.readme) {\n const content = safeTerminalText(info.readme.content);\n const rendered =\n info.readme.format === \"markdown\"\n ? render(content, {\n width: process.stdout.columns ?? 80,\n color: Boolean(process.stdout.isTTY),\n hyperlinks: false,\n })\n : content;\n sections.push(`补丁说明:\\n\\n${rendered.trim()}`);\n }\n if (dryRun) sections.push(\"试运行完成,未写入输出 ROM。\");\n sections.push(`${returnValue}\\n\\n原始 ROM 的 MD5:${inputMd5}\\n生成 ROM 的 MD5:${outputMd5}`);\n console.log(sections.join(\"\\n\\n\"));\n};\n\nmain().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Arm9Header, Banner, BinaryWriter, FatEntry, FileAllocationTable, FileNameTable, Header, ModcryptHeader, NDSFile, NitroFile, NitroFolder, OverlayItem, OverlayRange, OverlayTable, RomFileInfo, RomOptions, SignedTwlHeader, TWL, TwlHeader, UnitCode, aes128CtrCrypt, compressArm9, compressBLZ, crc16, crc32, decompressArm9, decompressBLZ, encryptSecureArea, modcryptKey, updateHeaderSignatures } from './nitro-helper.js';
2
- export { PatchHelper, PatchInfo, PatchMetadata, PatchOptions, PatchReadme, PatchResult, PatchReturnValue, XdeltaOptions, decodeXdelta, patchBuffer, patchIt, readPatchInfo, readPatchInfoFile, readPatchMetadata, readPatchMetadataFile, readPatchReadme, readPatchReadmeFile } from './nitro-patch-helper.js';
2
+ export { PatchHelper, PatchInfo, PatchMetadata, PatchOptions, PatchReadme, PatchResult, PatchReturnValue, XdeltaOptions, decodeXdelta, extractZipEntry, patchBuffer, patchIt, readPatchInfo, readPatchInfoFile, readPatchMetadata, readPatchMetadataFile, readPatchReadme, readPatchReadmeFile } from './nitro-patch-helper.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  PatchHelper,
4
4
  PatchReturnValue,
5
5
  decodeXdelta,
6
+ extractZipEntry,
6
7
  patchBuffer,
7
8
  patchIt,
8
9
  readPatchInfo,
@@ -11,7 +12,7 @@ import {
11
12
  readPatchMetadataFile,
12
13
  readPatchReadme,
13
14
  readPatchReadmeFile
14
- } from "./chunk-ZY3OSZCA.js";
15
+ } from "./chunk-IGURDH5L.js";
15
16
  import {
16
17
  Banner,
17
18
  BinaryWriter,
@@ -32,7 +33,7 @@ import {
32
33
  encryptSecureArea,
33
34
  modcryptKey,
34
35
  updateHeaderSignatures
35
- } from "./chunk-GFF67HX4.js";
36
+ } from "./chunk-GLK3K2HY.js";
36
37
  export {
37
38
  Banner,
38
39
  BinaryWriter,
@@ -54,6 +55,7 @@ export {
54
55
  decompressArm9,
55
56
  decompressBLZ,
56
57
  encryptSecureArea,
58
+ extractZipEntry,
57
59
  modcryptKey,
58
60
  patchBuffer,
59
61
  patchIt,
@@ -1,3 +1,70 @@
1
+ declare class Banner {
2
+ version: number;
3
+ raw: Buffer;
4
+ bannerCRC: boolean;
5
+ tileData: Buffer;
6
+ palette: Buffer;
7
+ japaneseTitle: string;
8
+ englishTitle: string;
9
+ frenchTitle: string;
10
+ germanTitle: string;
11
+ italianTitle: string;
12
+ spanishTitle: string;
13
+ chineseTitle: string;
14
+ koreanTitle: string;
15
+ constructor(input: Uint8Array, offset?: number, hardSize?: number);
16
+ get titleCount(): 8 | 7 | 6;
17
+ getDefSize(hardSize?: number): number;
18
+ toBuffer(): Buffer<ArrayBuffer>;
19
+ }
20
+
21
+ /** A file in the original ROM, optionally replaced with new bytes. */
22
+ interface NitroFile {
23
+ name: string;
24
+ id: number;
25
+ offset: number;
26
+ size: number;
27
+ replacement?: Buffer;
28
+ }
29
+ interface NitroFolder {
30
+ name: string;
31
+ id: number;
32
+ files: NitroFile[];
33
+ folders: NitroFolder[];
34
+ }
35
+ interface FatEntry {
36
+ id: number;
37
+ offset: number;
38
+ size: number;
39
+ }
40
+ declare class FileAllocationTable {
41
+ readonly entries: FatEntry[];
42
+ readonly sortedIDs: number[];
43
+ constructor(input: Uint8Array, offset: number, size: number);
44
+ }
45
+ declare class FileNameTable {
46
+ readonly root: NitroFolder;
47
+ constructor(fat: FileAllocationTable, input: Uint8Array, offset: number, size: number);
48
+ static findFile(id: number, folder: NitroFolder): NitroFile | undefined;
49
+ }
50
+ interface OverlayItem {
51
+ overlayId: number;
52
+ ramAddress: number;
53
+ ramSize: number;
54
+ bssSize: number;
55
+ staticInitialiserStartAddress: number;
56
+ staticInitialiserEndAddress: number;
57
+ fileId: number;
58
+ reserved: number;
59
+ }
60
+ declare class OverlayTable {
61
+ readonly isArm9: boolean;
62
+ readonly entries: OverlayItem[];
63
+ constructor(input: Uint8Array, offset: number, size: number, isArm9: boolean);
64
+ readBasicOverlays(fat: FileAllocationTable): NitroFile[];
65
+ toBuffer(): Buffer;
66
+ }
67
+
1
68
  declare const UnitCode: Readonly<{
2
69
  NDS: 0;
3
70
  NDS_DSi: 2;
@@ -109,73 +176,6 @@ declare class Header {
109
176
  toString(): string;
110
177
  }
111
178
 
112
- declare class Banner {
113
- version: number;
114
- raw: Buffer;
115
- bannerCRC: boolean;
116
- tileData: Buffer;
117
- palette: Buffer;
118
- japaneseTitle: string;
119
- englishTitle: string;
120
- frenchTitle: string;
121
- germanTitle: string;
122
- italianTitle: string;
123
- spanishTitle: string;
124
- chineseTitle: string;
125
- koreanTitle: string;
126
- constructor(input: Uint8Array, offset?: number, hardSize?: number);
127
- get titleCount(): 8 | 7 | 6;
128
- getDefSize(hardSize?: number): number;
129
- toBuffer(): Buffer<ArrayBuffer>;
130
- }
131
-
132
- /** A file in the original ROM, optionally replaced with new bytes. */
133
- interface NitroFile {
134
- name: string;
135
- id: number;
136
- offset: number;
137
- size: number;
138
- replacement?: Buffer;
139
- }
140
- interface NitroFolder {
141
- name: string;
142
- id: number;
143
- files: NitroFile[];
144
- folders: NitroFolder[];
145
- }
146
- interface FatEntry {
147
- id: number;
148
- offset: number;
149
- size: number;
150
- }
151
- declare class FileAllocationTable {
152
- readonly entries: FatEntry[];
153
- readonly sortedIDs: number[];
154
- constructor(input: Uint8Array, offset: number, size: number);
155
- }
156
- declare class FileNameTable {
157
- readonly root: NitroFolder;
158
- constructor(fat: FileAllocationTable, input: Uint8Array, offset: number, size: number);
159
- static findFile(id: number, folder: NitroFolder): NitroFile | undefined;
160
- }
161
- interface OverlayItem {
162
- overlayId: number;
163
- ramAddress: number;
164
- ramSize: number;
165
- bssSize: number;
166
- staticInitialiserStartAddress: number;
167
- staticInitialiserEndAddress: number;
168
- fileId: number;
169
- reserved: number;
170
- }
171
- declare class OverlayTable {
172
- readonly isArm9: boolean;
173
- readonly entries: OverlayItem[];
174
- constructor(input: Uint8Array, offset: number, size: number, isArm9: boolean);
175
- readBasicOverlays(fat: FileAllocationTable): NitroFile[];
176
- toBuffer(): Buffer;
177
- }
178
-
179
179
  /** Seekable, bounded in-memory writer; unwritten gaps have MemoryStream's zero fill. */
180
180
  declare class BinaryWriter {
181
181
  readonly maxSize: number;
@@ -190,17 +190,6 @@ declare class BinaryWriter {
190
190
  toBuffer(): Buffer<ArrayBufferLike>;
191
191
  }
192
192
 
193
- interface ModcryptHeader {
194
- gameCode: string;
195
- gameTitle: string;
196
- twlInternalFlags: number;
197
- appflags?: Buffer;
198
- hmac_arm9i?: Buffer;
199
- }
200
- declare const aes128CtrCrypt: (key: Buffer, counter: Buffer, data: Buffer) => Buffer;
201
- declare const modcryptKey: (header: ModcryptHeader) => Buffer;
202
- declare const encryptSecureArea: (gameCode: string, input: Buffer) => Buffer;
203
-
204
193
  interface Arm9Header {
205
194
  reserved2: Buffer;
206
195
  ARM9ramAddress: number;
@@ -214,6 +203,17 @@ declare const decompressArm9: (data: Buffer, header: Arm9Header) => {
214
203
  };
215
204
  declare const compressArm9: (data: Buffer, header: Arm9Header, postSize?: number) => Buffer;
216
205
 
206
+ interface ModcryptHeader {
207
+ gameCode: string;
208
+ gameTitle: string;
209
+ twlInternalFlags: number;
210
+ appflags?: Buffer;
211
+ hmac_arm9i?: Buffer;
212
+ }
213
+ declare const aes128CtrCrypt: (key: Buffer, counter: Buffer, data: Buffer) => Buffer;
214
+ declare const modcryptKey: (header: ModcryptHeader) => Buffer;
215
+ declare const encryptSecureArea: (gameCode: string, input: Buffer) => Buffer;
216
+
217
217
  interface TwlHeader extends ModcryptHeader {
218
218
  digest_sector_size: number;
219
219
  digest_block_sectorcount: number;
@@ -19,7 +19,7 @@ import {
19
19
  encryptSecureArea,
20
20
  modcryptKey,
21
21
  updateHeaderSignatures
22
- } from "./chunk-GFF67HX4.js";
22
+ } from "./chunk-GLK3K2HY.js";
23
23
  export {
24
24
  Banner,
25
25
  BinaryWriter,
@@ -42,7 +42,7 @@ interface PatchMetadata {
42
42
  isBeta?: boolean;
43
43
  }
44
44
  interface PatchReadme {
45
- format: 'markdown' | 'plaintext';
45
+ format: "markdown" | "plaintext";
46
46
  content: string;
47
47
  }
48
48
  interface PatchInfo {
@@ -55,6 +55,8 @@ declare const readPatchMetadata: (patch: Uint8Array, options?: PatchOptions) =>
55
55
  declare const readPatchReadme: (patch: Uint8Array, options?: PatchOptions) => PatchReadme | null;
56
56
  /** Read metadata and README content while parsing the patch package only once. */
57
57
  declare const readPatchInfo: (patch: Uint8Array, options?: PatchOptions) => PatchInfo;
58
+ /** Extract one file from a ZIP archive entirely in memory. */
59
+ declare const extractZipEntry: (archive: Uint8Array, entryName: string, options?: PatchOptions) => Buffer;
58
60
  /** Read metadata.json from a NitroPatcher ZIP package on disk. */
59
61
  declare const readPatchMetadataFile: (patchPath: string, options?: PatchOptions) => Promise<PatchMetadata | null>;
60
62
  /** Read a patch README from disk. */
@@ -67,6 +69,7 @@ declare const patchBuffer: (original: Uint8Array, patch: Uint8Array, options?: P
67
69
  declare const patchIt: (originalPath: string, patchPath: string, options?: PatchOptions) => Promise<PatchResult>;
68
70
  /** Namespaced counterpart of the original C# helper, sharing the same typed functions. */
69
71
  declare const PatchHelper: Readonly<{
72
+ extractZipEntry: (archive: Uint8Array, entryName: string, options?: PatchOptions) => Buffer;
70
73
  patchBuffer: (original: Uint8Array, patch: Uint8Array, options?: PatchOptions) => PatchResult;
71
74
  patchIt: (originalPath: string, patchPath: string, options?: PatchOptions) => Promise<PatchResult>;
72
75
  readPatchInfo: (patch: Uint8Array, options?: PatchOptions) => PatchInfo;
@@ -77,4 +80,4 @@ declare const PatchHelper: Readonly<{
77
80
  readPatchReadmeFile: (patchPath: string, options?: PatchOptions) => Promise<PatchReadme | null>;
78
81
  }>;
79
82
 
80
- export { PatchHelper, type PatchInfo, type PatchMetadata, type PatchOptions, type PatchReadme, type PatchResult, PatchReturnValue, type XdeltaOptions, decodeXdelta, patchBuffer, patchIt, readPatchInfo, readPatchInfoFile, readPatchMetadata, readPatchMetadataFile, readPatchReadme, readPatchReadmeFile };
83
+ export { PatchHelper, type PatchInfo, type PatchMetadata, type PatchOptions, type PatchReadme, type PatchResult, PatchReturnValue, type XdeltaOptions, decodeXdelta, extractZipEntry, patchBuffer, patchIt, readPatchInfo, readPatchInfoFile, readPatchMetadata, readPatchMetadataFile, readPatchReadme, readPatchReadmeFile };
@@ -2,6 +2,7 @@ import {
2
2
  PatchHelper,
3
3
  PatchReturnValue,
4
4
  decodeXdelta,
5
+ extractZipEntry,
5
6
  patchBuffer,
6
7
  patchIt,
7
8
  readPatchInfo,
@@ -10,12 +11,13 @@ import {
10
11
  readPatchMetadataFile,
11
12
  readPatchReadme,
12
13
  readPatchReadmeFile
13
- } from "./chunk-ZY3OSZCA.js";
14
- import "./chunk-GFF67HX4.js";
14
+ } from "./chunk-IGURDH5L.js";
15
+ import "./chunk-GLK3K2HY.js";
15
16
  export {
16
17
  PatchHelper,
17
18
  PatchReturnValue,
18
19
  decodeXdelta,
20
+ extractZipEntry,
19
21
  patchBuffer,
20
22
  patchIt,
21
23
  readPatchInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-patcher",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Native Node.js Nintendo DS ROM reader, repacker and ZIP/Xdelta patcher",
5
5
  "type": "module",
6
6
  "engines": {
@@ -50,15 +50,19 @@
50
50
  "@noble/hashes": "2.4.0",
51
51
  "@types/node": "22.20.2",
52
52
  "buffer": "6.0.3",
53
- "esbuild": "0.27.3",
53
+ "esbuild": "0.28.1",
54
54
  "eslint": "10.9.1",
55
55
  "eslint-config-prettier": "10.1.8",
56
+ "eslint-plugin-import-x": "4.17.1",
57
+ "eslint-plugin-prettier": "5.5.5",
58
+ "eslint-plugin-unused-imports": "4.4.1",
56
59
  "fflate": "0.8.3",
57
60
  "globals": "16.5.0",
58
61
  "jiti": "2.7.0",
59
62
  "prettier": "3.9.6",
60
63
  "tsup": "8.5.1",
61
64
  "tsx": "4.23.13",
65
+ "type-fest": "4.41.0",
62
66
  "typescript": "5.9.3",
63
67
  "typescript-eslint": "8.70.0"
64
68
  },
@@ -1 +1 @@
1
- export { Buffer } from 'buffer';
1
+ export { Buffer } from "buffer";
package/src/browser.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { NDSFile } from './nitro/nds-file';
1
+ import { NDSFile } from "./nitro/nds-file";
2
2
  import {
3
+ extractZipEntry as extractEntry,
3
4
  patchBuffer as patch,
4
5
  readPatchInfo as readInfo,
5
6
  readPatchMetadata as readMetadata,
6
7
  readPatchReadme as readReadme,
7
- } from './nitro-patch-helper';
8
+ } from "./nitro-patch-helper";
8
9
 
9
10
  export interface PatchMetadata {
10
11
  id?: string;
@@ -15,7 +16,7 @@ export interface PatchMetadata {
15
16
  isBeta?: boolean;
16
17
  }
17
18
  export interface PatchReadme {
18
- format: 'markdown' | 'plaintext';
19
+ format: "markdown" | "plaintext";
19
20
  content: string;
20
21
  }
21
22
  export interface PatchInfo {
@@ -31,7 +32,7 @@ export interface BrowserPatchOptions {
31
32
  }
32
33
  export interface BrowserPatchResult {
33
34
  buffer: Uint8Array<ArrayBuffer>;
34
- returnValue: 'SUCCESS' | 'MD5_MISMATCH';
35
+ returnValue: "SUCCESS" | "MD5_MISMATCH";
35
36
  inputMd5: string;
36
37
  outputMd5: string;
37
38
  }
@@ -79,20 +80,23 @@ export const patchBuffer = (
79
80
  };
80
81
  };
81
82
  /** Read and validate metadata.json from a patch package entirely in memory. */
82
- export const readPatchMetadata = (
83
- archive: Uint8Array,
84
- options: BrowserPatchOptions = {},
85
- ): PatchMetadata | null => readMetadata(archive, options);
83
+ export const readPatchMetadata = (archive: Uint8Array, options: BrowserPatchOptions = {}): PatchMetadata | null =>
84
+ readMetadata(archive, options);
86
85
  /** Read a root README.md or README.txt from a patch package entirely in memory. */
87
- export const readPatchReadme = (
88
- archive: Uint8Array,
89
- options: BrowserPatchOptions = {},
90
- ): PatchReadme | null => readReadme(archive, options);
86
+ export const readPatchReadme = (archive: Uint8Array, options: BrowserPatchOptions = {}): PatchReadme | null =>
87
+ readReadme(archive, options);
91
88
  /** Read metadata and README content while parsing the patch package only once. */
92
89
  export const readPatchInfo = (archive: Uint8Array, options: BrowserPatchOptions = {}): PatchInfo =>
93
90
  readInfo(archive, options);
91
+ /** Extract one file from a ZIP archive entirely in memory. */
92
+ export const extractZipEntry = (
93
+ archive: Uint8Array,
94
+ entryName: string,
95
+ options: BrowserPatchOptions = {},
96
+ ): Uint8Array<ArrayBuffer> => new Uint8Array(extractEntry(archive, entryName, options));
94
97
 
95
98
  export const PatchHelper = Object.freeze({
99
+ extractZipEntry,
96
100
  patchBuffer,
97
101
  readPatchInfo,
98
102
  readPatchMetadata,