nitro-patcher 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/browser.d.ts +29 -1
- package/dist/browser.js +1459 -1431
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-GFF67HX4.js → chunk-GLK3K2HY.js} +221 -282
- package/dist/chunk-GLK3K2HY.js.map +1 -0
- package/dist/{chunk-L7ZM4LLP.js → chunk-IGURDH5L.js} +254 -186
- package/dist/chunk-IGURDH5L.js.map +1 -0
- package/dist/cli.js +106 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -3
- package/dist/nitro-helper.d.ts +78 -78
- package/dist/nitro-helper.js +1 -1
- package/dist/nitro-patch-helper.d.ts +38 -1
- package/dist/nitro-patch-helper.js +18 -4
- package/package.json +9 -2
- package/src/browser/buffer.ts +1 -1
- package/src/browser.ts +49 -4
- package/src/cli.ts +123 -37
- package/src/index.ts +2 -2
- package/src/nitro/banner.ts +28 -31
- package/src/nitro/binary.ts +6 -16
- package/src/nitro/blz.ts +13 -21
- package/src/nitro/crypto.ts +78 -84
- package/src/nitro/filesystem.ts +36 -39
- package/src/nitro/header.ts +58 -71
- package/src/nitro/nds-file.ts +44 -63
- package/src/nitro/twl.ts +59 -100
- package/src/nitro-helper.ts +14 -14
- package/src/nitro-patch-helper.ts +140 -29
- package/src/patch/xdelta.ts +43 -49
- package/src/patch/zip.ts +47 -56
- package/src/runtime/browser.ts +9 -11
- package/src/runtime/node.ts +14 -15
- package/dist/chunk-GFF67HX4.js.map +0 -1
- package/dist/chunk-L7ZM4LLP.js.map +0 -1
package/src/nitro/crypto.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAesBlockCipher } from
|
|
1
|
+
import { createAesBlockCipher } from "#nitro-runtime";
|
|
2
2
|
|
|
3
3
|
export interface ModcryptHeader {
|
|
4
4
|
gameCode: string;
|
|
@@ -10,78 +10,77 @@ export interface ModcryptHeader {
|
|
|
10
10
|
|
|
11
11
|
// KEY1 table carried forward from NitroHelper/Arm9Encryptor.cs (Dirbaio/ndstool).
|
|
12
12
|
const KEY1 = Buffer.from(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
13
|
+
"99D5205F5744F5B96E19A4D99E6A5A94D8AEF1EB4175E23A9382D03233EE31D5CC57619A3706A21B793972F555AEF6BE5F1B69FBE59DF1E9CE2CD9A15E3205E6" +
|
|
14
|
+
"FED3FECFD462040D8BF5ECB72B6079BB1295310D6E3FDA2B8884F0F13D127E254522F1BB24061A0611ADDF288B6481342BEB332999AAF2BD9C14959D9FF7F58C" +
|
|
15
|
+
"7297A1299DD15FCF664D071ADED34A4B85C9A7A31795053A3D490ABF0A898BA24A8249DD2790F10BE9EB1C6A83764505BA817061173F4BDEAECFAB3957F23A56" +
|
|
16
|
+
"4811AD8A40E1453FFA9B0254CAA693FBEF4DFE6FA3D8879C08BAD5486A8D2DFD6E15F874BDBE528B18228A9EFB7437071B366C4A19BA4262B97991107B676596" +
|
|
17
|
+
"FE0223E8EE998C773E5C86644D6D7886A54F65E21EB2DF5A0AD07E0814B071ACBDDB831CB9D7A162CDC6637C5269C3E6BF75CE12445D2104FAFBD33C381163D4" +
|
|
18
|
+
"958541494609F2084311DC1F76C0156D1F3C6370EA87806CC3BD638BC2372137DCEE09232E376A4D7390F75030AC1C92041023914FD207AA683E4F9AC964606A" +
|
|
19
|
+
"C81421F3D62241124424CFE68A56DD0D534DE1851E8C525A9C1984C20357F16FE300BE58F64CEDD521649C1FBE55033C4ADCFFAAC9DAE05D5EBFE6DEF5D8B1F8" +
|
|
20
|
+
"FF36B3B9626795DB315F37ED4C70679990B518316C3D9999E442DAD3254213A0AED7706CB155CFC7D746D54361173D4428E93385D5D0A293AA25121FFBC50B46" +
|
|
21
|
+
"F597765645A6BE87B1946BE8B1FE3399AE1F3E6C39711D09009037E4103E7574FF8C833BB0F1B0F90105474295F1D6AC7E38E69E9574263FB4685018D04330B4" +
|
|
22
|
+
"4C4BE368BFE54DB6958B0AA074253277CFA1F72CD871135AABEAC951E80DEEEFE9937E19A71E433881162CA148E373CC29216CD35DCEA0D9617143A01513B564" +
|
|
23
|
+
"92CF2A19DCADB7A59F8665F81A9FE7FBF7FDB8136C27DB6FDF351CF78D2C5B9B12AB386406CCDE31E84E751164E3FAEAEB3454C2AD3F34EB932C7D26369D56F3" +
|
|
24
|
+
"5AE1F6B398634A9E3283E49A84607D902E130EEE934B36A285EC1638E8880602BFF0A03AEDD76A9A73E157CFF844B8DC2E2359D1DF9552719961A04BD57F6E78" +
|
|
25
|
+
"BAA9C530D34086329D320C9C37B7022FBA5498A9C41304C98DBEC8E75D97502E93D622590C27BC2292E0A7200F936F7F4C9FD3B5A62A0B7467497D1026CBD1C5" +
|
|
26
|
+
"8671E78CA09CE95BB21AF601EE8C9E5E83F21ADBE6E5EA845976D27CF68DA5493648C21652BB83A374B9070C3BFF6128E161E9E4EF6E15AA4EBAE85D0596BB32" +
|
|
27
|
+
"56B0FB72520F0EC84225657689AFF2DE1027F0014B74A79707D5265454091F820A867D30390EB3269B0B57BB360631AFFD79FCD930102B0CB3E19BD77BDC5FEF" +
|
|
28
|
+
"D2F813454D4775BD46963C7E75F33EB567C59A3BB05B296BDE805BC81505B131B6CE49DDAD84B5AE60DC67313430FE4EBD802FA6BF63392186D9357F16682205" +
|
|
29
|
+
"54E990268C076C51A43155D70907A83E2E5366C1F8F27BC4F258CFF187C5A2E7278F308758A064622318B9887CFACEC498AEAD17CC4A5BF3E948D556D30DF2C8" +
|
|
30
|
+
"92738CDBD72F56AC81F992694DC632F6E6C08D21E276806111BCDC6C93AF19699BD0BFB9319F0267A351EE8306227B0CAB494240B8D5017DCE5EF7555339C599" +
|
|
31
|
+
"46D8879FBAF764B4E39AFAA16D90681030CA8A54A79F60C319F56B0D7A5198E6984351B4D635E94FC3DF0F7BD62F5CBD3A156119F14BCBAADC6D64C9D3C61E56" +
|
|
32
|
+
"EF384C50718675CC0D0D4EE928F6065D701BAAD345CFA839AC95A62EB4E422D474A8375F487A04CCA54C40D828B428080D1C725241F07D47193A534E5884626B" +
|
|
33
|
+
"93B58A81214E0DDCB43FA2C6FCC92B40DA3804E95E5A866B0C22258568118D7C921D95554DAB8EBBDAA6E6B751B6325A0541DD052A0A5650911747CCC9E67EB5" +
|
|
34
|
+
"614ADB736751C833F5DA6E742E54C3370D6DAF08E8158A5FE25921CDA8DE0C065A776B5FDB18653EC850DE78E0B882B35D4E7232074FC13423BA96B7674EA428" +
|
|
35
|
+
"1E3462EB2D6A70E92F42C4704E5A319CF95B4728AADA716F381FB378C4926B1C9EF6359AB74D0EBFCC1829410348355D55D02BC629AF5C6074698E5E9B7CD4BD" +
|
|
36
|
+
"7B44647D3F925D69B61F004BD48335CF7E644E17AE8DD52E9A28124E2E2B49085CAEC64685AE41611E6F82D25137161F0BF659A49ACA5AAF0DD4338B2063F184" +
|
|
37
|
+
"805CCBCF08B4B9D31605BD6283319B5651989FBAB25BAAB2226B2CB5D448FA632B5F58FA61FA6409BB38E0B89D9260A80D676F0E37F50D019FC277D4FEECF173" +
|
|
38
|
+
"3039E07DF56198E42C2855045655DB2F6BECE55806B664806A2A1A4E5B0FD8C40A2E5219D962F53048BE8C7B4F389BA2C3AFC9D3C7C1624186B96121576F994F" +
|
|
39
|
+
"C1BACE7BB53B4D5E8A8B44575F135F706D5B2947DC38E2EC045565122AE81743E18EDD2AB3E294F7096E5CE6EB8AF86D89495448F52FADBFEA944BCAFC398782" +
|
|
40
|
+
"5F8A01F275F2E671D6D842DEF12D1D28A6887EA3A0471D30D9A371DF491CCB01F836B1F2F022585D456BBDA0BBB28842C78C28CE93E8906308907C893CF57DB7" +
|
|
41
|
+
"042D4F555116FD7E79E8BEC1F212D4F8B4840523A0CCD22BFDE1ABAD0DD1556C2341944D77374F05280CBF17B312676C8CC35AF741842A6DD09412272CB4ED9C" +
|
|
42
|
+
"4DEC478297D567B91B9DC055077EE58EE2A8E73E12E40E3A2A455534A2F92D5A1BAB527C83105F55D2F15A432BC6A7A4891595E8B44B9DF875E39F60785BD6E6" +
|
|
43
|
+
"0D44E62106BD472253A400AD8D43138539F7AAFC38AF7BEDFCE42B5450984CFC8580F7DF3C8022E194DADE24C6B07A3938DC0FA1A7F4F96F6318578B84412A2E" +
|
|
44
|
+
"D453F2D9000FD0DD996E19A60AD0EC5B5824ABC0CB0665EC1A1338940A67032F3FF7E377447733C61439D0E3C0A20879BB409957410B0190CDE1CC4867DBB3AF" +
|
|
45
|
+
"8874F34C828F72B1B52329C4126C19FC8E46A49CC4256587D36DBE8A93110338ED832BF346A493EA3B53851DCED4F1088327EDFC9B1A18BCF98BAEDC24AB5038" +
|
|
46
|
+
"E9724B1022177B465DAB5964F340AEF8BBE5C8F926034E557DEBEBFEF739E6E00A11BE2E28FF98EDC0C9425642C3FD00F6AF87A25B013F329247959A72A5323D" +
|
|
47
|
+
"AE6BD09B07D24992E3784AFAA1067DF241CF77740414B20C86846416D5BB51A1E56FF1D1F2E2F75F58204DB857C7CFDDC5D8BE763DF65F7EE72A8B88241B383F" +
|
|
48
|
+
"0E412377F5F04BD40C1FFAA40B805FCF45F6E0DA2F345953FB203C52625E35B562FE8B6063E3865A151A6ED14745BC32B4EB6738ABE46E333AB5EDA3AD67E04E" +
|
|
49
|
+
"4195EE626271261D31EF6230AFD782ACC2DC0504F59707BF11592307C06402E897E53EAF18AC59A68B4A33901C6E7C9C207E4C3C3E6164BBC56B7C7E3E9FC54C" +
|
|
50
|
+
"9FEA73F5D789C04CF4FBF42DEC141B51D5C112C810DF0B4A8B9CBC93456A3E3E7DC1A9BACDC1B407E4E1688643B26D38F3FB0C5C663771DE56EF6EA0104065A7" +
|
|
51
|
+
"98F7D0BE0EC83736EC10CA7C9CAB841E051776021C4F52AA5FC1C6A056B9D80484444DA759D8DE60E6380E058F03E13B6D8104336F300BCE69052133FB26BB89" +
|
|
52
|
+
"7DB6AE877E5107E0ACF7960A6BF9C45C1DE44447B85EFAE3788455424B485EF77D4735861D2B430503EC8AB81E063C760C481A43A7B78AED1E13C643EE10EFDB" +
|
|
53
|
+
"ECFB3C83B29544EFD854514E2D11441DFB36591E7A34C1C3CA570061EA67A5169B55D055E17FD936D24076AEDC01CEB07A83D5CB2098EC6BC1729234F3825737" +
|
|
54
|
+
"628A32360C9043AEAE5C9B788E136502FD6871C1FEB031A02482B0C3B17969A7F5D2EBD082C032DC9EC7263C6D8D98C1BB22D4D00F33EC3EB9CCE1DC6A4C7736" +
|
|
55
|
+
"141CF9BF819F285F71853229907548C4B34ACED8448F142FFD4057EFAA0875D946D1D66E32551FC318FE841FFC84D5FF715E1B48C386950E280827D33883717B" +
|
|
56
|
+
"4C8063549A56B0ACCF80CA3109EFFEF3BEAF247EA6FE533FC28D4A3368D122A666AD7BEADEB643B0A1259500A33F7546141144ECD795BC92F04FA91653629760" +
|
|
57
|
+
"2A0F41F17124BEEE947F08CD6093B3855B07003FD80F28839AD1699FD1DA2EC39001A2B96B4E2A669DDAAEA6EA2AD3682F0C0C9CD28C4AEDE29E57659D0987A3" +
|
|
58
|
+
"B4C4325DC9D4322BB1E0711E644DE69071E31E40ED7DF3840EEDC87876AEC0712772BB05EA0264FBF3486BB542933FED9F1353D2F7FE2AEC1D4725DB3C9186C6" +
|
|
59
|
+
"8EF011FD237436F7A4F59E7A7E535044D447CAD3EB386DE6D971947F4AC6694B11F452EA22FE8AB036678B59E8E6802AEB650413EEECDC9E5FB1EC056A59E69F" +
|
|
60
|
+
"5E596B89BFF71ACA44F95B6A718503E42962E0706F41C4CFB2B1CCE37EA607A887E77F8493DB524B6CEC7EDDD4244810699F046074E64818F3E42CB94F2E507A" +
|
|
61
|
+
"DFD454692B8BA7F3CEFF1FF33E26013917958489B0F04C4B82919FC44BAC9DA574AF1725C9CA32D3BC898A8489CC0DAE7CA2DB9C6A7891EEEA765D4E8760F569" +
|
|
62
|
+
"1567D402CFAF483607EABF6F662D068FC49AFEF9F6908775B8F7AD0F76105A3D59B02EB3C7352CCC70562BCBE33796C52F461B8A2246C788A726329861DF8622" +
|
|
63
|
+
"8AF41C2F87A109AACCA9AED3BD00451C9A5487865287EFFF1E8FA18FC1895C351BDA2D3A2C16B2C2F156E278C16B6397C5568FC9327F2CAAAFA6A8AC20912288" +
|
|
64
|
+
"DEE4608BF94B42251AE37F9C2C19893A7E05D436CC6958C2C1328B2F9085EB7A3950A5A12792C566B0204F587E5583432B45E29CE4D812902C168356167903B3" +
|
|
65
|
+
"AD2D61181A131F37E2E19C737B80D5FD2D5187FC7BAAD71F2C7A8EAFF48DBBCD95117C720BEE6FE2B9AFDE3783DE8C8D620567B796C68D56B60DD762BAD64636" +
|
|
66
|
+
"BD8EC8E6EA2A6C1014FF6B5BFA823C46B1304346518A7D9B923E83795B555DB26C5ECE90628E5398C90D6DE52D57CDC58157BAE1E8B88F72E54F13DCEA9D7115" +
|
|
67
|
+
"10B21188D509D47F5B657F2C3B384C1168508DFB9EB059BF9480894AC51A18128953D14A1029E88C1CECB6EA46C7178B251531A8A26B43B19DE2DB0B879BB011" +
|
|
68
|
+
"040E71D2297789820A66417F1D0B48FF72BB24FDC248A19BFE7B7FCE88DB86D9853B1CB0DCA83307BF512EE30E9A00971E06C097439DD8B645C486675F00F888" +
|
|
69
|
+
"9AA4529EC7AA8A8375ECC518AECEC32F1A2BF918FFAE1AF5530BB53351A7FDE8A8E1A264B622174380CC0AD8AE3BBA40D7D9924A89DF0410EE9B182B6A77698A" +
|
|
70
|
+
"68F4F9B9A221156EE61E3B0362309B60417E259B9E8FC5521008F8C269A1211188375E793566FF1042186EED97B66B1C4E36E56D7DB4E4BF20B9E0053A69D5B8" +
|
|
71
|
+
"E3D5DCE0B9AC533E07A457AD77FF4818762AAC492A8E47756D9F676330358C390539D56F643A5BADCA0BBB82529945B193363699AF13204436D8024409399285" +
|
|
72
|
+
"FF4A4A9787A663D7C7B5B524ED0FB46F0C585214D9A67BD379BC3858A1BD3B8406D81A06FD6BA8EA4B69280437AD8299FB0E1B85BDA85D73CDDC58750ABE636C" +
|
|
73
|
+
"48E74CE4302B0460B915D8DA8681758F96D48D1C5D70857C1C677BD50867A6CE4B0A6670B7E563D45B8A82EA1067CAE2F4EF17852F2A5F8A9782F86AD63410EA" +
|
|
74
|
+
"EBC95C3CE149F846EBDEBDF6A992F1AAA6A018B03AD30F1FF36FFF31454344D3509AF7880996C1CE76CCF22C2CBAAD82778F1884C0D2079C3690834E0BA54F43" +
|
|
75
|
+
"3E04AB784FD6FB09012490DA6F3C3A610D7F694AEB2B3002B4DBE084A9ECD735BF377D8558CEA94EE480C7A8D3306748EB29AF2F746AB4A73F0F3F92AFF3CAAC" +
|
|
76
|
+
"AF4BD994C043CA810D2F48A1B027D5D2EF4B0585A3DE4D93303CF0BB4A8F30274CEBE33E64ED9A2F3BF182F0BAF4CF7F40CBB0E17FBCAA57D3C974F2FA430D22" +
|
|
77
|
+
"D0F4774E93D785701F99BFB6DE35F130A75E71F06B012D7B64F033530A3988F36B3AA66B35D22F43CD02FDB5E9BC5BAAD8A4197E0E5D94819E6F77ADD60E7493" +
|
|
78
|
+
"96E7C4185FADF519",
|
|
79
|
+
"hex",
|
|
80
80
|
);
|
|
81
81
|
|
|
82
82
|
export const aes128CtrCrypt = (key: Buffer, counter: Buffer, data: Buffer): Buffer => {
|
|
83
|
-
if (key.length !== 16 || counter.length !== 16)
|
|
84
|
-
throw new RangeError('AES key and counter must have 16 bytes');
|
|
83
|
+
if (key.length !== 16 || counter.length !== 16) throw new RangeError("AES key and counter must have 16 bytes");
|
|
85
84
|
const ctr = Buffer.from(counter).reverse();
|
|
86
85
|
const aes = createAesBlockCipher(Buffer.from(key).reverse());
|
|
87
86
|
const output = Buffer.alloc(data.length);
|
|
@@ -97,20 +96,16 @@ export const aes128CtrCrypt = (key: Buffer, counter: Buffer, data: Buffer): Buff
|
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
export const modcryptKey = (header: ModcryptHeader): Buffer => {
|
|
100
|
-
if (typeof header.gameCode !==
|
|
101
|
-
throw new Error(
|
|
99
|
+
if (typeof header.gameCode !== "string" || header.gameCode.length !== 4)
|
|
100
|
+
throw new Error("Expected four-character game code");
|
|
102
101
|
if (header.twlInternalFlags & 4 || (header.appflags?.[3] ?? 0) & 0x80) {
|
|
103
102
|
const key = Buffer.alloc(16);
|
|
104
|
-
key.write(header.gameTitle, 0, 12,
|
|
105
|
-
key.write(header.gameCode, 12, 4,
|
|
103
|
+
key.write(header.gameTitle, 0, 12, "ascii");
|
|
104
|
+
key.write(header.gameCode, 12, 4, "ascii");
|
|
106
105
|
return key;
|
|
107
106
|
}
|
|
108
|
-
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16)
|
|
109
|
-
|
|
110
|
-
const x = Buffer.from(
|
|
111
|
-
'Nintendo' + header.gameCode + [...header.gameCode].reverse().join(''),
|
|
112
|
-
'ascii',
|
|
113
|
-
);
|
|
107
|
+
if (!header.hmac_arm9i || header.hmac_arm9i.length < 16) throw new Error("Missing ARM9i HMAC for modcrypt key");
|
|
108
|
+
const x = Buffer.from(`Nintendo${header.gameCode}${[...header.gameCode].reverse().join("")}`, "ascii");
|
|
114
109
|
for (let i = 0; i < 16; i++) x[i] ^= header.hmac_arm9i[i];
|
|
115
110
|
const mask = (1n << 128n) - 1n;
|
|
116
111
|
let v = x.readBigUInt64LE(0) | (x.readBigUInt64LE(8) << 64n);
|
|
@@ -123,9 +118,8 @@ export const modcryptKey = (header: ModcryptHeader): Buffer => {
|
|
|
123
118
|
};
|
|
124
119
|
|
|
125
120
|
export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
|
|
126
|
-
if (typeof gameCode !==
|
|
127
|
-
|
|
128
|
-
if (input.length < 0x800) throw new RangeError('Secure area must contain at least 0x800 bytes');
|
|
121
|
+
if (typeof gameCode !== "string" || gameCode.length !== 4) throw new Error("Expected four-character game code");
|
|
122
|
+
if (input.length < 0x800) throw new RangeError("Secure area must contain at least 0x800 bytes");
|
|
129
123
|
const output = Buffer.from(input);
|
|
130
124
|
let magic = new Uint32Array(0x412);
|
|
131
125
|
let args: [number, number, number] = [0, 0, 0];
|
|
@@ -155,7 +149,7 @@ export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
|
|
|
155
149
|
}
|
|
156
150
|
function init1() {
|
|
157
151
|
magic = Uint32Array.from({ length: 0x412 }, (_, i) => KEY1.readUInt32LE(i * 4));
|
|
158
|
-
const code = Buffer.from(gameCode,
|
|
152
|
+
const code = Buffer.from(gameCode, "ascii").readUInt32LE();
|
|
159
153
|
args = [code, code >>> 1, (code << 1) >>> 0];
|
|
160
154
|
init2();
|
|
161
155
|
init2();
|
package/src/nitro/filesystem.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Based on NitroHelper FNT/FAT/OverlayTable, GPL-3.0.
|
|
2
|
-
import { bytes, range } from
|
|
2
|
+
import { bytes, range } from "./binary";
|
|
3
3
|
|
|
4
4
|
/** A file in the original ROM, optionally replaced with new bytes. */
|
|
5
5
|
export interface NitroFile {
|
|
@@ -29,8 +29,8 @@ export class FileAllocationTable {
|
|
|
29
29
|
|
|
30
30
|
constructor(input: Uint8Array, offset: number, size: number) {
|
|
31
31
|
const data = bytes(input);
|
|
32
|
-
const table = range(data, offset, size,
|
|
33
|
-
if (size % 8 || size / 8 > 65535) throw new Error(
|
|
32
|
+
const table = range(data, offset, size, "FAT");
|
|
33
|
+
if (size % 8 || size / 8 > 65535) throw new Error("Invalid FAT entry count");
|
|
34
34
|
this.entries = [];
|
|
35
35
|
for (let p = 0; p < size; p += 8) {
|
|
36
36
|
const start = table.readUInt32LE(p),
|
|
@@ -38,9 +38,7 @@ export class FileAllocationTable {
|
|
|
38
38
|
range(data, start, end - start, `FAT entry ${p / 8}`);
|
|
39
39
|
this.entries.push({ id: p / 8, offset: start, size: end - start });
|
|
40
40
|
}
|
|
41
|
-
this.sortedIDs = [...this.entries]
|
|
42
|
-
.sort((a, b) => a.offset - b.offset || a.id - b.id)
|
|
43
|
-
.map((entry) => entry.id);
|
|
41
|
+
this.sortedIDs = [...this.entries].sort((a, b) => a.offset - b.offset || a.id - b.id).map((entry) => entry.id);
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -48,21 +46,20 @@ export class FileNameTable {
|
|
|
48
46
|
readonly root: NitroFolder;
|
|
49
47
|
|
|
50
48
|
constructor(fat: FileAllocationTable, input: Uint8Array, offset: number, size: number) {
|
|
51
|
-
const table = range(bytes(input), offset, size,
|
|
52
|
-
range(table, 0, 8,
|
|
49
|
+
const table = range(bytes(input), offset, size, "FNT");
|
|
50
|
+
range(table, 0, 8, "FNT root directory");
|
|
53
51
|
const count = table.readUInt16LE(6);
|
|
54
|
-
if (count < 1 || count > 4096) throw new Error(
|
|
55
|
-
range(table, 0, count * 8,
|
|
56
|
-
const decoder = new TextDecoder(
|
|
52
|
+
if (count < 1 || count > 4096) throw new Error("Invalid FNT directory count");
|
|
53
|
+
range(table, 0, count * 8, "FNT directory table");
|
|
54
|
+
const decoder = new TextDecoder("shift_jis");
|
|
57
55
|
const visited = new Set<number>();
|
|
58
56
|
const fileIds = new Set<number>();
|
|
59
57
|
const parse = (index: number, name: string, depth: number): NitroFolder => {
|
|
60
|
-
if (depth > 256 || index >= count || visited.has(index))
|
|
61
|
-
throw new Error('Invalid FNT directory tree or cycle');
|
|
58
|
+
if (depth > 256 || index >= count || visited.has(index)) throw new Error("Invalid FNT directory tree or cycle");
|
|
62
59
|
visited.add(index);
|
|
63
60
|
let cursor = table.readUInt32LE(index * 8);
|
|
64
61
|
let fileId = table.readUInt16LE(index * 8 + 4);
|
|
65
|
-
if (cursor < count * 8) throw new Error(
|
|
62
|
+
if (cursor < count * 8) throw new Error("FNT directory names overlap directory table");
|
|
66
63
|
const folder: NitroFolder = {
|
|
67
64
|
name,
|
|
68
65
|
id: index === 0 ? 0xffff : 0xf000 + index,
|
|
@@ -71,31 +68,31 @@ export class FileNameTable {
|
|
|
71
68
|
};
|
|
72
69
|
const names = new Set<string>();
|
|
73
70
|
while (true) {
|
|
74
|
-
const tag = range(table, cursor++, 1,
|
|
71
|
+
const tag = range(table, cursor++, 1, "FNT name")[0];
|
|
75
72
|
if (!tag) break;
|
|
76
73
|
const length = tag & 0x7f;
|
|
77
|
-
if (!length) throw new Error(
|
|
78
|
-
const filename = decoder.decode(range(table, cursor, length,
|
|
74
|
+
if (!length) throw new Error("Empty FNT directory name");
|
|
75
|
+
const filename = decoder.decode(range(table, cursor, length, "FNT filename"));
|
|
79
76
|
cursor += length;
|
|
80
77
|
if (
|
|
81
|
-
filename.includes(
|
|
82
|
-
filename.includes(
|
|
83
|
-
filename.includes(
|
|
84
|
-
filename ===
|
|
85
|
-
filename ===
|
|
78
|
+
filename.includes("/") ||
|
|
79
|
+
filename.includes("\\") ||
|
|
80
|
+
filename.includes("\0") ||
|
|
81
|
+
filename === "." ||
|
|
82
|
+
filename === ".."
|
|
86
83
|
)
|
|
87
|
-
throw new Error(
|
|
84
|
+
throw new Error("Invalid FNT path component");
|
|
88
85
|
const normalized = filename.toLowerCase();
|
|
89
86
|
if (names.has(normalized)) throw new Error(`Duplicate FNT path: ${filename}`);
|
|
90
87
|
names.add(normalized);
|
|
91
88
|
if (tag & 0x80) {
|
|
92
|
-
const id = range(table, cursor, 2,
|
|
89
|
+
const id = range(table, cursor, 2, "FNT directory id").readUInt16LE();
|
|
93
90
|
cursor += 2;
|
|
94
|
-
if (id < 0xf000) throw new Error(
|
|
91
|
+
if (id < 0xf000) throw new Error("Invalid FNT directory id");
|
|
95
92
|
folder.folders.push(parse(id & 0xfff, filename, depth + 1));
|
|
96
93
|
} else {
|
|
97
94
|
const entry = fat.entries[fileId++];
|
|
98
|
-
if (!entry) throw new Error(
|
|
95
|
+
if (!entry) throw new Error("FNT file id is outside FAT");
|
|
99
96
|
if (fileIds.has(entry.id)) throw new Error(`Duplicate FNT file id: ${entry.id}`);
|
|
100
97
|
fileIds.add(entry.id);
|
|
101
98
|
folder.files.push({ name: filename, ...entry });
|
|
@@ -103,7 +100,7 @@ export class FileNameTable {
|
|
|
103
100
|
}
|
|
104
101
|
return folder;
|
|
105
102
|
};
|
|
106
|
-
this.root = { name:
|
|
103
|
+
this.root = { name: "root", id: 0xffff, files: [], folders: [parse(0, "data", 0)] };
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
static findFile(id: number, folder: NitroFolder): NitroFile | undefined {
|
|
@@ -128,14 +125,14 @@ export interface OverlayItem {
|
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
const overlayFields = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
"overlayId",
|
|
129
|
+
"ramAddress",
|
|
130
|
+
"ramSize",
|
|
131
|
+
"bssSize",
|
|
132
|
+
"staticInitialiserStartAddress",
|
|
133
|
+
"staticInitialiserEndAddress",
|
|
134
|
+
"fileId",
|
|
135
|
+
"reserved",
|
|
139
136
|
] as const;
|
|
140
137
|
|
|
141
138
|
export class OverlayTable {
|
|
@@ -147,8 +144,8 @@ export class OverlayTable {
|
|
|
147
144
|
readonly isArm9: boolean,
|
|
148
145
|
) {
|
|
149
146
|
if (!size) return;
|
|
150
|
-
const table = range(bytes(input), offset, size,
|
|
151
|
-
if (size % 32) throw new Error(
|
|
147
|
+
const table = range(bytes(input), offset, size, "overlay table");
|
|
148
|
+
if (size % 32) throw new Error("Invalid overlay table size");
|
|
152
149
|
for (let p = 0; p < size; p += 32) {
|
|
153
150
|
if (table.readUInt32LE(p) === 0xffffffff) break;
|
|
154
151
|
this.entries.push({
|
|
@@ -166,10 +163,10 @@ export class OverlayTable {
|
|
|
166
163
|
readBasicOverlays(fat: FileAllocationTable): NitroFile[] {
|
|
167
164
|
return this.entries.map((item) => {
|
|
168
165
|
const file = fat.entries[item.fileId];
|
|
169
|
-
if (!file) throw new Error(
|
|
166
|
+
if (!file) throw new Error("Overlay file id is outside FAT");
|
|
170
167
|
return {
|
|
171
168
|
...file,
|
|
172
|
-
name: `overlay${this.isArm9 ?
|
|
169
|
+
name: `overlay${this.isArm9 ? "" : "7"}_${String(item.overlayId).padStart(4, "0")}.bin`,
|
|
173
170
|
};
|
|
174
171
|
});
|
|
175
172
|
}
|
package/src/nitro/header.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Port of NitroHelper/Header.cs. Copyright Xzonn and upstream contributors, GPL-3.0.
|
|
2
|
-
import { bytes, range } from
|
|
3
|
-
import { crc16 } from
|
|
4
|
-
import { encryptSecureArea } from
|
|
2
|
+
import { bytes, range } from "./binary";
|
|
3
|
+
import { crc16 } from "./crc";
|
|
4
|
+
import { encryptSecureArea } from "./crypto";
|
|
5
5
|
|
|
6
6
|
export const UnitCode = Object.freeze({ NDS: 0, NDS_DSi: 2, DSi: 3 });
|
|
7
7
|
|
|
@@ -106,43 +106,30 @@ export class Header {
|
|
|
106
106
|
declare secureDisable: bigint;
|
|
107
107
|
|
|
108
108
|
constructor(input: Uint8Array) {
|
|
109
|
-
const data = bytes(input,
|
|
110
|
-
range(data, 0, 0x170,
|
|
109
|
+
const data = bytes(input, "header");
|
|
110
|
+
range(data, 0, 0x170, "header");
|
|
111
111
|
const size = data.readUInt32LE(0x84);
|
|
112
112
|
if (size < 0x170 || size > data.length) throw new RangeError(`Invalid header size: ${size}`);
|
|
113
113
|
this.raw = Buffer.from(data.subarray(0, size));
|
|
114
|
-
if (this.unitCode & 2 && size > 0x200) range(this.raw, 0, 0x1000,
|
|
115
|
-
this.trimmedRom = this.total_rom_size
|
|
116
|
-
? this.total_rom_size >= data.length
|
|
117
|
-
: data.length !== this.size;
|
|
114
|
+
if (this.unitCode & 2 && size > 0x200) range(this.raw, 0, 0x1000, "DSi header");
|
|
115
|
+
this.trimmedRom = this.total_rom_size ? this.total_rom_size >= data.length : data.length !== this.size;
|
|
118
116
|
this.doublePadding =
|
|
119
|
-
(this.ARM9size % 0x400 < 0x200 &&
|
|
120
|
-
|
|
121
|
-
this.ARM9overlayOffset % 0x400 === 0) ||
|
|
122
|
-
(this.ARM7size % 0x400 < 0x200 &&
|
|
123
|
-
this.FNToffset % 0x400 === 0 &&
|
|
124
|
-
this.ARM7overlayOffset % 0x400 === 0) ||
|
|
117
|
+
(this.ARM9size % 0x400 < 0x200 && this.ARM7romOffset % 0x400 === 0 && this.ARM9overlayOffset % 0x400 === 0) ||
|
|
118
|
+
(this.ARM7size % 0x400 < 0x200 && this.FNToffset % 0x400 === 0 && this.ARM7overlayOffset % 0x400 === 0) ||
|
|
125
119
|
(this.FNTsize % 0x400 < 0x200 && this.FAToffset % 0x400 === 0) ||
|
|
126
120
|
(this.FATsize % 0x400 < 0x200 && this.bannerOffset % 0x400 === 0);
|
|
127
121
|
const nitroOffset = this.ARM9romOffset + this.ARM9size;
|
|
128
122
|
this.nitrocode =
|
|
129
|
-
nitroOffset >= size &&
|
|
130
|
-
nitroOffset + 4 <= data.length &&
|
|
131
|
-
data.readUInt32LE(nitroOffset) === 0xdec00621;
|
|
123
|
+
nitroOffset >= size && nitroOffset + 4 <= data.length && data.readUInt32LE(nitroOffset) === 0xdec00621;
|
|
132
124
|
this.decrypted = data.length >= 0x4008 && data.readBigUInt64LE(0x4000) === 0xe7ffdeffe7ffdeffn;
|
|
133
125
|
this.headerCRC = crc16(this.raw.subarray(0, 0x15e)) === this.headerCRC16;
|
|
134
126
|
this.logoCRC = crc16(this.logo) === this.logoCRC16;
|
|
135
127
|
const secure = data.subarray(0x4000, 0x8000);
|
|
136
128
|
this.secureCRC =
|
|
137
129
|
secure.length > 0 &&
|
|
138
|
-
crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) ===
|
|
139
|
-
this.secureCRC16;
|
|
130
|
+
crc16(this.decrypted ? encryptSecureArea(this.gameCode, secure) : secure) === this.secureCRC16;
|
|
140
131
|
this.dlp_signature = Buffer.alloc(0);
|
|
141
|
-
if (
|
|
142
|
-
this.ROMsize >= size &&
|
|
143
|
-
this.ROMsize + 0x88 <= data.length &&
|
|
144
|
-
data.readUInt32LE(this.ROMsize) === 0x00016361
|
|
145
|
-
) {
|
|
132
|
+
if (this.ROMsize >= size && this.ROMsize + 0x88 <= data.length && data.readUInt32LE(this.ROMsize) === 0x00016361) {
|
|
146
133
|
this.dlp_signature = Buffer.from(data.subarray(this.ROMsize, this.ROMsize + 0x88));
|
|
147
134
|
}
|
|
148
135
|
}
|
|
@@ -153,7 +140,7 @@ export class Header {
|
|
|
153
140
|
set size(value: number) {
|
|
154
141
|
const power = Math.log2(value);
|
|
155
142
|
if (!Number.isInteger(power) || power < 17 || power > 32)
|
|
156
|
-
throw new RangeError(
|
|
143
|
+
throw new RangeError("ROM capacity must be a power of two from 128 KiB to 4 GiB");
|
|
157
144
|
this.raw[0x14] = power - 17;
|
|
158
145
|
}
|
|
159
146
|
toBuffer() {
|
|
@@ -163,7 +150,7 @@ export class Header {
|
|
|
163
150
|
return data;
|
|
164
151
|
}
|
|
165
152
|
toString() {
|
|
166
|
-
return `NDS header: ${this.gameTitle.replace(/\0+$/,
|
|
153
|
+
return `NDS header: ${this.gameTitle.replace(/\0+$/, "")} (${this.gameCode})`;
|
|
167
154
|
}
|
|
168
155
|
}
|
|
169
156
|
|
|
@@ -171,44 +158,44 @@ function field(
|
|
|
171
158
|
name: string,
|
|
172
159
|
offset: number,
|
|
173
160
|
length: number,
|
|
174
|
-
type:
|
|
161
|
+
type: "buffer" | "string" | "number" = "buffer",
|
|
175
162
|
dsi = false,
|
|
176
163
|
) {
|
|
177
164
|
Object.defineProperty(Header.prototype, name, {
|
|
178
165
|
enumerable: true,
|
|
179
166
|
get(this: Header) {
|
|
180
167
|
if (dsi && (!(this.unitCode & 2) || this.raw.length <= 0x200))
|
|
181
|
-
return type ===
|
|
168
|
+
return type === "buffer" ? Buffer.alloc(length) : 0;
|
|
182
169
|
const data = range(this.raw, offset, length, `header ${name}`);
|
|
183
|
-
if (type ===
|
|
184
|
-
if (type ===
|
|
170
|
+
if (type === "string") return data.toString("latin1");
|
|
171
|
+
if (type === "buffer") return data;
|
|
185
172
|
return length === 8 ? data.readBigUInt64LE() : data.readUIntLE(0, length);
|
|
186
173
|
},
|
|
187
174
|
set(this: Header, value: string | number | bigint | Uint8Array) {
|
|
188
175
|
const target = range(this.raw, offset, length, `header ${name}`);
|
|
189
|
-
if (type ===
|
|
190
|
-
if (typeof value !==
|
|
176
|
+
if (type === "string") {
|
|
177
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "latin1") > length)
|
|
191
178
|
throw new TypeError(`Invalid ${name}`);
|
|
192
179
|
target.fill(0);
|
|
193
|
-
target.write(value,
|
|
194
|
-
} else if (type ===
|
|
180
|
+
target.write(value, "latin1");
|
|
181
|
+
} else if (type === "buffer") {
|
|
195
182
|
if (!(value instanceof Uint8Array)) throw new TypeError(`Invalid ${name}`);
|
|
196
183
|
const source = bytes(value, name);
|
|
197
184
|
if (source.length !== length) throw new RangeError(`${name} must contain ${length} bytes`);
|
|
198
185
|
source.copy(target);
|
|
199
186
|
} else if (length === 8) {
|
|
200
|
-
if (typeof value !==
|
|
187
|
+
if (typeof value !== "bigint") throw new TypeError(`Invalid ${name}`);
|
|
201
188
|
target.writeBigUInt64LE(value);
|
|
202
189
|
} else {
|
|
203
|
-
if (typeof value !==
|
|
190
|
+
if (typeof value !== "number") throw new TypeError(`Invalid ${name}`);
|
|
204
191
|
target.writeUIntLE(value, 0, length);
|
|
205
192
|
}
|
|
206
193
|
},
|
|
207
194
|
});
|
|
208
195
|
}
|
|
209
|
-
field(
|
|
210
|
-
field(
|
|
211
|
-
field(
|
|
196
|
+
field("gameTitle", 0, 12, "string");
|
|
197
|
+
field("gameCode", 12, 4, "string");
|
|
198
|
+
field("makerCode", 16, 2, "string");
|
|
212
199
|
for (const [name, offset] of Object.entries({
|
|
213
200
|
unitCode: 0x12,
|
|
214
201
|
encryptionSeed: 0x13,
|
|
@@ -217,38 +204,38 @@ for (const [name, offset] of Object.entries({
|
|
|
217
204
|
ROMversion: 0x1e,
|
|
218
205
|
internalFlags: 0x1f,
|
|
219
206
|
}))
|
|
220
|
-
field(name, offset, 1,
|
|
221
|
-
field(
|
|
207
|
+
field(name, offset, 1, "number");
|
|
208
|
+
field("reserved", 0x15, 7);
|
|
222
209
|
const ntr32 =
|
|
223
|
-
|
|
224
|
-
|
|
210
|
+
"ARM9romOffset ARM9entryAddress ARM9ramAddress ARM9size ARM7romOffset ARM7entryAddress ARM7ramAddress ARM7size FNToffset FNTsize FAToffset FATsize ARM9overlayOffset ARM9overlaySize ARM7overlayOffset ARM7overlaySize flagsRead flagsInit bannerOffset".split(
|
|
211
|
+
" ",
|
|
225
212
|
);
|
|
226
|
-
ntr32.forEach((name, i) => field(name, 0x20 + i * 4, 4,
|
|
227
|
-
field(
|
|
228
|
-
field(
|
|
229
|
-
field(
|
|
230
|
-
field(
|
|
231
|
-
field(
|
|
232
|
-
field(
|
|
233
|
-
field(
|
|
234
|
-
field(
|
|
235
|
-
field(
|
|
236
|
-
field(
|
|
237
|
-
field(
|
|
238
|
-
|
|
239
|
-
.split(
|
|
240
|
-
.forEach((name, i) => field(name, 0x160 + i * 4, 4,
|
|
241
|
-
field(
|
|
242
|
-
|
|
243
|
-
.split(
|
|
244
|
-
.forEach((name, i) => field(name, 0x1ac + i * 4, 4,
|
|
213
|
+
ntr32.forEach((name, i) => field(name, 0x20 + i * 4, 4, "number"));
|
|
214
|
+
field("secureCRC16", 0x6c, 2, "number");
|
|
215
|
+
field("ROMtimeout", 0x6e, 2, "number");
|
|
216
|
+
field("ARM9autoload", 0x70, 4, "number");
|
|
217
|
+
field("ARM7autoload", 0x74, 4, "number");
|
|
218
|
+
field("secureDisable", 0x78, 8, "number");
|
|
219
|
+
field("ROMsize", 0x80, 4, "number");
|
|
220
|
+
field("headerSize", 0x84, 4, "number");
|
|
221
|
+
field("reserved2", 0x88, 56);
|
|
222
|
+
field("logo", 0xc0, 156);
|
|
223
|
+
field("logoCRC16", 0x15c, 2, "number");
|
|
224
|
+
field("headerCRC16", 0x15e, 2, "number");
|
|
225
|
+
"debug_romOffset debug_size debug_ramAddress reserved3"
|
|
226
|
+
.split(" ")
|
|
227
|
+
.forEach((name, i) => field(name, 0x160 + i * 4, 4, "number"));
|
|
228
|
+
field("appflags", 0x1bc, 4, "buffer", true);
|
|
229
|
+
"mbk9_wramcnt_setting region_flags access_control scfg_ext_mask"
|
|
230
|
+
.split(" ")
|
|
231
|
+
.forEach((name, i) => field(name, 0x1ac + i * 4, 4, "number", true));
|
|
245
232
|
const twl32 =
|
|
246
|
-
|
|
247
|
-
|
|
233
|
+
"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(
|
|
234
|
+
" ",
|
|
248
235
|
);
|
|
249
|
-
twl32.forEach((name, i) => field(name, 0x1c0 + i * 4, 4,
|
|
250
|
-
|
|
251
|
-
.split(
|
|
252
|
-
.forEach((name, i) => field(name, 0x300 + i * 20, 20,
|
|
253
|
-
field(
|
|
254
|
-
field(
|
|
236
|
+
twl32.forEach((name, i) => field(name, 0x1c0 + i * 4, 4, "number", true));
|
|
237
|
+
"hmac_arm9 hmac_arm7 hmac_digest_master hmac_icon_title hmac_arm9i hmac_arm7i"
|
|
238
|
+
.split(" ")
|
|
239
|
+
.forEach((name, i) => field(name, 0x300 + i * 20, 20, "buffer", true));
|
|
240
|
+
field("hmac_arm9_no_secure", 0x3a0, 20, "buffer", true);
|
|
241
|
+
field("rsa_signature", 0xf80, 0x80, "buffer", true);
|