nitro-patcher 0.2.1 → 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 +95 -15
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +105 -18
package/dist/cli.js
CHANGED
|
@@ -3,24 +3,28 @@ import {
|
|
|
3
3
|
patchBuffer,
|
|
4
4
|
readPatchInfo
|
|
5
5
|
} from "./chunk-IGURDH5L.js";
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
NDSFile
|
|
8
|
+
} from "./chunk-GLK3K2HY.js";
|
|
7
9
|
|
|
8
10
|
// src/cli.ts
|
|
9
11
|
import { render } from "markdansi";
|
|
10
|
-
import { mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
11
|
-
import { basename, dirname, join, resolve } from "path";
|
|
12
|
+
import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
13
|
+
import { basename, dirname, join, resolve, sep } from "path";
|
|
12
14
|
import { parseArgs } from "util";
|
|
13
|
-
var help = `nitro-patcher \u2014
|
|
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 \
|
|
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 \
|
|
25
|
+
-i, --input \u8F93\u5165 ROM \u8DEF\u5F84
|
|
22
26
|
-p, --patch \u8865\u4E01 ZIP \u8DEF\u5F84
|
|
23
|
-
-o, --output \u8F93\u51FA ROM \
|
|
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,
|
|
@@ -84,12 +145,31 @@ var main = async () => {
|
|
|
84
145
|
console.log(metadata.version);
|
|
85
146
|
return;
|
|
86
147
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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))
|
|
91
170
|
throw new Error(help);
|
|
92
|
-
|
|
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];
|
|
93
173
|
if (!original || !patch) throw new Error(help);
|
|
94
174
|
if (!dryRun) {
|
|
95
175
|
if (!output) throw new Error(help);
|
|
@@ -112,7 +192,7 @@ var main = async () => {
|
|
|
112
192
|
}
|
|
113
193
|
}
|
|
114
194
|
const { returnValue, inputMd5, outputMd5 } = result;
|
|
115
|
-
if (
|
|
195
|
+
if (options.json) {
|
|
116
196
|
console.log(
|
|
117
197
|
JSON.stringify({
|
|
118
198
|
returnValue,
|
|
@@ -127,7 +207,7 @@ var main = async () => {
|
|
|
127
207
|
const sections = [];
|
|
128
208
|
if (info.metadata) {
|
|
129
209
|
const metadata = formatMetadata(info.metadata);
|
|
130
|
-
if (metadata) sections.push(`\u8865\u4E01\
|
|
210
|
+
if (metadata) sections.push(`\u8865\u4E01\u4FE1\u606F\uFF1A
|
|
131
211
|
|
|
132
212
|
${metadata}`);
|
|
133
213
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { render } from \"markdansi\";\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\";\n\nimport { type PatchMetadata, patchBuffer, readPatchInfo } 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(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 dryRun = values[\"dry-run\"] ?? false;\n const usesNamedPaths = 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) || (dryRun && (positionals.length < 2 || positionals.length > 3))))\n )\n throw new Error(help);\n const [original, patch, output] = usesNamedPaths ? [values.input, values.patch, values.output] : 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.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":[]}
|
|
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/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
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 —
|
|
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
|
|
18
|
+
--json 结果输出为 JSON 格式
|
|
16
19
|
--dry-run 完整执行补丁流程,但不写入输出 ROM
|
|
17
|
-
-i, --input
|
|
20
|
+
-i, --input 输入 ROM 路径
|
|
18
21
|
-p, --patch 补丁 ZIP 路径
|
|
19
|
-
-o, --output 输出 ROM
|
|
22
|
+
-o, --output 输出 ROM 或提取目录;试运行时可省略
|
|
20
23
|
-h, --help 显示帮助
|
|
21
24
|
-v, --version 显示版本`;
|
|
22
25
|
|
|
26
|
+
interface CommandOptions {
|
|
27
|
+
json: boolean;
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
input: string | undefined;
|
|
30
|
+
patch: string | undefined;
|
|
31
|
+
output: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
const sameFile = async (input: string, output: string): Promise<boolean> => {
|
|
24
35
|
if (resolve(input) === resolve(output)) return true;
|
|
25
36
|
const original = await stat(input);
|
|
@@ -65,6 +76,66 @@ const formatMetadata = (metadata: PatchMetadata): string => {
|
|
|
65
76
|
return lines.join("\n");
|
|
66
77
|
};
|
|
67
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}`);
|
|
102
|
+
}
|
|
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 ? "通过" : "失败"}`);
|
|
137
|
+
};
|
|
138
|
+
|
|
68
139
|
const main = async (): Promise<void> => {
|
|
69
140
|
const { values, positionals } = parseArgs({
|
|
70
141
|
allowPositionals: true,
|
|
@@ -89,16 +160,32 @@ const main = async (): Promise<void> => {
|
|
|
89
160
|
console.log(metadata.version);
|
|
90
161
|
return;
|
|
91
162
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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))
|
|
100
186
|
throw new Error(help);
|
|
101
|
-
|
|
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];
|
|
102
189
|
if (!original || !patch) throw new Error(help);
|
|
103
190
|
if (!dryRun) {
|
|
104
191
|
if (!output) throw new Error(help);
|
|
@@ -121,7 +208,7 @@ const main = async (): Promise<void> => {
|
|
|
121
208
|
}
|
|
122
209
|
}
|
|
123
210
|
const { returnValue, inputMd5, outputMd5 } = result;
|
|
124
|
-
if (
|
|
211
|
+
if (options.json) {
|
|
125
212
|
console.log(
|
|
126
213
|
JSON.stringify({
|
|
127
214
|
returnValue,
|
|
@@ -137,7 +224,7 @@ const main = async (): Promise<void> => {
|
|
|
137
224
|
const sections: string[] = [];
|
|
138
225
|
if (info.metadata) {
|
|
139
226
|
const metadata = formatMetadata(info.metadata);
|
|
140
|
-
if (metadata) sections.push(
|
|
227
|
+
if (metadata) sections.push(`补丁信息:\n\n${metadata}`);
|
|
141
228
|
}
|
|
142
229
|
if (info.readme) {
|
|
143
230
|
const content = safeTerminalText(info.readme.content);
|