nitro-patcher 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +5 -2
- package/dist/browser.js +1367 -1418
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-GFF67HX4.js → chunk-GLK3K2HY.js} +221 -282
- package/dist/chunk-GLK3K2HY.js.map +1 -0
- package/dist/{chunk-ZY3OSZCA.js → chunk-IGURDH5L.js} +180 -186
- package/dist/chunk-IGURDH5L.js.map +1 -0
- package/dist/cli.js +5 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/nitro-helper.d.ts +78 -78
- package/dist/nitro-helper.js +1 -1
- package/dist/nitro-patch-helper.d.ts +5 -2
- package/dist/nitro-patch-helper.js +4 -2
- package/package.json +6 -2
- package/src/browser/buffer.ts +1 -1
- package/src/browser.ts +16 -12
- package/src/cli.ts +38 -48
- package/src/index.ts +2 -2
- package/src/nitro/banner.ts +28 -31
- package/src/nitro/binary.ts +6 -16
- package/src/nitro/blz.ts +13 -21
- package/src/nitro/crypto.ts +78 -84
- package/src/nitro/filesystem.ts +36 -39
- package/src/nitro/header.ts +58 -71
- package/src/nitro/nds-file.ts +44 -63
- package/src/nitro/twl.ts +59 -100
- package/src/nitro-helper.ts +14 -14
- package/src/nitro-patch-helper.ts +49 -57
- package/src/patch/xdelta.ts +43 -49
- package/src/patch/zip.ts +47 -56
- package/src/runtime/browser.ts +9 -11
- package/src/runtime/node.ts +14 -15
- package/dist/chunk-GFF67HX4.js.map +0 -1
- package/dist/chunk-ZY3OSZCA.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/runtime/node.ts
|
|
2
|
-
import { createHash, createHmac,
|
|
2
|
+
import { createCipheriv, createHash, createHmac, createPublicKey, verify } from "crypto";
|
|
3
3
|
import { readFile, writeFile } from "fs/promises";
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { inflateRawSync } from "zlib";
|
|
@@ -33,15 +33,11 @@ var bytes = (value, name = "input") => {
|
|
|
33
33
|
};
|
|
34
34
|
var range = (buffer, offset, size, name = "binary data") => {
|
|
35
35
|
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(size) || offset < 0 || size < 0 || offset + size > buffer.length) {
|
|
36
|
-
throw new RangeError(
|
|
37
|
-
`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`
|
|
38
|
-
);
|
|
36
|
+
throw new RangeError(`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`);
|
|
39
37
|
}
|
|
40
38
|
return buffer.subarray(offset, offset + size);
|
|
41
39
|
};
|
|
42
|
-
var align = (value, alignment = 512) =>
|
|
43
|
-
return Math.ceil(value / alignment) * alignment;
|
|
44
|
-
};
|
|
40
|
+
var align = (value, alignment = 512) => Math.ceil(value / alignment) * alignment;
|
|
45
41
|
var BinaryWriter = class {
|
|
46
42
|
maxSize;
|
|
47
43
|
buffer;
|
|
@@ -65,8 +61,7 @@ var BinaryWriter = class {
|
|
|
65
61
|
}
|
|
66
62
|
write(data) {
|
|
67
63
|
data = bytes(data);
|
|
68
|
-
if (!Number.isSafeInteger(this.position) || this.position < 0)
|
|
69
|
-
throw new RangeError("Invalid writer position");
|
|
64
|
+
if (!Number.isSafeInteger(this.position) || this.position < 0) throw new RangeError("Invalid writer position");
|
|
70
65
|
this.ensure(this.position + data.length);
|
|
71
66
|
bytes(data).copy(this.buffer, this.position);
|
|
72
67
|
this.position += data.length;
|
|
@@ -110,14 +105,79 @@ var crc32 = (data) => {
|
|
|
110
105
|
return (crc ^ 4294967295) >>> 0;
|
|
111
106
|
};
|
|
112
107
|
|
|
108
|
+
// src/nitro/banner.ts
|
|
109
|
+
var titles = [
|
|
110
|
+
"japaneseTitle",
|
|
111
|
+
"englishTitle",
|
|
112
|
+
"frenchTitle",
|
|
113
|
+
"germanTitle",
|
|
114
|
+
"italianTitle",
|
|
115
|
+
"spanishTitle",
|
|
116
|
+
"chineseTitle",
|
|
117
|
+
"koreanTitle"
|
|
118
|
+
];
|
|
119
|
+
var Banner = class {
|
|
120
|
+
version;
|
|
121
|
+
raw;
|
|
122
|
+
bannerCRC;
|
|
123
|
+
tileData;
|
|
124
|
+
palette;
|
|
125
|
+
japaneseTitle = "";
|
|
126
|
+
englishTitle = "";
|
|
127
|
+
frenchTitle = "";
|
|
128
|
+
germanTitle = "";
|
|
129
|
+
italianTitle = "";
|
|
130
|
+
spanishTitle = "";
|
|
131
|
+
chineseTitle = "";
|
|
132
|
+
koreanTitle = "";
|
|
133
|
+
constructor(input, offset = 0, hardSize = 0) {
|
|
134
|
+
const data = bytes(input, "banner");
|
|
135
|
+
this.version = range(data, offset, 2, "banner").readUInt16LE();
|
|
136
|
+
let size = this.getDefSize(hardSize);
|
|
137
|
+
if (this.version >>> 8 === 1) size = hardSize > 0 && hardSize !== 4294967295 ? Math.min(hardSize, 9152) : 9152;
|
|
138
|
+
this.raw = Buffer.from(range(data, offset, size, "banner"));
|
|
139
|
+
this.bannerCRC = crc16(this.raw.subarray(32, 2112)) === this.raw.readUInt16LE(2);
|
|
140
|
+
this.tileData = Buffer.from(this.raw.subarray(32, 544));
|
|
141
|
+
this.palette = Buffer.from(this.raw.subarray(544, 576));
|
|
142
|
+
for (const [i, title] of titles.entries())
|
|
143
|
+
this[title] = i < this.titleCount ? this.raw.subarray(576 + i * 256, 832 + i * 256).toString("utf16le").split("\0")[0] : "";
|
|
144
|
+
}
|
|
145
|
+
get titleCount() {
|
|
146
|
+
return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
|
|
147
|
+
}
|
|
148
|
+
getDefSize(hardSize = 0) {
|
|
149
|
+
if (this.version === 2) return 2368;
|
|
150
|
+
if (this.version === 3) return 2624;
|
|
151
|
+
if (this.version === 259) return hardSize > 0 && hardSize !== 4294967295 ? hardSize : 9152;
|
|
152
|
+
return 2112;
|
|
153
|
+
}
|
|
154
|
+
toBuffer() {
|
|
155
|
+
const output = Buffer.from(this.raw);
|
|
156
|
+
output.writeUInt16LE(this.version);
|
|
157
|
+
if (this.tileData.length !== 512 || this.palette.length !== 32) throw new RangeError("Invalid banner icon size");
|
|
158
|
+
this.tileData.copy(output, 32);
|
|
159
|
+
this.palette.copy(output, 544);
|
|
160
|
+
for (let i = 0; i < this.titleCount; i++) {
|
|
161
|
+
const text = Buffer.from(this[titles[i]], "utf16le");
|
|
162
|
+
if (text.length > 256) throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
163
|
+
output.fill(0, 576 + i * 256, 832 + i * 256);
|
|
164
|
+
text.copy(output, 576 + i * 256);
|
|
165
|
+
}
|
|
166
|
+
output.writeUInt16LE(crc16(output.subarray(32, 2112)), 2);
|
|
167
|
+
if (this.version >= 2) output.writeUInt16LE(crc16(output.subarray(32, 2368)), 4);
|
|
168
|
+
if (this.version >= 3) output.writeUInt16LE(crc16(output.subarray(32, 2624)), 6);
|
|
169
|
+
if (this.version >>> 8 >= 1) output.writeUInt16LE(crc16(output.subarray(4672, 9152)), 8);
|
|
170
|
+
return output;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
113
174
|
// src/nitro/crypto.ts
|
|
114
175
|
var KEY1 = Buffer.from(
|
|
115
176
|
"99D5205F5744F5B96E19A4D99E6A5A94D8AEF1EB4175E23A9382D03233EE31D5CC57619A3706A21B793972F555AEF6BE5F1B69FBE59DF1E9CE2CD9A15E3205E6FED3FECFD462040D8BF5ECB72B6079BB1295310D6E3FDA2B8884F0F13D127E254522F1BB24061A0611ADDF288B6481342BEB332999AAF2BD9C14959D9FF7F58C7297A1299DD15FCF664D071ADED34A4B85C9A7A31795053A3D490ABF0A898BA24A8249DD2790F10BE9EB1C6A83764505BA817061173F4BDEAECFAB3957F23A564811AD8A40E1453FFA9B0254CAA693FBEF4DFE6FA3D8879C08BAD5486A8D2DFD6E15F874BDBE528B18228A9EFB7437071B366C4A19BA4262B97991107B676596FE0223E8EE998C773E5C86644D6D7886A54F65E21EB2DF5A0AD07E0814B071ACBDDB831CB9D7A162CDC6637C5269C3E6BF75CE12445D2104FAFBD33C381163D4958541494609F2084311DC1F76C0156D1F3C6370EA87806CC3BD638BC2372137DCEE09232E376A4D7390F75030AC1C92041023914FD207AA683E4F9AC964606AC81421F3D62241124424CFE68A56DD0D534DE1851E8C525A9C1984C20357F16FE300BE58F64CEDD521649C1FBE55033C4ADCFFAAC9DAE05D5EBFE6DEF5D8B1F8FF36B3B9626795DB315F37ED4C70679990B518316C3D9999E442DAD3254213A0AED7706CB155CFC7D746D54361173D4428E93385D5D0A293AA25121FFBC50B46F597765645A6BE87B1946BE8B1FE3399AE1F3E6C39711D09009037E4103E7574FF8C833BB0F1B0F90105474295F1D6AC7E38E69E9574263FB4685018D04330B44C4BE368BFE54DB6958B0AA074253277CFA1F72CD871135AABEAC951E80DEEEFE9937E19A71E433881162CA148E373CC29216CD35DCEA0D9617143A01513B56492CF2A19DCADB7A59F8665F81A9FE7FBF7FDB8136C27DB6FDF351CF78D2C5B9B12AB386406CCDE31E84E751164E3FAEAEB3454C2AD3F34EB932C7D26369D56F35AE1F6B398634A9E3283E49A84607D902E130EEE934B36A285EC1638E8880602BFF0A03AEDD76A9A73E157CFF844B8DC2E2359D1DF9552719961A04BD57F6E78BAA9C530D34086329D320C9C37B7022FBA5498A9C41304C98DBEC8E75D97502E93D622590C27BC2292E0A7200F936F7F4C9FD3B5A62A0B7467497D1026CBD1C58671E78CA09CE95BB21AF601EE8C9E5E83F21ADBE6E5EA845976D27CF68DA5493648C21652BB83A374B9070C3BFF6128E161E9E4EF6E15AA4EBAE85D0596BB3256B0FB72520F0EC84225657689AFF2DE1027F0014B74A79707D5265454091F820A867D30390EB3269B0B57BB360631AFFD79FCD930102B0CB3E19BD77BDC5FEFD2F813454D4775BD46963C7E75F33EB567C59A3BB05B296BDE805BC81505B131B6CE49DDAD84B5AE60DC67313430FE4EBD802FA6BF63392186D9357F1668220554E990268C076C51A43155D70907A83E2E5366C1F8F27BC4F258CFF187C5A2E7278F308758A064622318B9887CFACEC498AEAD17CC4A5BF3E948D556D30DF2C892738CDBD72F56AC81F992694DC632F6E6C08D21E276806111BCDC6C93AF19699BD0BFB9319F0267A351EE8306227B0CAB494240B8D5017DCE5EF7555339C59946D8879FBAF764B4E39AFAA16D90681030CA8A54A79F60C319F56B0D7A5198E6984351B4D635E94FC3DF0F7BD62F5CBD3A156119F14BCBAADC6D64C9D3C61E56EF384C50718675CC0D0D4EE928F6065D701BAAD345CFA839AC95A62EB4E422D474A8375F487A04CCA54C40D828B428080D1C725241F07D47193A534E5884626B93B58A81214E0DDCB43FA2C6FCC92B40DA3804E95E5A866B0C22258568118D7C921D95554DAB8EBBDAA6E6B751B6325A0541DD052A0A5650911747CCC9E67EB5614ADB736751C833F5DA6E742E54C3370D6DAF08E8158A5FE25921CDA8DE0C065A776B5FDB18653EC850DE78E0B882B35D4E7232074FC13423BA96B7674EA4281E3462EB2D6A70E92F42C4704E5A319CF95B4728AADA716F381FB378C4926B1C9EF6359AB74D0EBFCC1829410348355D55D02BC629AF5C6074698E5E9B7CD4BD7B44647D3F925D69B61F004BD48335CF7E644E17AE8DD52E9A28124E2E2B49085CAEC64685AE41611E6F82D25137161F0BF659A49ACA5AAF0DD4338B2063F184805CCBCF08B4B9D31605BD6283319B5651989FBAB25BAAB2226B2CB5D448FA632B5F58FA61FA6409BB38E0B89D9260A80D676F0E37F50D019FC277D4FEECF1733039E07DF56198E42C2855045655DB2F6BECE55806B664806A2A1A4E5B0FD8C40A2E5219D962F53048BE8C7B4F389BA2C3AFC9D3C7C1624186B96121576F994FC1BACE7BB53B4D5E8A8B44575F135F706D5B2947DC38E2EC045565122AE81743E18EDD2AB3E294F7096E5CE6EB8AF86D89495448F52FADBFEA944BCAFC3987825F8A01F275F2E671D6D842DEF12D1D28A6887EA3A0471D30D9A371DF491CCB01F836B1F2F022585D456BBDA0BBB28842C78C28CE93E8906308907C893CF57DB7042D4F555116FD7E79E8BEC1F212D4F8B4840523A0CCD22BFDE1ABAD0DD1556C2341944D77374F05280CBF17B312676C8CC35AF741842A6DD09412272CB4ED9C4DEC478297D567B91B9DC055077EE58EE2A8E73E12E40E3A2A455534A2F92D5A1BAB527C83105F55D2F15A432BC6A7A4891595E8B44B9DF875E39F60785BD6E60D44E62106BD472253A400AD8D43138539F7AAFC38AF7BEDFCE42B5450984CFC8580F7DF3C8022E194DADE24C6B07A3938DC0FA1A7F4F96F6318578B84412A2ED453F2D9000FD0DD996E19A60AD0EC5B5824ABC0CB0665EC1A1338940A67032F3FF7E377447733C61439D0E3C0A20879BB409957410B0190CDE1CC4867DBB3AF8874F34C828F72B1B52329C4126C19FC8E46A49CC4256587D36DBE8A93110338ED832BF346A493EA3B53851DCED4F1088327EDFC9B1A18BCF98BAEDC24AB5038E9724B1022177B465DAB5964F340AEF8BBE5C8F926034E557DEBEBFEF739E6E00A11BE2E28FF98EDC0C9425642C3FD00F6AF87A25B013F329247959A72A5323DAE6BD09B07D24992E3784AFAA1067DF241CF77740414B20C86846416D5BB51A1E56FF1D1F2E2F75F58204DB857C7CFDDC5D8BE763DF65F7EE72A8B88241B383F0E412377F5F04BD40C1FFAA40B805FCF45F6E0DA2F345953FB203C52625E35B562FE8B6063E3865A151A6ED14745BC32B4EB6738ABE46E333AB5EDA3AD67E04E4195EE626271261D31EF6230AFD782ACC2DC0504F59707BF11592307C06402E897E53EAF18AC59A68B4A33901C6E7C9C207E4C3C3E6164BBC56B7C7E3E9FC54C9FEA73F5D789C04CF4FBF42DEC141B51D5C112C810DF0B4A8B9CBC93456A3E3E7DC1A9BACDC1B407E4E1688643B26D38F3FB0C5C663771DE56EF6EA0104065A798F7D0BE0EC83736EC10CA7C9CAB841E051776021C4F52AA5FC1C6A056B9D80484444DA759D8DE60E6380E058F03E13B6D8104336F300BCE69052133FB26BB897DB6AE877E5107E0ACF7960A6BF9C45C1DE44447B85EFAE3788455424B485EF77D4735861D2B430503EC8AB81E063C760C481A43A7B78AED1E13C643EE10EFDBECFB3C83B29544EFD854514E2D11441DFB36591E7A34C1C3CA570061EA67A5169B55D055E17FD936D24076AEDC01CEB07A83D5CB2098EC6BC1729234F3825737628A32360C9043AEAE5C9B788E136502FD6871C1FEB031A02482B0C3B17969A7F5D2EBD082C032DC9EC7263C6D8D98C1BB22D4D00F33EC3EB9CCE1DC6A4C7736141CF9BF819F285F71853229907548C4B34ACED8448F142FFD4057EFAA0875D946D1D66E32551FC318FE841FFC84D5FF715E1B48C386950E280827D33883717B4C8063549A56B0ACCF80CA3109EFFEF3BEAF247EA6FE533FC28D4A3368D122A666AD7BEADEB643B0A1259500A33F7546141144ECD795BC92F04FA916536297602A0F41F17124BEEE947F08CD6093B3855B07003FD80F28839AD1699FD1DA2EC39001A2B96B4E2A669DDAAEA6EA2AD3682F0C0C9CD28C4AEDE29E57659D0987A3B4C4325DC9D4322BB1E0711E644DE69071E31E40ED7DF3840EEDC87876AEC0712772BB05EA0264FBF3486BB542933FED9F1353D2F7FE2AEC1D4725DB3C9186C68EF011FD237436F7A4F59E7A7E535044D447CAD3EB386DE6D971947F4AC6694B11F452EA22FE8AB036678B59E8E6802AEB650413EEECDC9E5FB1EC056A59E69F5E596B89BFF71ACA44F95B6A718503E42962E0706F41C4CFB2B1CCE37EA607A887E77F8493DB524B6CEC7EDDD4244810699F046074E64818F3E42CB94F2E507ADFD454692B8BA7F3CEFF1FF33E26013917958489B0F04C4B82919FC44BAC9DA574AF1725C9CA32D3BC898A8489CC0DAE7CA2DB9C6A7891EEEA765D4E8760F5691567D402CFAF483607EABF6F662D068FC49AFEF9F6908775B8F7AD0F76105A3D59B02EB3C7352CCC70562BCBE33796C52F461B8A2246C788A726329861DF86228AF41C2F87A109AACCA9AED3BD00451C9A5487865287EFFF1E8FA18FC1895C351BDA2D3A2C16B2C2F156E278C16B6397C5568FC9327F2CAAAFA6A8AC20912288DEE4608BF94B42251AE37F9C2C19893A7E05D436CC6958C2C1328B2F9085EB7A3950A5A12792C566B0204F587E5583432B45E29CE4D812902C168356167903B3AD2D61181A131F37E2E19C737B80D5FD2D5187FC7BAAD71F2C7A8EAFF48DBBCD95117C720BEE6FE2B9AFDE3783DE8C8D620567B796C68D56B60DD762BAD64636BD8EC8E6EA2A6C1014FF6B5BFA823C46B1304346518A7D9B923E83795B555DB26C5ECE90628E5398C90D6DE52D57CDC58157BAE1E8B88F72E54F13DCEA9D711510B21188D509D47F5B657F2C3B384C1168508DFB9EB059BF9480894AC51A18128953D14A1029E88C1CECB6EA46C7178B251531A8A26B43B19DE2DB0B879BB011040E71D2297789820A66417F1D0B48FF72BB24FDC248A19BFE7B7FCE88DB86D9853B1CB0DCA83307BF512EE30E9A00971E06C097439DD8B645C486675F00F8889AA4529EC7AA8A8375ECC518AECEC32F1A2BF918FFAE1AF5530BB53351A7FDE8A8E1A264B622174380CC0AD8AE3BBA40D7D9924A89DF0410EE9B182B6A77698A68F4F9B9A221156EE61E3B0362309B60417E259B9E8FC5521008F8C269A1211188375E793566FF1042186EED97B66B1C4E36E56D7DB4E4BF20B9E0053A69D5B8E3D5DCE0B9AC533E07A457AD77FF4818762AAC492A8E47756D9F676330358C390539D56F643A5BADCA0BBB82529945B193363699AF13204436D8024409399285FF4A4A9787A663D7C7B5B524ED0FB46F0C585214D9A67BD379BC3858A1BD3B8406D81A06FD6BA8EA4B69280437AD8299FB0E1B85BDA85D73CDDC58750ABE636C48E74CE4302B0460B915D8DA8681758F96D48D1C5D70857C1C677BD50867A6CE4B0A6670B7E563D45B8A82EA1067CAE2F4EF17852F2A5F8A9782F86AD63410EAEBC95C3CE149F846EBDEBDF6A992F1AAA6A018B03AD30F1FF36FFF31454344D3509AF7880996C1CE76CCF22C2CBAAD82778F1884C0D2079C3690834E0BA54F433E04AB784FD6FB09012490DA6F3C3A610D7F694AEB2B3002B4DBE084A9ECD735BF377D8558CEA94EE480C7A8D3306748EB29AF2F746AB4A73F0F3F92AFF3CAACAF4BD994C043CA810D2F48A1B027D5D2EF4B0585A3DE4D93303CF0BB4A8F30274CEBE33E64ED9A2F3BF182F0BAF4CF7F40CBB0E17FBCAA57D3C974F2FA430D22D0F4774E93D785701F99BFB6DE35F130A75E71F06B012D7B64F033530A3988F36B3AA66B35D22F43CD02FDB5E9BC5BAAD8A4197E0E5D94819E6F77ADD60E749396E7C4185FADF519",
|
|
116
177
|
"hex"
|
|
117
178
|
);
|
|
118
179
|
var aes128CtrCrypt = (key2, counter, data) => {
|
|
119
|
-
if (key2.length !== 16 || counter.length !== 16)
|
|
120
|
-
throw new RangeError("AES key and counter must have 16 bytes");
|
|
180
|
+
if (key2.length !== 16 || counter.length !== 16) throw new RangeError("AES key and counter must have 16 bytes");
|
|
121
181
|
const ctr = Buffer.from(counter).reverse();
|
|
122
182
|
const aes = createAesBlockCipher(Buffer.from(key2).reverse());
|
|
123
183
|
const output = Buffer.alloc(data.length);
|
|
@@ -140,12 +200,8 @@ var modcryptKey = (header) => {
|
|
|
140
200
|
key2.write(header.gameCode, 12, 4, "ascii");
|
|
141
201
|
return key2;
|
|
142
202
|
}
|
|
143
|
-
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16)
|
|
144
|
-
|
|
145
|
-
const x = Buffer.from(
|
|
146
|
-
"Nintendo" + header.gameCode + [...header.gameCode].reverse().join(""),
|
|
147
|
-
"ascii"
|
|
148
|
-
);
|
|
203
|
+
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16) throw new Error("Missing ARM9i HMAC for modcrypt key");
|
|
204
|
+
const x = Buffer.from(`Nintendo${header.gameCode}${[...header.gameCode].reverse().join("")}`, "ascii");
|
|
149
205
|
for (let i = 0; i < 16; i++) x[i] ^= header.hmac_arm9i[i];
|
|
150
206
|
const mask = (1n << 128n) - 1n;
|
|
151
207
|
let v = x.readBigUInt64LE(0) | x.readBigUInt64LE(8) << 64n;
|
|
@@ -157,8 +213,7 @@ var modcryptKey = (header) => {
|
|
|
157
213
|
return out;
|
|
158
214
|
};
|
|
159
215
|
var encryptSecureArea = (gameCode, input) => {
|
|
160
|
-
if (typeof gameCode !== "string" || gameCode.length !== 4)
|
|
161
|
-
throw new Error("Expected four-character game code");
|
|
216
|
+
if (typeof gameCode !== "string" || gameCode.length !== 4) throw new Error("Expected four-character game code");
|
|
162
217
|
if (input.length < 2048) throw new RangeError("Secure area must contain at least 0x800 bytes");
|
|
163
218
|
const output = Buffer.from(input);
|
|
164
219
|
let magic = new Uint32Array(1042);
|
|
@@ -210,199 +265,6 @@ var encryptSecureArea = (gameCode, input) => {
|
|
|
210
265
|
return output;
|
|
211
266
|
};
|
|
212
267
|
|
|
213
|
-
// src/nitro/header.ts
|
|
214
|
-
var UnitCode = Object.freeze({ NDS: 0, NDS_DSi: 2, DSi: 3 });
|
|
215
|
-
var Header = class {
|
|
216
|
-
raw;
|
|
217
|
-
trimmedRom;
|
|
218
|
-
doublePadding;
|
|
219
|
-
nitrocode;
|
|
220
|
-
decrypted;
|
|
221
|
-
headerCRC;
|
|
222
|
-
logoCRC;
|
|
223
|
-
secureCRC;
|
|
224
|
-
dlp_signature;
|
|
225
|
-
constructor(input) {
|
|
226
|
-
const data = bytes(input, "header");
|
|
227
|
-
range(data, 0, 368, "header");
|
|
228
|
-
const size = data.readUInt32LE(132);
|
|
229
|
-
if (size < 368 || size > data.length) throw new RangeError(`Invalid header size: ${size}`);
|
|
230
|
-
this.raw = Buffer.from(data.subarray(0, size));
|
|
231
|
-
if (this.unitCode & 2 && size > 512) range(this.raw, 0, 4096, "DSi header");
|
|
232
|
-
this.trimmedRom = this.total_rom_size ? this.total_rom_size >= data.length : data.length !== this.size;
|
|
233
|
-
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;
|
|
234
|
-
const nitroOffset = this.ARM9romOffset + this.ARM9size;
|
|
235
|
-
this.nitrocode = nitroOffset >= size && nitroOffset + 4 <= data.length && data.readUInt32LE(nitroOffset) === 3737126433;
|
|
236
|
-
this.decrypted = data.length >= 16392 && data.readBigUInt64LE(16384) === 0xe7ffdeffe7ffdeffn;
|
|
237
|
-
this.headerCRC = crc16(this.raw.subarray(0, 350)) === this.headerCRC16;
|
|
238
|
-
this.logoCRC = crc16(this.logo) === this.logoCRC16;
|
|
239
|
-
const secure = data.subarray(16384, 32768);
|
|
240
|
-
this.secureCRC = secure.length > 0 && crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) === this.secureCRC16;
|
|
241
|
-
this.dlp_signature = Buffer.alloc(0);
|
|
242
|
-
if (this.ROMsize >= size && this.ROMsize + 136 <= data.length && data.readUInt32LE(this.ROMsize) === 90977) {
|
|
243
|
-
this.dlp_signature = Buffer.from(data.subarray(this.ROMsize, this.ROMsize + 136));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
get size() {
|
|
247
|
-
return 2 ** (17 + this.raw[20]);
|
|
248
|
-
}
|
|
249
|
-
set size(value) {
|
|
250
|
-
const power = Math.log2(value);
|
|
251
|
-
if (!Number.isInteger(power) || power < 17 || power > 32)
|
|
252
|
-
throw new RangeError("ROM capacity must be a power of two from 128 KiB to 4 GiB");
|
|
253
|
-
this.raw[20] = power - 17;
|
|
254
|
-
}
|
|
255
|
-
toBuffer() {
|
|
256
|
-
const data = Buffer.from(this.raw);
|
|
257
|
-
data.writeUInt16LE(crc16(data.subarray(192, 348)), 348);
|
|
258
|
-
data.writeUInt16LE(crc16(data.subarray(0, 350)), 350);
|
|
259
|
-
return data;
|
|
260
|
-
}
|
|
261
|
-
toString() {
|
|
262
|
-
return `NDS header: ${this.gameTitle.replace(/\0+$/, "")} (${this.gameCode})`;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
function field(name, offset, length, type = "buffer", dsi = false) {
|
|
266
|
-
Object.defineProperty(Header.prototype, name, {
|
|
267
|
-
enumerable: true,
|
|
268
|
-
get() {
|
|
269
|
-
if (dsi && (!(this.unitCode & 2) || this.raw.length <= 512))
|
|
270
|
-
return type === "buffer" ? Buffer.alloc(length) : 0;
|
|
271
|
-
const data = range(this.raw, offset, length, `header ${name}`);
|
|
272
|
-
if (type === "string") return data.toString("latin1");
|
|
273
|
-
if (type === "buffer") return data;
|
|
274
|
-
return length === 8 ? data.readBigUInt64LE() : data.readUIntLE(0, length);
|
|
275
|
-
},
|
|
276
|
-
set(value) {
|
|
277
|
-
const target = range(this.raw, offset, length, `header ${name}`);
|
|
278
|
-
if (type === "string") {
|
|
279
|
-
if (typeof value !== "string" || Buffer.byteLength(value, "latin1") > length)
|
|
280
|
-
throw new TypeError(`Invalid ${name}`);
|
|
281
|
-
target.fill(0);
|
|
282
|
-
target.write(value, "latin1");
|
|
283
|
-
} else if (type === "buffer") {
|
|
284
|
-
if (!(value instanceof Uint8Array)) throw new TypeError(`Invalid ${name}`);
|
|
285
|
-
const source = bytes(value, name);
|
|
286
|
-
if (source.length !== length) throw new RangeError(`${name} must contain ${length} bytes`);
|
|
287
|
-
source.copy(target);
|
|
288
|
-
} else if (length === 8) {
|
|
289
|
-
if (typeof value !== "bigint") throw new TypeError(`Invalid ${name}`);
|
|
290
|
-
target.writeBigUInt64LE(value);
|
|
291
|
-
} else {
|
|
292
|
-
if (typeof value !== "number") throw new TypeError(`Invalid ${name}`);
|
|
293
|
-
target.writeUIntLE(value, 0, length);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
field("gameTitle", 0, 12, "string");
|
|
299
|
-
field("gameCode", 12, 4, "string");
|
|
300
|
-
field("makerCode", 16, 2, "string");
|
|
301
|
-
for (const [name, offset] of Object.entries({
|
|
302
|
-
unitCode: 18,
|
|
303
|
-
encryptionSeed: 19,
|
|
304
|
-
twlInternalFlags: 28,
|
|
305
|
-
permitsFlags: 29,
|
|
306
|
-
ROMversion: 30,
|
|
307
|
-
internalFlags: 31
|
|
308
|
-
}))
|
|
309
|
-
field(name, offset, 1, "number");
|
|
310
|
-
field("reserved", 21, 7);
|
|
311
|
-
var ntr32 = "ARM9romOffset ARM9entryAddress ARM9ramAddress ARM9size ARM7romOffset ARM7entryAddress ARM7ramAddress ARM7size FNToffset FNTsize FAToffset FATsize ARM9overlayOffset ARM9overlaySize ARM7overlayOffset ARM7overlaySize flagsRead flagsInit bannerOffset".split(
|
|
312
|
-
" "
|
|
313
|
-
);
|
|
314
|
-
ntr32.forEach((name, i) => field(name, 32 + i * 4, 4, "number"));
|
|
315
|
-
field("secureCRC16", 108, 2, "number");
|
|
316
|
-
field("ROMtimeout", 110, 2, "number");
|
|
317
|
-
field("ARM9autoload", 112, 4, "number");
|
|
318
|
-
field("ARM7autoload", 116, 4, "number");
|
|
319
|
-
field("secureDisable", 120, 8, "number");
|
|
320
|
-
field("ROMsize", 128, 4, "number");
|
|
321
|
-
field("headerSize", 132, 4, "number");
|
|
322
|
-
field("reserved2", 136, 56);
|
|
323
|
-
field("logo", 192, 156);
|
|
324
|
-
field("logoCRC16", 348, 2, "number");
|
|
325
|
-
field("headerCRC16", 350, 2, "number");
|
|
326
|
-
"debug_romOffset debug_size debug_ramAddress reserved3".split(" ").forEach((name, i) => field(name, 352 + i * 4, 4, "number"));
|
|
327
|
-
field("appflags", 444, 4, "buffer", true);
|
|
328
|
-
"mbk9_wramcnt_setting region_flags access_control scfg_ext_mask".split(" ").forEach((name, i) => field(name, 428 + i * 4, 4, "number", true));
|
|
329
|
-
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(
|
|
330
|
-
" "
|
|
331
|
-
);
|
|
332
|
-
twl32.forEach((name, i) => field(name, 448 + i * 4, 4, "number", true));
|
|
333
|
-
"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));
|
|
334
|
-
field("hmac_arm9_no_secure", 928, 20, "buffer", true);
|
|
335
|
-
field("rsa_signature", 3968, 128, "buffer", true);
|
|
336
|
-
|
|
337
|
-
// src/nitro/banner.ts
|
|
338
|
-
var titles = [
|
|
339
|
-
"japaneseTitle",
|
|
340
|
-
"englishTitle",
|
|
341
|
-
"frenchTitle",
|
|
342
|
-
"germanTitle",
|
|
343
|
-
"italianTitle",
|
|
344
|
-
"spanishTitle",
|
|
345
|
-
"chineseTitle",
|
|
346
|
-
"koreanTitle"
|
|
347
|
-
];
|
|
348
|
-
var Banner = class {
|
|
349
|
-
version;
|
|
350
|
-
raw;
|
|
351
|
-
bannerCRC;
|
|
352
|
-
tileData;
|
|
353
|
-
palette;
|
|
354
|
-
japaneseTitle = "";
|
|
355
|
-
englishTitle = "";
|
|
356
|
-
frenchTitle = "";
|
|
357
|
-
germanTitle = "";
|
|
358
|
-
italianTitle = "";
|
|
359
|
-
spanishTitle = "";
|
|
360
|
-
chineseTitle = "";
|
|
361
|
-
koreanTitle = "";
|
|
362
|
-
constructor(input, offset = 0, hardSize = 0) {
|
|
363
|
-
const data = bytes(input, "banner");
|
|
364
|
-
this.version = range(data, offset, 2, "banner").readUInt16LE();
|
|
365
|
-
let size = this.getDefSize(hardSize);
|
|
366
|
-
if (this.version >>> 8 === 1)
|
|
367
|
-
size = hardSize > 0 && hardSize !== 4294967295 ? Math.min(hardSize, 9152) : 9152;
|
|
368
|
-
this.raw = Buffer.from(range(data, offset, size, "banner"));
|
|
369
|
-
this.bannerCRC = crc16(this.raw.subarray(32, 2112)) === this.raw.readUInt16LE(2);
|
|
370
|
-
this.tileData = Buffer.from(this.raw.subarray(32, 544));
|
|
371
|
-
this.palette = Buffer.from(this.raw.subarray(544, 576));
|
|
372
|
-
for (const [i, title] of titles.entries())
|
|
373
|
-
this[title] = i < this.titleCount ? this.raw.subarray(576 + i * 256, 832 + i * 256).toString("utf16le").split("\0")[0] : "";
|
|
374
|
-
}
|
|
375
|
-
get titleCount() {
|
|
376
|
-
return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
|
|
377
|
-
}
|
|
378
|
-
getDefSize(hardSize = 0) {
|
|
379
|
-
if (this.version === 2) return 2368;
|
|
380
|
-
if (this.version === 3) return 2624;
|
|
381
|
-
if (this.version === 259) return hardSize > 0 && hardSize !== 4294967295 ? hardSize : 9152;
|
|
382
|
-
return 2112;
|
|
383
|
-
}
|
|
384
|
-
toBuffer() {
|
|
385
|
-
const output = Buffer.from(this.raw);
|
|
386
|
-
output.writeUInt16LE(this.version);
|
|
387
|
-
if (this.tileData.length !== 512 || this.palette.length !== 32)
|
|
388
|
-
throw new RangeError("Invalid banner icon size");
|
|
389
|
-
this.tileData.copy(output, 32);
|
|
390
|
-
this.palette.copy(output, 544);
|
|
391
|
-
for (let i = 0; i < this.titleCount; i++) {
|
|
392
|
-
const text = Buffer.from(this[titles[i]], "utf16le");
|
|
393
|
-
if (text.length > 256)
|
|
394
|
-
throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
|
|
395
|
-
output.fill(0, 576 + i * 256, 832 + i * 256);
|
|
396
|
-
text.copy(output, 576 + i * 256);
|
|
397
|
-
}
|
|
398
|
-
output.writeUInt16LE(crc16(output.subarray(32, 2112)), 2);
|
|
399
|
-
if (this.version >= 2) output.writeUInt16LE(crc16(output.subarray(32, 2368)), 4);
|
|
400
|
-
if (this.version >= 3) output.writeUInt16LE(crc16(output.subarray(32, 2624)), 6);
|
|
401
|
-
if (this.version >>> 8 >= 1) output.writeUInt16LE(crc16(output.subarray(4672, 9152)), 8);
|
|
402
|
-
return output;
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
|
|
406
268
|
// src/nitro/filesystem.ts
|
|
407
269
|
var FileAllocationTable = class {
|
|
408
270
|
entries;
|
|
@@ -432,8 +294,7 @@ var FileNameTable = class _FileNameTable {
|
|
|
432
294
|
const visited = /* @__PURE__ */ new Set();
|
|
433
295
|
const fileIds = /* @__PURE__ */ new Set();
|
|
434
296
|
const parse = (index, name, depth) => {
|
|
435
|
-
if (depth > 256 || index >= count || visited.has(index))
|
|
436
|
-
throw new Error("Invalid FNT directory tree or cycle");
|
|
297
|
+
if (depth > 256 || index >= count || visited.has(index)) throw new Error("Invalid FNT directory tree or cycle");
|
|
437
298
|
visited.add(index);
|
|
438
299
|
let cursor = table.readUInt32LE(index * 8);
|
|
439
300
|
let fileId = table.readUInt16LE(index * 8 + 4);
|
|
@@ -513,7 +374,6 @@ var OverlayTable = class {
|
|
|
513
374
|
});
|
|
514
375
|
}
|
|
515
376
|
}
|
|
516
|
-
isArm9;
|
|
517
377
|
entries = [];
|
|
518
378
|
readBasicOverlays(fat) {
|
|
519
379
|
return this.entries.map((item) => {
|
|
@@ -534,6 +394,130 @@ var OverlayTable = class {
|
|
|
534
394
|
}
|
|
535
395
|
};
|
|
536
396
|
|
|
397
|
+
// src/nitro/header.ts
|
|
398
|
+
var UnitCode = Object.freeze({ NDS: 0, NDS_DSi: 2, DSi: 3 });
|
|
399
|
+
var Header = class {
|
|
400
|
+
raw;
|
|
401
|
+
trimmedRom;
|
|
402
|
+
doublePadding;
|
|
403
|
+
nitrocode;
|
|
404
|
+
decrypted;
|
|
405
|
+
headerCRC;
|
|
406
|
+
logoCRC;
|
|
407
|
+
secureCRC;
|
|
408
|
+
dlp_signature;
|
|
409
|
+
constructor(input) {
|
|
410
|
+
const data = bytes(input, "header");
|
|
411
|
+
range(data, 0, 368, "header");
|
|
412
|
+
const size = data.readUInt32LE(132);
|
|
413
|
+
if (size < 368 || size > data.length) throw new RangeError(`Invalid header size: ${size}`);
|
|
414
|
+
this.raw = Buffer.from(data.subarray(0, size));
|
|
415
|
+
if (this.unitCode & 2 && size > 512) range(this.raw, 0, 4096, "DSi header");
|
|
416
|
+
this.trimmedRom = this.total_rom_size ? this.total_rom_size >= data.length : data.length !== this.size;
|
|
417
|
+
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;
|
|
418
|
+
const nitroOffset = this.ARM9romOffset + this.ARM9size;
|
|
419
|
+
this.nitrocode = nitroOffset >= size && nitroOffset + 4 <= data.length && data.readUInt32LE(nitroOffset) === 3737126433;
|
|
420
|
+
this.decrypted = data.length >= 16392 && data.readBigUInt64LE(16384) === 0xe7ffdeffe7ffdeffn;
|
|
421
|
+
this.headerCRC = crc16(this.raw.subarray(0, 350)) === this.headerCRC16;
|
|
422
|
+
this.logoCRC = crc16(this.logo) === this.logoCRC16;
|
|
423
|
+
const secure = data.subarray(16384, 32768);
|
|
424
|
+
this.secureCRC = secure.length > 0 && crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) === this.secureCRC16;
|
|
425
|
+
this.dlp_signature = Buffer.alloc(0);
|
|
426
|
+
if (this.ROMsize >= size && this.ROMsize + 136 <= data.length && data.readUInt32LE(this.ROMsize) === 90977) {
|
|
427
|
+
this.dlp_signature = Buffer.from(data.subarray(this.ROMsize, this.ROMsize + 136));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
get size() {
|
|
431
|
+
return 2 ** (17 + this.raw[20]);
|
|
432
|
+
}
|
|
433
|
+
set size(value) {
|
|
434
|
+
const power = Math.log2(value);
|
|
435
|
+
if (!Number.isInteger(power) || power < 17 || power > 32)
|
|
436
|
+
throw new RangeError("ROM capacity must be a power of two from 128 KiB to 4 GiB");
|
|
437
|
+
this.raw[20] = power - 17;
|
|
438
|
+
}
|
|
439
|
+
toBuffer() {
|
|
440
|
+
const data = Buffer.from(this.raw);
|
|
441
|
+
data.writeUInt16LE(crc16(data.subarray(192, 348)), 348);
|
|
442
|
+
data.writeUInt16LE(crc16(data.subarray(0, 350)), 350);
|
|
443
|
+
return data;
|
|
444
|
+
}
|
|
445
|
+
toString() {
|
|
446
|
+
return `NDS header: ${this.gameTitle.replace(/\0+$/, "")} (${this.gameCode})`;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
function field(name, offset, length, type = "buffer", dsi = false) {
|
|
450
|
+
Object.defineProperty(Header.prototype, name, {
|
|
451
|
+
enumerable: true,
|
|
452
|
+
get() {
|
|
453
|
+
if (dsi && (!(this.unitCode & 2) || this.raw.length <= 512))
|
|
454
|
+
return type === "buffer" ? Buffer.alloc(length) : 0;
|
|
455
|
+
const data = range(this.raw, offset, length, `header ${name}`);
|
|
456
|
+
if (type === "string") return data.toString("latin1");
|
|
457
|
+
if (type === "buffer") return data;
|
|
458
|
+
return length === 8 ? data.readBigUInt64LE() : data.readUIntLE(0, length);
|
|
459
|
+
},
|
|
460
|
+
set(value) {
|
|
461
|
+
const target = range(this.raw, offset, length, `header ${name}`);
|
|
462
|
+
if (type === "string") {
|
|
463
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "latin1") > length)
|
|
464
|
+
throw new TypeError(`Invalid ${name}`);
|
|
465
|
+
target.fill(0);
|
|
466
|
+
target.write(value, "latin1");
|
|
467
|
+
} else if (type === "buffer") {
|
|
468
|
+
if (!(value instanceof Uint8Array)) throw new TypeError(`Invalid ${name}`);
|
|
469
|
+
const source = bytes(value, name);
|
|
470
|
+
if (source.length !== length) throw new RangeError(`${name} must contain ${length} bytes`);
|
|
471
|
+
source.copy(target);
|
|
472
|
+
} else if (length === 8) {
|
|
473
|
+
if (typeof value !== "bigint") throw new TypeError(`Invalid ${name}`);
|
|
474
|
+
target.writeBigUInt64LE(value);
|
|
475
|
+
} else {
|
|
476
|
+
if (typeof value !== "number") throw new TypeError(`Invalid ${name}`);
|
|
477
|
+
target.writeUIntLE(value, 0, length);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
field("gameTitle", 0, 12, "string");
|
|
483
|
+
field("gameCode", 12, 4, "string");
|
|
484
|
+
field("makerCode", 16, 2, "string");
|
|
485
|
+
for (const [name, offset] of Object.entries({
|
|
486
|
+
unitCode: 18,
|
|
487
|
+
encryptionSeed: 19,
|
|
488
|
+
twlInternalFlags: 28,
|
|
489
|
+
permitsFlags: 29,
|
|
490
|
+
ROMversion: 30,
|
|
491
|
+
internalFlags: 31
|
|
492
|
+
}))
|
|
493
|
+
field(name, offset, 1, "number");
|
|
494
|
+
field("reserved", 21, 7);
|
|
495
|
+
var ntr32 = "ARM9romOffset ARM9entryAddress ARM9ramAddress ARM9size ARM7romOffset ARM7entryAddress ARM7ramAddress ARM7size FNToffset FNTsize FAToffset FATsize ARM9overlayOffset ARM9overlaySize ARM7overlayOffset ARM7overlaySize flagsRead flagsInit bannerOffset".split(
|
|
496
|
+
" "
|
|
497
|
+
);
|
|
498
|
+
ntr32.forEach((name, i) => field(name, 32 + i * 4, 4, "number"));
|
|
499
|
+
field("secureCRC16", 108, 2, "number");
|
|
500
|
+
field("ROMtimeout", 110, 2, "number");
|
|
501
|
+
field("ARM9autoload", 112, 4, "number");
|
|
502
|
+
field("ARM7autoload", 116, 4, "number");
|
|
503
|
+
field("secureDisable", 120, 8, "number");
|
|
504
|
+
field("ROMsize", 128, 4, "number");
|
|
505
|
+
field("headerSize", 132, 4, "number");
|
|
506
|
+
field("reserved2", 136, 56);
|
|
507
|
+
field("logo", 192, 156);
|
|
508
|
+
field("logoCRC16", 348, 2, "number");
|
|
509
|
+
field("headerCRC16", 350, 2, "number");
|
|
510
|
+
"debug_romOffset debug_size debug_ramAddress reserved3".split(" ").forEach((name, i) => field(name, 352 + i * 4, 4, "number"));
|
|
511
|
+
field("appflags", 444, 4, "buffer", true);
|
|
512
|
+
"mbk9_wramcnt_setting region_flags access_control scfg_ext_mask".split(" ").forEach((name, i) => field(name, 428 + i * 4, 4, "number", true));
|
|
513
|
+
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(
|
|
514
|
+
" "
|
|
515
|
+
);
|
|
516
|
+
twl32.forEach((name, i) => field(name, 448 + i * 4, 4, "number", true));
|
|
517
|
+
"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));
|
|
518
|
+
field("hmac_arm9_no_secure", 928, 20, "buffer", true);
|
|
519
|
+
field("rsa_signature", 3968, 128, "buffer", true);
|
|
520
|
+
|
|
537
521
|
// src/nitro/blz.ts
|
|
538
522
|
var decompressBLZ = (input, maxSize = 268435456) => {
|
|
539
523
|
if (input.length < 8) throw new Error("BLZ footer is truncated");
|
|
@@ -555,8 +539,7 @@ var decompressBLZ = (input, maxSize = 268435456) => {
|
|
|
555
539
|
if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
|
|
556
540
|
const high = input[--src], low = input[--src];
|
|
557
541
|
const count = (high >>> 4) + 3, offset = ((high & 15) << 8 | low) + 3;
|
|
558
|
-
if (dst - count < prefix || dst - 1 + offset >= size)
|
|
559
|
-
throw new Error("Invalid BLZ reference");
|
|
542
|
+
if (dst - count < prefix || dst - 1 + offset >= size) throw new Error("Invalid BLZ reference");
|
|
560
543
|
for (let j = 0; j < count; j++) {
|
|
561
544
|
--dst;
|
|
562
545
|
out[dst] = out[dst + offset];
|
|
@@ -650,10 +633,7 @@ var decompressArm9 = (data, header) => {
|
|
|
650
633
|
const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
|
|
651
634
|
return {
|
|
652
635
|
compressed,
|
|
653
|
-
data: compressed ? Buffer.concat([
|
|
654
|
-
decompressBLZ(data.subarray(0, data.length - nitro)),
|
|
655
|
-
data.subarray(data.length - nitro)
|
|
656
|
-
]) : Buffer.from(data)
|
|
636
|
+
data: compressed ? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)]) : Buffer.from(data)
|
|
657
637
|
};
|
|
658
638
|
};
|
|
659
639
|
var compressArm9 = (data, header, postSize = 0) => {
|
|
@@ -703,8 +683,7 @@ function validate(h) {
|
|
|
703
683
|
}
|
|
704
684
|
}
|
|
705
685
|
const s = h.digest_sector_size;
|
|
706
|
-
if (!Number.isSafeInteger(s) || s <= 0 || s > 16384 || 16384 % s)
|
|
707
|
-
throw new Error("Invalid TWL digest sector size");
|
|
686
|
+
if (!Number.isSafeInteger(s) || s <= 0 || s > 16384 || 16384 % s) throw new Error("Invalid TWL digest sector size");
|
|
708
687
|
if (!Number.isSafeInteger(h.digest_block_sectorcount) || h.digest_block_sectorcount <= 0 || h.digest_block_sectorcount > 1048576)
|
|
709
688
|
throw new Error("Invalid TWL digest block sector count");
|
|
710
689
|
if (h.digest_ntr_size < 16384 || h.digest_ntr_size % s || h.digest_twl_size % s)
|
|
@@ -734,45 +713,25 @@ var TWL = class {
|
|
|
734
713
|
this.Hashtable2Data = Buffer.from(
|
|
735
714
|
slice(original, h.block_hashtable_start, h.block_hashtable_size, "Block hash table")
|
|
736
715
|
);
|
|
737
|
-
this.DSi9Data = Buffer.from(
|
|
738
|
-
|
|
739
|
-
);
|
|
740
|
-
this.DSi7Data = Buffer.from(
|
|
741
|
-
slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), "ARM7i")
|
|
742
|
-
);
|
|
716
|
+
this.DSi9Data = Buffer.from(slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), "ARM9i"));
|
|
717
|
+
this.DSi7Data = Buffer.from(slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), "ARM7i"));
|
|
743
718
|
this.Header2Data = h.trimmedRom ? null : Array.from(
|
|
744
719
|
{ length: 3 },
|
|
745
|
-
(_, i) => Buffer.from(
|
|
746
|
-
slice(original, h.digest_twl_start - 12288 + 4096 * i, 4096, "Secondary header")
|
|
747
|
-
)
|
|
748
|
-
);
|
|
749
|
-
this.Overlays9Sha1Hmac = overlays.map(
|
|
750
|
-
(o) => hash(slice(original, o.offset, o.size, "ARM9 overlay"))
|
|
720
|
+
(_, i) => Buffer.from(slice(original, h.digest_twl_start - 12288 + 4096 * i, 4096, "Secondary header"))
|
|
751
721
|
);
|
|
722
|
+
this.Overlays9Sha1Hmac = overlays.map((o) => hash(slice(original, o.offset, o.size, "ARM9 overlay")));
|
|
752
723
|
let encrypted = false;
|
|
753
724
|
const modcryptStart = [
|
|
754
725
|
{ start: h.modcrypt1_start, size: h.modcrypt1_size },
|
|
755
726
|
{ start: h.modcrypt2_start, size: h.modcrypt2_size }
|
|
756
|
-
].find(
|
|
757
|
-
({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 4294967295
|
|
758
|
-
)?.start;
|
|
727
|
+
].find(({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 4294967295)?.start;
|
|
759
728
|
if (modcryptStart !== void 0) {
|
|
760
729
|
const index = Math.floor((modcryptStart - h.digest_twl_start) / h.digest_sector_size);
|
|
761
730
|
const actual = hash(
|
|
762
|
-
slice(
|
|
763
|
-
original,
|
|
764
|
-
h.digest_twl_start + index * h.digest_sector_size,
|
|
765
|
-
h.digest_sector_size,
|
|
766
|
-
"Modcrypt sector"
|
|
767
|
-
)
|
|
731
|
+
slice(original, h.digest_twl_start + index * h.digest_sector_size, h.digest_sector_size, "Modcrypt sector")
|
|
768
732
|
);
|
|
769
733
|
encrypted = !actual.equals(
|
|
770
|
-
slice(
|
|
771
|
-
this.Hashtable1Data,
|
|
772
|
-
this.firstDSiHashOffset + index * 20,
|
|
773
|
-
20,
|
|
774
|
-
"Modcrypt sector hash"
|
|
775
|
-
)
|
|
734
|
+
slice(this.Hashtable1Data, this.firstDSiHashOffset + index * 20, 20, "Modcrypt sector hash")
|
|
776
735
|
);
|
|
777
736
|
}
|
|
778
737
|
if (encrypted) this.cryptSections(h);
|
|
@@ -828,8 +787,7 @@ var TWL = class {
|
|
|
828
787
|
break;
|
|
829
788
|
}
|
|
830
789
|
}
|
|
831
|
-
if (found < 0)
|
|
832
|
-
throw new Error("ARM9 overlay hashes changed but original hash table was not found");
|
|
790
|
+
if (found < 0) throw new Error("ARM9 overlay hashes changed but original hash table was not found");
|
|
833
791
|
hashes.forEach((h, i) => h.copy(unpacked.data, found + i * 20));
|
|
834
792
|
return unpacked.compressed ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr) : unpacked.data;
|
|
835
793
|
}
|
|
@@ -868,8 +826,7 @@ var TWL = class {
|
|
|
868
826
|
writer.position = h.sector_hashtable_start + index++ * 20;
|
|
869
827
|
writer.write(hash(b));
|
|
870
828
|
};
|
|
871
|
-
for (let p = 0; p < 16384; p += h.digest_sector_size)
|
|
872
|
-
put(secure.subarray(p, p + h.digest_sector_size));
|
|
829
|
+
for (let p = 0; p < 16384; p += h.digest_sector_size) put(secure.subarray(p, p + h.digest_sector_size));
|
|
873
830
|
for (let p = h.digest_ntr_start + 16384; p < h.digest_ntr_start + h.digest_ntr_size; p += h.digest_sector_size)
|
|
874
831
|
put(read(p, h.digest_sector_size));
|
|
875
832
|
for (let p = h.digest_twl_start; p < h.digest_twl_start + h.digest_twl_size; p += h.digest_sector_size)
|
|
@@ -936,18 +893,8 @@ var NDSFile = class _NDSFile {
|
|
|
936
893
|
this.fatTable = new FileAllocationTable(this.original, h.FAToffset, h.FATsize);
|
|
937
894
|
this.fntTable = new FileNameTable(this.fatTable, this.original, h.FNToffset, h.FNTsize);
|
|
938
895
|
this.root = this.fntTable.root;
|
|
939
|
-
this.overlay9Table = new OverlayTable(
|
|
940
|
-
|
|
941
|
-
h.ARM9overlayOffset,
|
|
942
|
-
h.ARM9overlaySize,
|
|
943
|
-
true
|
|
944
|
-
);
|
|
945
|
-
this.overlay7Table = new OverlayTable(
|
|
946
|
-
this.original,
|
|
947
|
-
h.ARM7overlayOffset,
|
|
948
|
-
h.ARM7overlaySize,
|
|
949
|
-
false
|
|
950
|
-
);
|
|
896
|
+
this.overlay9Table = new OverlayTable(this.original, h.ARM9overlayOffset, h.ARM9overlaySize, true);
|
|
897
|
+
this.overlay7Table = new OverlayTable(this.original, h.ARM7overlayOffset, h.ARM7overlaySize, false);
|
|
951
898
|
const arm9Overlays = this.overlay9Table.readBasicOverlays(this.fatTable);
|
|
952
899
|
this.root.folders.push({
|
|
953
900
|
name: "overlay",
|
|
@@ -973,7 +920,7 @@ var NDSFile = class _NDSFile {
|
|
|
973
920
|
if (this.paths.has(key(path))) throw new Error(`Duplicate ROM path: ${path}`);
|
|
974
921
|
this.paths.set(key(path), { path, file });
|
|
975
922
|
}
|
|
976
|
-
for (const child of folder.folders) visit(child, prefix + child.name
|
|
923
|
+
for (const child of folder.folders) visit(child, `${prefix + child.name}/`);
|
|
977
924
|
};
|
|
978
925
|
visit(this.root, "");
|
|
979
926
|
if (h.unitCode & 2 && h.twlInternalFlags & 1 && h.tid_high && h.tid_high !== 4294967295) {
|
|
@@ -1018,15 +965,8 @@ var NDSFile = class _NDSFile {
|
|
|
1018
965
|
toBuffer() {
|
|
1019
966
|
const fntReplacement = this.root.files.find((file) => file.name === "fnt.bin")?.replacement;
|
|
1020
967
|
if (fntReplacement) {
|
|
1021
|
-
const replacementTree = new FileNameTable(
|
|
1022
|
-
|
|
1023
|
-
fntReplacement,
|
|
1024
|
-
0,
|
|
1025
|
-
fntReplacement.length
|
|
1026
|
-
);
|
|
1027
|
-
const collect = (folder) => [...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort(
|
|
1028
|
-
(a, b) => a - b
|
|
1029
|
-
);
|
|
968
|
+
const replacementTree = new FileNameTable(this.fatTable, fntReplacement, 0, fntReplacement.length);
|
|
969
|
+
const collect = (folder) => [...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort((a, b) => a - b);
|
|
1030
970
|
if (collect(replacementTree.root).join(",") !== collect(this.data).join(","))
|
|
1031
971
|
throw new Error("Replacement FNT must preserve existing file IDs");
|
|
1032
972
|
}
|
|
@@ -1071,8 +1011,7 @@ var NDSFile = class _NDSFile {
|
|
|
1071
1011
|
item.reserved = (item.reserved & 4278190080 | size & 16777215) >>> 0;
|
|
1072
1012
|
overlayIds.add(file.id);
|
|
1073
1013
|
}
|
|
1074
|
-
if (table.entries.length !== files.length)
|
|
1075
|
-
throw new Error("Changing overlay file count is unsupported");
|
|
1014
|
+
if (table.entries.length !== files.length) throw new Error("Changing overlay file count is unsupported");
|
|
1076
1015
|
const encoded = table.toBuffer();
|
|
1077
1016
|
if (arm92) {
|
|
1078
1017
|
h.ARM9overlayOffset = writer.position;
|
|
@@ -1161,15 +1100,15 @@ export {
|
|
|
1161
1100
|
BinaryWriter,
|
|
1162
1101
|
crc16,
|
|
1163
1102
|
crc32,
|
|
1103
|
+
Banner,
|
|
1164
1104
|
aes128CtrCrypt,
|
|
1165
1105
|
modcryptKey,
|
|
1166
1106
|
encryptSecureArea,
|
|
1167
|
-
UnitCode,
|
|
1168
|
-
Header,
|
|
1169
|
-
Banner,
|
|
1170
1107
|
FileAllocationTable,
|
|
1171
1108
|
FileNameTable,
|
|
1172
1109
|
OverlayTable,
|
|
1110
|
+
UnitCode,
|
|
1111
|
+
Header,
|
|
1173
1112
|
decompressBLZ,
|
|
1174
1113
|
compressBLZ,
|
|
1175
1114
|
decompressArm9,
|
|
@@ -1178,4 +1117,4 @@ export {
|
|
|
1178
1117
|
updateHeaderSignatures,
|
|
1179
1118
|
NDSFile
|
|
1180
1119
|
};
|
|
1181
|
-
//# sourceMappingURL=chunk-
|
|
1120
|
+
//# sourceMappingURL=chunk-GLK3K2HY.js.map
|