nitro-patcher 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/browser.d.ts +29 -1
- package/dist/browser.js +1459 -1431
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-GFF67HX4.js → chunk-GLK3K2HY.js} +221 -282
- package/dist/chunk-GLK3K2HY.js.map +1 -0
- package/dist/{chunk-L7ZM4LLP.js → chunk-IGURDH5L.js} +254 -186
- package/dist/chunk-IGURDH5L.js.map +1 -0
- package/dist/cli.js +106 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -3
- package/dist/nitro-helper.d.ts +78 -78
- package/dist/nitro-helper.js +1 -1
- package/dist/nitro-patch-helper.d.ts +38 -1
- package/dist/nitro-patch-helper.js +18 -4
- package/package.json +9 -2
- package/src/browser/buffer.ts +1 -1
- package/src/browser.ts +49 -4
- package/src/cli.ts +123 -37
- package/src/index.ts +2 -2
- package/src/nitro/banner.ts +28 -31
- package/src/nitro/binary.ts +6 -16
- package/src/nitro/blz.ts +13 -21
- package/src/nitro/crypto.ts +78 -84
- package/src/nitro/filesystem.ts +36 -39
- package/src/nitro/header.ts +58 -71
- package/src/nitro/nds-file.ts +44 -63
- package/src/nitro/twl.ts +59 -100
- package/src/nitro-helper.ts +14 -14
- package/src/nitro-patch-helper.ts +140 -29
- package/src/patch/xdelta.ts +43 -49
- package/src/patch/zip.ts +47 -56
- package/src/runtime/browser.ts +9 -11
- package/src/runtime/node.ts +14 -15
- package/dist/chunk-GFF67HX4.js.map +0 -1
- package/dist/chunk-L7ZM4LLP.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-patcher",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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.
|
|
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
|
},
|
|
@@ -68,6 +72,9 @@
|
|
|
68
72
|
"#browser-hmac": "@noble/hashes/hmac.js",
|
|
69
73
|
"#browser-aes": "@noble/ciphers/aes.js"
|
|
70
74
|
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"markdansi": "0.3.3"
|
|
77
|
+
},
|
|
71
78
|
"scripts": {
|
|
72
79
|
"clean": "tsx scripts/clean.ts",
|
|
73
80
|
"build": "pnpm run build:node && pnpm run build:browser",
|
package/src/browser/buffer.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Buffer } from
|
|
1
|
+
export { Buffer } from "buffer";
|
package/src/browser.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
import { NDSFile } from
|
|
2
|
-
import {
|
|
1
|
+
import { NDSFile } from "./nitro/nds-file";
|
|
2
|
+
import {
|
|
3
|
+
extractZipEntry as extractEntry,
|
|
4
|
+
patchBuffer as patch,
|
|
5
|
+
readPatchInfo as readInfo,
|
|
6
|
+
readPatchMetadata as readMetadata,
|
|
7
|
+
readPatchReadme as readReadme,
|
|
8
|
+
} from "./nitro-patch-helper";
|
|
9
|
+
|
|
10
|
+
export interface PatchMetadata {
|
|
11
|
+
id?: string;
|
|
12
|
+
author?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
homepage?: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
isBeta?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface PatchReadme {
|
|
19
|
+
format: "markdown" | "plaintext";
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
export interface PatchInfo {
|
|
23
|
+
metadata: PatchMetadata | null;
|
|
24
|
+
readme: PatchReadme | null;
|
|
25
|
+
}
|
|
3
26
|
|
|
4
27
|
export interface BrowserPatchOptions {
|
|
5
28
|
maxOutputSize?: number;
|
|
@@ -9,7 +32,7 @@ export interface BrowserPatchOptions {
|
|
|
9
32
|
}
|
|
10
33
|
export interface BrowserPatchResult {
|
|
11
34
|
buffer: Uint8Array<ArrayBuffer>;
|
|
12
|
-
returnValue:
|
|
35
|
+
returnValue: "SUCCESS" | "MD5_MISMATCH";
|
|
13
36
|
inputMd5: string;
|
|
14
37
|
outputMd5: string;
|
|
15
38
|
}
|
|
@@ -56,5 +79,27 @@ export const patchBuffer = (
|
|
|
56
79
|
outputMd5: result.outputMd5,
|
|
57
80
|
};
|
|
58
81
|
};
|
|
59
|
-
|
|
82
|
+
/** Read and validate metadata.json from a patch package entirely in memory. */
|
|
83
|
+
export const readPatchMetadata = (archive: Uint8Array, options: BrowserPatchOptions = {}): PatchMetadata | null =>
|
|
84
|
+
readMetadata(archive, options);
|
|
85
|
+
/** Read a root README.md or README.txt from a patch package entirely in memory. */
|
|
86
|
+
export const readPatchReadme = (archive: Uint8Array, options: BrowserPatchOptions = {}): PatchReadme | null =>
|
|
87
|
+
readReadme(archive, options);
|
|
88
|
+
/** Read metadata and README content while parsing the patch package only once. */
|
|
89
|
+
export const readPatchInfo = (archive: Uint8Array, options: BrowserPatchOptions = {}): PatchInfo =>
|
|
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));
|
|
97
|
+
|
|
98
|
+
export const PatchHelper = Object.freeze({
|
|
99
|
+
extractZipEntry,
|
|
100
|
+
patchBuffer,
|
|
101
|
+
readPatchInfo,
|
|
102
|
+
readPatchMetadata,
|
|
103
|
+
readPatchReadme,
|
|
104
|
+
});
|
|
60
105
|
export const NitroHelper = Object.freeze({ inspectRom });
|
package/src/cli.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { type PatchMetadata, patchBuffer, readPatchInfo } from "./nitro-patch-helper";
|
|
6
8
|
|
|
7
9
|
const help = `nitro-patcher — 原生 Node.js NDS ROM 补丁工具
|
|
8
10
|
|
|
9
|
-
用法:
|
|
11
|
+
用法:
|
|
12
|
+
nitro-patcher [--json] [--dry-run] <原始ROM> <补丁ZIP> [输出ROM]
|
|
13
|
+
nitro-patcher [--json] [--dry-run] --input <原始ROM> --patch <补丁ZIP> [--output <输出ROM>]
|
|
10
14
|
|
|
11
|
-
--json
|
|
12
|
-
-
|
|
13
|
-
-
|
|
15
|
+
--json 输出 JSON 状态和 MD5
|
|
16
|
+
--dry-run 完整执行补丁流程,但不写入输出 ROM
|
|
17
|
+
-i, --input 原始 ROM 路径
|
|
18
|
+
-p, --patch 补丁 ZIP 路径
|
|
19
|
+
-o, --output 输出 ROM 路径;试运行时可省略
|
|
20
|
+
-h, --help 显示帮助
|
|
21
|
+
-v, --version 显示版本`;
|
|
14
22
|
|
|
15
23
|
const sameFile = async (input: string, output: string): Promise<boolean> => {
|
|
16
24
|
if (resolve(input) === resolve(output)) return true;
|
|
@@ -19,18 +27,55 @@ const sameFile = async (input: string, output: string): Promise<boolean> => {
|
|
|
19
27
|
const destination = await stat(output);
|
|
20
28
|
return original.dev === destination.dev && original.ino === destination.ino;
|
|
21
29
|
} catch (error) {
|
|
22
|
-
if (!(error instanceof Error &&
|
|
30
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
23
31
|
return false;
|
|
24
32
|
}
|
|
25
33
|
};
|
|
26
34
|
|
|
35
|
+
const safeTerminalText = (value: string): string => {
|
|
36
|
+
const characters: string[] = [];
|
|
37
|
+
for (const character of value.replaceAll("\r\n", "\n").replaceAll("\r", "\n")) {
|
|
38
|
+
const code = character.codePointAt(0)!;
|
|
39
|
+
if (
|
|
40
|
+
(code < 0x20 && code !== 0x09 && code !== 0x0a) ||
|
|
41
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
42
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
43
|
+
(code >= 0x2066 && code <= 0x2069)
|
|
44
|
+
)
|
|
45
|
+
continue;
|
|
46
|
+
characters.push(character);
|
|
47
|
+
}
|
|
48
|
+
return characters.join("");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const formatMetadata = (metadata: PatchMetadata): string => {
|
|
52
|
+
const labels = {
|
|
53
|
+
author: "作者",
|
|
54
|
+
name: "名称",
|
|
55
|
+
homepage: "主页",
|
|
56
|
+
version: "版本",
|
|
57
|
+
} as const;
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
for (const field of ["author", "name", "homepage", "version"] as const) {
|
|
60
|
+
const value = metadata[field];
|
|
61
|
+
if (value === undefined) continue;
|
|
62
|
+
const suffix = field === "version" && metadata.isBeta ? "(测试版)" : "";
|
|
63
|
+
lines.push(`- ${labels[field]}:${safeTerminalText(value).replaceAll("\n", " ")}${suffix}`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
};
|
|
67
|
+
|
|
27
68
|
const main = async (): Promise<void> => {
|
|
28
69
|
const { values, positionals } = parseArgs({
|
|
29
70
|
allowPositionals: true,
|
|
30
71
|
options: {
|
|
31
|
-
help: { type:
|
|
32
|
-
version: { type:
|
|
33
|
-
json: { type:
|
|
72
|
+
help: { type: "boolean", short: "h" },
|
|
73
|
+
version: { type: "boolean", short: "v" },
|
|
74
|
+
json: { type: "boolean" },
|
|
75
|
+
"dry-run": { type: "boolean" },
|
|
76
|
+
input: { type: "string", short: "i" },
|
|
77
|
+
patch: { type: "string", short: "p" },
|
|
78
|
+
output: { type: "string", short: "o" },
|
|
34
79
|
},
|
|
35
80
|
});
|
|
36
81
|
if (values.help) {
|
|
@@ -38,36 +83,77 @@ const main = async (): Promise<void> => {
|
|
|
38
83
|
return;
|
|
39
84
|
}
|
|
40
85
|
if (values.version) {
|
|
41
|
-
const metadata: unknown = JSON.parse(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
!metadata ||
|
|
46
|
-
typeof metadata !== 'object' ||
|
|
47
|
-
!('version' in metadata) ||
|
|
48
|
-
typeof metadata.version !== 'string'
|
|
49
|
-
)
|
|
50
|
-
throw new Error('Invalid package version metadata');
|
|
86
|
+
const metadata: unknown = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
87
|
+
if (!metadata || typeof metadata !== "object" || !("version" in metadata) || typeof metadata.version !== "string")
|
|
88
|
+
throw new Error("Invalid package version metadata");
|
|
51
89
|
console.log(metadata.version);
|
|
52
90
|
return;
|
|
53
91
|
}
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
)
|
|
100
|
+
throw new Error(help);
|
|
101
|
+
const [original, patch, output] = usesNamedPaths ? [values.input, values.patch, values.output] : positionals;
|
|
102
|
+
if (!original || !patch) throw new Error(help);
|
|
103
|
+
if (!dryRun) {
|
|
104
|
+
if (!output) throw new Error(help);
|
|
105
|
+
if ((await sameFile(original, output)) || (await sameFile(patch, output)))
|
|
106
|
+
throw new Error("输出路径不能覆盖原始 ROM 或补丁包。");
|
|
107
|
+
}
|
|
108
|
+
const [originalBytes, patchBytes] = await Promise.all([readFile(original), readFile(patch)]);
|
|
109
|
+
const info = readPatchInfo(patchBytes);
|
|
110
|
+
const result = patchBuffer(originalBytes, patchBytes);
|
|
111
|
+
if (!dryRun) {
|
|
112
|
+
if (!output) throw new Error(help);
|
|
113
|
+
const destination = join(await realpath(dirname(resolve(output))), basename(output));
|
|
114
|
+
const temporary = await mkdtemp(join(dirname(destination), ".nitro-patcher-"));
|
|
115
|
+
try {
|
|
116
|
+
const file = join(temporary, "output.nds");
|
|
117
|
+
await writeFile(file, result.buffer);
|
|
118
|
+
await rename(file, destination);
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(temporary, { recursive: true, force: true });
|
|
121
|
+
}
|
|
67
122
|
}
|
|
68
123
|
const { returnValue, inputMd5, outputMd5 } = result;
|
|
69
|
-
if (values.json)
|
|
70
|
-
|
|
124
|
+
if (values.json) {
|
|
125
|
+
console.log(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
returnValue,
|
|
128
|
+
inputMd5,
|
|
129
|
+
outputMd5,
|
|
130
|
+
...info,
|
|
131
|
+
...(dryRun && { dryRun: true }),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sections: string[] = [];
|
|
138
|
+
if (info.metadata) {
|
|
139
|
+
const metadata = formatMetadata(info.metadata);
|
|
140
|
+
if (metadata) sections.push(`补丁元数据:\n\n${metadata}`);
|
|
141
|
+
}
|
|
142
|
+
if (info.readme) {
|
|
143
|
+
const content = safeTerminalText(info.readme.content);
|
|
144
|
+
const rendered =
|
|
145
|
+
info.readme.format === "markdown"
|
|
146
|
+
? render(content, {
|
|
147
|
+
width: process.stdout.columns ?? 80,
|
|
148
|
+
color: Boolean(process.stdout.isTTY),
|
|
149
|
+
hyperlinks: false,
|
|
150
|
+
})
|
|
151
|
+
: content;
|
|
152
|
+
sections.push(`补丁说明:\n\n${rendered.trim()}`);
|
|
153
|
+
}
|
|
154
|
+
if (dryRun) sections.push("试运行完成,未写入输出 ROM。");
|
|
155
|
+
sections.push(`${returnValue}\n\n原始 ROM 的 MD5:${inputMd5}\n生成 ROM 的 MD5:${outputMd5}`);
|
|
156
|
+
console.log(sections.join("\n\n"));
|
|
71
157
|
};
|
|
72
158
|
|
|
73
159
|
main().catch((error: unknown) => {
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
1
|
+
export * from "./nitro-helper";
|
|
2
|
+
export * from "./nitro-patch-helper";
|
package/src/nitro/banner.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Port of NitroHelper/Banner.cs. Copyright Xzonn and upstream contributors, GPL-3.0.
|
|
2
|
-
import { bytes, range } from
|
|
3
|
-
import { crc16 } from
|
|
2
|
+
import { bytes, range } from "./binary";
|
|
3
|
+
import { crc16 } from "./crc";
|
|
4
4
|
|
|
5
5
|
const titles = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
"japaneseTitle",
|
|
7
|
+
"englishTitle",
|
|
8
|
+
"frenchTitle",
|
|
9
|
+
"germanTitle",
|
|
10
|
+
"italianTitle",
|
|
11
|
+
"spanishTitle",
|
|
12
|
+
"chineseTitle",
|
|
13
|
+
"koreanTitle",
|
|
14
14
|
] as const;
|
|
15
15
|
export class Banner {
|
|
16
16
|
version: number;
|
|
@@ -18,21 +18,20 @@ export class Banner {
|
|
|
18
18
|
bannerCRC: boolean;
|
|
19
19
|
tileData: Buffer;
|
|
20
20
|
palette: Buffer;
|
|
21
|
-
japaneseTitle =
|
|
22
|
-
englishTitle =
|
|
23
|
-
frenchTitle =
|
|
24
|
-
germanTitle =
|
|
25
|
-
italianTitle =
|
|
26
|
-
spanishTitle =
|
|
27
|
-
chineseTitle =
|
|
28
|
-
koreanTitle =
|
|
21
|
+
japaneseTitle = "";
|
|
22
|
+
englishTitle = "";
|
|
23
|
+
frenchTitle = "";
|
|
24
|
+
germanTitle = "";
|
|
25
|
+
italianTitle = "";
|
|
26
|
+
spanishTitle = "";
|
|
27
|
+
chineseTitle = "";
|
|
28
|
+
koreanTitle = "";
|
|
29
29
|
constructor(input: Uint8Array, offset = 0, hardSize = 0) {
|
|
30
|
-
const data = bytes(input,
|
|
31
|
-
this.version = range(data, offset, 2,
|
|
30
|
+
const data = bytes(input, "banner");
|
|
31
|
+
this.version = range(data, offset, 2, "banner").readUInt16LE();
|
|
32
32
|
let size = this.getDefSize(hardSize);
|
|
33
|
-
if (this.version >>> 8 === 1)
|
|
34
|
-
|
|
35
|
-
this.raw = Buffer.from(range(data, offset, size, 'banner'));
|
|
33
|
+
if (this.version >>> 8 === 1) size = hardSize > 0 && hardSize !== 0xffffffff ? Math.min(hardSize, 0x23c0) : 0x23c0;
|
|
34
|
+
this.raw = Buffer.from(range(data, offset, size, "banner"));
|
|
36
35
|
this.bannerCRC = crc16(this.raw.subarray(0x20, 0x840)) === this.raw.readUInt16LE(2);
|
|
37
36
|
this.tileData = Buffer.from(this.raw.subarray(0x20, 0x220));
|
|
38
37
|
this.palette = Buffer.from(this.raw.subarray(0x220, 0x240));
|
|
@@ -41,9 +40,9 @@ export class Banner {
|
|
|
41
40
|
i < this.titleCount
|
|
42
41
|
? this.raw
|
|
43
42
|
.subarray(0x240 + i * 0x100, 0x340 + i * 0x100)
|
|
44
|
-
.toString(
|
|
45
|
-
.split(
|
|
46
|
-
:
|
|
43
|
+
.toString("utf16le")
|
|
44
|
+
.split("\0")[0]
|
|
45
|
+
: "";
|
|
47
46
|
}
|
|
48
47
|
get titleCount() {
|
|
49
48
|
return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
|
|
@@ -57,14 +56,12 @@ export class Banner {
|
|
|
57
56
|
toBuffer() {
|
|
58
57
|
const output = Buffer.from(this.raw);
|
|
59
58
|
output.writeUInt16LE(this.version);
|
|
60
|
-
if (this.tileData.length !== 512 || this.palette.length !== 32)
|
|
61
|
-
throw new RangeError('Invalid banner icon size');
|
|
59
|
+
if (this.tileData.length !== 512 || this.palette.length !== 32) throw new RangeError("Invalid banner icon size");
|
|
62
60
|
this.tileData.copy(output, 0x20);
|
|
63
61
|
this.palette.copy(output, 0x220);
|
|
64
62
|
for (let i = 0; i < this.titleCount; i++) {
|
|
65
|
-
const text = Buffer.from(this[titles[i]],
|
|
66
|
-
if (text.length > 256)
|
|
67
|
-
throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
63
|
+
const text = Buffer.from(this[titles[i]], "utf16le");
|
|
64
|
+
if (text.length > 256) throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
68
65
|
output.fill(0, 0x240 + i * 256, 0x340 + i * 256);
|
|
69
66
|
text.copy(output, 0x240 + i * 256);
|
|
70
67
|
}
|
package/src/nitro/binary.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
export const DEFAULT_MAX_SIZE = 0x40000000;
|
|
2
2
|
|
|
3
|
-
export const bytes = (value: Uint8Array, name =
|
|
3
|
+
export const bytes = (value: Uint8Array, name = "input"): Buffer => {
|
|
4
4
|
if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Buffer or Uint8Array`);
|
|
5
5
|
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export const range = (
|
|
9
|
-
buffer: Buffer,
|
|
10
|
-
offset: number,
|
|
11
|
-
size: number,
|
|
12
|
-
name = 'binary data',
|
|
13
|
-
): Buffer => {
|
|
8
|
+
export const range = (buffer: Buffer, offset: number, size: number, name = "binary data"): Buffer => {
|
|
14
9
|
if (
|
|
15
10
|
!Number.isSafeInteger(offset) ||
|
|
16
11
|
!Number.isSafeInteger(size) ||
|
|
@@ -18,16 +13,12 @@ export const range = (
|
|
|
18
13
|
size < 0 ||
|
|
19
14
|
offset + size > buffer.length
|
|
20
15
|
) {
|
|
21
|
-
throw new RangeError(
|
|
22
|
-
`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`,
|
|
23
|
-
);
|
|
16
|
+
throw new RangeError(`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`);
|
|
24
17
|
}
|
|
25
18
|
return buffer.subarray(offset, offset + size);
|
|
26
19
|
};
|
|
27
20
|
|
|
28
|
-
export const align = (value: number, alignment = 0x200): number =>
|
|
29
|
-
return Math.ceil(value / alignment) * alignment;
|
|
30
|
-
};
|
|
21
|
+
export const align = (value: number, alignment = 0x200): number => Math.ceil(value / alignment) * alignment;
|
|
31
22
|
|
|
32
23
|
/** Seekable, bounded in-memory writer; unwritten gaps have MemoryStream's zero fill. */
|
|
33
24
|
export class BinaryWriter {
|
|
@@ -37,7 +28,7 @@ export class BinaryWriter {
|
|
|
37
28
|
length: number;
|
|
38
29
|
constructor(initialSize = 65536, maxSize = DEFAULT_MAX_SIZE) {
|
|
39
30
|
if (!Number.isSafeInteger(maxSize) || maxSize < 1 || maxSize > 0xffffffff)
|
|
40
|
-
throw new RangeError(
|
|
31
|
+
throw new RangeError("Invalid output size limit");
|
|
41
32
|
this.maxSize = maxSize;
|
|
42
33
|
this.buffer = Buffer.alloc(Math.min(initialSize, maxSize));
|
|
43
34
|
this.position = 0;
|
|
@@ -55,8 +46,7 @@ export class BinaryWriter {
|
|
|
55
46
|
|
|
56
47
|
write(data: Uint8Array) {
|
|
57
48
|
data = bytes(data);
|
|
58
|
-
if (!Number.isSafeInteger(this.position) || this.position < 0)
|
|
59
|
-
throw new RangeError('Invalid writer position');
|
|
49
|
+
if (!Number.isSafeInteger(this.position) || this.position < 0) throw new RangeError("Invalid writer position");
|
|
60
50
|
this.ensure(this.position + data.length);
|
|
61
51
|
bytes(data).copy(this.buffer, this.position);
|
|
62
52
|
this.position += data.length;
|
package/src/nitro/blz.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface Arm9Header {
|
|
|
7
7
|
// Native port of Xzonn/BlzHelper BLZ.cs (MIT), including overwrite-safe prefix.
|
|
8
8
|
// Compression returns an empty Buffer when no smaller representation exists.
|
|
9
9
|
export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
10
|
-
if (input.length < 8) throw new Error(
|
|
10
|
+
if (input.length < 8) throw new Error("BLZ footer is truncated");
|
|
11
11
|
const diff = input.readUInt32LE(input.length - 4);
|
|
12
12
|
const packed = input.readUInt32LE(input.length - 8),
|
|
13
13
|
header = packed >>> 24,
|
|
@@ -15,7 +15,7 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
|
15
15
|
const size = input.length + diff,
|
|
16
16
|
prefix = input.length - length;
|
|
17
17
|
if (header < 8 || header > length || length > input.length || size > maxSize)
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error("Invalid BLZ size or footer");
|
|
19
19
|
const out = Buffer.alloc(size);
|
|
20
20
|
input.copy(out, 0, 0, prefix);
|
|
21
21
|
let src = input.length - header,
|
|
@@ -24,16 +24,15 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
|
24
24
|
const flags = input[--src];
|
|
25
25
|
for (let bit = 128; bit && src > prefix; bit >>= 1) {
|
|
26
26
|
if (!(flags & bit)) {
|
|
27
|
-
if (dst <= prefix) throw new Error(
|
|
27
|
+
if (dst <= prefix) throw new Error("BLZ output overflow");
|
|
28
28
|
out[--dst] = input[--src];
|
|
29
29
|
} else {
|
|
30
|
-
if (src - 2 < prefix) throw new Error(
|
|
30
|
+
if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
|
|
31
31
|
const high = input[--src],
|
|
32
32
|
low = input[--src];
|
|
33
33
|
const count = (high >>> 4) + 3,
|
|
34
34
|
offset = (((high & 15) << 8) | low) + 3;
|
|
35
|
-
if (dst - count < prefix || dst - 1 + offset >= size)
|
|
36
|
-
throw new Error('Invalid BLZ reference');
|
|
35
|
+
if (dst - count < prefix || dst - 1 + offset >= size) throw new Error("Invalid BLZ reference");
|
|
37
36
|
for (let j = 0; j < count; j++) {
|
|
38
37
|
--dst;
|
|
39
38
|
out[dst] = out[dst + offset];
|
|
@@ -41,12 +40,12 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
if (dst !== prefix) throw new Error(
|
|
43
|
+
if (dst !== prefix) throw new Error("BLZ decompressed size mismatch");
|
|
45
44
|
return out;
|
|
46
45
|
};
|
|
47
46
|
|
|
48
47
|
export const compressBLZ = (input: Buffer): Buffer => {
|
|
49
|
-
if (input.length > 0xffffff) throw new Error(
|
|
48
|
+
if (input.length > 0xffffff) throw new Error("BLZ input exceeds 24-bit format limit");
|
|
50
49
|
const temp = Buffer.alloc(input.length);
|
|
51
50
|
let src = input.length,
|
|
52
51
|
dst = input.length;
|
|
@@ -131,31 +130,24 @@ export const compressBLZ = (input: Buffer): Buffer => {
|
|
|
131
130
|
return out;
|
|
132
131
|
};
|
|
133
132
|
|
|
134
|
-
export const decompressArm9 = (
|
|
135
|
-
data
|
|
136
|
-
header: Arm9Header,
|
|
137
|
-
): { compressed: boolean; data: Buffer } => {
|
|
138
|
-
if (data.length < 0x18) throw new Error('ARM9 data too short');
|
|
133
|
+
export const decompressArm9 = (data: Buffer, header: Arm9Header): { compressed: boolean; data: Buffer } => {
|
|
134
|
+
if (data.length < 0x18) throw new Error("ARM9 data too short");
|
|
139
135
|
const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
|
|
140
136
|
const init = header.reserved2.readUInt32LE(0) & 0x3fff;
|
|
141
137
|
const ptr = init ? data.readUInt32LE(init + 0x14) : header.ARM9ramAddress + header.ARM9size;
|
|
142
|
-
const compressed =
|
|
143
|
-
ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
|
|
138
|
+
const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
|
|
144
139
|
return {
|
|
145
140
|
compressed,
|
|
146
141
|
data: compressed
|
|
147
|
-
? Buffer.concat([
|
|
148
|
-
decompressBLZ(data.subarray(0, data.length - nitro)),
|
|
149
|
-
data.subarray(data.length - nitro),
|
|
150
|
-
])
|
|
142
|
+
? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)])
|
|
151
143
|
: Buffer.from(data),
|
|
152
144
|
};
|
|
153
145
|
};
|
|
154
146
|
export const compressArm9 = (data: Buffer, header: Arm9Header, postSize = 0): Buffer => {
|
|
155
|
-
if (data.length < 0x4000) throw new Error(
|
|
147
|
+
if (data.length < 0x4000) throw new Error("ARM9 secure area is truncated");
|
|
156
148
|
const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
|
|
157
149
|
const packed = compressBLZ(data.subarray(0x4000, data.length - nitro));
|
|
158
|
-
if (!packed.length) throw new Error(
|
|
150
|
+
if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
|
|
159
151
|
const out = Buffer.concat([data.subarray(0, 0x4000), packed, data.subarray(data.length - nitro)]);
|
|
160
152
|
const init = header.reserved2.readUInt32LE(0) & 0x3fff;
|
|
161
153
|
if (init) out.writeUInt32LE(out.length - postSize + header.ARM9ramAddress, init + 0x14);
|