nitro-patcher 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +25 -2
- package/dist/browser.js +10507 -78
- package/dist/browser.js.LEGAL.txt +460 -0
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-IGURDH5L.js → chunk-B7NTPBDX.js} +70 -8
- package/dist/chunk-B7NTPBDX.js.map +1 -0
- package/dist/chunk-LK3JXA24.js +2067 -0
- package/dist/chunk-LK3JXA24.js.map +1 -0
- package/dist/cli.js +109 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -2
- package/dist/nitro-helper.d.ts +43 -134
- package/dist/nitro-helper.js +3 -1
- package/dist/nitro-patch-helper.d.ts +4 -0
- package/dist/nitro-patch-helper.js +2 -2
- package/dist/twl-complete-B2is30H0.d.ts +201 -0
- package/package.json +8 -2
- package/src/browser/buffer.ts +1 -0
- package/src/browser.ts +23 -2
- package/src/cli.ts +118 -18
- package/src/nitro/blz.ts +13 -4
- package/src/nitro/crypto.ts +29 -11
- package/src/nitro/filesystem.ts +88 -5
- package/src/nitro/header.ts +4 -0
- package/src/nitro/nds-file.ts +378 -30
- package/src/nitro/twl-complete.ts +665 -0
- package/src/nitro/twl.ts +53 -19
- package/src/nitro-helper.ts +2 -1
- package/src/nitro-patch-helper.ts +64 -3
- package/src/patch/zip.ts +14 -4
- package/src/runtime/browser.ts +2 -2
- package/src/runtime/node.ts +2 -15
- package/src/runtime/rsa.ts +14 -0
- package/dist/chunk-GLK3K2HY.js +0 -1120
- package/dist/chunk-GLK3K2HY.js.map +0 -1
- package/dist/chunk-IGURDH5L.js.map +0 -1
|
@@ -0,0 +1,2067 @@
|
|
|
1
|
+
// src/runtime/node.ts
|
|
2
|
+
import { createCipheriv, createHash, createHmac } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/runtime/rsa.ts
|
|
5
|
+
import forge from "node-forge";
|
|
6
|
+
var publicKey = forge.pki.rsa.setPublicKey(
|
|
7
|
+
new forge.jsbn.BigInteger(
|
|
8
|
+
"956F790DF08BB85A76AAEFA27FE874758BED9EDF9E9A670CD818BEB9B2885203B3FA11AEAA186513B5D6BB85A384D0D0EFB366CBC6051AAA86827AB74311F59C9BFC6C7079D5F17BD0819F522056738C721F40CF2361932590A3C5DC94CFD17A8CBC954A918AA858F4D804BAF7D3C1C4D7B8F077012FA170260B2C049056F3A5",
|
|
9
|
+
16
|
|
10
|
+
),
|
|
11
|
+
new forge.jsbn.BigInteger("10001", 16)
|
|
12
|
+
);
|
|
13
|
+
var verifyDsiSignature = (data, signature) => publicKey.verify(hashDigest("sha1", data).toString("latin1"), Buffer.from(signature).toString("latin1"), "NONE");
|
|
14
|
+
|
|
15
|
+
// src/runtime/node.ts
|
|
16
|
+
import { readFile, writeFile } from "fs/promises";
|
|
17
|
+
import { readFileSync } from "fs";
|
|
18
|
+
import { inflateRawSync } from "zlib";
|
|
19
|
+
var hashDigest = (algorithm, data) => createHash(algorithm).update(data).digest();
|
|
20
|
+
var hmacSha1 = (key2, data) => createHmac("sha1", key2).update(data).digest();
|
|
21
|
+
var createAesBlockCipher = (key2) => {
|
|
22
|
+
const cipher = createCipheriv("aes-128-ecb", key2, null);
|
|
23
|
+
cipher.setAutoPadding(false);
|
|
24
|
+
return (block) => cipher.update(block);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/nitro/binary.ts
|
|
28
|
+
var DEFAULT_MAX_SIZE = 1073741824;
|
|
29
|
+
var bytes = (value, name = "input") => {
|
|
30
|
+
if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Buffer or Uint8Array`);
|
|
31
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
32
|
+
};
|
|
33
|
+
var range = (buffer, offset, size, name = "binary data") => {
|
|
34
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(size) || offset < 0 || size < 0 || offset + size > buffer.length) {
|
|
35
|
+
throw new RangeError(`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`);
|
|
36
|
+
}
|
|
37
|
+
return buffer.subarray(offset, offset + size);
|
|
38
|
+
};
|
|
39
|
+
var align = (value, alignment = 512) => Math.ceil(value / alignment) * alignment;
|
|
40
|
+
var BinaryWriter = class {
|
|
41
|
+
maxSize;
|
|
42
|
+
buffer;
|
|
43
|
+
position;
|
|
44
|
+
length;
|
|
45
|
+
constructor(initialSize = 65536, maxSize = DEFAULT_MAX_SIZE) {
|
|
46
|
+
if (!Number.isSafeInteger(maxSize) || maxSize < 1 || maxSize > 4294967295)
|
|
47
|
+
throw new RangeError("Invalid output size limit");
|
|
48
|
+
this.maxSize = maxSize;
|
|
49
|
+
this.buffer = Buffer.alloc(Math.min(initialSize, maxSize));
|
|
50
|
+
this.position = 0;
|
|
51
|
+
this.length = 0;
|
|
52
|
+
}
|
|
53
|
+
ensure(end) {
|
|
54
|
+
if (!Number.isSafeInteger(end) || end < 0 || end > this.maxSize)
|
|
55
|
+
throw new RangeError(`Output exceeds size limit (${this.maxSize} bytes)`);
|
|
56
|
+
if (end <= this.buffer.length) return;
|
|
57
|
+
const next = Buffer.alloc(Math.min(this.maxSize, Math.max(end, this.buffer.length * 2, 1024)));
|
|
58
|
+
this.buffer.copy(next, 0, 0, this.length);
|
|
59
|
+
this.buffer = next;
|
|
60
|
+
}
|
|
61
|
+
write(data) {
|
|
62
|
+
data = bytes(data);
|
|
63
|
+
if (!Number.isSafeInteger(this.position) || this.position < 0) throw new RangeError("Invalid writer position");
|
|
64
|
+
this.ensure(this.position + data.length);
|
|
65
|
+
bytes(data).copy(this.buffer, this.position);
|
|
66
|
+
this.position += data.length;
|
|
67
|
+
this.length = Math.max(this.length, this.position);
|
|
68
|
+
}
|
|
69
|
+
fillTo(end, fill = 255) {
|
|
70
|
+
this.ensure(end);
|
|
71
|
+
if (end <= this.position) return;
|
|
72
|
+
this.buffer.fill(fill, this.position, end);
|
|
73
|
+
this.position = end;
|
|
74
|
+
this.length = Math.max(this.length, end);
|
|
75
|
+
}
|
|
76
|
+
pad(alignment, fill = 255) {
|
|
77
|
+
this.fillTo(align(this.position, alignment), fill);
|
|
78
|
+
}
|
|
79
|
+
toBuffer() {
|
|
80
|
+
return this.buffer.subarray(0, this.length);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/nitro/crc.ts
|
|
85
|
+
var table16 = new Uint16Array(256);
|
|
86
|
+
var table32 = new Uint32Array(256);
|
|
87
|
+
for (let i = 0; i < 256; i++) {
|
|
88
|
+
let a = i, b = i;
|
|
89
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
90
|
+
a = a >>> 1 ^ (a & 1 ? 40961 : 0);
|
|
91
|
+
b = b >>> 1 ^ (b & 1 ? 3988292384 : 0);
|
|
92
|
+
}
|
|
93
|
+
table16[i] = a;
|
|
94
|
+
table32[i] = b;
|
|
95
|
+
}
|
|
96
|
+
var crc16 = (data, initial = 65535) => {
|
|
97
|
+
let crc = initial;
|
|
98
|
+
for (const byte of data) crc = crc >>> 8 ^ table16[(crc ^ byte) & 255];
|
|
99
|
+
return crc;
|
|
100
|
+
};
|
|
101
|
+
var crc32 = (data) => {
|
|
102
|
+
let crc = 4294967295;
|
|
103
|
+
for (const byte of data) crc = crc >>> 8 ^ table32[(crc ^ byte) & 255];
|
|
104
|
+
return (crc ^ 4294967295) >>> 0;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/nitro/banner.ts
|
|
108
|
+
var titles = [
|
|
109
|
+
"japaneseTitle",
|
|
110
|
+
"englishTitle",
|
|
111
|
+
"frenchTitle",
|
|
112
|
+
"germanTitle",
|
|
113
|
+
"italianTitle",
|
|
114
|
+
"spanishTitle",
|
|
115
|
+
"chineseTitle",
|
|
116
|
+
"koreanTitle"
|
|
117
|
+
];
|
|
118
|
+
var Banner = class {
|
|
119
|
+
version;
|
|
120
|
+
raw;
|
|
121
|
+
bannerCRC;
|
|
122
|
+
tileData;
|
|
123
|
+
palette;
|
|
124
|
+
japaneseTitle = "";
|
|
125
|
+
englishTitle = "";
|
|
126
|
+
frenchTitle = "";
|
|
127
|
+
germanTitle = "";
|
|
128
|
+
italianTitle = "";
|
|
129
|
+
spanishTitle = "";
|
|
130
|
+
chineseTitle = "";
|
|
131
|
+
koreanTitle = "";
|
|
132
|
+
constructor(input, offset = 0, hardSize = 0) {
|
|
133
|
+
const data = bytes(input, "banner");
|
|
134
|
+
this.version = range(data, offset, 2, "banner").readUInt16LE();
|
|
135
|
+
let size = this.getDefSize(hardSize);
|
|
136
|
+
if (this.version >>> 8 === 1) size = hardSize > 0 && hardSize !== 4294967295 ? Math.min(hardSize, 9152) : 9152;
|
|
137
|
+
this.raw = Buffer.from(range(data, offset, size, "banner"));
|
|
138
|
+
this.bannerCRC = crc16(this.raw.subarray(32, 2112)) === this.raw.readUInt16LE(2);
|
|
139
|
+
this.tileData = Buffer.from(this.raw.subarray(32, 544));
|
|
140
|
+
this.palette = Buffer.from(this.raw.subarray(544, 576));
|
|
141
|
+
for (const [i, title] of titles.entries())
|
|
142
|
+
this[title] = i < this.titleCount ? this.raw.subarray(576 + i * 256, 832 + i * 256).toString("utf16le").split("\0")[0] : "";
|
|
143
|
+
}
|
|
144
|
+
get titleCount() {
|
|
145
|
+
return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
|
|
146
|
+
}
|
|
147
|
+
getDefSize(hardSize = 0) {
|
|
148
|
+
if (this.version === 2) return 2368;
|
|
149
|
+
if (this.version === 3) return 2624;
|
|
150
|
+
if (this.version === 259) return hardSize > 0 && hardSize !== 4294967295 ? hardSize : 9152;
|
|
151
|
+
return 2112;
|
|
152
|
+
}
|
|
153
|
+
toBuffer() {
|
|
154
|
+
const output = Buffer.from(this.raw);
|
|
155
|
+
output.writeUInt16LE(this.version);
|
|
156
|
+
if (this.tileData.length !== 512 || this.palette.length !== 32) throw new RangeError("Invalid banner icon size");
|
|
157
|
+
this.tileData.copy(output, 32);
|
|
158
|
+
this.palette.copy(output, 544);
|
|
159
|
+
for (let i = 0; i < this.titleCount; i++) {
|
|
160
|
+
const text = Buffer.from(this[titles[i]], "utf16le");
|
|
161
|
+
if (text.length > 256) throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
162
|
+
output.fill(0, 576 + i * 256, 832 + i * 256);
|
|
163
|
+
text.copy(output, 576 + i * 256);
|
|
164
|
+
}
|
|
165
|
+
output.writeUInt16LE(crc16(output.subarray(32, 2112)), 2);
|
|
166
|
+
if (this.version >= 2) output.writeUInt16LE(crc16(output.subarray(32, 2368)), 4);
|
|
167
|
+
if (this.version >= 3) output.writeUInt16LE(crc16(output.subarray(32, 2624)), 6);
|
|
168
|
+
if (this.version >>> 8 >= 1) output.writeUInt16LE(crc16(output.subarray(4672, 9152)), 8);
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/nitro/crypto.ts
|
|
174
|
+
var KEY1 = Buffer.from(
|
|
175
|
+
"99D5205F5744F5B96E19A4D99E6A5A94D8AEF1EB4175E23A9382D03233EE31D5CC57619A3706A21B793972F555AEF6BE5F1B69FBE59DF1E9CE2CD9A15E3205E6FED3FECFD462040D8BF5ECB72B6079BB1295310D6E3FDA2B8884F0F13D127E254522F1BB24061A0611ADDF288B6481342BEB332999AAF2BD9C14959D9FF7F58C7297A1299DD15FCF664D071ADED34A4B85C9A7A31795053A3D490ABF0A898BA24A8249DD2790F10BE9EB1C6A83764505BA817061173F4BDEAECFAB3957F23A564811AD8A40E1453FFA9B0254CAA693FBEF4DFE6FA3D8879C08BAD5486A8D2DFD6E15F874BDBE528B18228A9EFB7437071B366C4A19BA4262B97991107B676596FE0223E8EE998C773E5C86644D6D7886A54F65E21EB2DF5A0AD07E0814B071ACBDDB831CB9D7A162CDC6637C5269C3E6BF75CE12445D2104FAFBD33C381163D4958541494609F2084311DC1F76C0156D1F3C6370EA87806CC3BD638BC2372137DCEE09232E376A4D7390F75030AC1C92041023914FD207AA683E4F9AC964606AC81421F3D62241124424CFE68A56DD0D534DE1851E8C525A9C1984C20357F16FE300BE58F64CEDD521649C1FBE55033C4ADCFFAAC9DAE05D5EBFE6DEF5D8B1F8FF36B3B9626795DB315F37ED4C70679990B518316C3D9999E442DAD3254213A0AED7706CB155CFC7D746D54361173D4428E93385D5D0A293AA25121FFBC50B46F597765645A6BE87B1946BE8B1FE3399AE1F3E6C39711D09009037E4103E7574FF8C833BB0F1B0F90105474295F1D6AC7E38E69E9574263FB4685018D04330B44C4BE368BFE54DB6958B0AA074253277CFA1F72CD871135AABEAC951E80DEEEFE9937E19A71E433881162CA148E373CC29216CD35DCEA0D9617143A01513B56492CF2A19DCADB7A59F8665F81A9FE7FBF7FDB8136C27DB6FDF351CF78D2C5B9B12AB386406CCDE31E84E751164E3FAEAEB3454C2AD3F34EB932C7D26369D56F35AE1F6B398634A9E3283E49A84607D902E130EEE934B36A285EC1638E8880602BFF0A03AEDD76A9A73E157CFF844B8DC2E2359D1DF9552719961A04BD57F6E78BAA9C530D34086329D320C9C37B7022FBA5498A9C41304C98DBEC8E75D97502E93D622590C27BC2292E0A7200F936F7F4C9FD3B5A62A0B7467497D1026CBD1C58671E78CA09CE95BB21AF601EE8C9E5E83F21ADBE6E5EA845976D27CF68DA5493648C21652BB83A374B9070C3BFF6128E161E9E4EF6E15AA4EBAE85D0596BB3256B0FB72520F0EC84225657689AFF2DE1027F0014B74A79707D5265454091F820A867D30390EB3269B0B57BB360631AFFD79FCD930102B0CB3E19BD77BDC5FEFD2F813454D4775BD46963C7E75F33EB567C59A3BB05B296BDE805BC81505B131B6CE49DDAD84B5AE60DC67313430FE4EBD802FA6BF63392186D9357F1668220554E990268C076C51A43155D70907A83E2E5366C1F8F27BC4F258CFF187C5A2E7278F308758A064622318B9887CFACEC498AEAD17CC4A5BF3E948D556D30DF2C892738CDBD72F56AC81F992694DC632F6E6C08D21E276806111BCDC6C93AF19699BD0BFB9319F0267A351EE8306227B0CAB494240B8D5017DCE5EF7555339C59946D8879FBAF764B4E39AFAA16D90681030CA8A54A79F60C319F56B0D7A5198E6984351B4D635E94FC3DF0F7BD62F5CBD3A156119F14BCBAADC6D64C9D3C61E56EF384C50718675CC0D0D4EE928F6065D701BAAD345CFA839AC95A62EB4E422D474A8375F487A04CCA54C40D828B428080D1C725241F07D47193A534E5884626B93B58A81214E0DDCB43FA2C6FCC92B40DA3804E95E5A866B0C22258568118D7C921D95554DAB8EBBDAA6E6B751B6325A0541DD052A0A5650911747CCC9E67EB5614ADB736751C833F5DA6E742E54C3370D6DAF08E8158A5FE25921CDA8DE0C065A776B5FDB18653EC850DE78E0B882B35D4E7232074FC13423BA96B7674EA4281E3462EB2D6A70E92F42C4704E5A319CF95B4728AADA716F381FB378C4926B1C9EF6359AB74D0EBFCC1829410348355D55D02BC629AF5C6074698E5E9B7CD4BD7B44647D3F925D69B61F004BD48335CF7E644E17AE8DD52E9A28124E2E2B49085CAEC64685AE41611E6F82D25137161F0BF659A49ACA5AAF0DD4338B2063F184805CCBCF08B4B9D31605BD6283319B5651989FBAB25BAAB2226B2CB5D448FA632B5F58FA61FA6409BB38E0B89D9260A80D676F0E37F50D019FC277D4FEECF1733039E07DF56198E42C2855045655DB2F6BECE55806B664806A2A1A4E5B0FD8C40A2E5219D962F53048BE8C7B4F389BA2C3AFC9D3C7C1624186B96121576F994FC1BACE7BB53B4D5E8A8B44575F135F706D5B2947DC38E2EC045565122AE81743E18EDD2AB3E294F7096E5CE6EB8AF86D89495448F52FADBFEA944BCAFC3987825F8A01F275F2E671D6D842DEF12D1D28A6887EA3A0471D30D9A371DF491CCB01F836B1F2F022585D456BBDA0BBB28842C78C28CE93E8906308907C893CF57DB7042D4F555116FD7E79E8BEC1F212D4F8B4840523A0CCD22BFDE1ABAD0DD1556C2341944D77374F05280CBF17B312676C8CC35AF741842A6DD09412272CB4ED9C4DEC478297D567B91B9DC055077EE58EE2A8E73E12E40E3A2A455534A2F92D5A1BAB527C83105F55D2F15A432BC6A7A4891595E8B44B9DF875E39F60785BD6E60D44E62106BD472253A400AD8D43138539F7AAFC38AF7BEDFCE42B5450984CFC8580F7DF3C8022E194DADE24C6B07A3938DC0FA1A7F4F96F6318578B84412A2ED453F2D9000FD0DD996E19A60AD0EC5B5824ABC0CB0665EC1A1338940A67032F3FF7E377447733C61439D0E3C0A20879BB409957410B0190CDE1CC4867DBB3AF8874F34C828F72B1B52329C4126C19FC8E46A49CC4256587D36DBE8A93110338ED832BF346A493EA3B53851DCED4F1088327EDFC9B1A18BCF98BAEDC24AB5038E9724B1022177B465DAB5964F340AEF8BBE5C8F926034E557DEBEBFEF739E6E00A11BE2E28FF98EDC0C9425642C3FD00F6AF87A25B013F329247959A72A5323DAE6BD09B07D24992E3784AFAA1067DF241CF77740414B20C86846416D5BB51A1E56FF1D1F2E2F75F58204DB857C7CFDDC5D8BE763DF65F7EE72A8B88241B383F0E412377F5F04BD40C1FFAA40B805FCF45F6E0DA2F345953FB203C52625E35B562FE8B6063E3865A151A6ED14745BC32B4EB6738ABE46E333AB5EDA3AD67E04E4195EE626271261D31EF6230AFD782ACC2DC0504F59707BF11592307C06402E897E53EAF18AC59A68B4A33901C6E7C9C207E4C3C3E6164BBC56B7C7E3E9FC54C9FEA73F5D789C04CF4FBF42DEC141B51D5C112C810DF0B4A8B9CBC93456A3E3E7DC1A9BACDC1B407E4E1688643B26D38F3FB0C5C663771DE56EF6EA0104065A798F7D0BE0EC83736EC10CA7C9CAB841E051776021C4F52AA5FC1C6A056B9D80484444DA759D8DE60E6380E058F03E13B6D8104336F300BCE69052133FB26BB897DB6AE877E5107E0ACF7960A6BF9C45C1DE44447B85EFAE3788455424B485EF77D4735861D2B430503EC8AB81E063C760C481A43A7B78AED1E13C643EE10EFDBECFB3C83B29544EFD854514E2D11441DFB36591E7A34C1C3CA570061EA67A5169B55D055E17FD936D24076AEDC01CEB07A83D5CB2098EC6BC1729234F3825737628A32360C9043AEAE5C9B788E136502FD6871C1FEB031A02482B0C3B17969A7F5D2EBD082C032DC9EC7263C6D8D98C1BB22D4D00F33EC3EB9CCE1DC6A4C7736141CF9BF819F285F71853229907548C4B34ACED8448F142FFD4057EFAA0875D946D1D66E32551FC318FE841FFC84D5FF715E1B48C386950E280827D33883717B4C8063549A56B0ACCF80CA3109EFFEF3BEAF247EA6FE533FC28D4A3368D122A666AD7BEADEB643B0A1259500A33F7546141144ECD795BC92F04FA916536297602A0F41F17124BEEE947F08CD6093B3855B07003FD80F28839AD1699FD1DA2EC39001A2B96B4E2A669DDAAEA6EA2AD3682F0C0C9CD28C4AEDE29E57659D0987A3B4C4325DC9D4322BB1E0711E644DE69071E31E40ED7DF3840EEDC87876AEC0712772BB05EA0264FBF3486BB542933FED9F1353D2F7FE2AEC1D4725DB3C9186C68EF011FD237436F7A4F59E7A7E535044D447CAD3EB386DE6D971947F4AC6694B11F452EA22FE8AB036678B59E8E6802AEB650413EEECDC9E5FB1EC056A59E69F5E596B89BFF71ACA44F95B6A718503E42962E0706F41C4CFB2B1CCE37EA607A887E77F8493DB524B6CEC7EDDD4244810699F046074E64818F3E42CB94F2E507ADFD454692B8BA7F3CEFF1FF33E26013917958489B0F04C4B82919FC44BAC9DA574AF1725C9CA32D3BC898A8489CC0DAE7CA2DB9C6A7891EEEA765D4E8760F5691567D402CFAF483607EABF6F662D068FC49AFEF9F6908775B8F7AD0F76105A3D59B02EB3C7352CCC70562BCBE33796C52F461B8A2246C788A726329861DF86228AF41C2F87A109AACCA9AED3BD00451C9A5487865287EFFF1E8FA18FC1895C351BDA2D3A2C16B2C2F156E278C16B6397C5568FC9327F2CAAAFA6A8AC20912288DEE4608BF94B42251AE37F9C2C19893A7E05D436CC6958C2C1328B2F9085EB7A3950A5A12792C566B0204F587E5583432B45E29CE4D812902C168356167903B3AD2D61181A131F37E2E19C737B80D5FD2D5187FC7BAAD71F2C7A8EAFF48DBBCD95117C720BEE6FE2B9AFDE3783DE8C8D620567B796C68D56B60DD762BAD64636BD8EC8E6EA2A6C1014FF6B5BFA823C46B1304346518A7D9B923E83795B555DB26C5ECE90628E5398C90D6DE52D57CDC58157BAE1E8B88F72E54F13DCEA9D711510B21188D509D47F5B657F2C3B384C1168508DFB9EB059BF9480894AC51A18128953D14A1029E88C1CECB6EA46C7178B251531A8A26B43B19DE2DB0B879BB011040E71D2297789820A66417F1D0B48FF72BB24FDC248A19BFE7B7FCE88DB86D9853B1CB0DCA83307BF512EE30E9A00971E06C097439DD8B645C486675F00F8889AA4529EC7AA8A8375ECC518AECEC32F1A2BF918FFAE1AF5530BB53351A7FDE8A8E1A264B622174380CC0AD8AE3BBA40D7D9924A89DF0410EE9B182B6A77698A68F4F9B9A221156EE61E3B0362309B60417E259B9E8FC5521008F8C269A1211188375E793566FF1042186EED97B66B1C4E36E56D7DB4E4BF20B9E0053A69D5B8E3D5DCE0B9AC533E07A457AD77FF4818762AAC492A8E47756D9F676330358C390539D56F643A5BADCA0BBB82529945B193363699AF13204436D8024409399285FF4A4A9787A663D7C7B5B524ED0FB46F0C585214D9A67BD379BC3858A1BD3B8406D81A06FD6BA8EA4B69280437AD8299FB0E1B85BDA85D73CDDC58750ABE636C48E74CE4302B0460B915D8DA8681758F96D48D1C5D70857C1C677BD50867A6CE4B0A6670B7E563D45B8A82EA1067CAE2F4EF17852F2A5F8A9782F86AD63410EAEBC95C3CE149F846EBDEBDF6A992F1AAA6A018B03AD30F1FF36FFF31454344D3509AF7880996C1CE76CCF22C2CBAAD82778F1884C0D2079C3690834E0BA54F433E04AB784FD6FB09012490DA6F3C3A610D7F694AEB2B3002B4DBE084A9ECD735BF377D8558CEA94EE480C7A8D3306748EB29AF2F746AB4A73F0F3F92AFF3CAACAF4BD994C043CA810D2F48A1B027D5D2EF4B0585A3DE4D93303CF0BB4A8F30274CEBE33E64ED9A2F3BF182F0BAF4CF7F40CBB0E17FBCAA57D3C974F2FA430D22D0F4774E93D785701F99BFB6DE35F130A75E71F06B012D7B64F033530A3988F36B3AA66B35D22F43CD02FDB5E9BC5BAAD8A4197E0E5D94819E6F77ADD60E749396E7C4185FADF519",
|
|
176
|
+
"hex"
|
|
177
|
+
);
|
|
178
|
+
var aes128CtrCrypt = (key2, counter, data) => {
|
|
179
|
+
if (key2.length !== 16 || counter.length !== 16) throw new RangeError("AES key and counter must have 16 bytes");
|
|
180
|
+
const ctr = Buffer.from(counter).reverse();
|
|
181
|
+
const aes = createAesBlockCipher(Buffer.from(key2).reverse());
|
|
182
|
+
const output = Buffer.alloc(data.length);
|
|
183
|
+
for (let p = 0; p < data.length; p += 16) {
|
|
184
|
+
const mask = aes(ctr);
|
|
185
|
+
for (let j = 0; j < 16 && p + j < data.length; j++) output[p + j] = data[p + j] ^ mask[15 - j];
|
|
186
|
+
for (let j = 15; j >= 0; j--) {
|
|
187
|
+
ctr[j] = ctr[j] + 1 & 255;
|
|
188
|
+
if (ctr[j]) break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return output;
|
|
192
|
+
};
|
|
193
|
+
var modcryptKey = (header) => {
|
|
194
|
+
if (typeof header.gameCode !== "string" || header.gameCode.length !== 4)
|
|
195
|
+
throw new Error("Expected four-character game code");
|
|
196
|
+
if (header.twlInternalFlags & 4 || (header.appflags?.[3] ?? 0) & 128) {
|
|
197
|
+
const key2 = Buffer.alloc(16);
|
|
198
|
+
key2.write(header.gameTitle, 0, 12, "ascii");
|
|
199
|
+
key2.write(header.gameCode, 12, 4, "ascii");
|
|
200
|
+
return key2;
|
|
201
|
+
}
|
|
202
|
+
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16) throw new Error("Missing ARM9i HMAC for modcrypt key");
|
|
203
|
+
const x = Buffer.from(`Nintendo${header.gameCode}${[...header.gameCode].reverse().join("")}`, "ascii");
|
|
204
|
+
for (let i = 0; i < 16; i++) x[i] ^= header.hmac_arm9i[i];
|
|
205
|
+
const mask = (1n << 128n) - 1n;
|
|
206
|
+
let v = x.readBigUInt64LE(0) | x.readBigUInt64LE(8) << 64n;
|
|
207
|
+
v = v + 0xfffefb4e295902582a680f5f1a4f3e79n & mask;
|
|
208
|
+
v = (v << 42n | v >> 86n) & mask;
|
|
209
|
+
const out = Buffer.alloc(16);
|
|
210
|
+
out.writeBigUInt64LE(v & (1n << 64n) - 1n);
|
|
211
|
+
out.writeBigUInt64LE(v >> 64n, 8);
|
|
212
|
+
return out;
|
|
213
|
+
};
|
|
214
|
+
var encryptSecureArea = (gameCode, input, refreshCrc = false) => secureAreaCrypt(gameCode, input, false, refreshCrc);
|
|
215
|
+
var decryptSecureArea = (gameCode, input) => secureAreaCrypt(gameCode, input, true);
|
|
216
|
+
var secureAreaCrypt = (gameCode, input, decrypting, refreshCrc = false) => {
|
|
217
|
+
if (typeof gameCode !== "string" || gameCode.length !== 4) throw new Error("Expected four-character game code");
|
|
218
|
+
if (input.length < 2048) throw new RangeError("Secure area must contain at least 0x800 bytes");
|
|
219
|
+
const output = Buffer.from(input);
|
|
220
|
+
if (refreshCrc) output.writeUInt16LE(crc16(output.subarray(16, 2048)), 14);
|
|
221
|
+
let magic = new Uint32Array(1042);
|
|
222
|
+
let args = [0, 0, 0];
|
|
223
|
+
function crypt(a2, b2, reverse = false) {
|
|
224
|
+
for (let i = 0; i < 16; i++) {
|
|
225
|
+
const c = (magic[reverse ? 17 - i : i] ^ a2) >>> 0;
|
|
226
|
+
const sum = magic[18 + (c >>> 24)] + magic[274 + (c >>> 16 & 255)] >>> 0;
|
|
227
|
+
const f = magic[786 + (c & 255)] + (magic[530 + (c >>> 8 & 255)] ^ sum) >>> 0;
|
|
228
|
+
a2 = (b2 ^ f) >>> 0;
|
|
229
|
+
b2 = c;
|
|
230
|
+
}
|
|
231
|
+
return [(b2 ^ magic[reverse ? 0 : 17]) >>> 0, (a2 ^ magic[reverse ? 1 : 16]) >>> 0];
|
|
232
|
+
}
|
|
233
|
+
function init2() {
|
|
234
|
+
[args[2], args[1]] = crypt(args[2], args[1]);
|
|
235
|
+
[args[1], args[0]] = crypt(args[1], args[0]);
|
|
236
|
+
const key2 = Buffer.alloc(12);
|
|
237
|
+
args.forEach((v, i) => key2.writeUInt32LE(v, i * 4));
|
|
238
|
+
for (let j = 0; j < 18; j++) magic[j] ^= key2.readUInt32BE(j * 4 & 7);
|
|
239
|
+
let a2 = 0, b2 = 0;
|
|
240
|
+
for (let i = 0; i < 1042; i += 2) {
|
|
241
|
+
[a2, b2] = crypt(a2, b2);
|
|
242
|
+
magic[i] = a2;
|
|
243
|
+
magic[i + 1] = b2;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function init1() {
|
|
247
|
+
magic = Uint32Array.from({ length: 1042 }, (_, i) => KEY1.readUInt32LE(i * 4));
|
|
248
|
+
const code = Buffer.from(gameCode, "ascii").readUInt32LE();
|
|
249
|
+
args = [code, code >>> 1, code << 1 >>> 0];
|
|
250
|
+
init2();
|
|
251
|
+
init2();
|
|
252
|
+
}
|
|
253
|
+
init1();
|
|
254
|
+
if (decrypting) {
|
|
255
|
+
const [b2, a2] = crypt(output.readUInt32LE(4), output.readUInt32LE(0), true);
|
|
256
|
+
output.writeUInt32LE(a2, 0);
|
|
257
|
+
output.writeUInt32LE(b2, 4);
|
|
258
|
+
}
|
|
259
|
+
args[1] = args[1] << 1 >>> 0;
|
|
260
|
+
args[2] >>>= 1;
|
|
261
|
+
init2();
|
|
262
|
+
for (let p = decrypting ? 0 : 8; p < 2048; p += 8) {
|
|
263
|
+
const [b2, a2] = crypt(output.readUInt32LE(p + 4), output.readUInt32LE(p), decrypting);
|
|
264
|
+
output.writeUInt32LE(a2, p);
|
|
265
|
+
output.writeUInt32LE(b2, p + 4);
|
|
266
|
+
}
|
|
267
|
+
if (decrypting) {
|
|
268
|
+
if (output.readUInt32LE(0) !== 1919118949 || output.readUInt32LE(4) !== 1784827769)
|
|
269
|
+
throw new Error("Invalid KEY1 secure-area marker");
|
|
270
|
+
output.writeBigUInt64LE(0xe7ffdeffe7ffdeffn);
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
273
|
+
let [b, a] = crypt(1784827769, 1919118949);
|
|
274
|
+
init1();
|
|
275
|
+
[b, a] = crypt(b, a);
|
|
276
|
+
output.writeUInt32LE(a, 0);
|
|
277
|
+
output.writeUInt32LE(b, 4);
|
|
278
|
+
return output;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/nitro/filesystem.ts
|
|
282
|
+
import iconv from "iconv-lite";
|
|
283
|
+
var encodeName = (name) => {
|
|
284
|
+
const encoded = iconv.encode(name, "shift_jis");
|
|
285
|
+
if (new TextDecoder("shift_jis").decode(encoded) !== name)
|
|
286
|
+
throw new Error(`FNT name cannot be represented in Shift_JIS: ${name}`);
|
|
287
|
+
if (!encoded.length || encoded.length > 127) throw new Error("FNT name must contain 1 to 127 encoded bytes");
|
|
288
|
+
return encoded;
|
|
289
|
+
};
|
|
290
|
+
var FileAllocationTable = class {
|
|
291
|
+
entries;
|
|
292
|
+
sortedIDs;
|
|
293
|
+
constructor(input, offset, size) {
|
|
294
|
+
const data = bytes(input);
|
|
295
|
+
const table = range(data, offset, size, "FAT");
|
|
296
|
+
if (size % 8 || size / 8 > 65535) throw new Error("Invalid FAT entry count");
|
|
297
|
+
this.entries = [];
|
|
298
|
+
for (let p = 0; p < size; p += 8) {
|
|
299
|
+
const start = table.readUInt32LE(p), end = table.readUInt32LE(p + 4);
|
|
300
|
+
range(data, start, end - start, `FAT entry ${p / 8}`);
|
|
301
|
+
this.entries.push({ id: p / 8, offset: start, size: end - start });
|
|
302
|
+
}
|
|
303
|
+
this.sortedIDs = [...this.entries].sort((a, b) => a.offset - b.offset || a.id - b.id).map((entry) => entry.id);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
var FileNameTable = class _FileNameTable {
|
|
307
|
+
root;
|
|
308
|
+
constructor(fat, input, offset, size) {
|
|
309
|
+
const table = range(bytes(input), offset, size, "FNT");
|
|
310
|
+
range(table, 0, 8, "FNT root directory");
|
|
311
|
+
const count = table.readUInt16LE(6);
|
|
312
|
+
if (count < 1 || count > 4096) throw new Error("Invalid FNT directory count");
|
|
313
|
+
range(table, 0, count * 8, "FNT directory table");
|
|
314
|
+
const decoder = new TextDecoder("shift_jis");
|
|
315
|
+
const visited = /* @__PURE__ */ new Set();
|
|
316
|
+
const fileIds = /* @__PURE__ */ new Set();
|
|
317
|
+
const parse = (index, name, depth) => {
|
|
318
|
+
if (depth > 256 || index >= count || visited.has(index)) throw new Error("Invalid FNT directory tree or cycle");
|
|
319
|
+
visited.add(index);
|
|
320
|
+
let cursor = table.readUInt32LE(index * 8);
|
|
321
|
+
let fileId = table.readUInt16LE(index * 8 + 4);
|
|
322
|
+
if (cursor < count * 8) throw new Error("FNT directory names overlap directory table");
|
|
323
|
+
const folder = {
|
|
324
|
+
name,
|
|
325
|
+
id: index === 0 ? 65535 : 61440 + index,
|
|
326
|
+
files: [],
|
|
327
|
+
folders: []
|
|
328
|
+
};
|
|
329
|
+
const names = /* @__PURE__ */ new Set();
|
|
330
|
+
while (true) {
|
|
331
|
+
const tag = range(table, cursor++, 1, "FNT name")[0];
|
|
332
|
+
if (!tag) break;
|
|
333
|
+
const length = tag & 127;
|
|
334
|
+
if (!length) throw new Error("Empty FNT directory name");
|
|
335
|
+
const nameBytes = Buffer.from(range(table, cursor, length, "FNT filename"));
|
|
336
|
+
const filename = decoder.decode(nameBytes);
|
|
337
|
+
cursor += length;
|
|
338
|
+
if (filename.includes("/") || filename.includes("\\") || filename.includes("\0") || filename === "." || filename === "..")
|
|
339
|
+
throw new Error("Invalid FNT path component");
|
|
340
|
+
const normalized = filename.toLowerCase();
|
|
341
|
+
if (names.has(normalized)) throw new Error(`Duplicate FNT path: ${filename}`);
|
|
342
|
+
names.add(normalized);
|
|
343
|
+
if (tag & 128) {
|
|
344
|
+
const id = range(table, cursor, 2, "FNT directory id").readUInt16LE();
|
|
345
|
+
cursor += 2;
|
|
346
|
+
if (id < 61440) throw new Error("Invalid FNT directory id");
|
|
347
|
+
const child = parse(id & 4095, filename, depth + 1);
|
|
348
|
+
child.nameBytes = nameBytes;
|
|
349
|
+
folder.folders.push(child);
|
|
350
|
+
} else {
|
|
351
|
+
const entry = fat.entries[fileId++];
|
|
352
|
+
if (!entry) throw new Error("FNT file id is outside FAT");
|
|
353
|
+
if (fileIds.has(entry.id)) throw new Error(`Duplicate FNT file id: ${entry.id}`);
|
|
354
|
+
fileIds.add(entry.id);
|
|
355
|
+
folder.files.push({ name: filename, nameBytes, ...entry });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return folder;
|
|
359
|
+
};
|
|
360
|
+
this.root = { name: "root", id: 65535, files: [], folders: [parse(0, "data", 0)] };
|
|
361
|
+
}
|
|
362
|
+
serialize(fat, overlays) {
|
|
363
|
+
const directories = [];
|
|
364
|
+
const named = [];
|
|
365
|
+
const sort = (items) => [...items].sort((a, b) => Buffer.compare(a.nameBytes ?? encodeName(a.name), b.nameBytes ?? encodeName(b.name)));
|
|
366
|
+
const visit = (folder, parent) => {
|
|
367
|
+
const index = directories.length;
|
|
368
|
+
directories.push({ folder, parent, firstId: named.length });
|
|
369
|
+
if (directories.length > 4096) throw new Error("FNT directory count exceeds 4096");
|
|
370
|
+
named.push(...sort(folder.files));
|
|
371
|
+
for (const child of sort(folder.folders)) visit(child, 61440 + index);
|
|
372
|
+
};
|
|
373
|
+
visit(this.root.folders[0], 61440);
|
|
374
|
+
const namedIds = new Set(named.filter((file) => file.id >= 0).map((file) => file.id));
|
|
375
|
+
const overlayIds = new Set(overlays.map((file) => file.id));
|
|
376
|
+
const unnamed = fat.entries.filter((entry) => !namedIds.has(entry.id)).map((entry) => ({ ...entry, name: `FAT entry ${entry.id}` }));
|
|
377
|
+
const files = [
|
|
378
|
+
...unnamed.filter((entry) => overlayIds.has(entry.id)),
|
|
379
|
+
...overlays.filter((file) => file.id < 0).sort(
|
|
380
|
+
(a, b) => Number(a.name.startsWith("overlay7_")) - Number(b.name.startsWith("overlay7_")) || Number(a.name.slice(a.name.indexOf("_") + 1, -4)) - Number(b.name.slice(b.name.indexOf("_") + 1, -4))
|
|
381
|
+
),
|
|
382
|
+
...unnamed.filter((entry) => !overlayIds.has(entry.id))
|
|
383
|
+
];
|
|
384
|
+
const firstNamed = files.length;
|
|
385
|
+
files.push(...named);
|
|
386
|
+
if (files.length > 61440) throw new Error("Filesystem file IDs exceed 0xEFFF");
|
|
387
|
+
const ids = new Map(files.map((file, id) => [file, id]));
|
|
388
|
+
const sourceIds = new Map(files.filter((file) => file.id >= 0).map((file) => [file.id, ids.get(file)]));
|
|
389
|
+
const directoryIds = new Map(directories.map(({ folder }, index) => [folder, 61440 + index]));
|
|
390
|
+
const writer = new BinaryWriter();
|
|
391
|
+
writer.write(Buffer.alloc(directories.length * 8));
|
|
392
|
+
for (const [index, { folder, parent, firstId }] of directories.entries()) {
|
|
393
|
+
const record = Buffer.alloc(8);
|
|
394
|
+
record.writeUInt32LE(writer.position);
|
|
395
|
+
record.writeUInt16LE(firstNamed + firstId, 4);
|
|
396
|
+
record.writeUInt16LE(index === 0 ? directories.length : parent, 6);
|
|
397
|
+
record.copy(writer.buffer, index * 8);
|
|
398
|
+
for (const file of sort(folder.files)) {
|
|
399
|
+
const name = file.nameBytes ?? encodeName(file.name);
|
|
400
|
+
writer.write(Buffer.from([name.length]));
|
|
401
|
+
writer.write(name);
|
|
402
|
+
}
|
|
403
|
+
for (const child of sort(folder.folders)) {
|
|
404
|
+
const name = child.nameBytes ?? encodeName(child.name);
|
|
405
|
+
writer.write(Buffer.from([128 | name.length]));
|
|
406
|
+
writer.write(name);
|
|
407
|
+
const id = Buffer.alloc(2);
|
|
408
|
+
id.writeUInt16LE(directoryIds.get(child));
|
|
409
|
+
writer.write(id);
|
|
410
|
+
}
|
|
411
|
+
writer.write(Buffer.from([0]));
|
|
412
|
+
}
|
|
413
|
+
return { fnt: writer.toBuffer(), files, ids, sourceIds };
|
|
414
|
+
}
|
|
415
|
+
static findFile(id, folder) {
|
|
416
|
+
for (const file of folder.files) if (file.id === id) return file;
|
|
417
|
+
for (const subfolder of folder.folders) {
|
|
418
|
+
const file = _FileNameTable.findFile(id, subfolder);
|
|
419
|
+
if (file) return file;
|
|
420
|
+
}
|
|
421
|
+
return void 0;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var overlayFields = [
|
|
425
|
+
"overlayId",
|
|
426
|
+
"ramAddress",
|
|
427
|
+
"ramSize",
|
|
428
|
+
"bssSize",
|
|
429
|
+
"staticInitialiserStartAddress",
|
|
430
|
+
"staticInitialiserEndAddress",
|
|
431
|
+
"fileId",
|
|
432
|
+
"reserved"
|
|
433
|
+
];
|
|
434
|
+
var OverlayTable = class {
|
|
435
|
+
constructor(input, offset, size, isArm9) {
|
|
436
|
+
this.isArm9 = isArm9;
|
|
437
|
+
if (!size) return;
|
|
438
|
+
const table = range(bytes(input), offset, size, "overlay table");
|
|
439
|
+
if (size % 32) throw new Error("Invalid overlay table size");
|
|
440
|
+
for (let p = 0; p < size; p += 32) {
|
|
441
|
+
if (table.readUInt32LE(p) === 4294967295) break;
|
|
442
|
+
this.entries.push({
|
|
443
|
+
overlayId: table.readUInt32LE(p),
|
|
444
|
+
ramAddress: table.readUInt32LE(p + 4),
|
|
445
|
+
ramSize: table.readUInt32LE(p + 8),
|
|
446
|
+
bssSize: table.readUInt32LE(p + 12),
|
|
447
|
+
staticInitialiserStartAddress: table.readUInt32LE(p + 16),
|
|
448
|
+
staticInitialiserEndAddress: table.readUInt32LE(p + 20),
|
|
449
|
+
fileId: table.readUInt32LE(p + 24),
|
|
450
|
+
reserved: table.readUInt32LE(p + 28)
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
isArm9;
|
|
455
|
+
entries = [];
|
|
456
|
+
readBasicOverlays(fat) {
|
|
457
|
+
return this.entries.map((item) => {
|
|
458
|
+
const file = fat.entries[item.fileId];
|
|
459
|
+
if (!file) throw new Error("Overlay file id is outside FAT");
|
|
460
|
+
return {
|
|
461
|
+
...file,
|
|
462
|
+
name: `overlay${this.isArm9 ? "" : "7"}_${String(item.overlayId).padStart(4, "0")}.bin`
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
toBuffer() {
|
|
467
|
+
const data = Buffer.alloc(this.entries.length * 32);
|
|
468
|
+
this.entries.forEach(
|
|
469
|
+
(entry, i) => overlayFields.forEach((field2, j) => data.writeUInt32LE(entry[field2], i * 32 + j * 4))
|
|
470
|
+
);
|
|
471
|
+
return data;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/nitro/header.ts
|
|
476
|
+
var UnitCode = Object.freeze({ NDS: 0, NDS_DSi: 2, DSi: 3 });
|
|
477
|
+
var Header = class {
|
|
478
|
+
raw;
|
|
479
|
+
trimmedRom;
|
|
480
|
+
doublePadding;
|
|
481
|
+
nitrocode;
|
|
482
|
+
decrypted;
|
|
483
|
+
headerCRC;
|
|
484
|
+
logoCRC;
|
|
485
|
+
secureCRC;
|
|
486
|
+
dlp_signature;
|
|
487
|
+
constructor(input) {
|
|
488
|
+
const data = bytes(input, "header");
|
|
489
|
+
range(data, 0, 368, "header");
|
|
490
|
+
const size = data.readUInt32LE(132);
|
|
491
|
+
if (size < 368 || size > data.length) throw new RangeError(`Invalid header size: ${size}`);
|
|
492
|
+
this.raw = Buffer.from(data.subarray(0, size));
|
|
493
|
+
if (this.unitCode & 2 && size > 512) range(this.raw, 0, 4096, "DSi header");
|
|
494
|
+
this.trimmedRom = this.total_rom_size ? this.total_rom_size >= data.length : data.length !== this.size;
|
|
495
|
+
this.doublePadding = this.ARM9size % 1024 < 512 && this.ARM7romOffset % 1024 === 0 && this.ARM9overlayOffset % 1024 === 0 || this.ARM7size % 1024 < 512 && this.FNToffset % 1024 === 0 && this.ARM7overlayOffset % 1024 === 0 || this.FNTsize % 1024 < 512 && this.FAToffset % 1024 === 0 || this.FATsize % 1024 < 512 && this.bannerOffset % 1024 === 0;
|
|
496
|
+
const nitroOffset = this.ARM9romOffset + this.ARM9size;
|
|
497
|
+
this.nitrocode = nitroOffset >= size && nitroOffset + 4 <= data.length && data.readUInt32LE(nitroOffset) === 3737126433;
|
|
498
|
+
this.decrypted = data.length >= 16392 && data.readBigUInt64LE(16384) === 0xe7ffdeffe7ffdeffn;
|
|
499
|
+
this.headerCRC = crc16(this.raw.subarray(0, 350)) === this.headerCRC16;
|
|
500
|
+
this.logoCRC = crc16(this.logo) === this.logoCRC16;
|
|
501
|
+
const secure = data.subarray(16384, 32768);
|
|
502
|
+
this.secureCRC = secure.length > 0 && crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) === this.secureCRC16;
|
|
503
|
+
this.dlp_signature = Buffer.alloc(0);
|
|
504
|
+
if (this.ROMsize >= size && this.ROMsize + 136 <= data.length && data.readUInt32LE(this.ROMsize) === 90977) {
|
|
505
|
+
this.dlp_signature = Buffer.from(data.subarray(this.ROMsize, this.ROMsize + 136));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
get size() {
|
|
509
|
+
return 2 ** (17 + this.raw[20]);
|
|
510
|
+
}
|
|
511
|
+
set size(value) {
|
|
512
|
+
const power = Math.log2(value);
|
|
513
|
+
if (!Number.isInteger(power) || power < 17 || power > 32)
|
|
514
|
+
throw new RangeError("ROM capacity must be a power of two from 128 KiB to 4 GiB");
|
|
515
|
+
this.raw[20] = power - 17;
|
|
516
|
+
}
|
|
517
|
+
toBuffer() {
|
|
518
|
+
const data = Buffer.from(this.raw);
|
|
519
|
+
data.writeUInt16LE(crc16(data.subarray(192, 348)), 348);
|
|
520
|
+
data.writeUInt16LE(crc16(data.subarray(0, 350)), 350);
|
|
521
|
+
return data;
|
|
522
|
+
}
|
|
523
|
+
toString() {
|
|
524
|
+
return `NDS header: ${this.gameTitle.replace(/\0+$/, "")} (${this.gameCode})`;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
function field(name, offset, length, type = "buffer", dsi = false) {
|
|
528
|
+
Object.defineProperty(Header.prototype, name, {
|
|
529
|
+
enumerable: true,
|
|
530
|
+
get() {
|
|
531
|
+
if (dsi && (!(this.unitCode & 2) || this.raw.length <= 512))
|
|
532
|
+
return type === "buffer" ? Buffer.alloc(length) : 0;
|
|
533
|
+
const data = range(this.raw, offset, length, `header ${name}`);
|
|
534
|
+
if (type === "string") return data.toString("latin1");
|
|
535
|
+
if (type === "buffer") return data;
|
|
536
|
+
return length === 8 ? data.readBigUInt64LE() : data.readUIntLE(0, length);
|
|
537
|
+
},
|
|
538
|
+
set(value) {
|
|
539
|
+
const target = range(this.raw, offset, length, `header ${name}`);
|
|
540
|
+
if (type === "string") {
|
|
541
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "latin1") > length)
|
|
542
|
+
throw new TypeError(`Invalid ${name}`);
|
|
543
|
+
target.fill(0);
|
|
544
|
+
target.write(value, "latin1");
|
|
545
|
+
} else if (type === "buffer") {
|
|
546
|
+
if (!(value instanceof Uint8Array)) throw new TypeError(`Invalid ${name}`);
|
|
547
|
+
const source = bytes(value, name);
|
|
548
|
+
if (source.length !== length) throw new RangeError(`${name} must contain ${length} bytes`);
|
|
549
|
+
source.copy(target);
|
|
550
|
+
} else if (length === 8) {
|
|
551
|
+
if (typeof value !== "bigint") throw new TypeError(`Invalid ${name}`);
|
|
552
|
+
target.writeBigUInt64LE(value);
|
|
553
|
+
} else {
|
|
554
|
+
if (typeof value !== "number") throw new TypeError(`Invalid ${name}`);
|
|
555
|
+
target.writeUIntLE(value, 0, length);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
field("gameTitle", 0, 12, "string");
|
|
561
|
+
field("gameCode", 12, 4, "string");
|
|
562
|
+
field("makerCode", 16, 2, "string");
|
|
563
|
+
for (const [name, offset] of Object.entries({
|
|
564
|
+
unitCode: 18,
|
|
565
|
+
encryptionSeed: 19,
|
|
566
|
+
twlInternalFlags: 28,
|
|
567
|
+
permitsFlags: 29,
|
|
568
|
+
ROMversion: 30,
|
|
569
|
+
internalFlags: 31
|
|
570
|
+
}))
|
|
571
|
+
field(name, offset, 1, "number");
|
|
572
|
+
field("reserved", 21, 7);
|
|
573
|
+
var ntr32 = "ARM9romOffset ARM9entryAddress ARM9ramAddress ARM9size ARM7romOffset ARM7entryAddress ARM7ramAddress ARM7size FNToffset FNTsize FAToffset FATsize ARM9overlayOffset ARM9overlaySize ARM7overlayOffset ARM7overlaySize flagsRead flagsInit bannerOffset".split(
|
|
574
|
+
" "
|
|
575
|
+
);
|
|
576
|
+
ntr32.forEach((name, i) => field(name, 32 + i * 4, 4, "number"));
|
|
577
|
+
field("secureCRC16", 108, 2, "number");
|
|
578
|
+
field("ROMtimeout", 110, 2, "number");
|
|
579
|
+
field("ARM9autoload", 112, 4, "number");
|
|
580
|
+
field("ARM7autoload", 116, 4, "number");
|
|
581
|
+
field("secureDisable", 120, 8, "number");
|
|
582
|
+
field("ROMsize", 128, 4, "number");
|
|
583
|
+
field("headerSize", 132, 4, "number");
|
|
584
|
+
field("reserved2", 136, 56);
|
|
585
|
+
field("ntr_region_end", 144, 2, "number");
|
|
586
|
+
field("twl_region_start", 146, 2, "number");
|
|
587
|
+
field("logo", 192, 156);
|
|
588
|
+
field("logoCRC16", 348, 2, "number");
|
|
589
|
+
field("headerCRC16", 350, 2, "number");
|
|
590
|
+
"debug_romOffset debug_size debug_ramAddress reserved3".split(" ").forEach((name, i) => field(name, 352 + i * 4, 4, "number"));
|
|
591
|
+
field("appflags", 444, 4, "buffer", true);
|
|
592
|
+
"mbk9_wramcnt_setting region_flags access_control scfg_ext_mask".split(" ").forEach((name, i) => field(name, 428 + i * 4, 4, "number", true));
|
|
593
|
+
var twl32 = "dsi9_rom_offset offset_0x1C4 dsi9_ram_address dsi9_size dsi7_rom_offset offset_0x1D4 dsi7_ram_address dsi7_size digest_ntr_start digest_ntr_size digest_twl_start digest_twl_size sector_hashtable_start sector_hashtable_size block_hashtable_start block_hashtable_size digest_sector_size digest_block_sectorcount banner_size offset_0x20C total_rom_size offset_0x214 offset_0x218 offset_0x21C modcrypt1_start modcrypt1_size modcrypt2_start modcrypt2_size tid_low tid_high public_sav_size private_sav_size".split(
|
|
594
|
+
" "
|
|
595
|
+
);
|
|
596
|
+
twl32.forEach((name, i) => field(name, 448 + i * 4, 4, "number", true));
|
|
597
|
+
"hmac_arm9 hmac_arm7 hmac_digest_master hmac_icon_title hmac_arm9i hmac_arm7i".split(" ").forEach((name, i) => field(name, 768 + i * 20, 20, "buffer", true));
|
|
598
|
+
field("hmac_arm9_no_secure", 928, 20, "buffer", true);
|
|
599
|
+
field("rsa_signature", 3968, 128, "buffer", true);
|
|
600
|
+
|
|
601
|
+
// src/nitro/blz.ts
|
|
602
|
+
var decompressBLZ = (input, maxSize = 268435456) => {
|
|
603
|
+
if (input.length < 8) throw new Error("BLZ footer is truncated");
|
|
604
|
+
const diff = input.readUInt32LE(input.length - 4);
|
|
605
|
+
const packed = input.readUInt32LE(input.length - 8), header = packed >>> 24, length = packed & 16777215;
|
|
606
|
+
const size = input.length + diff, prefix = input.length - length;
|
|
607
|
+
if (header < 8 || header > length || length > input.length || size > maxSize)
|
|
608
|
+
throw new Error("Invalid BLZ size or footer");
|
|
609
|
+
const out = Buffer.alloc(size);
|
|
610
|
+
input.copy(out, 0, 0, prefix);
|
|
611
|
+
let src = input.length - header, dst = size;
|
|
612
|
+
while (src > prefix) {
|
|
613
|
+
const flags = input[--src];
|
|
614
|
+
for (let bit = 128; bit && src > prefix; bit >>= 1) {
|
|
615
|
+
if (!(flags & bit)) {
|
|
616
|
+
if (dst <= prefix) throw new Error("BLZ output overflow");
|
|
617
|
+
out[--dst] = input[--src];
|
|
618
|
+
} else {
|
|
619
|
+
if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
|
|
620
|
+
const high = input[--src], low = input[--src];
|
|
621
|
+
const count = (high >>> 4) + 3, offset = ((high & 15) << 8 | low) + 3;
|
|
622
|
+
if (dst - count < prefix || dst - 1 + offset >= size) throw new Error("Invalid BLZ reference");
|
|
623
|
+
for (let j = 0; j < count; j++) {
|
|
624
|
+
--dst;
|
|
625
|
+
out[dst] = out[dst + offset];
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (dst !== prefix) throw new Error("BLZ decompressed size mismatch");
|
|
631
|
+
return out;
|
|
632
|
+
};
|
|
633
|
+
var compressBLZ = (input) => {
|
|
634
|
+
if (input.length > 16777215) throw new Error("BLZ input exceeds 24-bit format limit");
|
|
635
|
+
const temp = Buffer.alloc(input.length);
|
|
636
|
+
let src = input.length, dst = input.length;
|
|
637
|
+
const heads = new Int32Array(256).fill(-1);
|
|
638
|
+
const next = new Int32Array(input.length);
|
|
639
|
+
let indexedFrom = input.length;
|
|
640
|
+
while (src > 0) {
|
|
641
|
+
if (dst < 1) return Buffer.alloc(0);
|
|
642
|
+
const flagPos = --dst;
|
|
643
|
+
let flags = 0;
|
|
644
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
645
|
+
flags <<= 1;
|
|
646
|
+
if (!src) continue;
|
|
647
|
+
while (indexedFrom > src) {
|
|
648
|
+
const p = --indexedFrom;
|
|
649
|
+
next[p] = heads[input[p]];
|
|
650
|
+
heads[input[p]] = p;
|
|
651
|
+
}
|
|
652
|
+
const maxCount = Math.min(src, 18);
|
|
653
|
+
let best = 0, offset = 0;
|
|
654
|
+
for (let p = heads[input[src - 1]]; p >= 0; p = next[p]) {
|
|
655
|
+
const d = p - src + 1;
|
|
656
|
+
if (d > 4098) break;
|
|
657
|
+
let n = 0;
|
|
658
|
+
const limit = Math.min(d, maxCount);
|
|
659
|
+
while (n < limit && input[src - 1 - n] === input[src + d - 1 - n]) n++;
|
|
660
|
+
if (n > best) {
|
|
661
|
+
best = n;
|
|
662
|
+
offset = d;
|
|
663
|
+
if (best === maxCount) break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (best < 3) {
|
|
667
|
+
if (dst < 1) return Buffer.alloc(0);
|
|
668
|
+
temp[--dst] = input[--src];
|
|
669
|
+
} else {
|
|
670
|
+
if (dst < 2) return Buffer.alloc(0);
|
|
671
|
+
src -= best;
|
|
672
|
+
dst -= 2;
|
|
673
|
+
temp[dst] = offset - 3 & 255;
|
|
674
|
+
temp[dst + 1] = best - 3 << 4 | offset - 3 >>> 8;
|
|
675
|
+
flags |= 1;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
temp[flagPos] = flags;
|
|
679
|
+
}
|
|
680
|
+
let packed = temp.subarray(dst), sourcePos = input.length, compressedPos = packed.length, prefix = 0, skip = 0;
|
|
681
|
+
outer: while (sourcePos > 0) {
|
|
682
|
+
const flags = packed[--compressedPos];
|
|
683
|
+
for (let bit = 128; bit && sourcePos > 0; bit >>= 1) {
|
|
684
|
+
if (!(flags & bit)) {
|
|
685
|
+
compressedPos--;
|
|
686
|
+
sourcePos--;
|
|
687
|
+
} else {
|
|
688
|
+
sourcePos -= (packed[compressedPos - 1] >>> 4) + 3;
|
|
689
|
+
compressedPos -= 2;
|
|
690
|
+
if (sourcePos < compressedPos) {
|
|
691
|
+
prefix = sourcePos;
|
|
692
|
+
skip = compressedPos;
|
|
693
|
+
break outer;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
packed = Buffer.concat([input.subarray(0, prefix), packed.subarray(skip)]);
|
|
699
|
+
const aligned = Math.ceil(packed.length / 4) * 4, finalSize = aligned + 8;
|
|
700
|
+
if (finalSize >= input.length) return Buffer.alloc(0);
|
|
701
|
+
const out = Buffer.alloc(finalSize, 255);
|
|
702
|
+
packed.copy(out);
|
|
703
|
+
const header = finalSize - packed.length, length = packed.length - prefix + header;
|
|
704
|
+
out.writeUInt32LE((header << 24 | length) >>> 0, finalSize - 8);
|
|
705
|
+
out.writeUInt32LE(input.length - finalSize, finalSize - 4);
|
|
706
|
+
return out;
|
|
707
|
+
};
|
|
708
|
+
var decompressArm9 = (data, header, maxSize = 268435456) => {
|
|
709
|
+
if (data.length < 24) throw new Error("ARM9 data too short");
|
|
710
|
+
if (data.length > maxSize) throw new RangeError("ARM9 exceeds output size limit");
|
|
711
|
+
const nitro = header.nitrocode ?? data.readUInt32LE(12) === 3737126433 ? 12 : 0;
|
|
712
|
+
const init = header.reserved2.readUInt32LE(0) & 16383;
|
|
713
|
+
const ptr = init ? data.readUInt32LE(init + 20) : header.ARM9ramAddress + header.ARM9size;
|
|
714
|
+
const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
|
|
715
|
+
return {
|
|
716
|
+
compressed,
|
|
717
|
+
data: compressed ? Buffer.concat([
|
|
718
|
+
decompressBLZ(data.subarray(0, data.length - nitro), maxSize - nitro),
|
|
719
|
+
data.subarray(data.length - nitro)
|
|
720
|
+
]) : Buffer.from(data)
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
var compressArm9 = (data, header, postSize = 0) => {
|
|
724
|
+
if (data.length < 16384) throw new Error("ARM9 secure area is truncated");
|
|
725
|
+
const nitro = header.nitrocode ?? data.readUInt32LE(12) === 3737126433 ? 12 : 0;
|
|
726
|
+
const packed = compressBLZ(data.subarray(16384, data.length - nitro));
|
|
727
|
+
if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
|
|
728
|
+
const out = Buffer.concat([data.subarray(0, 16384), packed, data.subarray(data.length - nitro)]);
|
|
729
|
+
const init = header.reserved2.readUInt32LE(0) & 16383;
|
|
730
|
+
if (init) out.writeUInt32LE(out.length - postSize + header.ARM9ramAddress, init + 20);
|
|
731
|
+
return out;
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/nitro/twl-complete.ts
|
|
735
|
+
var hash = (data) => hmacSha1(HMAC_SHA1_KEY, data);
|
|
736
|
+
var check = (valid) => ({ status: valid ? "valid" : "invalid" });
|
|
737
|
+
var absent = (reason) => ({ status: "not-applicable", reason });
|
|
738
|
+
var fields = {
|
|
739
|
+
arm9: ["ARM9romOffset", "ARM9size", "hmac_arm9"],
|
|
740
|
+
arm7: ["ARM7romOffset", "ARM7size", "hmac_arm7"],
|
|
741
|
+
arm9i: ["dsi9_rom_offset", "dsi9_size", "hmac_arm9i"],
|
|
742
|
+
arm7i: ["dsi7_rom_offset", "dsi7_size", "hmac_arm7i"]
|
|
743
|
+
};
|
|
744
|
+
var digestFields = [
|
|
745
|
+
"digest_ntr_start",
|
|
746
|
+
"digest_ntr_size",
|
|
747
|
+
"digest_twl_start",
|
|
748
|
+
"digest_twl_size",
|
|
749
|
+
"sector_hashtable_start",
|
|
750
|
+
"sector_hashtable_size",
|
|
751
|
+
"block_hashtable_start",
|
|
752
|
+
"block_hashtable_size",
|
|
753
|
+
"digest_sector_size",
|
|
754
|
+
"digest_block_sectorcount"
|
|
755
|
+
];
|
|
756
|
+
var noDsiVerification = () => Object.fromEntries(
|
|
757
|
+
["arm9", "arm7", "arm9i", "arm7i", "arm9NoSecure", "banner", "sectors", "blocks", "master", "signature"].map(
|
|
758
|
+
(name) => [name, absent("Not a DSi-enhanced cartridge")]
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
var CompleteTWL = class {
|
|
762
|
+
header;
|
|
763
|
+
programs;
|
|
764
|
+
regions;
|
|
765
|
+
hasDigests;
|
|
766
|
+
Hashtable1Data;
|
|
767
|
+
Hashtable2Data;
|
|
768
|
+
Header2Data;
|
|
769
|
+
Overlays9Sha1Hmac;
|
|
770
|
+
twlEncrypted;
|
|
771
|
+
firstDSiHashOffset;
|
|
772
|
+
dsiHashSize;
|
|
773
|
+
original;
|
|
774
|
+
limit;
|
|
775
|
+
constructor(header, overlays, original, limit) {
|
|
776
|
+
if (header.unitCode !== 2) throw new Error("Complete DSi processing requires a DSi-enhanced cartridge");
|
|
777
|
+
if (header.tid_high && header.tid_high !== 196608)
|
|
778
|
+
throw new Error("DSiWare and non-cartridge titles are unsupported");
|
|
779
|
+
this.header = new Header(header.raw);
|
|
780
|
+
this.header.decrypted = header.decrypted;
|
|
781
|
+
this.header.nitrocode = header.nitrocode;
|
|
782
|
+
this.original = original;
|
|
783
|
+
this.limit = limit;
|
|
784
|
+
this.hasDigests = digestFields.some((name) => header[name] !== 0);
|
|
785
|
+
this.validateDigests(header, original);
|
|
786
|
+
if (header.sector_hashtable_size > limit || header.block_hashtable_size > limit)
|
|
787
|
+
throw new RangeError("DSi digest table exceeds output size limit");
|
|
788
|
+
for (const name of ["dsi9_ram_address", "dsi7_ram_address"])
|
|
789
|
+
if (!header[name] || header[name] === 4294967295) throw new Error(`Missing DSi program load address: ${name}`);
|
|
790
|
+
this.Hashtable1Data = Buffer.from(
|
|
791
|
+
range(original, header.sector_hashtable_start, header.sector_hashtable_size, "DSi sector table")
|
|
792
|
+
);
|
|
793
|
+
this.Hashtable2Data = Buffer.from(
|
|
794
|
+
range(original, header.block_hashtable_start, header.block_hashtable_size, "DSi block table")
|
|
795
|
+
);
|
|
796
|
+
this.firstDSiHashOffset = this.hasDigests ? header.digest_ntr_size / header.digest_sector_size * 20 : 0;
|
|
797
|
+
this.dsiHashSize = this.hasDigests ? header.digest_twl_size / header.digest_sector_size * 20 : 0;
|
|
798
|
+
this.programs = new Map(
|
|
799
|
+
Object.entries(fields).map(([name, [offset, size]]) => {
|
|
800
|
+
const start = header[offset];
|
|
801
|
+
const length = header[size];
|
|
802
|
+
if (length && !start) throw new Error(`Missing ${name} ROM offset`);
|
|
803
|
+
if (align(length, 16) > limit) throw new RangeError(`${name} exceeds output size limit`);
|
|
804
|
+
const data = Buffer.from(range(original, start, length ? align(length, 16) : 0, name));
|
|
805
|
+
return [
|
|
806
|
+
name,
|
|
807
|
+
{ name, start, length, data, original: data, replaced: false }
|
|
808
|
+
];
|
|
809
|
+
})
|
|
810
|
+
);
|
|
811
|
+
const ordered = [...this.programs.values()].filter((program) => program.length).sort((a, b) => a.start - b.start);
|
|
812
|
+
for (let index = 1; index < ordered.length; index++)
|
|
813
|
+
if (ordered[index - 1].start + ordered[index - 1].data.length > ordered[index].start)
|
|
814
|
+
throw new Error("DSi program regions overlap");
|
|
815
|
+
this.regions = [];
|
|
816
|
+
for (const id of [1, 2]) {
|
|
817
|
+
const start = header[`modcrypt${id}_start`];
|
|
818
|
+
const length = header[`modcrypt${id}_size`];
|
|
819
|
+
if (!start && !length) continue;
|
|
820
|
+
if (!start || !length || length % 16) throw new Error(`Invalid Modcrypt region ${id}`);
|
|
821
|
+
const candidates = ordered.filter(
|
|
822
|
+
(program2) => start >= program2.start && start + length <= program2.start + program2.data.length
|
|
823
|
+
);
|
|
824
|
+
if (candidates.length !== 1)
|
|
825
|
+
throw new Error(`Modcrypt region ${id} conflicts with program ranges: ${start}..${start + length}`);
|
|
826
|
+
const program = candidates[0];
|
|
827
|
+
if (program.name === "arm9" || program.name === "arm7")
|
|
828
|
+
throw new Error(`Modcrypt region ${id} targets ${program.name}; dual-mode cartridge startup is unsupported`);
|
|
829
|
+
const offset = start - program.start;
|
|
830
|
+
this.regions.push({
|
|
831
|
+
id,
|
|
832
|
+
program: program.name,
|
|
833
|
+
offset,
|
|
834
|
+
length,
|
|
835
|
+
kind: offset === 0 && length === program.data.length ? "whole" : offset === 0 && program.name === "arm9i" && length === Math.min(16384, program.data.length) ? "secure" : "fixed"
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
this.twlEncrypted = !!(header.twlInternalFlags & 2);
|
|
839
|
+
this.decryptPrograms();
|
|
840
|
+
for (const program of this.programs.values()) program.original = Buffer.from(program.data);
|
|
841
|
+
const boundary = header.twl_region_start * 524288;
|
|
842
|
+
const first = Math.min(...ordered.filter((program) => program.name.endsWith("i")).map((program) => program.start));
|
|
843
|
+
const prefixEnd = boundary + 12288;
|
|
844
|
+
this.Header2Data = boundary && prefixEnd <= original.length && (Number.isFinite(first) && first >= prefixEnd || header.digest_twl_size && header.digest_twl_start >= prefixEnd || header.total_rom_size >= prefixEnd) ? Array.from(
|
|
845
|
+
{ length: 3 },
|
|
846
|
+
(_, index) => Buffer.from(range(original, boundary + index * 4096, 4096, "TWL unreadable region"))
|
|
847
|
+
) : [];
|
|
848
|
+
this.Overlays9Sha1Hmac = overlays.map(hash);
|
|
849
|
+
}
|
|
850
|
+
validateDigests(h, original) {
|
|
851
|
+
if (!this.hasDigests) return;
|
|
852
|
+
const s = h.digest_sector_size;
|
|
853
|
+
const b = h.digest_block_sectorcount;
|
|
854
|
+
if (!Number.isSafeInteger(s) || s <= 0 || s > 16384 || 16384 % s)
|
|
855
|
+
throw new Error("Invalid DSi digest sector size");
|
|
856
|
+
if (!b || b > 1048576) throw new Error("Invalid DSi digest block sector count");
|
|
857
|
+
if (!h.digest_ntr_start || h.digest_ntr_size < 16384 || h.digest_ntr_size % s || h.digest_twl_size % s || h.digest_twl_size && !h.digest_twl_start)
|
|
858
|
+
throw new Error("Incomplete DSi digest regions");
|
|
859
|
+
const count = (h.digest_ntr_size + h.digest_twl_size) / s;
|
|
860
|
+
if (!h.sector_hashtable_start || !h.block_hashtable_start || !h.sector_hashtable_size || h.sector_hashtable_size % (b * 20) || h.sector_hashtable_size < count * 20 || h.block_hashtable_size !== h.sector_hashtable_size / b)
|
|
861
|
+
throw new Error("Incomplete or invalid DSi digest tables");
|
|
862
|
+
for (const [start, size, label] of [
|
|
863
|
+
[h.digest_ntr_start, h.digest_ntr_size, "NTR digest region"],
|
|
864
|
+
[h.digest_twl_start, h.digest_twl_size, "TWL digest region"],
|
|
865
|
+
[h.sector_hashtable_start, h.sector_hashtable_size, "DSi sector table"],
|
|
866
|
+
[h.block_hashtable_start, h.block_hashtable_size, "DSi block table"]
|
|
867
|
+
])
|
|
868
|
+
range(original, start, size, label);
|
|
869
|
+
if (h.digest_ntr_start + h.digest_ntr_size > h.sector_hashtable_start || h.sector_hashtable_start + h.sector_hashtable_size > h.block_hashtable_start || h.digest_twl_size && h.block_hashtable_start + h.block_hashtable_size > h.digest_twl_start)
|
|
870
|
+
throw new Error("DSi digest regions overlap");
|
|
871
|
+
}
|
|
872
|
+
decryptPrograms() {
|
|
873
|
+
if (!this.regions.length) return;
|
|
874
|
+
const h = this.header;
|
|
875
|
+
const key2 = modcryptKey(h);
|
|
876
|
+
const solutions = [];
|
|
877
|
+
const programs = [...this.programs.values()];
|
|
878
|
+
for (let mask = 0; mask < 2 ** this.regions.length; mask++) {
|
|
879
|
+
const views = programs.map((program) => Buffer.from(program.data));
|
|
880
|
+
for (let index = this.regions.length - 1; index >= 0; index--) {
|
|
881
|
+
if (!(mask & 1 << index)) continue;
|
|
882
|
+
const region = this.regions[index];
|
|
883
|
+
const data = views[programs.findIndex((program) => program.name === region.program)];
|
|
884
|
+
aes128CtrCrypt(
|
|
885
|
+
key2,
|
|
886
|
+
(region.id === 1 ? h.hmac_arm9 : h.hmac_arm7).subarray(0, 16),
|
|
887
|
+
data.subarray(region.offset, region.offset + region.length)
|
|
888
|
+
).copy(data, region.offset);
|
|
889
|
+
}
|
|
890
|
+
const accepted = [...new Set(this.regions.map((region) => region.program))].every((name) => {
|
|
891
|
+
const index = programs.findIndex((program2) => program2.name === name);
|
|
892
|
+
const program = programs[index];
|
|
893
|
+
if (hash(views[index].subarray(0, program.length)).equals(h[fields[name][2]])) return true;
|
|
894
|
+
if (!this.hasDigests) return false;
|
|
895
|
+
const relevant = this.regions.filter((region) => region.program === name);
|
|
896
|
+
return relevant.every((region) => {
|
|
897
|
+
const address = program.start + region.offset;
|
|
898
|
+
const base = address >= h.digest_twl_start && address < h.digest_twl_start + h.digest_twl_size ? h.digest_twl_start : h.digest_ntr_start;
|
|
899
|
+
const length = base === h.digest_twl_start ? h.digest_twl_size : h.digest_ntr_size;
|
|
900
|
+
if (address < base || address >= base + length) return false;
|
|
901
|
+
const first = base + Math.floor((address - base) / h.digest_sector_size) * h.digest_sector_size;
|
|
902
|
+
const last = Math.min(base + length, align(address + region.length, h.digest_sector_size));
|
|
903
|
+
for (let sector = first; sector < last; sector += h.digest_sector_size) {
|
|
904
|
+
const bytes2 = Buffer.from(range(this.original, sector, h.digest_sector_size, "Modcrypt evidence sector"));
|
|
905
|
+
for (const [p, view] of programs.map((program2, i) => [program2, views[i]])) {
|
|
906
|
+
const start = Math.max(sector, p.start), end = Math.min(sector + bytes2.length, p.start + view.length);
|
|
907
|
+
if (end > start) view.copy(bytes2, start - sector, start - p.start, end - p.start);
|
|
908
|
+
}
|
|
909
|
+
const digest = ((base === h.digest_twl_start ? h.digest_ntr_size : 0) + sector - base) / h.digest_sector_size * 20;
|
|
910
|
+
if (hash(bytes2).equals(range(this.Hashtable1Data, digest, 20, "Modcrypt evidence hash"))) return true;
|
|
911
|
+
}
|
|
912
|
+
return false;
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
if (accepted && !solutions.some((solution) => solution.every((data, index) => data.equals(views[index]))))
|
|
916
|
+
solutions.push(views);
|
|
917
|
+
}
|
|
918
|
+
if (solutions.length !== 1)
|
|
919
|
+
throw new Error(
|
|
920
|
+
`Cannot determine Modcrypt state for regions ${this.regions.map((region) => region.id).join(", ")}`
|
|
921
|
+
);
|
|
922
|
+
programs.forEach((program, index) => {
|
|
923
|
+
program.data = solutions[0][index];
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
get DSi9Data() {
|
|
927
|
+
return this.getProgram("arm9i");
|
|
928
|
+
}
|
|
929
|
+
set DSi9Data(data) {
|
|
930
|
+
this.setProgram("arm9i", data);
|
|
931
|
+
}
|
|
932
|
+
get DSi7Data() {
|
|
933
|
+
return this.getProgram("arm7i");
|
|
934
|
+
}
|
|
935
|
+
set DSi7Data(data) {
|
|
936
|
+
this.setProgram("arm7i", data);
|
|
937
|
+
}
|
|
938
|
+
getProgram(name) {
|
|
939
|
+
const program = this.programs.get(name);
|
|
940
|
+
return program.data.subarray(0, program.length);
|
|
941
|
+
}
|
|
942
|
+
setProgram(name, data) {
|
|
943
|
+
if (align(data.length, 16) > this.limit) throw new RangeError(`${name} exceeds output size limit`);
|
|
944
|
+
const program = this.programs.get(name);
|
|
945
|
+
const stored = Buffer.alloc(align(data.length, 16), 255);
|
|
946
|
+
if (program.length === data.length) program.data.copy(stored);
|
|
947
|
+
data.copy(stored);
|
|
948
|
+
program.data = stored;
|
|
949
|
+
program.length = data.length;
|
|
950
|
+
program.replaced = true;
|
|
951
|
+
}
|
|
952
|
+
importArm9iData(data, offset = 0, size) {
|
|
953
|
+
const input = typeof data === "string" ? readFileSync(data) : data;
|
|
954
|
+
this.setProgram("arm9i", range(input, offset, size ?? input.length - offset, "ARM9i import"));
|
|
955
|
+
}
|
|
956
|
+
importArm7iData(data, offset = 0, size) {
|
|
957
|
+
const input = typeof data === "string" ? readFileSync(data) : data;
|
|
958
|
+
this.setProgram("arm7i", range(input, offset, size ?? input.length - offset, "ARM7i import"));
|
|
959
|
+
}
|
|
960
|
+
unchanged() {
|
|
961
|
+
return [...this.programs.values()].every(
|
|
962
|
+
(program) => !(program.replaced && !program.length) && program.length === this.header[fields[program.name][1]] && program.data.equals(program.original)
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
updateOverlays9Sha1Hmac(arm9, h, overlays) {
|
|
966
|
+
if (overlays.length !== this.Overlays9Sha1Hmac.length)
|
|
967
|
+
throw new Error("Changing signed ARM9 overlay count is unsupported");
|
|
968
|
+
const hashes = overlays.map(hash);
|
|
969
|
+
if (hashes.every((value, index) => value.equals(this.Overlays9Sha1Hmac[index]))) return arm9;
|
|
970
|
+
const unpacked = decompressArm9(arm9, h, this.limit);
|
|
971
|
+
const oldSequence = Buffer.concat(this.Overlays9Sha1Hmac);
|
|
972
|
+
const newSequence = Buffer.concat(hashes);
|
|
973
|
+
const matches = [];
|
|
974
|
+
for (const [sequence, current] of [
|
|
975
|
+
[oldSequence, false],
|
|
976
|
+
[newSequence, true]
|
|
977
|
+
])
|
|
978
|
+
for (let position = unpacked.data.indexOf(sequence, 16384); position >= 0; position = unpacked.data.indexOf(sequence, position + 1))
|
|
979
|
+
matches.push({ position, current });
|
|
980
|
+
if (matches.length !== 1) throw new Error("ARM9 overlay HMAC table cannot be uniquely located");
|
|
981
|
+
if (matches[0].current) return arm9;
|
|
982
|
+
newSequence.copy(unpacked.data, matches[0].position);
|
|
983
|
+
if (!unpacked.compressed) return unpacked.data;
|
|
984
|
+
const init = h.reserved2.readUInt32LE(0) & 16383;
|
|
985
|
+
const end = init ? arm9.readUInt32LE(init + 20) - h.ARM9ramAddress : arm9.length;
|
|
986
|
+
return compressArm9(unpacked.data, h, arm9.length - end);
|
|
987
|
+
}
|
|
988
|
+
view(data, h, start, length, programs = this.programs) {
|
|
989
|
+
const output = Buffer.from(range(data, start, length, "DSi digest input"));
|
|
990
|
+
for (const program of programs.values()) {
|
|
991
|
+
const [offset, size] = fields[program.name];
|
|
992
|
+
const address = h[offset];
|
|
993
|
+
const limit = program.name === "arm9" ? h[size] : align(h[size], 16);
|
|
994
|
+
const from = Math.max(start, address), to = Math.min(start + length, address + limit);
|
|
995
|
+
if (to <= from) continue;
|
|
996
|
+
let source = program.data;
|
|
997
|
+
if (program.name === "arm9" && h.decrypted) source = encryptSecureArea(h.gameCode, source, true);
|
|
998
|
+
source.copy(output, from - start, from - address, to - address);
|
|
999
|
+
}
|
|
1000
|
+
return output;
|
|
1001
|
+
}
|
|
1002
|
+
verify(data = this.original, h = this.header) {
|
|
1003
|
+
const programCheck = (name) => {
|
|
1004
|
+
const program = this.programs.get(name);
|
|
1005
|
+
if (!program.length) return absent("Program has zero logical length");
|
|
1006
|
+
const input = name === "arm9" && h.decrypted ? encryptSecureArea(h.gameCode, program.data, true) : program.data;
|
|
1007
|
+
return check(hash(input.subarray(0, program.length)).equals(h[fields[name][2]]));
|
|
1008
|
+
};
|
|
1009
|
+
const result = {
|
|
1010
|
+
arm9: programCheck("arm9"),
|
|
1011
|
+
arm7: programCheck("arm7"),
|
|
1012
|
+
arm9i: programCheck("arm9i"),
|
|
1013
|
+
arm7i: programCheck("arm7i"),
|
|
1014
|
+
arm9NoSecure: h.ARM9size >= 16384 ? check(hash(this.getProgram("arm9").subarray(16384)).equals(h.hmac_arm9_no_secure)) : absent("ARM9 has no complete secure area"),
|
|
1015
|
+
banner: check(hash(range(data, h.bannerOffset, h.banner_size, "DSi banner")).equals(h.hmac_icon_title)),
|
|
1016
|
+
sectors: absent("Digest tables omitted"),
|
|
1017
|
+
blocks: absent("Digest tables omitted"),
|
|
1018
|
+
master: absent("Digest tables omitted"),
|
|
1019
|
+
signature: h.rsa_signature.every((value) => value === 0) ? absent("Signature omitted") : check(verifyDsiSignature(range(data, 0, 3584, "Signed DSi header"), h.rsa_signature))
|
|
1020
|
+
};
|
|
1021
|
+
if (this.hasDigests) {
|
|
1022
|
+
let valid = true, index = 0;
|
|
1023
|
+
for (const [start, size] of [
|
|
1024
|
+
[h.digest_ntr_start, h.digest_ntr_size],
|
|
1025
|
+
[h.digest_twl_start, h.digest_twl_size]
|
|
1026
|
+
])
|
|
1027
|
+
for (let position = start; position < start + size; position += h.digest_sector_size)
|
|
1028
|
+
valid = hash(this.view(data, h, position, h.digest_sector_size)).equals(
|
|
1029
|
+
range(data, h.sector_hashtable_start + index++ * 20, 20, "DSi sector digest")
|
|
1030
|
+
) && valid;
|
|
1031
|
+
result.sectors = check(valid);
|
|
1032
|
+
valid = true;
|
|
1033
|
+
for (let offset = 0; offset < h.sector_hashtable_size; offset += h.digest_block_sectorcount * 20)
|
|
1034
|
+
valid = hash(
|
|
1035
|
+
range(data, h.sector_hashtable_start + offset, h.digest_block_sectorcount * 20, "DSi sector block")
|
|
1036
|
+
).equals(
|
|
1037
|
+
range(data, h.block_hashtable_start + offset / h.digest_block_sectorcount, 20, "DSi block digest")
|
|
1038
|
+
) && valid;
|
|
1039
|
+
result.blocks = check(valid);
|
|
1040
|
+
result.master = check(
|
|
1041
|
+
hash(range(data, h.block_hashtable_start, h.block_hashtable_size, "DSi master input")).equals(
|
|
1042
|
+
h.hmac_digest_master
|
|
1043
|
+
)
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
1048
|
+
serialize(h, sections, forceLayout) {
|
|
1049
|
+
const originalHeader = this.header;
|
|
1050
|
+
const dsiPrograms = [this.programs.get("arm9i"), this.programs.get("arm7i")];
|
|
1051
|
+
const originalEnd = Math.max(
|
|
1052
|
+
originalHeader.ROMsize,
|
|
1053
|
+
originalHeader.total_rom_size,
|
|
1054
|
+
originalHeader.dsi9_rom_offset + align(originalHeader.dsi9_size, 16),
|
|
1055
|
+
originalHeader.dsi7_rom_offset + align(originalHeader.dsi7_size, 16),
|
|
1056
|
+
originalHeader.digest_twl_start + originalHeader.digest_twl_size
|
|
1057
|
+
);
|
|
1058
|
+
const ntrLimit = this.hasDigests ? originalHeader.digest_ntr_start + originalHeader.digest_ntr_size : originalHeader.twl_region_start * 524288 || Math.min(...dsiPrograms.filter((program) => program.length).map((program) => program.start));
|
|
1059
|
+
const ordered = sections.filter((section) => section.originalSize || section.data.length).sort((a, b) => a.start - b.start);
|
|
1060
|
+
let preserve = !forceLayout && ordered.every((section, index) => {
|
|
1061
|
+
const next = ordered[index + 1]?.start ?? ntrLimit;
|
|
1062
|
+
if (section.start < h.headerSize || section.start + section.originalSize > next || section.start + section.data.length > next)
|
|
1063
|
+
return false;
|
|
1064
|
+
const extra = this.original.subarray(section.start + section.originalSize, section.start + section.data.length);
|
|
1065
|
+
return extra.every((value) => value === 0) || extra.every((value) => value === 255);
|
|
1066
|
+
});
|
|
1067
|
+
const originalDsi = [...dsiPrograms].filter((program) => originalHeader[fields[program.name][1]]).sort((a, b) => a.start - b.start);
|
|
1068
|
+
for (const [index, program] of originalDsi.entries()) {
|
|
1069
|
+
const next = originalDsi[index + 1]?.start ?? originalEnd;
|
|
1070
|
+
if (program.start + program.data.length > next) preserve = false;
|
|
1071
|
+
const extra = this.original.subarray(
|
|
1072
|
+
program.start + align(originalHeader[fields[program.name][1]], 16),
|
|
1073
|
+
program.start + program.data.length
|
|
1074
|
+
);
|
|
1075
|
+
if (!(extra.every((value) => value === 0) || extra.every((value) => value === 255))) preserve = false;
|
|
1076
|
+
}
|
|
1077
|
+
if (dsiPrograms.some((program) => program.length && !originalHeader[fields[program.name][1]])) preserve = false;
|
|
1078
|
+
let ntrEnd;
|
|
1079
|
+
let end;
|
|
1080
|
+
let boundary = originalHeader.twl_region_start * 524288;
|
|
1081
|
+
if (preserve) {
|
|
1082
|
+
for (const section of sections) section.place(section.start);
|
|
1083
|
+
ntrEnd = originalHeader.ROMsize;
|
|
1084
|
+
end = originalEnd;
|
|
1085
|
+
} else {
|
|
1086
|
+
this.checkMovableTwl(boundary, originalDsi, originalEnd);
|
|
1087
|
+
let position = Math.max(h.headerSize, 16384);
|
|
1088
|
+
for (const section of sections) {
|
|
1089
|
+
section.start = position;
|
|
1090
|
+
section.place(position);
|
|
1091
|
+
position = align(position + section.data.length, 512);
|
|
1092
|
+
}
|
|
1093
|
+
const ntrDataEnd = position;
|
|
1094
|
+
if (this.hasDigests) {
|
|
1095
|
+
const s = h.digest_sector_size;
|
|
1096
|
+
h.digest_ntr_start = h.ARM9romOffset;
|
|
1097
|
+
h.digest_ntr_size = align(ntrDataEnd - h.digest_ntr_start, s);
|
|
1098
|
+
h.sector_hashtable_start = align(h.digest_ntr_start + h.digest_ntr_size, 512);
|
|
1099
|
+
}
|
|
1100
|
+
let twlLength = 0;
|
|
1101
|
+
for (const program of dsiPrograms) if (program.length) twlLength = align(twlLength, 1024) + program.data.length;
|
|
1102
|
+
if (this.hasDigests) {
|
|
1103
|
+
h.digest_twl_size = twlLength ? align(twlLength, h.digest_sector_size) : 0;
|
|
1104
|
+
const count = (h.digest_ntr_size + h.digest_twl_size) / h.digest_sector_size;
|
|
1105
|
+
h.sector_hashtable_size = align(count * 20, h.digest_block_sectorcount * 20);
|
|
1106
|
+
h.block_hashtable_start = align(h.sector_hashtable_start + h.sector_hashtable_size, 512);
|
|
1107
|
+
h.block_hashtable_size = h.sector_hashtable_size / h.digest_block_sectorcount;
|
|
1108
|
+
ntrEnd = h.block_hashtable_start + h.block_hashtable_size;
|
|
1109
|
+
} else ntrEnd = ntrDataEnd;
|
|
1110
|
+
const originalBoundary = originalHeader.twl_region_start * 524288;
|
|
1111
|
+
const boundaryAlignment = originalBoundary ? Math.max(524288, originalBoundary & -originalBoundary) : 524288;
|
|
1112
|
+
boundary = align(ntrEnd, boundaryAlignment);
|
|
1113
|
+
position = boundary + 12288;
|
|
1114
|
+
let first = 0;
|
|
1115
|
+
for (const program of dsiPrograms) {
|
|
1116
|
+
const [offset] = fields[program.name];
|
|
1117
|
+
if (!program.length) {
|
|
1118
|
+
h[offset] = 0;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
position = align(position, 1024);
|
|
1122
|
+
h[offset] = position;
|
|
1123
|
+
first ||= position;
|
|
1124
|
+
position += program.data.length;
|
|
1125
|
+
}
|
|
1126
|
+
if (this.hasDigests) h.digest_twl_start = first;
|
|
1127
|
+
end = this.hasDigests && first ? first + h.digest_twl_size : position;
|
|
1128
|
+
}
|
|
1129
|
+
h.size = Math.max(originalHeader.size, 2 ** Math.ceil(Math.log2(Math.max(end, 131072))));
|
|
1130
|
+
if (h.size > this.limit) throw new RangeError("Output exceeds size limit");
|
|
1131
|
+
const writer = new BinaryWriter(h.size, this.limit);
|
|
1132
|
+
writer.fillTo(h.size);
|
|
1133
|
+
if (preserve) this.original.copy(writer.buffer);
|
|
1134
|
+
else {
|
|
1135
|
+
range(this.original, 0, Math.max(h.headerSize, 16384), "DSi original header area").copy(writer.buffer);
|
|
1136
|
+
if (this.hasDigests) {
|
|
1137
|
+
const originalGap = this.original.subarray(
|
|
1138
|
+
originalHeader.sector_hashtable_start + originalHeader.sector_hashtable_size,
|
|
1139
|
+
originalHeader.block_hashtable_start
|
|
1140
|
+
);
|
|
1141
|
+
if (originalGap.length && originalGap.every((value) => value === 0))
|
|
1142
|
+
writer.buffer.fill(0, h.sector_hashtable_start + h.sector_hashtable_size, h.block_hashtable_start);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
for (const section of sections) {
|
|
1146
|
+
writer.position = section.start;
|
|
1147
|
+
writer.write(section.data);
|
|
1148
|
+
if (preserve && section.data.length < section.originalSize) {
|
|
1149
|
+
writer.fillTo(section.start + section.originalSize);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
for (const program of dsiPrograms) {
|
|
1153
|
+
const [offset, size, mac] = fields[program.name];
|
|
1154
|
+
h[size] = program.length;
|
|
1155
|
+
if (!program.length && program.replaced) {
|
|
1156
|
+
h[offset] = 0;
|
|
1157
|
+
h[mac] = Buffer.alloc(20);
|
|
1158
|
+
if (preserve && originalHeader[size]) {
|
|
1159
|
+
writer.position = originalHeader[offset];
|
|
1160
|
+
writer.fillTo(originalHeader[offset] + align(originalHeader[size], 16));
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (program.length) {
|
|
1164
|
+
writer.position = h[offset];
|
|
1165
|
+
writer.write(program.data);
|
|
1166
|
+
const oldSize = originalHeader[size];
|
|
1167
|
+
if (preserve && align(oldSize, 16) > program.data.length) writer.fillTo(h[offset] + align(oldSize, 16));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const outputPrograms = new Map([...this.programs].map(([name, program]) => [name, { ...program }]));
|
|
1171
|
+
for (const name of ["arm9", "arm7"]) {
|
|
1172
|
+
const program = outputPrograms.get(name);
|
|
1173
|
+
const [offset, size] = fields[name];
|
|
1174
|
+
program.data = Buffer.from(range(writer.buffer, h[offset], h[size], name));
|
|
1175
|
+
program.length = h[size];
|
|
1176
|
+
}
|
|
1177
|
+
for (const program of outputPrograms.values()) {
|
|
1178
|
+
if (!program.length) continue;
|
|
1179
|
+
const data = program.name === "arm9" && h.decrypted ? encryptSecureArea(h.gameCode, program.data, true) : program.data;
|
|
1180
|
+
h[fields[program.name][2]] = hash(data.subarray(0, program.length));
|
|
1181
|
+
}
|
|
1182
|
+
h.hmac_arm9_no_secure = hash(outputPrograms.get("arm9").data.subarray(16384));
|
|
1183
|
+
h.hmac_icon_title = hash(range(writer.buffer, h.bannerOffset, h.banner_size, "DSi output banner"));
|
|
1184
|
+
if (this.hasDigests) {
|
|
1185
|
+
range(writer.buffer, h.sector_hashtable_start, h.sector_hashtable_size, "DSi output sector table").fill(0);
|
|
1186
|
+
range(writer.buffer, h.block_hashtable_start, h.block_hashtable_size, "DSi output block table").fill(0);
|
|
1187
|
+
let index = 0;
|
|
1188
|
+
for (const [start, size] of [
|
|
1189
|
+
[h.digest_ntr_start, h.digest_ntr_size],
|
|
1190
|
+
[h.digest_twl_start, h.digest_twl_size]
|
|
1191
|
+
])
|
|
1192
|
+
for (let position = start; position < start + size; position += h.digest_sector_size)
|
|
1193
|
+
hash(this.view(writer.buffer, h, position, h.digest_sector_size, outputPrograms)).copy(
|
|
1194
|
+
writer.buffer,
|
|
1195
|
+
h.sector_hashtable_start + index++ * 20
|
|
1196
|
+
);
|
|
1197
|
+
for (let offset = 0; offset < h.sector_hashtable_size; offset += h.digest_block_sectorcount * 20)
|
|
1198
|
+
hash(
|
|
1199
|
+
range(
|
|
1200
|
+
writer.buffer,
|
|
1201
|
+
h.sector_hashtable_start + offset,
|
|
1202
|
+
h.digest_block_sectorcount * 20,
|
|
1203
|
+
"DSi output sector block"
|
|
1204
|
+
)
|
|
1205
|
+
).copy(writer.buffer, h.block_hashtable_start + offset / h.digest_block_sectorcount);
|
|
1206
|
+
h.hmac_digest_master = hash(
|
|
1207
|
+
range(writer.buffer, h.block_hashtable_start, h.block_hashtable_size, "DSi output master input")
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
const key2 = this.regions.length ? modcryptKey(h) : Buffer.alloc(0);
|
|
1211
|
+
for (const region of this.regions) {
|
|
1212
|
+
const program = this.programs.get(region.program);
|
|
1213
|
+
const size = !program.length ? 0 : region.kind === "whole" ? program.data.length : region.kind === "secure" ? Math.min(16384, program.data.length) : region.length;
|
|
1214
|
+
if (size && region.offset + size > program.data.length)
|
|
1215
|
+
throw new Error(`Modcrypt region ${region.id} exceeds ${program.name} after replacement`);
|
|
1216
|
+
const start = size ? h[fields[program.name][0]] + region.offset : 0;
|
|
1217
|
+
h[`modcrypt${region.id}_start`] = start;
|
|
1218
|
+
h[`modcrypt${region.id}_size`] = size;
|
|
1219
|
+
if (size && h.twlInternalFlags & 2) {
|
|
1220
|
+
const input = range(writer.buffer, start, size, `Output Modcrypt region ${region.id}`);
|
|
1221
|
+
aes128CtrCrypt(key2, (region.id === 1 ? h.hmac_arm9 : h.hmac_arm7).subarray(0, 16), input).copy(
|
|
1222
|
+
writer.buffer,
|
|
1223
|
+
start
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
h.ROMsize = ntrEnd;
|
|
1228
|
+
h.total_rom_size = end;
|
|
1229
|
+
h.ntr_region_end = boundary / 524288;
|
|
1230
|
+
h.twl_region_start = boundary / 524288;
|
|
1231
|
+
h.size = Math.max(originalHeader.size, 2 ** Math.ceil(Math.log2(Math.max(end, 131072))));
|
|
1232
|
+
const secure = range(writer.buffer, h.ARM9romOffset, 16384, "DSi secure area");
|
|
1233
|
+
h.secureCRC16 = crc16(h.decrypted ? encryptSecureArea(h.gameCode, secure, true) : secure);
|
|
1234
|
+
h.rsa_signature = originalHeader.rsa_signature;
|
|
1235
|
+
writer.position = 0;
|
|
1236
|
+
writer.write(h.toBuffer());
|
|
1237
|
+
const originalMirror = range(this.original, 32768, 4096, "Original cartridge mirror");
|
|
1238
|
+
for (let start = 4096; start < 16384; start += 4096)
|
|
1239
|
+
if (range(this.original, start, 4096, "NTR unreadable region").equals(originalMirror))
|
|
1240
|
+
range(writer.buffer, 32768, 4096, "Updated cartridge mirror").copy(writer.buffer, start);
|
|
1241
|
+
if (this.Header2Data.length) {
|
|
1242
|
+
for (const [index, data] of this.Header2Data.entries()) {
|
|
1243
|
+
writer.position = boundary + index * 4096;
|
|
1244
|
+
writer.write(
|
|
1245
|
+
data.equals(originalMirror) ? range(writer.buffer, 32768, 4096, "Updated cartridge mirror") : data
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return writer.toBuffer();
|
|
1250
|
+
}
|
|
1251
|
+
checkMovableTwl(boundary, programs, end) {
|
|
1252
|
+
let position = boundary + this.Header2Data.length * 4096;
|
|
1253
|
+
for (const program of programs) {
|
|
1254
|
+
if (program.start < position) throw new Error("TWL input regions overlap");
|
|
1255
|
+
const gap = range(this.original, position, program.start - position, "TWL preserved bytes");
|
|
1256
|
+
const padding = gap;
|
|
1257
|
+
if (padding.length && !(padding.every((value) => value === 0) || padding.every((value) => value === 255)))
|
|
1258
|
+
throw new Error("TWL contains unrecognized bytes whose relocation is unsupported");
|
|
1259
|
+
position = program.start + align(this.header[fields[program.name][1]], 16);
|
|
1260
|
+
}
|
|
1261
|
+
const tail = range(this.original, position, end - position, "TWL preserved tail");
|
|
1262
|
+
if (tail.length && !(tail.every((value) => value === 0) || tail.every((value) => value === 255)))
|
|
1263
|
+
throw new Error("TWL contains unrecognized trailing data whose relocation is unsupported");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// src/nitro/twl.ts
|
|
1268
|
+
var HMAC_SHA1_KEY = Buffer.from(
|
|
1269
|
+
"2106c0deba98ce3fa692e39d46f2ed0176e3cc08562363facad4ecdf9a6278348f6d633cfe22ca9220889723d2cfaec232678dfeca836498acfd3e3787465824",
|
|
1270
|
+
"hex"
|
|
1271
|
+
);
|
|
1272
|
+
var hash2 = (data) => hmacSha1(HMAC_SHA1_KEY, data);
|
|
1273
|
+
function slice(data, start, size, label) {
|
|
1274
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || start < 0 || size < 0 || start + size > data.length)
|
|
1275
|
+
throw new RangeError(`${label} outside data range`);
|
|
1276
|
+
return data.subarray(start, start + size);
|
|
1277
|
+
}
|
|
1278
|
+
function validate(h) {
|
|
1279
|
+
for (const field2 of [
|
|
1280
|
+
"digest_ntr_start",
|
|
1281
|
+
"digest_ntr_size",
|
|
1282
|
+
"digest_twl_start",
|
|
1283
|
+
"digest_twl_size",
|
|
1284
|
+
"sector_hashtable_start",
|
|
1285
|
+
"sector_hashtable_size",
|
|
1286
|
+
"block_hashtable_start",
|
|
1287
|
+
"block_hashtable_size",
|
|
1288
|
+
"dsi9_rom_offset",
|
|
1289
|
+
"dsi9_size",
|
|
1290
|
+
"dsi7_rom_offset",
|
|
1291
|
+
"dsi7_size",
|
|
1292
|
+
"modcrypt1_start",
|
|
1293
|
+
"modcrypt1_size",
|
|
1294
|
+
"modcrypt2_start",
|
|
1295
|
+
"modcrypt2_size",
|
|
1296
|
+
"total_rom_size"
|
|
1297
|
+
]) {
|
|
1298
|
+
if (!Number.isSafeInteger(h[field2]) || h[field2] < 0 || h[field2] > 4294967295) {
|
|
1299
|
+
throw new Error(`Invalid TWL ${field2}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const s = h.digest_sector_size;
|
|
1303
|
+
if (!Number.isSafeInteger(s) || s <= 0 || s > 16384 || 16384 % s) throw new Error("Invalid TWL digest sector size");
|
|
1304
|
+
if (!Number.isSafeInteger(h.digest_block_sectorcount) || h.digest_block_sectorcount <= 0 || h.digest_block_sectorcount > 1048576)
|
|
1305
|
+
throw new Error("Invalid TWL digest block sector count");
|
|
1306
|
+
if (h.digest_ntr_size < 16384 || h.digest_ntr_size % s || h.digest_twl_size % s)
|
|
1307
|
+
throw new Error("Invalid TWL digest region size");
|
|
1308
|
+
const hashes = (h.digest_ntr_size + h.digest_twl_size) / s;
|
|
1309
|
+
if (hashes * 20 > h.sector_hashtable_size || Math.ceil(h.sector_hashtable_size / (h.digest_block_sectorcount * 20)) * 20 > h.block_hashtable_size)
|
|
1310
|
+
throw new Error("TWL hash tables are too small");
|
|
1311
|
+
}
|
|
1312
|
+
var TWL = class {
|
|
1313
|
+
firstDSiHashOffset;
|
|
1314
|
+
dsiHashSize;
|
|
1315
|
+
Hashtable1Data;
|
|
1316
|
+
Hashtable2Data;
|
|
1317
|
+
Header2Data;
|
|
1318
|
+
Overlays9Sha1Hmac;
|
|
1319
|
+
twlEncrypted;
|
|
1320
|
+
complete;
|
|
1321
|
+
dsi9Data = Buffer.alloc(0);
|
|
1322
|
+
dsi7Data = Buffer.alloc(0);
|
|
1323
|
+
get DSi9Data() {
|
|
1324
|
+
return this.complete?.DSi9Data ?? this.dsi9Data;
|
|
1325
|
+
}
|
|
1326
|
+
set DSi9Data(data) {
|
|
1327
|
+
if (this.complete) this.complete.DSi9Data = data;
|
|
1328
|
+
else this.dsi9Data = data;
|
|
1329
|
+
}
|
|
1330
|
+
get DSi7Data() {
|
|
1331
|
+
return this.complete?.DSi7Data ?? this.dsi7Data;
|
|
1332
|
+
}
|
|
1333
|
+
set DSi7Data(data) {
|
|
1334
|
+
if (this.complete) this.complete.DSi7Data = data;
|
|
1335
|
+
else this.dsi7Data = data;
|
|
1336
|
+
}
|
|
1337
|
+
constructor(header, overlays, original, options = {}) {
|
|
1338
|
+
if (options.dsiMode === "complete") {
|
|
1339
|
+
if (!(header instanceof Header)) throw new TypeError("Complete DSi handling requires a parsed Header");
|
|
1340
|
+
this.complete = new CompleteTWL(
|
|
1341
|
+
header,
|
|
1342
|
+
overlays.map((overlay) => slice(original, overlay.offset, overlay.size, "Signed ARM9 overlay")),
|
|
1343
|
+
original,
|
|
1344
|
+
options.maxOutputSize ?? DEFAULT_MAX_SIZE
|
|
1345
|
+
);
|
|
1346
|
+
this.firstDSiHashOffset = this.complete.firstDSiHashOffset;
|
|
1347
|
+
this.dsiHashSize = this.complete.dsiHashSize;
|
|
1348
|
+
this.Hashtable1Data = this.complete.Hashtable1Data;
|
|
1349
|
+
this.Hashtable2Data = this.complete.Hashtable2Data;
|
|
1350
|
+
this.Header2Data = this.complete.Header2Data;
|
|
1351
|
+
this.Overlays9Sha1Hmac = this.complete.Overlays9Sha1Hmac;
|
|
1352
|
+
this.twlEncrypted = this.complete.twlEncrypted;
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
validate(header);
|
|
1356
|
+
const h = header;
|
|
1357
|
+
this.firstDSiHashOffset = h.digest_ntr_size / h.digest_sector_size * 20;
|
|
1358
|
+
this.dsiHashSize = h.digest_twl_size / h.digest_sector_size * 20;
|
|
1359
|
+
this.Hashtable1Data = Buffer.from(
|
|
1360
|
+
slice(original, h.sector_hashtable_start, h.sector_hashtable_size, "Sector hash table")
|
|
1361
|
+
);
|
|
1362
|
+
this.Hashtable2Data = Buffer.from(
|
|
1363
|
+
slice(original, h.block_hashtable_start, h.block_hashtable_size, "Block hash table")
|
|
1364
|
+
);
|
|
1365
|
+
this.DSi9Data = Buffer.from(slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), "ARM9i"));
|
|
1366
|
+
this.DSi7Data = Buffer.from(slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), "ARM7i"));
|
|
1367
|
+
this.Header2Data = h.trimmedRom ? null : Array.from(
|
|
1368
|
+
{ length: 3 },
|
|
1369
|
+
(_, i) => Buffer.from(slice(original, h.digest_twl_start - 12288 + 4096 * i, 4096, "Secondary header"))
|
|
1370
|
+
);
|
|
1371
|
+
this.Overlays9Sha1Hmac = overlays.map((o) => hash2(slice(original, o.offset, o.size, "ARM9 overlay")));
|
|
1372
|
+
let encrypted = false;
|
|
1373
|
+
const modcryptStart = [
|
|
1374
|
+
{ start: h.modcrypt1_start, size: h.modcrypt1_size },
|
|
1375
|
+
{ start: h.modcrypt2_start, size: h.modcrypt2_size }
|
|
1376
|
+
].find(({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 4294967295)?.start;
|
|
1377
|
+
if (modcryptStart !== void 0) {
|
|
1378
|
+
const index = Math.floor((modcryptStart - h.digest_twl_start) / h.digest_sector_size);
|
|
1379
|
+
const actual = hash2(
|
|
1380
|
+
slice(original, h.digest_twl_start + index * h.digest_sector_size, h.digest_sector_size, "Modcrypt sector")
|
|
1381
|
+
);
|
|
1382
|
+
encrypted = !actual.equals(
|
|
1383
|
+
slice(this.Hashtable1Data, this.firstDSiHashOffset + index * 20, 20, "Modcrypt sector hash")
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (encrypted) this.cryptSections(h);
|
|
1387
|
+
this.twlEncrypted = !!(h.twlInternalFlags & 2);
|
|
1388
|
+
}
|
|
1389
|
+
cryptSections(h, writer) {
|
|
1390
|
+
const key2 = modcryptKey(h);
|
|
1391
|
+
for (const [id, data, start, counter] of [
|
|
1392
|
+
[1, this.DSi9Data, h.dsi9_rom_offset, h.hmac_arm9],
|
|
1393
|
+
[2, this.DSi7Data, h.dsi7_rom_offset, h.hmac_arm7]
|
|
1394
|
+
]) {
|
|
1395
|
+
const size = h[`modcrypt${id}_size`];
|
|
1396
|
+
if (!size) continue;
|
|
1397
|
+
const off = h[`modcrypt${id}_start`] - start;
|
|
1398
|
+
const result = aes128CtrCrypt(
|
|
1399
|
+
key2,
|
|
1400
|
+
slice(counter, 0, 16, "Modcrypt counter"),
|
|
1401
|
+
slice(data, off, size, "Modcrypt payload")
|
|
1402
|
+
);
|
|
1403
|
+
if (writer) {
|
|
1404
|
+
writer.position = start + off;
|
|
1405
|
+
writer.write(result);
|
|
1406
|
+
} else result.copy(data, off);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
importArm9iData(data, offset = 0, size) {
|
|
1410
|
+
if (this.complete) return this.complete.importArm9iData(data, offset, size);
|
|
1411
|
+
this.DSi9Data = this.importData(data, offset, size);
|
|
1412
|
+
}
|
|
1413
|
+
importArm7iData(data, offset = 0, size) {
|
|
1414
|
+
if (this.complete) return this.complete.importArm7iData(data, offset, size);
|
|
1415
|
+
this.DSi7Data = this.importData(data, offset, size);
|
|
1416
|
+
}
|
|
1417
|
+
importData(data, offset, size) {
|
|
1418
|
+
if (typeof data === "string") data = readFileSync(data);
|
|
1419
|
+
size ??= data.length - offset;
|
|
1420
|
+
const src = slice(data, offset, size, "DSi import");
|
|
1421
|
+
const out = Buffer.alloc(Math.ceil(size / 16) * 16, 255);
|
|
1422
|
+
src.copy(out);
|
|
1423
|
+
return out;
|
|
1424
|
+
}
|
|
1425
|
+
updateOverlays9Sha1Hmac(arm9Data, header, overlays) {
|
|
1426
|
+
if (this.complete) return this.complete.updateOverlays9Sha1Hmac(arm9Data, header, overlays);
|
|
1427
|
+
if (overlays.length !== this.Overlays9Sha1Hmac.length)
|
|
1428
|
+
throw new Error("Changing ARM9 overlay count is unsupported");
|
|
1429
|
+
const hashes = overlays.map(hash2);
|
|
1430
|
+
if (hashes.every((h, i) => h.equals(this.Overlays9Sha1Hmac[i]))) return Buffer.from(arm9Data);
|
|
1431
|
+
const init = header.reserved2.readUInt32LE(0) & 16383;
|
|
1432
|
+
const hdrptr = arm9Data.readUInt32LE(init + 20) - header.ARM9ramAddress;
|
|
1433
|
+
const unpacked = decompressArm9(arm9Data, header);
|
|
1434
|
+
const end = unpacked.data.readUInt32LE(init + 8) - header.ARM9ramAddress;
|
|
1435
|
+
let found = -1;
|
|
1436
|
+
for (let p = Math.min(end - hashes.length * 20, unpacked.data.length - hashes.length * 20); p >= 0; p--) {
|
|
1437
|
+
if (unpacked.data.subarray(p, p + 20).equals(this.Overlays9Sha1Hmac[0])) {
|
|
1438
|
+
found = p;
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (found < 0) throw new Error("ARM9 overlay hashes changed but original hash table was not found");
|
|
1443
|
+
hashes.forEach((h, i) => h.copy(unpacked.data, found + i * 20));
|
|
1444
|
+
return unpacked.compressed ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr) : unpacked.data;
|
|
1445
|
+
}
|
|
1446
|
+
writeTo(writer, h) {
|
|
1447
|
+
if (this.complete) throw new Error("Complete DSi serialization requires NDSFile.toBuffer()");
|
|
1448
|
+
validate(h);
|
|
1449
|
+
if (writer.position > h.sector_hashtable_start) {
|
|
1450
|
+
throw new Error("NTR output overlaps the fixed DSi sector hash table");
|
|
1451
|
+
}
|
|
1452
|
+
if (h.sector_hashtable_start + h.sector_hashtable_size > h.block_hashtable_start || h.block_hashtable_start + h.block_hashtable_size > h.dsi9_rom_offset || h.dsi9_rom_offset + this.DSi9Data.length > h.dsi7_rom_offset || h.dsi7_rom_offset + this.DSi7Data.length > h.total_rom_size) {
|
|
1453
|
+
throw new Error("DSi sections overlap or exceed the ROM size");
|
|
1454
|
+
}
|
|
1455
|
+
const fill = (end) => {
|
|
1456
|
+
if (writer.position < end) writer.fillTo(end, 255);
|
|
1457
|
+
};
|
|
1458
|
+
fill(h.sector_hashtable_start);
|
|
1459
|
+
writer.position = h.sector_hashtable_start + h.sector_hashtable_size;
|
|
1460
|
+
fill(h.block_hashtable_start);
|
|
1461
|
+
writer.position = h.block_hashtable_start + h.block_hashtable_size;
|
|
1462
|
+
fill(h.dsi9_rom_offset);
|
|
1463
|
+
if (this.Header2Data && !h.trimmedRom) {
|
|
1464
|
+
writer.position = h.digest_twl_start - 12288;
|
|
1465
|
+
this.Header2Data.forEach((b) => writer.write(b));
|
|
1466
|
+
}
|
|
1467
|
+
writer.position = h.dsi9_rom_offset;
|
|
1468
|
+
writer.write(this.DSi9Data);
|
|
1469
|
+
fill(h.dsi7_rom_offset);
|
|
1470
|
+
writer.position = h.dsi7_rom_offset;
|
|
1471
|
+
writer.write(this.DSi7Data);
|
|
1472
|
+
fill(h.total_rom_size);
|
|
1473
|
+
const finalPosition = writer.position;
|
|
1474
|
+
const read = (p, n) => slice(writer.buffer.subarray(0, writer.length), p, n, "TWL output digest");
|
|
1475
|
+
let secure = Buffer.from(read(h.digest_ntr_start, 16384));
|
|
1476
|
+
if (h.decrypted) secure = encryptSecureArea(h.gameCode, secure);
|
|
1477
|
+
let index = 0;
|
|
1478
|
+
const put = (b) => {
|
|
1479
|
+
writer.position = h.sector_hashtable_start + index++ * 20;
|
|
1480
|
+
writer.write(hash2(b));
|
|
1481
|
+
};
|
|
1482
|
+
for (let p = 0; p < 16384; p += h.digest_sector_size) put(secure.subarray(p, p + h.digest_sector_size));
|
|
1483
|
+
for (let p = h.digest_ntr_start + 16384; p < h.digest_ntr_start + h.digest_ntr_size; p += h.digest_sector_size)
|
|
1484
|
+
put(read(p, h.digest_sector_size));
|
|
1485
|
+
for (let p = h.digest_twl_start; p < h.digest_twl_start + h.digest_twl_size; p += h.digest_sector_size)
|
|
1486
|
+
put(read(p, h.digest_sector_size));
|
|
1487
|
+
index = 0;
|
|
1488
|
+
for (let p = h.sector_hashtable_start; p < h.sector_hashtable_start + h.sector_hashtable_size; p += h.digest_block_sectorcount * 20) {
|
|
1489
|
+
const digest = hash2(read(p, h.digest_block_sectorcount * 20));
|
|
1490
|
+
writer.position = h.block_hashtable_start + index++ * 20;
|
|
1491
|
+
writer.write(digest);
|
|
1492
|
+
}
|
|
1493
|
+
h.hmac_digest_master = hash2(read(h.block_hashtable_start, h.block_hashtable_size));
|
|
1494
|
+
if (this.twlEncrypted) this.cryptSections(h, writer);
|
|
1495
|
+
writer.position = finalPosition;
|
|
1496
|
+
return h.hmac_digest_master;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
var updateHeaderSignatures = (writer, header, originalHeader, _keepOriginal = true) => {
|
|
1500
|
+
const signedData = Buffer.from(slice(originalHeader, 0, 3584, "Signed header"));
|
|
1501
|
+
const digest = slice(header.hmac_digest_master, 0, 20, "Master digest");
|
|
1502
|
+
const signature = slice(header.rsa_signature, 0, 128, "RSA signature");
|
|
1503
|
+
digest.copy(signedData, 808);
|
|
1504
|
+
verifyDsiSignature(signedData, signature);
|
|
1505
|
+
const position = writer.position;
|
|
1506
|
+
writer.position = 808;
|
|
1507
|
+
writer.write(digest);
|
|
1508
|
+
writer.position = 3968;
|
|
1509
|
+
writer.write(signature);
|
|
1510
|
+
writer.position = position;
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
// src/nitro/nds-file.ts
|
|
1514
|
+
var key = (path) => path.replaceAll("\\", "/").toLowerCase();
|
|
1515
|
+
var NDSFile = class _NDSFile {
|
|
1516
|
+
original;
|
|
1517
|
+
header;
|
|
1518
|
+
banner;
|
|
1519
|
+
fatTable;
|
|
1520
|
+
fntTable;
|
|
1521
|
+
overlay9Table;
|
|
1522
|
+
overlay7Table;
|
|
1523
|
+
root;
|
|
1524
|
+
twl;
|
|
1525
|
+
warnings = [];
|
|
1526
|
+
paths = /* @__PURE__ */ new Map();
|
|
1527
|
+
maxOutputSize;
|
|
1528
|
+
addedOverlays = /* @__PURE__ */ new Map();
|
|
1529
|
+
structureChanged = false;
|
|
1530
|
+
constructor(input, options = {}) {
|
|
1531
|
+
this.original = bytes(input, "ROM");
|
|
1532
|
+
this.maxOutputSize = options.maxOutputSize ?? DEFAULT_MAX_SIZE;
|
|
1533
|
+
if (!Number.isSafeInteger(this.maxOutputSize) || this.maxOutputSize < 1 || this.maxOutputSize > 4294967295)
|
|
1534
|
+
throw new RangeError("Invalid output size limit");
|
|
1535
|
+
this.header = new Header(this.original);
|
|
1536
|
+
const h = this.header;
|
|
1537
|
+
if (options.dsiMode === "complete" && h.unitCode === 3)
|
|
1538
|
+
throw new Error("DSi-exclusive cartridges and DSiWare are unsupported");
|
|
1539
|
+
range(this.original, h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0), "ARM9");
|
|
1540
|
+
range(this.original, h.ARM7romOffset, h.ARM7size, "ARM7");
|
|
1541
|
+
this.banner = new Banner(this.original, h.bannerOffset, h.banner_size);
|
|
1542
|
+
this.fatTable = new FileAllocationTable(this.original, h.FAToffset, h.FATsize);
|
|
1543
|
+
this.fntTable = new FileNameTable(this.fatTable, this.original, h.FNToffset, h.FNTsize);
|
|
1544
|
+
this.root = this.fntTable.root;
|
|
1545
|
+
this.overlay9Table = new OverlayTable(this.original, h.ARM9overlayOffset, h.ARM9overlaySize, true);
|
|
1546
|
+
this.overlay7Table = new OverlayTable(this.original, h.ARM7overlayOffset, h.ARM7overlaySize, false);
|
|
1547
|
+
const arm9Overlays = this.overlay9Table.readBasicOverlays(this.fatTable);
|
|
1548
|
+
this.root.folders.push({
|
|
1549
|
+
name: "overlay",
|
|
1550
|
+
id: 65535,
|
|
1551
|
+
files: [...arm9Overlays, ...this.overlay7Table.readBasicOverlays(this.fatTable)],
|
|
1552
|
+
folders: []
|
|
1553
|
+
});
|
|
1554
|
+
const system = (name, offset, size) => {
|
|
1555
|
+
range(this.original, offset, size, name);
|
|
1556
|
+
this.root.files.push({ name, id: 65535, offset, size });
|
|
1557
|
+
};
|
|
1558
|
+
system("header.bin", 0, h.headerSize);
|
|
1559
|
+
system("banner.bin", h.bannerOffset, this.banner.raw.length);
|
|
1560
|
+
system("fnt.bin", h.FNToffset, h.FNTsize);
|
|
1561
|
+
system("fat.bin", h.FAToffset, h.FATsize);
|
|
1562
|
+
system("arm9.bin", h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0));
|
|
1563
|
+
system("arm7.bin", h.ARM7romOffset, h.ARM7size);
|
|
1564
|
+
if (h.ARM9overlaySize) system("overarm9.bin", h.ARM9overlayOffset, h.ARM9overlaySize);
|
|
1565
|
+
if (h.ARM7overlaySize) system("overarm7.bin", h.ARM7overlayOffset, h.ARM7overlaySize);
|
|
1566
|
+
const visit = (folder, prefix) => {
|
|
1567
|
+
for (const file of folder.files) {
|
|
1568
|
+
const path = prefix + file.name;
|
|
1569
|
+
if (this.paths.has(key(path))) throw new Error(`Duplicate ROM path: ${path}`);
|
|
1570
|
+
this.paths.set(key(path), { path, file });
|
|
1571
|
+
}
|
|
1572
|
+
for (const child of folder.folders) visit(child, `${prefix + child.name}/`);
|
|
1573
|
+
};
|
|
1574
|
+
visit(this.root, "");
|
|
1575
|
+
if (options.dsiMode === "complete" && h.unitCode & 2) {
|
|
1576
|
+
const regions = [
|
|
1577
|
+
...this.root.files.map((file) => ({ start: file.offset, size: file.size, name: file.name })),
|
|
1578
|
+
...this.fatTable.entries.map((file) => ({
|
|
1579
|
+
start: file.offset,
|
|
1580
|
+
size: file.size,
|
|
1581
|
+
name: `FAT ${file.id}`,
|
|
1582
|
+
payload: true
|
|
1583
|
+
})),
|
|
1584
|
+
{ start: h.dsi9_rom_offset, size: h.dsi9_size, name: "ARM9i" },
|
|
1585
|
+
{ start: h.dsi7_rom_offset, size: h.dsi7_size, name: "ARM7i" },
|
|
1586
|
+
{ start: h.sector_hashtable_start, size: h.sector_hashtable_size, name: "DSi sector table" },
|
|
1587
|
+
{ start: h.block_hashtable_start, size: h.block_hashtable_size, name: "DSi block table" }
|
|
1588
|
+
].filter((region) => region.size).sort((a, b) => a.start - b.start);
|
|
1589
|
+
let systemEnd;
|
|
1590
|
+
let payloadEnd;
|
|
1591
|
+
for (const region of regions) {
|
|
1592
|
+
range(this.original, region.start, region.size, region.name);
|
|
1593
|
+
for (const previous of [systemEnd, region.payload ? void 0 : payloadEnd])
|
|
1594
|
+
if (previous && previous.start + previous.size > region.start)
|
|
1595
|
+
throw new Error(`DSi ROM regions overlap: ${previous.name} and ${region.name}`);
|
|
1596
|
+
if (region.payload) {
|
|
1597
|
+
if (!payloadEnd || region.start + region.size > payloadEnd.start + payloadEnd.size) payloadEnd = region;
|
|
1598
|
+
} else systemEnd = region;
|
|
1599
|
+
}
|
|
1600
|
+
this.twl = new TWL(
|
|
1601
|
+
h,
|
|
1602
|
+
this.overlay9Table.entries.filter((entry) => entry.reserved & 33554432).map((entry) => {
|
|
1603
|
+
const file = this.fatTable.entries[entry.fileId];
|
|
1604
|
+
return { offset: file.offset, size: file.size };
|
|
1605
|
+
}),
|
|
1606
|
+
this.original,
|
|
1607
|
+
{ dsiMode: "complete", maxOutputSize: this.maxOutputSize }
|
|
1608
|
+
);
|
|
1609
|
+
for (const [name, offset, size] of [
|
|
1610
|
+
["arm9i.bin", h.dsi9_rom_offset, h.dsi9_size],
|
|
1611
|
+
["arm7i.bin", h.dsi7_rom_offset, h.dsi7_size]
|
|
1612
|
+
]) {
|
|
1613
|
+
const file = { name, id: 65535, offset, size };
|
|
1614
|
+
this.root.files.push(file);
|
|
1615
|
+
this.paths.set(name, { path: name, file });
|
|
1616
|
+
}
|
|
1617
|
+
} else if (h.unitCode & 2 && h.twlInternalFlags & 1 && h.tid_high && h.tid_high !== 4294967295) {
|
|
1618
|
+
this.twl = new TWL(h, arm9Overlays, this.original);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
static async fromFile(path, options = {}) {
|
|
1622
|
+
return new _NDSFile(await readFile(path), options);
|
|
1623
|
+
}
|
|
1624
|
+
get data() {
|
|
1625
|
+
return this.root.folders[0];
|
|
1626
|
+
}
|
|
1627
|
+
get overlay() {
|
|
1628
|
+
return this.root.folders[1];
|
|
1629
|
+
}
|
|
1630
|
+
listFiles() {
|
|
1631
|
+
return [...this.paths.values()].map(({ path, file }) => ({
|
|
1632
|
+
path,
|
|
1633
|
+
id: file.id,
|
|
1634
|
+
offset: file.offset,
|
|
1635
|
+
size: this.twl?.complete && (path === "arm9i.bin" || path === "arm7i.bin") ? this.twl.complete.getProgram(path === "arm9i.bin" ? "arm9i" : "arm7i").length : this.addedOverlays.size && (path === "overarm9.bin" || path === "overarm7.bin") ? this.readOverlayTable(path === "overarm9.bin").entries.length * 32 : file.replacement?.length ?? file.size
|
|
1636
|
+
}));
|
|
1637
|
+
}
|
|
1638
|
+
hasFile(path) {
|
|
1639
|
+
return this.paths.has(key(path));
|
|
1640
|
+
}
|
|
1641
|
+
getFile(path) {
|
|
1642
|
+
const entry = this.paths.get(key(path));
|
|
1643
|
+
if (!entry) throw new Error(`ROM file not found: ${path}`);
|
|
1644
|
+
return Buffer.from(this.fileBytes(entry.file));
|
|
1645
|
+
}
|
|
1646
|
+
/** Replace an existing file. */
|
|
1647
|
+
replaceFile(path, content) {
|
|
1648
|
+
let entry = this.paths.get(key(path));
|
|
1649
|
+
if (!entry && (key(path) === "overarm9.bin" || key(path) === "overarm7.bin")) {
|
|
1650
|
+
if (content.byteLength > this.maxOutputSize) throw new RangeError("Replacement exceeds output size limit");
|
|
1651
|
+
new OverlayTable(content, 0, content.byteLength, key(path) === "overarm9.bin");
|
|
1652
|
+
const file = { name: key(path), id: 65535, offset: 0, size: 0 };
|
|
1653
|
+
entry = { path: file.name, file };
|
|
1654
|
+
this.root.files.push(file);
|
|
1655
|
+
this.paths.set(file.name, entry);
|
|
1656
|
+
this.structureChanged = true;
|
|
1657
|
+
}
|
|
1658
|
+
if (!entry) throw new Error(`ROM file not found: ${path}`);
|
|
1659
|
+
if (this.twl?.complete && content.byteLength > this.maxOutputSize)
|
|
1660
|
+
throw new RangeError("Replacement exceeds output size limit");
|
|
1661
|
+
if (this.twl?.complete && (key(path) === "arm9i.bin" || key(path) === "arm7i.bin")) {
|
|
1662
|
+
this.twl.complete.setProgram(key(path) === "arm9i.bin" ? "arm9i" : "arm7i", bytes(content));
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
entry.file.replacement = Buffer.from(bytes(content));
|
|
1666
|
+
}
|
|
1667
|
+
/** Add a NitroFS file and any missing parent directories. */
|
|
1668
|
+
addFile(path, content) {
|
|
1669
|
+
const components = path.replaceAll("\\", "/").split("/");
|
|
1670
|
+
if (components[0]?.toLowerCase() !== "data" || components.length < 2 || components.slice(1).some((name) => !name || name === "." || name === ".." || name.includes("\0")))
|
|
1671
|
+
throw new Error(`Invalid NitroFS path: ${path}`);
|
|
1672
|
+
if (components.length > 258) throw new Error("FNT directory depth exceeds 256");
|
|
1673
|
+
if (this.hasFile(path)) throw new Error(`ROM file already exists: ${path}`);
|
|
1674
|
+
const replacement = Buffer.from(bytes(content));
|
|
1675
|
+
const names = components.slice(1).map((name) => ({ name }));
|
|
1676
|
+
let existing = this.data;
|
|
1677
|
+
for (const [index, component] of names.entries()) {
|
|
1678
|
+
const normalized = component.name.toLowerCase();
|
|
1679
|
+
if (existing?.files.some((file) => file.name.toLowerCase() === normalized))
|
|
1680
|
+
throw new Error(`ROM path conflicts with a file: ${path}`);
|
|
1681
|
+
const child = existing?.folders.find((child2) => child2.name.toLowerCase() === normalized);
|
|
1682
|
+
if (index === names.length - 1 && child) throw new Error(`ROM path conflicts with a directory: ${path}`);
|
|
1683
|
+
if (!child) component.nameBytes = encodeName(component.name);
|
|
1684
|
+
existing = child;
|
|
1685
|
+
}
|
|
1686
|
+
let folder = this.data;
|
|
1687
|
+
let prefix = "data";
|
|
1688
|
+
for (const [index, component] of names.entries()) {
|
|
1689
|
+
const normalized = component.name.toLowerCase();
|
|
1690
|
+
const child = folder.folders.find((child2) => child2.name.toLowerCase() === normalized);
|
|
1691
|
+
if (index === names.length - 1) {
|
|
1692
|
+
const file = { ...component, id: -1, offset: -1, size: replacement.length, replacement };
|
|
1693
|
+
folder.files.push(file);
|
|
1694
|
+
const outputPath = `${prefix}/${file.name}`;
|
|
1695
|
+
this.paths.set(key(outputPath), { path: outputPath, file });
|
|
1696
|
+
} else {
|
|
1697
|
+
if (child) folder = child;
|
|
1698
|
+
else {
|
|
1699
|
+
const created = { ...component, id: -1, files: [], folders: [] };
|
|
1700
|
+
folder.folders.push(created);
|
|
1701
|
+
folder = created;
|
|
1702
|
+
}
|
|
1703
|
+
prefix += `/${folder.name}`;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
this.structureChanged = true;
|
|
1707
|
+
}
|
|
1708
|
+
/** Add an overlay; file IDs are assigned when the ROM is serialized. */
|
|
1709
|
+
addOverlay(arm9, metadata, content) {
|
|
1710
|
+
const path = `overlay/overlay${arm9 ? "" : "7"}_${String(metadata.overlayId).padStart(4, "0")}.bin`;
|
|
1711
|
+
if (!Number.isSafeInteger(metadata.overlayId) || metadata.overlayId < 0 || metadata.overlayId >= 4294967295)
|
|
1712
|
+
throw new RangeError("Invalid overlay ID");
|
|
1713
|
+
if (this.hasFile(path)) throw new Error(`ROM file already exists: ${path}`);
|
|
1714
|
+
if (content.byteLength > this.maxOutputSize) throw new RangeError("Overlay exceeds output size limit");
|
|
1715
|
+
const entry = { ...metadata, fileId: 0 };
|
|
1716
|
+
const table = new OverlayTable(Buffer.alloc(0), 0, 0, arm9);
|
|
1717
|
+
table.entries.push(entry);
|
|
1718
|
+
table.toBuffer();
|
|
1719
|
+
const replacement = Buffer.from(bytes(content));
|
|
1720
|
+
const file = { name: path.slice(8), id: -1, offset: -1, size: replacement.length, replacement };
|
|
1721
|
+
const tableName = arm9 ? "overarm9.bin" : "overarm7.bin";
|
|
1722
|
+
if (!this.hasFile(tableName)) this.replaceFile(tableName, Buffer.alloc(0));
|
|
1723
|
+
this.overlay.files.push(file);
|
|
1724
|
+
this.paths.set(path, { path, file });
|
|
1725
|
+
this.addedOverlays.set(path, { arm9, entry });
|
|
1726
|
+
this.structureChanged = true;
|
|
1727
|
+
}
|
|
1728
|
+
readOverlayTable(arm9) {
|
|
1729
|
+
const original = arm9 ? this.overlay9Table : this.overlay7Table;
|
|
1730
|
+
const replacement = this.paths.get(arm9 ? "overarm9.bin" : "overarm7.bin")?.file.replacement;
|
|
1731
|
+
const raw = replacement ?? original.toBuffer();
|
|
1732
|
+
const table = new OverlayTable(raw, 0, raw.length, arm9);
|
|
1733
|
+
for (const { arm9: target, entry } of this.addedOverlays.values()) {
|
|
1734
|
+
if (target !== arm9) continue;
|
|
1735
|
+
const existing = table.entries.find((item) => item.overlayId === entry.overlayId);
|
|
1736
|
+
if (existing) Object.assign(existing, entry);
|
|
1737
|
+
else table.entries.push({ ...entry });
|
|
1738
|
+
}
|
|
1739
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1740
|
+
for (const entry of table.entries) {
|
|
1741
|
+
if (ids.has(entry.overlayId)) throw new Error(`Duplicate overlay ID: ${entry.overlayId}`);
|
|
1742
|
+
ids.add(entry.overlayId);
|
|
1743
|
+
const path = `overlay/overlay${arm9 ? "" : "7"}_${String(entry.overlayId).padStart(4, "0")}.bin`;
|
|
1744
|
+
if (!this.hasFile(path)) throw new Error(`Overlay table entry has no payload: ${path}`);
|
|
1745
|
+
}
|
|
1746
|
+
if (original.entries.some((entry) => !ids.has(entry.overlayId)))
|
|
1747
|
+
throw new Error("Removing overlays is unsupported");
|
|
1748
|
+
return table;
|
|
1749
|
+
}
|
|
1750
|
+
fileBytes(file) {
|
|
1751
|
+
if (this.twl?.complete && (file.name === "arm9i.bin" || file.name === "arm7i.bin"))
|
|
1752
|
+
return this.twl.complete.getProgram(file.name === "arm9i.bin" ? "arm9i" : "arm7i");
|
|
1753
|
+
if (this.addedOverlays.size && (file.name === "overarm9.bin" || file.name === "overarm7.bin"))
|
|
1754
|
+
return this.readOverlayTable(file.name === "overarm9.bin").toBuffer();
|
|
1755
|
+
return file.replacement ?? range(this.original, file.offset, file.size, file.name);
|
|
1756
|
+
}
|
|
1757
|
+
verifyDsi() {
|
|
1758
|
+
if (this.twl?.complete) return this.twl.complete.verify();
|
|
1759
|
+
if (this.header.unitCode & 2)
|
|
1760
|
+
return new _NDSFile(this.original, { dsiMode: "complete", maxOutputSize: this.maxOutputSize }).verifyDsi();
|
|
1761
|
+
return noDsiVerification();
|
|
1762
|
+
}
|
|
1763
|
+
/** Rebuild offsets, allocation table, CRCs and ROM padding. Safe to call repeatedly. */
|
|
1764
|
+
toBuffer() {
|
|
1765
|
+
if (this.twl?.complete && !this.structureChanged && this.twl.complete.unchanged() && this.header.raw.equals(this.original.subarray(0, this.header.raw.length)) && this.banner.toBuffer().equals(new Banner(this.original, this.header.bannerOffset, this.header.banner_size).toBuffer()) && [...this.paths.values()].every(
|
|
1766
|
+
({ file }) => !file.replacement || file.replacement.equals(range(this.original, file.offset, file.size, file.name))
|
|
1767
|
+
)) {
|
|
1768
|
+
if (this.original.length > this.maxOutputSize) throw new RangeError("Output exceeds size limit");
|
|
1769
|
+
return Buffer.from(this.original);
|
|
1770
|
+
}
|
|
1771
|
+
const fntReplacement = this.root.files.find((file) => file.name === "fnt.bin")?.replacement;
|
|
1772
|
+
if (this.structureChanged && fntReplacement) throw new Error("Adding files conflicts with an fnt.bin replacement");
|
|
1773
|
+
if (fntReplacement) {
|
|
1774
|
+
const replacementTree = new FileNameTable(this.fatTable, fntReplacement, 0, fntReplacement.length);
|
|
1775
|
+
const collect = (folder) => [...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort((a, b) => a - b);
|
|
1776
|
+
if (collect(replacementTree.root).join(",") !== collect(this.data).join(","))
|
|
1777
|
+
throw new Error("Replacement FNT must preserve existing file IDs");
|
|
1778
|
+
}
|
|
1779
|
+
const layout = this.structureChanged ? this.fntTable.serialize(this.fatTable, this.overlay.files) : void 0;
|
|
1780
|
+
if (this.twl?.complete) return this.serializeDsi(layout);
|
|
1781
|
+
const overlayFiles = new Map(this.overlay.files.map((file) => [file.id, file]));
|
|
1782
|
+
const h = new Header(this.header.toBuffer());
|
|
1783
|
+
h.nitrocode = this.header.nitrocode;
|
|
1784
|
+
h.decrypted = this.header.decrypted;
|
|
1785
|
+
h.trimmedRom = this.header.trimmedRom;
|
|
1786
|
+
h.dlp_signature = this.header.dlp_signature;
|
|
1787
|
+
const writer = new BinaryWriter(65536, this.maxOutputSize);
|
|
1788
|
+
const system = (name) => {
|
|
1789
|
+
const file = this.root.files.find((file2) => file2.name === name);
|
|
1790
|
+
if (!file) throw new Error(`Missing ROM system file: ${name}`);
|
|
1791
|
+
return file;
|
|
1792
|
+
};
|
|
1793
|
+
const replacedHeader = system("header.bin").replacement;
|
|
1794
|
+
if (replacedHeader) {
|
|
1795
|
+
const replacement = new Header(replacedHeader);
|
|
1796
|
+
h.gameTitle = replacement.gameTitle;
|
|
1797
|
+
h.gameCode = replacement.gameCode;
|
|
1798
|
+
}
|
|
1799
|
+
const placements = /* @__PURE__ */ new Map();
|
|
1800
|
+
const write = (file, padding = true) => {
|
|
1801
|
+
const id = layout ? file.id >= 0 ? layout.sourceIds.get(file.id) : layout.ids.get(file) : file.id;
|
|
1802
|
+
if (layout && id !== void 0 && placements.has(id)) return;
|
|
1803
|
+
if (layout && file.id >= 0) file = overlayFiles.get(file.id) ?? file;
|
|
1804
|
+
const start = writer.position;
|
|
1805
|
+
writer.write(this.fileBytes(file));
|
|
1806
|
+
if (id !== void 0 && id !== 65535) placements.set(id, { start, end: writer.position });
|
|
1807
|
+
if (padding) writer.pad(512);
|
|
1808
|
+
};
|
|
1809
|
+
const overlayIds = /* @__PURE__ */ new Set();
|
|
1810
|
+
const writeOverlays = (arm92) => {
|
|
1811
|
+
const name = arm92 ? "overarm9.bin" : "overarm7.bin";
|
|
1812
|
+
const entry = this.root.files.find((file) => file.name === name);
|
|
1813
|
+
if (!entry) return;
|
|
1814
|
+
const table = this.readOverlayTable(arm92);
|
|
1815
|
+
const files = this.overlay.files.filter((file) => file.name.startsWith(arm92 ? "overlay_" : "overlay7_")).sort((a, b) => a.id < 0 ? b.id < 0 ? 0 : 1 : b.id < 0 ? -1 : a.id - b.id);
|
|
1816
|
+
for (const file of files) {
|
|
1817
|
+
const item = table.entries.find(
|
|
1818
|
+
(item2) => file.name === `overlay${arm92 ? "" : "7"}_${String(item2.overlayId).padStart(4, "0")}.bin`
|
|
1819
|
+
);
|
|
1820
|
+
const size = this.fileBytes(layout && file.id >= 0 ? overlayFiles.get(file.id) : file).length;
|
|
1821
|
+
if (item.reserved >>> 24 & 1 || !arm92 && item.reserved > 0)
|
|
1822
|
+
item.reserved = (item.reserved & 4278190080 | size & 16777215) >>> 0;
|
|
1823
|
+
item.fileId = layout ? file.id >= 0 ? layout.sourceIds.get(file.id) : layout.ids.get(file) : file.id;
|
|
1824
|
+
overlayIds.add(file.id);
|
|
1825
|
+
}
|
|
1826
|
+
const encoded = table.toBuffer();
|
|
1827
|
+
if (arm92) {
|
|
1828
|
+
h.ARM9overlayOffset = writer.position;
|
|
1829
|
+
h.ARM9overlaySize = encoded.length;
|
|
1830
|
+
} else {
|
|
1831
|
+
h.ARM7overlayOffset = writer.position;
|
|
1832
|
+
h.ARM7overlaySize = encoded.length;
|
|
1833
|
+
}
|
|
1834
|
+
writer.write(encoded);
|
|
1835
|
+
writer.pad(512);
|
|
1836
|
+
for (const file of files) write(file);
|
|
1837
|
+
};
|
|
1838
|
+
writer.position = h.headerSize;
|
|
1839
|
+
h.ARM9romOffset = writer.position;
|
|
1840
|
+
const arm9 = system("arm9.bin");
|
|
1841
|
+
const arm9Bytes = this.fileBytes(arm9);
|
|
1842
|
+
if (arm9.replacement) {
|
|
1843
|
+
h.nitrocode = arm9Bytes.length >= 12 && arm9Bytes.readUInt32LE(arm9Bytes.length - 12) === 3737126433;
|
|
1844
|
+
}
|
|
1845
|
+
h.ARM9size = arm9Bytes.length - (h.nitrocode ? 12 : 0);
|
|
1846
|
+
write(arm9);
|
|
1847
|
+
h.ARM9overlayOffset = 0;
|
|
1848
|
+
writeOverlays(true);
|
|
1849
|
+
h.ARM7romOffset = writer.position;
|
|
1850
|
+
h.ARM7size = this.fileBytes(system("arm7.bin")).length;
|
|
1851
|
+
write(system("arm7.bin"));
|
|
1852
|
+
h.ARM7overlayOffset = 0;
|
|
1853
|
+
writeOverlays(false);
|
|
1854
|
+
h.FNToffset = writer.position;
|
|
1855
|
+
const fnt = layout?.fnt ?? this.fileBytes(system("fnt.bin"));
|
|
1856
|
+
h.FNTsize = fnt.length;
|
|
1857
|
+
writer.write(fnt);
|
|
1858
|
+
writer.pad(512);
|
|
1859
|
+
h.FAToffset = writer.position;
|
|
1860
|
+
h.FATsize = (layout?.files.length ?? this.fatTable.entries.length) * 8;
|
|
1861
|
+
writer.write(Buffer.alloc(h.FATsize));
|
|
1862
|
+
writer.pad(512);
|
|
1863
|
+
h.bannerOffset = writer.position;
|
|
1864
|
+
const bannerFile = system("banner.bin");
|
|
1865
|
+
const banner = bannerFile.replacement ? new Banner(bannerFile.replacement, 0, bannerFile.replacement.length) : this.banner;
|
|
1866
|
+
const bannerBytes = banner.toBuffer();
|
|
1867
|
+
if (h.unitCode & 2 && h.headerSize > 512) h.banner_size = bannerBytes.length;
|
|
1868
|
+
writer.write(bannerBytes);
|
|
1869
|
+
writer.pad(512);
|
|
1870
|
+
if (layout) {
|
|
1871
|
+
for (const [index, file] of layout.files.entries()) write(file, index < layout.files.length - 1);
|
|
1872
|
+
} else
|
|
1873
|
+
for (const [index, id] of this.fatTable.sortedIDs.entries()) {
|
|
1874
|
+
if (overlayIds.has(id)) continue;
|
|
1875
|
+
const file = FileNameTable.findFile(id, this.root);
|
|
1876
|
+
if (file) write(file, index < this.fatTable.sortedIDs.length - 1);
|
|
1877
|
+
else {
|
|
1878
|
+
const entry = this.fatTable.entries[id];
|
|
1879
|
+
if (entry.size > 0) {
|
|
1880
|
+
write({ ...entry, name: `FAT entry ${id}` }, index < this.fatTable.sortedIDs.length - 1);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const logicalEnd = writer.position;
|
|
1885
|
+
const fat = Buffer.alloc(h.FATsize);
|
|
1886
|
+
for (const [id, location] of placements) {
|
|
1887
|
+
fat.writeUInt32LE(location.start, id * 8);
|
|
1888
|
+
fat.writeUInt32LE(location.end, id * 8 + 4);
|
|
1889
|
+
}
|
|
1890
|
+
writer.position = h.FAToffset;
|
|
1891
|
+
writer.write(fat);
|
|
1892
|
+
writer.position = logicalEnd;
|
|
1893
|
+
h.decrypted = writer.length >= 16392 && writer.buffer.readBigUInt64LE(16384) === 0xe7ffdeffe7ffdeffn;
|
|
1894
|
+
if (this.twl) this.twl.writeTo(writer, h);
|
|
1895
|
+
h.ROMsize = writer.position;
|
|
1896
|
+
h.size = 2 ** Math.ceil(Math.log2(Math.max(h.ROMsize + h.dlp_signature.length, 131072)));
|
|
1897
|
+
const secure = writer.toBuffer().subarray(16384, 32768);
|
|
1898
|
+
h.secureCRC16 = crc16(h.decrypted ? encryptSecureArea(h.gameCode, secure) : secure);
|
|
1899
|
+
writer.position = 0;
|
|
1900
|
+
writer.write(h.toBuffer());
|
|
1901
|
+
writer.position = h.ROMsize;
|
|
1902
|
+
writer.write(h.dlp_signature);
|
|
1903
|
+
writer.fillTo(h.size);
|
|
1904
|
+
return writer.toBuffer();
|
|
1905
|
+
}
|
|
1906
|
+
async saveAs(path) {
|
|
1907
|
+
await writeFile(path, this.toBuffer());
|
|
1908
|
+
}
|
|
1909
|
+
serializeDsi(layout) {
|
|
1910
|
+
const twl = this.twl.complete;
|
|
1911
|
+
const h = new Header(this.header.raw);
|
|
1912
|
+
h.nitrocode = this.header.nitrocode;
|
|
1913
|
+
const system = (name) => this.paths.get(name).file;
|
|
1914
|
+
const replacement = system("header.bin").replacement;
|
|
1915
|
+
if (replacement) {
|
|
1916
|
+
const source = new Header(replacement);
|
|
1917
|
+
if (source.unitCode !== 2 || source.tid_high !== h.tid_high || source.tid_low !== h.tid_low || h.dsi9_size && !source.dsi9_size || h.dsi7_size && !source.dsi7_size)
|
|
1918
|
+
throw new Error("Replacement header changes or clears the DSi cartridge identity or programs");
|
|
1919
|
+
source.raw.copy(h.raw);
|
|
1920
|
+
for (const [start, end] of [
|
|
1921
|
+
[32, 64],
|
|
1922
|
+
[64, 96],
|
|
1923
|
+
[104, 110],
|
|
1924
|
+
[128, 136],
|
|
1925
|
+
[144, 148],
|
|
1926
|
+
[448, 452],
|
|
1927
|
+
[460, 468],
|
|
1928
|
+
[476, 532],
|
|
1929
|
+
[544, 560],
|
|
1930
|
+
[768, 888],
|
|
1931
|
+
[928, 948],
|
|
1932
|
+
[3968, 4096]
|
|
1933
|
+
])
|
|
1934
|
+
this.header.raw.copy(h.raw, start, start, end);
|
|
1935
|
+
h.ARM9entryAddress = source.ARM9entryAddress;
|
|
1936
|
+
h.ARM9ramAddress = source.ARM9ramAddress;
|
|
1937
|
+
h.ARM7entryAddress = source.ARM7entryAddress;
|
|
1938
|
+
h.ARM7ramAddress = source.ARM7ramAddress;
|
|
1939
|
+
h.dsi7_ram_address = source.dsi7_ram_address;
|
|
1940
|
+
h.headerSize = this.header.headerSize;
|
|
1941
|
+
}
|
|
1942
|
+
const sections = [];
|
|
1943
|
+
const add = (label, file, data, place) => {
|
|
1944
|
+
sections.push({ label, start: file.offset, originalSize: file.size, data, place });
|
|
1945
|
+
};
|
|
1946
|
+
const arm9File = system("arm9.bin");
|
|
1947
|
+
let arm9 = this.fileBytes(arm9File);
|
|
1948
|
+
if (arm9File.replacement) h.nitrocode = arm9.length >= 12 && arm9.readUInt32LE(arm9.length - 12) === 3737126433;
|
|
1949
|
+
if (h.gameCode !== twl.header.gameCode && arm9.readBigUInt64LE(0) !== 0xe7ffdeffe7ffdeffn)
|
|
1950
|
+
arm9 = encryptSecureArea(h.gameCode, decryptSecureArea(twl.header.gameCode, arm9));
|
|
1951
|
+
const signedOverlays = this.readOverlayTable(true).entries.filter((entry) => entry.reserved & 33554432);
|
|
1952
|
+
if (signedOverlays.length !== twl.Overlays9Sha1Hmac.length) {
|
|
1953
|
+
if (!arm9File.replacement) throw new Error("Changing signed overlay count requires an ARM9 replacement");
|
|
1954
|
+
} else
|
|
1955
|
+
arm9 = twl.updateOverlays9Sha1Hmac(
|
|
1956
|
+
arm9,
|
|
1957
|
+
h,
|
|
1958
|
+
signedOverlays.map((entry) => this.getFile(`overlay/overlay_${String(entry.overlayId).padStart(4, "0")}.bin`))
|
|
1959
|
+
);
|
|
1960
|
+
h.ARM9size = arm9.length - (h.nitrocode ? 12 : 0);
|
|
1961
|
+
h.decrypted = arm9.length >= 8 && arm9.readBigUInt64LE(0) === 0xe7ffdeffe7ffdeffn;
|
|
1962
|
+
const plainArm9 = h.decrypted ? Buffer.from(arm9) : decryptSecureArea(h.gameCode, arm9);
|
|
1963
|
+
plainArm9.writeUInt16LE(crc16(range(plainArm9, 16, 2032, "ARM9 secure-area CRC")), 14);
|
|
1964
|
+
arm9 = h.decrypted ? plainArm9 : encryptSecureArea(h.gameCode, plainArm9);
|
|
1965
|
+
add("ARM9", arm9File, arm9, (start) => {
|
|
1966
|
+
h.ARM9romOffset = start;
|
|
1967
|
+
});
|
|
1968
|
+
const written = /* @__PURE__ */ new Set();
|
|
1969
|
+
const fileList = layout?.files ?? this.fatTable.entries.map(
|
|
1970
|
+
(entry) => FileNameTable.findFile(entry.id, this.root) ?? { ...entry, name: `FAT entry ${entry.id}` }
|
|
1971
|
+
);
|
|
1972
|
+
const fat = Buffer.alloc(fileList.length * 8);
|
|
1973
|
+
const addPayload = (file) => {
|
|
1974
|
+
const id = layout ? file.id >= 0 ? layout.sourceIds.get(file.id) : layout.ids.get(file) : file.id;
|
|
1975
|
+
if (written.has(id)) return;
|
|
1976
|
+
written.add(id);
|
|
1977
|
+
const overlay = file.id >= 0 ? this.overlay.files.find((overlay2) => overlay2.id === file.id) : void 0;
|
|
1978
|
+
if (overlay) file = overlay;
|
|
1979
|
+
const data = this.fileBytes(file);
|
|
1980
|
+
add(file.name, file, data, (start) => {
|
|
1981
|
+
fat.writeUInt32LE(start, id * 8);
|
|
1982
|
+
fat.writeUInt32LE(start + data.length, id * 8 + 4);
|
|
1983
|
+
});
|
|
1984
|
+
};
|
|
1985
|
+
const overlays = (arm92) => {
|
|
1986
|
+
const name = arm92 ? "overarm9.bin" : "overarm7.bin";
|
|
1987
|
+
if (!this.hasFile(name)) return;
|
|
1988
|
+
const file = system(name);
|
|
1989
|
+
const table = this.readOverlayTable(arm92);
|
|
1990
|
+
for (const entry of table.entries) {
|
|
1991
|
+
const payload = this.paths.get(
|
|
1992
|
+
`overlay/overlay${arm92 ? "" : "7"}_${String(entry.overlayId).padStart(4, "0")}.bin`
|
|
1993
|
+
).file;
|
|
1994
|
+
const size = this.fileBytes(payload).length;
|
|
1995
|
+
if (entry.reserved >>> 24 & 1 || !arm92 && entry.reserved > 0)
|
|
1996
|
+
entry.reserved = (entry.reserved & 4278190080 | size) >>> 0;
|
|
1997
|
+
entry.fileId = layout ? payload.id >= 0 ? layout.sourceIds.get(payload.id) : layout.ids.get(payload) : payload.id;
|
|
1998
|
+
}
|
|
1999
|
+
add(name, file, table.toBuffer(), (start) => {
|
|
2000
|
+
if (arm92) {
|
|
2001
|
+
h.ARM9overlayOffset = start;
|
|
2002
|
+
h.ARM9overlaySize = table.entries.length * 32;
|
|
2003
|
+
} else {
|
|
2004
|
+
h.ARM7overlayOffset = start;
|
|
2005
|
+
h.ARM7overlaySize = table.entries.length * 32;
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
for (const entry of table.entries)
|
|
2009
|
+
addPayload(
|
|
2010
|
+
this.paths.get(`overlay/overlay${arm92 ? "" : "7"}_${String(entry.overlayId).padStart(4, "0")}.bin`).file
|
|
2011
|
+
);
|
|
2012
|
+
};
|
|
2013
|
+
overlays(true);
|
|
2014
|
+
const arm7 = system("arm7.bin");
|
|
2015
|
+
h.ARM7size = this.fileBytes(arm7).length;
|
|
2016
|
+
add("ARM7", arm7, this.fileBytes(arm7), (start) => {
|
|
2017
|
+
h.ARM7romOffset = start;
|
|
2018
|
+
});
|
|
2019
|
+
overlays(false);
|
|
2020
|
+
const fnt = layout?.fnt ?? this.fileBytes(system("fnt.bin"));
|
|
2021
|
+
h.FNTsize = fnt.length;
|
|
2022
|
+
add("FNT", system("fnt.bin"), fnt, (start) => {
|
|
2023
|
+
h.FNToffset = start;
|
|
2024
|
+
});
|
|
2025
|
+
h.FATsize = fat.length;
|
|
2026
|
+
add("FAT", system("fat.bin"), fat, (start) => {
|
|
2027
|
+
h.FAToffset = start;
|
|
2028
|
+
});
|
|
2029
|
+
const bannerFile = system("banner.bin");
|
|
2030
|
+
const banner = bannerFile.replacement ? new Banner(bannerFile.replacement, 0, bannerFile.replacement.length) : this.banner;
|
|
2031
|
+
const bannerBytes = banner.toBuffer();
|
|
2032
|
+
h.banner_size = bannerBytes.length;
|
|
2033
|
+
add("Banner", bannerFile, bannerBytes, (start) => {
|
|
2034
|
+
h.bannerOffset = start;
|
|
2035
|
+
});
|
|
2036
|
+
for (const file of fileList) addPayload(file);
|
|
2037
|
+
return twl.serialize(h, sections, !!layout);
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
export {
|
|
2042
|
+
hashDigest,
|
|
2043
|
+
readFile,
|
|
2044
|
+
inflateRawSync,
|
|
2045
|
+
bytes,
|
|
2046
|
+
BinaryWriter,
|
|
2047
|
+
crc16,
|
|
2048
|
+
crc32,
|
|
2049
|
+
Banner,
|
|
2050
|
+
aes128CtrCrypt,
|
|
2051
|
+
modcryptKey,
|
|
2052
|
+
encryptSecureArea,
|
|
2053
|
+
decryptSecureArea,
|
|
2054
|
+
FileAllocationTable,
|
|
2055
|
+
FileNameTable,
|
|
2056
|
+
OverlayTable,
|
|
2057
|
+
UnitCode,
|
|
2058
|
+
Header,
|
|
2059
|
+
decompressBLZ,
|
|
2060
|
+
compressBLZ,
|
|
2061
|
+
decompressArm9,
|
|
2062
|
+
compressArm9,
|
|
2063
|
+
TWL,
|
|
2064
|
+
updateHeaderSignatures,
|
|
2065
|
+
NDSFile
|
|
2066
|
+
};
|
|
2067
|
+
//# sourceMappingURL=chunk-LK3JXA24.js.map
|