nitro-patcher 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +22 -0
  3. package/dist/browser.d.ts +34 -0
  4. package/dist/browser.js +4616 -0
  5. package/dist/browser.js.LEGAL.txt +147 -0
  6. package/dist/browser.js.map +7 -0
  7. package/dist/chunk-ANN47R34.js +1 -0
  8. package/dist/chunk-ANN47R34.js.map +1 -0
  9. package/dist/chunk-GFF67HX4.js +1181 -0
  10. package/dist/chunk-GFF67HX4.js.map +1 -0
  11. package/dist/chunk-L7ZM4LLP.js +396 -0
  12. package/dist/chunk-L7ZM4LLP.js.map +1 -0
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +76 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +56 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/nitro-helper.d.ts +315 -0
  20. package/dist/nitro-helper.js +44 -0
  21. package/dist/nitro-helper.js.map +1 -0
  22. package/dist/nitro-patch-helper.d.ts +46 -0
  23. package/dist/nitro-patch-helper.js +16 -0
  24. package/dist/nitro-patch-helper.js.map +1 -0
  25. package/package.json +86 -0
  26. package/src/browser/buffer.ts +1 -0
  27. package/src/browser.ts +60 -0
  28. package/src/cli.ts +76 -0
  29. package/src/index.ts +2 -0
  30. package/src/nitro/banner.ts +77 -0
  31. package/src/nitro/binary.ts +80 -0
  32. package/src/nitro/blz.ts +163 -0
  33. package/src/nitro/crc.ts +23 -0
  34. package/src/nitro/crypto.ts +178 -0
  35. package/src/nitro/filesystem.ts +183 -0
  36. package/src/nitro/header.ts +254 -0
  37. package/src/nitro/nds-file.ts +285 -0
  38. package/src/nitro/twl.ts +338 -0
  39. package/src/nitro-helper.ts +14 -0
  40. package/src/nitro-patch-helper.ts +95 -0
  41. package/src/patch/xdelta.ts +214 -0
  42. package/src/patch/zip.ts +206 -0
  43. package/src/runtime/browser.ts +40 -0
  44. package/src/runtime/node.ts +28 -0
@@ -0,0 +1,1181 @@
1
+ // src/runtime/node.ts
2
+ import { createHash, createHmac, createCipheriv, createPublicKey, verify } from "crypto";
3
+ import { readFile, writeFile } from "fs/promises";
4
+ import { readFileSync } from "fs";
5
+ import { inflateRawSync } from "zlib";
6
+ var hashDigest = (algorithm, data) => createHash(algorithm).update(data).digest();
7
+ var hmacSha1 = (key2, data) => createHmac("sha1", key2).update(data).digest();
8
+ var createAesBlockCipher = (key2) => {
9
+ const cipher = createCipheriv("aes-128-ecb", key2, null);
10
+ cipher.setAutoPadding(false);
11
+ return (block) => cipher.update(block);
12
+ };
13
+ var verifyRsaSha1 = (data, signature) => {
14
+ const key2 = createPublicKey({
15
+ key: {
16
+ kty: "RSA",
17
+ n: Buffer.from(
18
+ "956F790DF08BB85A76AAEFA27FE874758BED9EDF9E9A670CD818BEB9B2885203B3FA11AEAA186513B5D6BB85A384D0D0EFB366CBC6051AAA86827AB74311F59C9BFC6C7079D5F17BD0819F522056738C721F40CF2361932590A3C5DC94CFD17A8CBC954A918AA858F4D804BAF7D3C1C4D7B8F077012FA170260B2C049056F3A5",
19
+ "hex"
20
+ ).toString("base64url"),
21
+ e: "AQAB"
22
+ },
23
+ format: "jwk"
24
+ });
25
+ return verify("RSA-SHA1", data, key2, signature);
26
+ };
27
+
28
+ // src/nitro/binary.ts
29
+ var DEFAULT_MAX_SIZE = 1073741824;
30
+ var bytes = (value, name = "input") => {
31
+ if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Buffer or Uint8Array`);
32
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
33
+ };
34
+ var range = (buffer, offset, size, name = "binary data") => {
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
+ );
39
+ }
40
+ return buffer.subarray(offset, offset + size);
41
+ };
42
+ var align = (value, alignment = 512) => {
43
+ return Math.ceil(value / alignment) * alignment;
44
+ };
45
+ var BinaryWriter = class {
46
+ maxSize;
47
+ buffer;
48
+ position;
49
+ length;
50
+ constructor(initialSize = 65536, maxSize = DEFAULT_MAX_SIZE) {
51
+ if (!Number.isSafeInteger(maxSize) || maxSize < 1 || maxSize > 4294967295)
52
+ throw new RangeError("Invalid output size limit");
53
+ this.maxSize = maxSize;
54
+ this.buffer = Buffer.alloc(Math.min(initialSize, maxSize));
55
+ this.position = 0;
56
+ this.length = 0;
57
+ }
58
+ ensure(end) {
59
+ if (!Number.isSafeInteger(end) || end < 0 || end > this.maxSize)
60
+ throw new RangeError(`Output exceeds size limit (${this.maxSize} bytes)`);
61
+ if (end <= this.buffer.length) return;
62
+ const next = Buffer.alloc(Math.min(this.maxSize, Math.max(end, this.buffer.length * 2, 1024)));
63
+ this.buffer.copy(next, 0, 0, this.length);
64
+ this.buffer = next;
65
+ }
66
+ write(data) {
67
+ data = bytes(data);
68
+ if (!Number.isSafeInteger(this.position) || this.position < 0)
69
+ throw new RangeError("Invalid writer position");
70
+ this.ensure(this.position + data.length);
71
+ bytes(data).copy(this.buffer, this.position);
72
+ this.position += data.length;
73
+ this.length = Math.max(this.length, this.position);
74
+ }
75
+ fillTo(end, fill = 255) {
76
+ this.ensure(end);
77
+ if (end <= this.position) return;
78
+ this.buffer.fill(fill, this.position, end);
79
+ this.position = end;
80
+ this.length = Math.max(this.length, end);
81
+ }
82
+ pad(alignment, fill = 255) {
83
+ this.fillTo(align(this.position, alignment), fill);
84
+ }
85
+ toBuffer() {
86
+ return this.buffer.subarray(0, this.length);
87
+ }
88
+ };
89
+
90
+ // src/nitro/crc.ts
91
+ var table16 = new Uint16Array(256);
92
+ var table32 = new Uint32Array(256);
93
+ for (let i = 0; i < 256; i++) {
94
+ let a = i, b = i;
95
+ for (let bit = 0; bit < 8; bit++) {
96
+ a = a >>> 1 ^ (a & 1 ? 40961 : 0);
97
+ b = b >>> 1 ^ (b & 1 ? 3988292384 : 0);
98
+ }
99
+ table16[i] = a;
100
+ table32[i] = b;
101
+ }
102
+ var crc16 = (data, initial = 65535) => {
103
+ let crc = initial;
104
+ for (const byte of data) crc = crc >>> 8 ^ table16[(crc ^ byte) & 255];
105
+ return crc;
106
+ };
107
+ var crc32 = (data) => {
108
+ let crc = 4294967295;
109
+ for (const byte of data) crc = crc >>> 8 ^ table32[(crc ^ byte) & 255];
110
+ return (crc ^ 4294967295) >>> 0;
111
+ };
112
+
113
+ // src/nitro/crypto.ts
114
+ var KEY1 = Buffer.from(
115
+ "99D5205F5744F5B96E19A4D99E6A5A94D8AEF1EB4175E23A9382D03233EE31D5CC57619A3706A21B793972F555AEF6BE5F1B69FBE59DF1E9CE2CD9A15E3205E6FED3FECFD462040D8BF5ECB72B6079BB1295310D6E3FDA2B8884F0F13D127E254522F1BB24061A0611ADDF288B6481342BEB332999AAF2BD9C14959D9FF7F58C7297A1299DD15FCF664D071ADED34A4B85C9A7A31795053A3D490ABF0A898BA24A8249DD2790F10BE9EB1C6A83764505BA817061173F4BDEAECFAB3957F23A564811AD8A40E1453FFA9B0254CAA693FBEF4DFE6FA3D8879C08BAD5486A8D2DFD6E15F874BDBE528B18228A9EFB7437071B366C4A19BA4262B97991107B676596FE0223E8EE998C773E5C86644D6D7886A54F65E21EB2DF5A0AD07E0814B071ACBDDB831CB9D7A162CDC6637C5269C3E6BF75CE12445D2104FAFBD33C381163D4958541494609F2084311DC1F76C0156D1F3C6370EA87806CC3BD638BC2372137DCEE09232E376A4D7390F75030AC1C92041023914FD207AA683E4F9AC964606AC81421F3D62241124424CFE68A56DD0D534DE1851E8C525A9C1984C20357F16FE300BE58F64CEDD521649C1FBE55033C4ADCFFAAC9DAE05D5EBFE6DEF5D8B1F8FF36B3B9626795DB315F37ED4C70679990B518316C3D9999E442DAD3254213A0AED7706CB155CFC7D746D54361173D4428E93385D5D0A293AA25121FFBC50B46F597765645A6BE87B1946BE8B1FE3399AE1F3E6C39711D09009037E4103E7574FF8C833BB0F1B0F90105474295F1D6AC7E38E69E9574263FB4685018D04330B44C4BE368BFE54DB6958B0AA074253277CFA1F72CD871135AABEAC951E80DEEEFE9937E19A71E433881162CA148E373CC29216CD35DCEA0D9617143A01513B56492CF2A19DCADB7A59F8665F81A9FE7FBF7FDB8136C27DB6FDF351CF78D2C5B9B12AB386406CCDE31E84E751164E3FAEAEB3454C2AD3F34EB932C7D26369D56F35AE1F6B398634A9E3283E49A84607D902E130EEE934B36A285EC1638E8880602BFF0A03AEDD76A9A73E157CFF844B8DC2E2359D1DF9552719961A04BD57F6E78BAA9C530D34086329D320C9C37B7022FBA5498A9C41304C98DBEC8E75D97502E93D622590C27BC2292E0A7200F936F7F4C9FD3B5A62A0B7467497D1026CBD1C58671E78CA09CE95BB21AF601EE8C9E5E83F21ADBE6E5EA845976D27CF68DA5493648C21652BB83A374B9070C3BFF6128E161E9E4EF6E15AA4EBAE85D0596BB3256B0FB72520F0EC84225657689AFF2DE1027F0014B74A79707D5265454091F820A867D30390EB3269B0B57BB360631AFFD79FCD930102B0CB3E19BD77BDC5FEFD2F813454D4775BD46963C7E75F33EB567C59A3BB05B296BDE805BC81505B131B6CE49DDAD84B5AE60DC67313430FE4EBD802FA6BF63392186D9357F1668220554E990268C076C51A43155D70907A83E2E5366C1F8F27BC4F258CFF187C5A2E7278F308758A064622318B9887CFACEC498AEAD17CC4A5BF3E948D556D30DF2C892738CDBD72F56AC81F992694DC632F6E6C08D21E276806111BCDC6C93AF19699BD0BFB9319F0267A351EE8306227B0CAB494240B8D5017DCE5EF7555339C59946D8879FBAF764B4E39AFAA16D90681030CA8A54A79F60C319F56B0D7A5198E6984351B4D635E94FC3DF0F7BD62F5CBD3A156119F14BCBAADC6D64C9D3C61E56EF384C50718675CC0D0D4EE928F6065D701BAAD345CFA839AC95A62EB4E422D474A8375F487A04CCA54C40D828B428080D1C725241F07D47193A534E5884626B93B58A81214E0DDCB43FA2C6FCC92B40DA3804E95E5A866B0C22258568118D7C921D95554DAB8EBBDAA6E6B751B6325A0541DD052A0A5650911747CCC9E67EB5614ADB736751C833F5DA6E742E54C3370D6DAF08E8158A5FE25921CDA8DE0C065A776B5FDB18653EC850DE78E0B882B35D4E7232074FC13423BA96B7674EA4281E3462EB2D6A70E92F42C4704E5A319CF95B4728AADA716F381FB378C4926B1C9EF6359AB74D0EBFCC1829410348355D55D02BC629AF5C6074698E5E9B7CD4BD7B44647D3F925D69B61F004BD48335CF7E644E17AE8DD52E9A28124E2E2B49085CAEC64685AE41611E6F82D25137161F0BF659A49ACA5AAF0DD4338B2063F184805CCBCF08B4B9D31605BD6283319B5651989FBAB25BAAB2226B2CB5D448FA632B5F58FA61FA6409BB38E0B89D9260A80D676F0E37F50D019FC277D4FEECF1733039E07DF56198E42C2855045655DB2F6BECE55806B664806A2A1A4E5B0FD8C40A2E5219D962F53048BE8C7B4F389BA2C3AFC9D3C7C1624186B96121576F994FC1BACE7BB53B4D5E8A8B44575F135F706D5B2947DC38E2EC045565122AE81743E18EDD2AB3E294F7096E5CE6EB8AF86D89495448F52FADBFEA944BCAFC3987825F8A01F275F2E671D6D842DEF12D1D28A6887EA3A0471D30D9A371DF491CCB01F836B1F2F022585D456BBDA0BBB28842C78C28CE93E8906308907C893CF57DB7042D4F555116FD7E79E8BEC1F212D4F8B4840523A0CCD22BFDE1ABAD0DD1556C2341944D77374F05280CBF17B312676C8CC35AF741842A6DD09412272CB4ED9C4DEC478297D567B91B9DC055077EE58EE2A8E73E12E40E3A2A455534A2F92D5A1BAB527C83105F55D2F15A432BC6A7A4891595E8B44B9DF875E39F60785BD6E60D44E62106BD472253A400AD8D43138539F7AAFC38AF7BEDFCE42B5450984CFC8580F7DF3C8022E194DADE24C6B07A3938DC0FA1A7F4F96F6318578B84412A2ED453F2D9000FD0DD996E19A60AD0EC5B5824ABC0CB0665EC1A1338940A67032F3FF7E377447733C61439D0E3C0A20879BB409957410B0190CDE1CC4867DBB3AF8874F34C828F72B1B52329C4126C19FC8E46A49CC4256587D36DBE8A93110338ED832BF346A493EA3B53851DCED4F1088327EDFC9B1A18BCF98BAEDC24AB5038E9724B1022177B465DAB5964F340AEF8BBE5C8F926034E557DEBEBFEF739E6E00A11BE2E28FF98EDC0C9425642C3FD00F6AF87A25B013F329247959A72A5323DAE6BD09B07D24992E3784AFAA1067DF241CF77740414B20C86846416D5BB51A1E56FF1D1F2E2F75F58204DB857C7CFDDC5D8BE763DF65F7EE72A8B88241B383F0E412377F5F04BD40C1FFAA40B805FCF45F6E0DA2F345953FB203C52625E35B562FE8B6063E3865A151A6ED14745BC32B4EB6738ABE46E333AB5EDA3AD67E04E4195EE626271261D31EF6230AFD782ACC2DC0504F59707BF11592307C06402E897E53EAF18AC59A68B4A33901C6E7C9C207E4C3C3E6164BBC56B7C7E3E9FC54C9FEA73F5D789C04CF4FBF42DEC141B51D5C112C810DF0B4A8B9CBC93456A3E3E7DC1A9BACDC1B407E4E1688643B26D38F3FB0C5C663771DE56EF6EA0104065A798F7D0BE0EC83736EC10CA7C9CAB841E051776021C4F52AA5FC1C6A056B9D80484444DA759D8DE60E6380E058F03E13B6D8104336F300BCE69052133FB26BB897DB6AE877E5107E0ACF7960A6BF9C45C1DE44447B85EFAE3788455424B485EF77D4735861D2B430503EC8AB81E063C760C481A43A7B78AED1E13C643EE10EFDBECFB3C83B29544EFD854514E2D11441DFB36591E7A34C1C3CA570061EA67A5169B55D055E17FD936D24076AEDC01CEB07A83D5CB2098EC6BC1729234F3825737628A32360C9043AEAE5C9B788E136502FD6871C1FEB031A02482B0C3B17969A7F5D2EBD082C032DC9EC7263C6D8D98C1BB22D4D00F33EC3EB9CCE1DC6A4C7736141CF9BF819F285F71853229907548C4B34ACED8448F142FFD4057EFAA0875D946D1D66E32551FC318FE841FFC84D5FF715E1B48C386950E280827D33883717B4C8063549A56B0ACCF80CA3109EFFEF3BEAF247EA6FE533FC28D4A3368D122A666AD7BEADEB643B0A1259500A33F7546141144ECD795BC92F04FA916536297602A0F41F17124BEEE947F08CD6093B3855B07003FD80F28839AD1699FD1DA2EC39001A2B96B4E2A669DDAAEA6EA2AD3682F0C0C9CD28C4AEDE29E57659D0987A3B4C4325DC9D4322BB1E0711E644DE69071E31E40ED7DF3840EEDC87876AEC0712772BB05EA0264FBF3486BB542933FED9F1353D2F7FE2AEC1D4725DB3C9186C68EF011FD237436F7A4F59E7A7E535044D447CAD3EB386DE6D971947F4AC6694B11F452EA22FE8AB036678B59E8E6802AEB650413EEECDC9E5FB1EC056A59E69F5E596B89BFF71ACA44F95B6A718503E42962E0706F41C4CFB2B1CCE37EA607A887E77F8493DB524B6CEC7EDDD4244810699F046074E64818F3E42CB94F2E507ADFD454692B8BA7F3CEFF1FF33E26013917958489B0F04C4B82919FC44BAC9DA574AF1725C9CA32D3BC898A8489CC0DAE7CA2DB9C6A7891EEEA765D4E8760F5691567D402CFAF483607EABF6F662D068FC49AFEF9F6908775B8F7AD0F76105A3D59B02EB3C7352CCC70562BCBE33796C52F461B8A2246C788A726329861DF86228AF41C2F87A109AACCA9AED3BD00451C9A5487865287EFFF1E8FA18FC1895C351BDA2D3A2C16B2C2F156E278C16B6397C5568FC9327F2CAAAFA6A8AC20912288DEE4608BF94B42251AE37F9C2C19893A7E05D436CC6958C2C1328B2F9085EB7A3950A5A12792C566B0204F587E5583432B45E29CE4D812902C168356167903B3AD2D61181A131F37E2E19C737B80D5FD2D5187FC7BAAD71F2C7A8EAFF48DBBCD95117C720BEE6FE2B9AFDE3783DE8C8D620567B796C68D56B60DD762BAD64636BD8EC8E6EA2A6C1014FF6B5BFA823C46B1304346518A7D9B923E83795B555DB26C5ECE90628E5398C90D6DE52D57CDC58157BAE1E8B88F72E54F13DCEA9D711510B21188D509D47F5B657F2C3B384C1168508DFB9EB059BF9480894AC51A18128953D14A1029E88C1CECB6EA46C7178B251531A8A26B43B19DE2DB0B879BB011040E71D2297789820A66417F1D0B48FF72BB24FDC248A19BFE7B7FCE88DB86D9853B1CB0DCA83307BF512EE30E9A00971E06C097439DD8B645C486675F00F8889AA4529EC7AA8A8375ECC518AECEC32F1A2BF918FFAE1AF5530BB53351A7FDE8A8E1A264B622174380CC0AD8AE3BBA40D7D9924A89DF0410EE9B182B6A77698A68F4F9B9A221156EE61E3B0362309B60417E259B9E8FC5521008F8C269A1211188375E793566FF1042186EED97B66B1C4E36E56D7DB4E4BF20B9E0053A69D5B8E3D5DCE0B9AC533E07A457AD77FF4818762AAC492A8E47756D9F676330358C390539D56F643A5BADCA0BBB82529945B193363699AF13204436D8024409399285FF4A4A9787A663D7C7B5B524ED0FB46F0C585214D9A67BD379BC3858A1BD3B8406D81A06FD6BA8EA4B69280437AD8299FB0E1B85BDA85D73CDDC58750ABE636C48E74CE4302B0460B915D8DA8681758F96D48D1C5D70857C1C677BD50867A6CE4B0A6670B7E563D45B8A82EA1067CAE2F4EF17852F2A5F8A9782F86AD63410EAEBC95C3CE149F846EBDEBDF6A992F1AAA6A018B03AD30F1FF36FFF31454344D3509AF7880996C1CE76CCF22C2CBAAD82778F1884C0D2079C3690834E0BA54F433E04AB784FD6FB09012490DA6F3C3A610D7F694AEB2B3002B4DBE084A9ECD735BF377D8558CEA94EE480C7A8D3306748EB29AF2F746AB4A73F0F3F92AFF3CAACAF4BD994C043CA810D2F48A1B027D5D2EF4B0585A3DE4D93303CF0BB4A8F30274CEBE33E64ED9A2F3BF182F0BAF4CF7F40CBB0E17FBCAA57D3C974F2FA430D22D0F4774E93D785701F99BFB6DE35F130A75E71F06B012D7B64F033530A3988F36B3AA66B35D22F43CD02FDB5E9BC5BAAD8A4197E0E5D94819E6F77ADD60E749396E7C4185FADF519",
116
+ "hex"
117
+ );
118
+ 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");
121
+ const ctr = Buffer.from(counter).reverse();
122
+ const aes = createAesBlockCipher(Buffer.from(key2).reverse());
123
+ const output = Buffer.alloc(data.length);
124
+ for (let p = 0; p < data.length; p += 16) {
125
+ const mask = aes(ctr);
126
+ for (let j = 0; j < 16 && p + j < data.length; j++) output[p + j] = data[p + j] ^ mask[15 - j];
127
+ for (let j = 15; j >= 0; j--) {
128
+ ctr[j] = ctr[j] + 1 & 255;
129
+ if (ctr[j]) break;
130
+ }
131
+ }
132
+ return output;
133
+ };
134
+ var modcryptKey = (header) => {
135
+ if (typeof header.gameCode !== "string" || header.gameCode.length !== 4)
136
+ throw new Error("Expected four-character game code");
137
+ if (header.twlInternalFlags & 4 || (header.appflags?.[3] ?? 0) & 128) {
138
+ const key2 = Buffer.alloc(16);
139
+ key2.write(header.gameTitle, 0, 12, "ascii");
140
+ key2.write(header.gameCode, 12, 4, "ascii");
141
+ return key2;
142
+ }
143
+ if (!header.hmac_arm9i || header.hmac_arm9i.length < 16)
144
+ throw new Error("Missing ARM9i HMAC for modcrypt key");
145
+ const x = Buffer.from(
146
+ "Nintendo" + header.gameCode + [...header.gameCode].reverse().join(""),
147
+ "ascii"
148
+ );
149
+ for (let i = 0; i < 16; i++) x[i] ^= header.hmac_arm9i[i];
150
+ const mask = (1n << 128n) - 1n;
151
+ let v = x.readBigUInt64LE(0) | x.readBigUInt64LE(8) << 64n;
152
+ v = v + 0xfffefb4e295902582a680f5f1a4f3e79n & mask;
153
+ v = (v << 42n | v >> 86n) & mask;
154
+ const out = Buffer.alloc(16);
155
+ out.writeBigUInt64LE(v & (1n << 64n) - 1n);
156
+ out.writeBigUInt64LE(v >> 64n, 8);
157
+ return out;
158
+ };
159
+ var encryptSecureArea = (gameCode, input) => {
160
+ if (typeof gameCode !== "string" || gameCode.length !== 4)
161
+ throw new Error("Expected four-character game code");
162
+ if (input.length < 2048) throw new RangeError("Secure area must contain at least 0x800 bytes");
163
+ const output = Buffer.from(input);
164
+ let magic = new Uint32Array(1042);
165
+ let args = [0, 0, 0];
166
+ function encrypt(a2, b2) {
167
+ for (let i = 0; i < 16; i++) {
168
+ const c = (magic[i] ^ a2) >>> 0;
169
+ const sum = magic[18 + (c >>> 24)] + magic[274 + (c >>> 16 & 255)] >>> 0;
170
+ const f = magic[786 + (c & 255)] + (magic[530 + (c >>> 8 & 255)] ^ sum) >>> 0;
171
+ a2 = (b2 ^ f) >>> 0;
172
+ b2 = c;
173
+ }
174
+ return [(b2 ^ magic[17]) >>> 0, (a2 ^ magic[16]) >>> 0];
175
+ }
176
+ function init2() {
177
+ [args[2], args[1]] = encrypt(args[2], args[1]);
178
+ [args[1], args[0]] = encrypt(args[1], args[0]);
179
+ const key2 = Buffer.alloc(12);
180
+ args.forEach((v, i) => key2.writeUInt32LE(v, i * 4));
181
+ for (let j = 0; j < 18; j++) magic[j] ^= key2.readUInt32BE(j * 4 & 7);
182
+ let a2 = 0, b2 = 0;
183
+ for (let i = 0; i < 1042; i += 2) {
184
+ [a2, b2] = encrypt(a2, b2);
185
+ magic[i] = a2;
186
+ magic[i + 1] = b2;
187
+ }
188
+ }
189
+ function init1() {
190
+ magic = Uint32Array.from({ length: 1042 }, (_, i) => KEY1.readUInt32LE(i * 4));
191
+ const code = Buffer.from(gameCode, "ascii").readUInt32LE();
192
+ args = [code, code >>> 1, code << 1 >>> 0];
193
+ init2();
194
+ init2();
195
+ }
196
+ init1();
197
+ args[1] = args[1] << 1 >>> 0;
198
+ args[2] >>>= 1;
199
+ init2();
200
+ for (let p = 8; p < 2048; p += 8) {
201
+ const [b2, a2] = encrypt(output.readUInt32LE(p + 4), output.readUInt32LE(p));
202
+ output.writeUInt32LE(a2, p);
203
+ output.writeUInt32LE(b2, p + 4);
204
+ }
205
+ let [b, a] = encrypt(1784827769, 1919118949);
206
+ init1();
207
+ [b, a] = encrypt(b, a);
208
+ output.writeUInt32LE(a, 0);
209
+ output.writeUInt32LE(b, 4);
210
+ return output;
211
+ };
212
+
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
+ // src/nitro/filesystem.ts
407
+ var FileAllocationTable = class {
408
+ entries;
409
+ sortedIDs;
410
+ constructor(input, offset, size) {
411
+ const data = bytes(input);
412
+ const table = range(data, offset, size, "FAT");
413
+ if (size % 8 || size / 8 > 65535) throw new Error("Invalid FAT entry count");
414
+ this.entries = [];
415
+ for (let p = 0; p < size; p += 8) {
416
+ const start = table.readUInt32LE(p), end = table.readUInt32LE(p + 4);
417
+ range(data, start, end - start, `FAT entry ${p / 8}`);
418
+ this.entries.push({ id: p / 8, offset: start, size: end - start });
419
+ }
420
+ this.sortedIDs = [...this.entries].sort((a, b) => a.offset - b.offset || a.id - b.id).map((entry) => entry.id);
421
+ }
422
+ };
423
+ var FileNameTable = class _FileNameTable {
424
+ root;
425
+ constructor(fat, input, offset, size) {
426
+ const table = range(bytes(input), offset, size, "FNT");
427
+ range(table, 0, 8, "FNT root directory");
428
+ const count = table.readUInt16LE(6);
429
+ if (count < 1 || count > 4096) throw new Error("Invalid FNT directory count");
430
+ range(table, 0, count * 8, "FNT directory table");
431
+ const decoder = new TextDecoder("shift_jis");
432
+ const visited = /* @__PURE__ */ new Set();
433
+ const fileIds = /* @__PURE__ */ new Set();
434
+ const parse = (index, name, depth) => {
435
+ if (depth > 256 || index >= count || visited.has(index))
436
+ throw new Error("Invalid FNT directory tree or cycle");
437
+ visited.add(index);
438
+ let cursor = table.readUInt32LE(index * 8);
439
+ let fileId = table.readUInt16LE(index * 8 + 4);
440
+ if (cursor < count * 8) throw new Error("FNT directory names overlap directory table");
441
+ const folder = {
442
+ name,
443
+ id: index === 0 ? 65535 : 61440 + index,
444
+ files: [],
445
+ folders: []
446
+ };
447
+ const names = /* @__PURE__ */ new Set();
448
+ while (true) {
449
+ const tag = range(table, cursor++, 1, "FNT name")[0];
450
+ if (!tag) break;
451
+ const length = tag & 127;
452
+ if (!length) throw new Error("Empty FNT directory name");
453
+ const filename = decoder.decode(range(table, cursor, length, "FNT filename"));
454
+ cursor += length;
455
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("\0") || filename === "." || filename === "..")
456
+ throw new Error("Invalid FNT path component");
457
+ const normalized = filename.toLowerCase();
458
+ if (names.has(normalized)) throw new Error(`Duplicate FNT path: ${filename}`);
459
+ names.add(normalized);
460
+ if (tag & 128) {
461
+ const id = range(table, cursor, 2, "FNT directory id").readUInt16LE();
462
+ cursor += 2;
463
+ if (id < 61440) throw new Error("Invalid FNT directory id");
464
+ folder.folders.push(parse(id & 4095, filename, depth + 1));
465
+ } else {
466
+ const entry = fat.entries[fileId++];
467
+ if (!entry) throw new Error("FNT file id is outside FAT");
468
+ if (fileIds.has(entry.id)) throw new Error(`Duplicate FNT file id: ${entry.id}`);
469
+ fileIds.add(entry.id);
470
+ folder.files.push({ name: filename, ...entry });
471
+ }
472
+ }
473
+ return folder;
474
+ };
475
+ this.root = { name: "root", id: 65535, files: [], folders: [parse(0, "data", 0)] };
476
+ }
477
+ static findFile(id, folder) {
478
+ for (const file of folder.files) if (file.id === id) return file;
479
+ for (const subfolder of folder.folders) {
480
+ const file = _FileNameTable.findFile(id, subfolder);
481
+ if (file) return file;
482
+ }
483
+ return void 0;
484
+ }
485
+ };
486
+ var overlayFields = [
487
+ "overlayId",
488
+ "ramAddress",
489
+ "ramSize",
490
+ "bssSize",
491
+ "staticInitialiserStartAddress",
492
+ "staticInitialiserEndAddress",
493
+ "fileId",
494
+ "reserved"
495
+ ];
496
+ var OverlayTable = class {
497
+ constructor(input, offset, size, isArm9) {
498
+ this.isArm9 = isArm9;
499
+ if (!size) return;
500
+ const table = range(bytes(input), offset, size, "overlay table");
501
+ if (size % 32) throw new Error("Invalid overlay table size");
502
+ for (let p = 0; p < size; p += 32) {
503
+ if (table.readUInt32LE(p) === 4294967295) break;
504
+ this.entries.push({
505
+ overlayId: table.readUInt32LE(p),
506
+ ramAddress: table.readUInt32LE(p + 4),
507
+ ramSize: table.readUInt32LE(p + 8),
508
+ bssSize: table.readUInt32LE(p + 12),
509
+ staticInitialiserStartAddress: table.readUInt32LE(p + 16),
510
+ staticInitialiserEndAddress: table.readUInt32LE(p + 20),
511
+ fileId: table.readUInt32LE(p + 24),
512
+ reserved: table.readUInt32LE(p + 28)
513
+ });
514
+ }
515
+ }
516
+ isArm9;
517
+ entries = [];
518
+ readBasicOverlays(fat) {
519
+ return this.entries.map((item) => {
520
+ const file = fat.entries[item.fileId];
521
+ if (!file) throw new Error("Overlay file id is outside FAT");
522
+ return {
523
+ ...file,
524
+ name: `overlay${this.isArm9 ? "" : "7"}_${String(item.overlayId).padStart(4, "0")}.bin`
525
+ };
526
+ });
527
+ }
528
+ toBuffer() {
529
+ const data = Buffer.alloc(this.entries.length * 32);
530
+ this.entries.forEach(
531
+ (entry, i) => overlayFields.forEach((field2, j) => data.writeUInt32LE(entry[field2], i * 32 + j * 4))
532
+ );
533
+ return data;
534
+ }
535
+ };
536
+
537
+ // src/nitro/blz.ts
538
+ var decompressBLZ = (input, maxSize = 268435456) => {
539
+ if (input.length < 8) throw new Error("BLZ footer is truncated");
540
+ const diff = input.readUInt32LE(input.length - 4);
541
+ const packed = input.readUInt32LE(input.length - 8), header = packed >>> 24, length = packed & 16777215;
542
+ const size = input.length + diff, prefix = input.length - length;
543
+ if (header < 8 || header > length || length > input.length || size > maxSize)
544
+ throw new Error("Invalid BLZ size or footer");
545
+ const out = Buffer.alloc(size);
546
+ input.copy(out, 0, 0, prefix);
547
+ let src = input.length - header, dst = size;
548
+ while (src > prefix) {
549
+ const flags = input[--src];
550
+ for (let bit = 128; bit && src > prefix; bit >>= 1) {
551
+ if (!(flags & bit)) {
552
+ if (dst <= prefix) throw new Error("BLZ output overflow");
553
+ out[--dst] = input[--src];
554
+ } else {
555
+ if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
556
+ const high = input[--src], low = input[--src];
557
+ 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");
560
+ for (let j = 0; j < count; j++) {
561
+ --dst;
562
+ out[dst] = out[dst + offset];
563
+ }
564
+ }
565
+ }
566
+ }
567
+ if (dst !== prefix) throw new Error("BLZ decompressed size mismatch");
568
+ return out;
569
+ };
570
+ var compressBLZ = (input) => {
571
+ if (input.length > 16777215) throw new Error("BLZ input exceeds 24-bit format limit");
572
+ const temp = Buffer.alloc(input.length);
573
+ let src = input.length, dst = input.length;
574
+ const heads = new Int32Array(256).fill(-1);
575
+ const next = new Int32Array(input.length);
576
+ let indexedFrom = input.length;
577
+ while (src > 0) {
578
+ if (dst < 1) return Buffer.alloc(0);
579
+ const flagPos = --dst;
580
+ let flags = 0;
581
+ for (let bit = 0; bit < 8; bit++) {
582
+ flags <<= 1;
583
+ if (!src) continue;
584
+ while (indexedFrom > src) {
585
+ const p = --indexedFrom;
586
+ next[p] = heads[input[p]];
587
+ heads[input[p]] = p;
588
+ }
589
+ const maxCount = Math.min(src, 18);
590
+ let best = 0, offset = 0;
591
+ for (let p = heads[input[src - 1]]; p >= 0; p = next[p]) {
592
+ const d = p - src + 1;
593
+ if (d > 4098) break;
594
+ let n = 0;
595
+ const limit = Math.min(d, maxCount);
596
+ while (n < limit && input[src - 1 - n] === input[src + d - 1 - n]) n++;
597
+ if (n > best) {
598
+ best = n;
599
+ offset = d;
600
+ if (best === maxCount) break;
601
+ }
602
+ }
603
+ if (best < 3) {
604
+ if (dst < 1) return Buffer.alloc(0);
605
+ temp[--dst] = input[--src];
606
+ } else {
607
+ if (dst < 2) return Buffer.alloc(0);
608
+ src -= best;
609
+ dst -= 2;
610
+ temp[dst] = offset - 3 & 255;
611
+ temp[dst + 1] = best - 3 << 4 | offset - 3 >>> 8;
612
+ flags |= 1;
613
+ }
614
+ }
615
+ temp[flagPos] = flags;
616
+ }
617
+ let packed = temp.subarray(dst), sourcePos = input.length, compressedPos = packed.length, prefix = 0, skip = 0;
618
+ outer: while (sourcePos > 0) {
619
+ const flags = packed[--compressedPos];
620
+ for (let bit = 128; bit && sourcePos > 0; bit >>= 1) {
621
+ if (!(flags & bit)) {
622
+ compressedPos--;
623
+ sourcePos--;
624
+ } else {
625
+ sourcePos -= (packed[compressedPos - 1] >>> 4) + 3;
626
+ compressedPos -= 2;
627
+ if (sourcePos < compressedPos) {
628
+ prefix = sourcePos;
629
+ skip = compressedPos;
630
+ break outer;
631
+ }
632
+ }
633
+ }
634
+ }
635
+ packed = Buffer.concat([input.subarray(0, prefix), packed.subarray(skip)]);
636
+ const aligned = Math.ceil(packed.length / 4) * 4, finalSize = aligned + 8;
637
+ if (finalSize >= input.length) return Buffer.alloc(0);
638
+ const out = Buffer.alloc(finalSize, 255);
639
+ packed.copy(out);
640
+ const header = finalSize - packed.length, length = packed.length - prefix + header;
641
+ out.writeUInt32LE((header << 24 | length) >>> 0, finalSize - 8);
642
+ out.writeUInt32LE(input.length - finalSize, finalSize - 4);
643
+ return out;
644
+ };
645
+ var decompressArm9 = (data, header) => {
646
+ if (data.length < 24) throw new Error("ARM9 data too short");
647
+ const nitro = data.readUInt32LE(12) === 3737126433 ? 12 : 0;
648
+ const init = header.reserved2.readUInt32LE(0) & 16383;
649
+ const ptr = init ? data.readUInt32LE(init + 20) : header.ARM9ramAddress + header.ARM9size;
650
+ const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
651
+ return {
652
+ compressed,
653
+ data: compressed ? Buffer.concat([
654
+ decompressBLZ(data.subarray(0, data.length - nitro)),
655
+ data.subarray(data.length - nitro)
656
+ ]) : Buffer.from(data)
657
+ };
658
+ };
659
+ var compressArm9 = (data, header, postSize = 0) => {
660
+ if (data.length < 16384) throw new Error("ARM9 secure area is truncated");
661
+ const nitro = data.readUInt32LE(12) === 3737126433 ? 12 : 0;
662
+ const packed = compressBLZ(data.subarray(16384, data.length - nitro));
663
+ if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
664
+ const out = Buffer.concat([data.subarray(0, 16384), packed, data.subarray(data.length - nitro)]);
665
+ const init = header.reserved2.readUInt32LE(0) & 16383;
666
+ if (init) out.writeUInt32LE(out.length - postSize + header.ARM9ramAddress, init + 20);
667
+ return out;
668
+ };
669
+
670
+ // src/nitro/twl.ts
671
+ var HMAC_SHA1_KEY = Buffer.from(
672
+ "2106c0deba98ce3fa692e39d46f2ed0176e3cc08562363facad4ecdf9a6278348f6d633cfe22ca9220889723d2cfaec232678dfeca836498acfd3e3787465824",
673
+ "hex"
674
+ );
675
+ var hash = (data) => hmacSha1(HMAC_SHA1_KEY, data);
676
+ function slice(data, start, size, label) {
677
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || start < 0 || size < 0 || start + size > data.length)
678
+ throw new RangeError(`${label} outside data range`);
679
+ return data.subarray(start, start + size);
680
+ }
681
+ function validate(h) {
682
+ for (const field2 of [
683
+ "digest_ntr_start",
684
+ "digest_ntr_size",
685
+ "digest_twl_start",
686
+ "digest_twl_size",
687
+ "sector_hashtable_start",
688
+ "sector_hashtable_size",
689
+ "block_hashtable_start",
690
+ "block_hashtable_size",
691
+ "dsi9_rom_offset",
692
+ "dsi9_size",
693
+ "dsi7_rom_offset",
694
+ "dsi7_size",
695
+ "modcrypt1_start",
696
+ "modcrypt1_size",
697
+ "modcrypt2_start",
698
+ "modcrypt2_size",
699
+ "total_rom_size"
700
+ ]) {
701
+ if (!Number.isSafeInteger(h[field2]) || h[field2] < 0 || h[field2] > 4294967295) {
702
+ throw new Error(`Invalid TWL ${field2}`);
703
+ }
704
+ }
705
+ 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");
708
+ if (!Number.isSafeInteger(h.digest_block_sectorcount) || h.digest_block_sectorcount <= 0 || h.digest_block_sectorcount > 1048576)
709
+ throw new Error("Invalid TWL digest block sector count");
710
+ if (h.digest_ntr_size < 16384 || h.digest_ntr_size % s || h.digest_twl_size % s)
711
+ throw new Error("Invalid TWL digest region size");
712
+ const hashes = (h.digest_ntr_size + h.digest_twl_size) / s;
713
+ if (hashes * 20 > h.sector_hashtable_size || Math.ceil(h.sector_hashtable_size / (h.digest_block_sectorcount * 20)) * 20 > h.block_hashtable_size)
714
+ throw new Error("TWL hash tables are too small");
715
+ }
716
+ var TWL = class {
717
+ firstDSiHashOffset;
718
+ dsiHashSize;
719
+ Hashtable1Data;
720
+ Hashtable2Data;
721
+ Header2Data;
722
+ Overlays9Sha1Hmac;
723
+ twlEncrypted;
724
+ DSi9Data;
725
+ DSi7Data;
726
+ constructor(header, overlays, original) {
727
+ validate(header);
728
+ const h = header;
729
+ this.firstDSiHashOffset = h.digest_ntr_size / h.digest_sector_size * 20;
730
+ this.dsiHashSize = h.digest_twl_size / h.digest_sector_size * 20;
731
+ this.Hashtable1Data = Buffer.from(
732
+ slice(original, h.sector_hashtable_start, h.sector_hashtable_size, "Sector hash table")
733
+ );
734
+ this.Hashtable2Data = Buffer.from(
735
+ slice(original, h.block_hashtable_start, h.block_hashtable_size, "Block hash table")
736
+ );
737
+ this.DSi9Data = Buffer.from(
738
+ slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), "ARM9i")
739
+ );
740
+ this.DSi7Data = Buffer.from(
741
+ slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), "ARM7i")
742
+ );
743
+ this.Header2Data = h.trimmedRom ? null : Array.from(
744
+ { 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"))
751
+ );
752
+ let encrypted = false;
753
+ const modcryptStart = [
754
+ { start: h.modcrypt1_start, size: h.modcrypt1_size },
755
+ { start: h.modcrypt2_start, size: h.modcrypt2_size }
756
+ ].find(
757
+ ({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 4294967295
758
+ )?.start;
759
+ if (modcryptStart !== void 0) {
760
+ const index = Math.floor((modcryptStart - h.digest_twl_start) / h.digest_sector_size);
761
+ 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
+ )
768
+ );
769
+ encrypted = !actual.equals(
770
+ slice(
771
+ this.Hashtable1Data,
772
+ this.firstDSiHashOffset + index * 20,
773
+ 20,
774
+ "Modcrypt sector hash"
775
+ )
776
+ );
777
+ }
778
+ if (encrypted) this.cryptSections(h);
779
+ this.twlEncrypted = !!(h.twlInternalFlags & 2);
780
+ }
781
+ cryptSections(h, writer) {
782
+ const key2 = modcryptKey(h);
783
+ for (const [id, data, start, counter] of [
784
+ [1, this.DSi9Data, h.dsi9_rom_offset, h.hmac_arm9],
785
+ [2, this.DSi7Data, h.dsi7_rom_offset, h.hmac_arm7]
786
+ ]) {
787
+ const size = h[`modcrypt${id}_size`];
788
+ if (!size) continue;
789
+ const off = h[`modcrypt${id}_start`] - start;
790
+ const result = aes128CtrCrypt(
791
+ key2,
792
+ slice(counter, 0, 16, "Modcrypt counter"),
793
+ slice(data, off, size, "Modcrypt payload")
794
+ );
795
+ if (writer) {
796
+ writer.position = start + off;
797
+ writer.write(result);
798
+ } else result.copy(data, off);
799
+ }
800
+ }
801
+ importArm9iData(data, offset = 0, size) {
802
+ this.DSi9Data = this.importData(data, offset, size);
803
+ }
804
+ importArm7iData(data, offset = 0, size) {
805
+ this.DSi7Data = this.importData(data, offset, size);
806
+ }
807
+ importData(data, offset, size) {
808
+ if (typeof data === "string") data = readFileSync(data);
809
+ size ??= data.length - offset;
810
+ const src = slice(data, offset, size, "DSi import");
811
+ const out = Buffer.alloc(Math.ceil(size / 16) * 16, 255);
812
+ src.copy(out);
813
+ return out;
814
+ }
815
+ updateOverlays9Sha1Hmac(arm9Data, header, overlays) {
816
+ if (overlays.length !== this.Overlays9Sha1Hmac.length)
817
+ throw new Error("Changing ARM9 overlay count is unsupported");
818
+ const hashes = overlays.map(hash);
819
+ if (hashes.every((h, i) => h.equals(this.Overlays9Sha1Hmac[i]))) return Buffer.from(arm9Data);
820
+ const init = header.reserved2.readUInt32LE(0) & 16383;
821
+ const hdrptr = arm9Data.readUInt32LE(init + 20) - header.ARM9ramAddress;
822
+ const unpacked = decompressArm9(arm9Data, header);
823
+ const end = unpacked.data.readUInt32LE(init + 8) - header.ARM9ramAddress;
824
+ let found = -1;
825
+ for (let p = Math.min(end - hashes.length * 20, unpacked.data.length - hashes.length * 20); p >= 0; p--) {
826
+ if (unpacked.data.subarray(p, p + 20).equals(this.Overlays9Sha1Hmac[0])) {
827
+ found = p;
828
+ break;
829
+ }
830
+ }
831
+ if (found < 0)
832
+ throw new Error("ARM9 overlay hashes changed but original hash table was not found");
833
+ hashes.forEach((h, i) => h.copy(unpacked.data, found + i * 20));
834
+ return unpacked.compressed ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr) : unpacked.data;
835
+ }
836
+ writeTo(writer, h) {
837
+ validate(h);
838
+ if (writer.position > h.sector_hashtable_start) {
839
+ throw new Error("NTR output overlaps the fixed DSi sector hash table");
840
+ }
841
+ 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) {
842
+ throw new Error("DSi sections overlap or exceed the ROM size");
843
+ }
844
+ const fill = (end) => {
845
+ if (writer.position < end) writer.fillTo(end, 255);
846
+ };
847
+ fill(h.sector_hashtable_start);
848
+ writer.position = h.sector_hashtable_start + h.sector_hashtable_size;
849
+ fill(h.block_hashtable_start);
850
+ writer.position = h.block_hashtable_start + h.block_hashtable_size;
851
+ fill(h.dsi9_rom_offset);
852
+ if (this.Header2Data && !h.trimmedRom) {
853
+ writer.position = h.digest_twl_start - 12288;
854
+ this.Header2Data.forEach((b) => writer.write(b));
855
+ }
856
+ writer.position = h.dsi9_rom_offset;
857
+ writer.write(this.DSi9Data);
858
+ fill(h.dsi7_rom_offset);
859
+ writer.position = h.dsi7_rom_offset;
860
+ writer.write(this.DSi7Data);
861
+ fill(h.total_rom_size);
862
+ const finalPosition = writer.position;
863
+ const read = (p, n) => slice(writer.buffer.subarray(0, writer.length), p, n, "TWL output digest");
864
+ let secure = Buffer.from(read(h.digest_ntr_start, 16384));
865
+ if (h.decrypted) secure = encryptSecureArea(h.gameCode, secure);
866
+ let index = 0;
867
+ const put = (b) => {
868
+ writer.position = h.sector_hashtable_start + index++ * 20;
869
+ writer.write(hash(b));
870
+ };
871
+ for (let p = 0; p < 16384; p += h.digest_sector_size)
872
+ put(secure.subarray(p, p + h.digest_sector_size));
873
+ for (let p = h.digest_ntr_start + 16384; p < h.digest_ntr_start + h.digest_ntr_size; p += h.digest_sector_size)
874
+ put(read(p, h.digest_sector_size));
875
+ for (let p = h.digest_twl_start; p < h.digest_twl_start + h.digest_twl_size; p += h.digest_sector_size)
876
+ put(read(p, h.digest_sector_size));
877
+ index = 0;
878
+ for (let p = h.sector_hashtable_start; p < h.sector_hashtable_start + h.sector_hashtable_size; p += h.digest_block_sectorcount * 20) {
879
+ const digest = hash(read(p, h.digest_block_sectorcount * 20));
880
+ writer.position = h.block_hashtable_start + index++ * 20;
881
+ writer.write(digest);
882
+ }
883
+ h.hmac_digest_master = hash(read(h.block_hashtable_start, h.block_hashtable_size));
884
+ if (this.twlEncrypted) this.cryptSections(h, writer);
885
+ writer.position = finalPosition;
886
+ return h.hmac_digest_master;
887
+ }
888
+ };
889
+ var updateHeaderSignatures = (writer, header, originalHeader, keepOriginal) => {
890
+ const signedData = Buffer.from(slice(originalHeader, 0, 3584, "Signed header"));
891
+ const digest = slice(header.hmac_digest_master, 0, 20, "Master digest");
892
+ const signature = slice(header.rsa_signature, 0, 128, "RSA signature");
893
+ digest.copy(signedData, 808);
894
+ const valid = verifyRsaSha1(signedData, signature);
895
+ const position = writer.position;
896
+ writer.position = 808;
897
+ writer.write(digest);
898
+ if (!valid) {
899
+ if (!keepOriginal) {
900
+ const mask = Buffer.alloc(128, 255);
901
+ mask[0] = 0;
902
+ mask[1] = 1;
903
+ mask[107] = 0;
904
+ hashDigest("sha1", signedData).copy(mask, 108);
905
+ header.rsa_signature = mask;
906
+ }
907
+ writer.position = 3968;
908
+ writer.write(header.rsa_signature);
909
+ }
910
+ writer.position = position;
911
+ };
912
+
913
+ // src/nitro/nds-file.ts
914
+ var key = (path) => path.replaceAll("\\", "/").toLowerCase();
915
+ var NDSFile = class _NDSFile {
916
+ original;
917
+ header;
918
+ banner;
919
+ fatTable;
920
+ fntTable;
921
+ overlay9Table;
922
+ overlay7Table;
923
+ root;
924
+ twl;
925
+ warnings = [];
926
+ paths = /* @__PURE__ */ new Map();
927
+ maxOutputSize;
928
+ constructor(input, options = {}) {
929
+ this.original = bytes(input, "ROM");
930
+ this.maxOutputSize = options.maxOutputSize ?? DEFAULT_MAX_SIZE;
931
+ this.header = new Header(this.original);
932
+ const h = this.header;
933
+ range(this.original, h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0), "ARM9");
934
+ range(this.original, h.ARM7romOffset, h.ARM7size, "ARM7");
935
+ this.banner = new Banner(this.original, h.bannerOffset, h.banner_size);
936
+ this.fatTable = new FileAllocationTable(this.original, h.FAToffset, h.FATsize);
937
+ this.fntTable = new FileNameTable(this.fatTable, this.original, h.FNToffset, h.FNTsize);
938
+ this.root = this.fntTable.root;
939
+ this.overlay9Table = new OverlayTable(
940
+ this.original,
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
+ );
951
+ const arm9Overlays = this.overlay9Table.readBasicOverlays(this.fatTable);
952
+ this.root.folders.push({
953
+ name: "overlay",
954
+ id: 65535,
955
+ files: [...arm9Overlays, ...this.overlay7Table.readBasicOverlays(this.fatTable)],
956
+ folders: []
957
+ });
958
+ const system = (name, offset, size) => {
959
+ range(this.original, offset, size, name);
960
+ this.root.files.push({ name, id: 65535, offset, size });
961
+ };
962
+ system("header.bin", 0, h.headerSize);
963
+ system("banner.bin", h.bannerOffset, this.banner.raw.length);
964
+ system("fnt.bin", h.FNToffset, h.FNTsize);
965
+ system("fat.bin", h.FAToffset, h.FATsize);
966
+ system("arm9.bin", h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0));
967
+ system("arm7.bin", h.ARM7romOffset, h.ARM7size);
968
+ if (h.ARM9overlaySize) system("overarm9.bin", h.ARM9overlayOffset, h.ARM9overlaySize);
969
+ if (h.ARM7overlaySize) system("overarm7.bin", h.ARM7overlayOffset, h.ARM7overlaySize);
970
+ const visit = (folder, prefix) => {
971
+ for (const file of folder.files) {
972
+ const path = prefix + file.name;
973
+ if (this.paths.has(key(path))) throw new Error(`Duplicate ROM path: ${path}`);
974
+ this.paths.set(key(path), { path, file });
975
+ }
976
+ for (const child of folder.folders) visit(child, prefix + child.name + "/");
977
+ };
978
+ visit(this.root, "");
979
+ if (h.unitCode & 2 && h.twlInternalFlags & 1 && h.tid_high && h.tid_high !== 4294967295) {
980
+ this.twl = new TWL(h, arm9Overlays, this.original);
981
+ }
982
+ }
983
+ static async fromFile(path, options = {}) {
984
+ return new _NDSFile(await readFile(path), options);
985
+ }
986
+ get data() {
987
+ return this.root.folders[0];
988
+ }
989
+ get overlay() {
990
+ return this.root.folders[1];
991
+ }
992
+ listFiles() {
993
+ return [...this.paths.values()].map(({ path, file }) => ({
994
+ path,
995
+ id: file.id,
996
+ offset: file.offset,
997
+ size: file.replacement?.length ?? file.size
998
+ }));
999
+ }
1000
+ hasFile(path) {
1001
+ return this.paths.has(key(path));
1002
+ }
1003
+ getFile(path) {
1004
+ const entry = this.paths.get(key(path));
1005
+ if (!entry) throw new Error(`ROM file not found: ${path}`);
1006
+ return Buffer.from(this.fileBytes(entry.file));
1007
+ }
1008
+ /** Replace an existing file. Adding or deleting filesystem entries is not supported. */
1009
+ replaceFile(path, content) {
1010
+ const entry = this.paths.get(key(path));
1011
+ if (!entry) throw new Error(`ROM file not found: ${path}`);
1012
+ entry.file.replacement = Buffer.from(bytes(content));
1013
+ }
1014
+ fileBytes(file) {
1015
+ return file.replacement ?? range(this.original, file.offset, file.size, file.name);
1016
+ }
1017
+ /** Rebuild offsets, allocation table, CRCs and ROM padding. Safe to call repeatedly. */
1018
+ toBuffer() {
1019
+ const fntReplacement = this.root.files.find((file) => file.name === "fnt.bin")?.replacement;
1020
+ if (fntReplacement) {
1021
+ const replacementTree = new FileNameTable(
1022
+ this.fatTable,
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
+ );
1030
+ if (collect(replacementTree.root).join(",") !== collect(this.data).join(","))
1031
+ throw new Error("Replacement FNT must preserve existing file IDs");
1032
+ }
1033
+ const h = new Header(this.header.toBuffer());
1034
+ h.nitrocode = this.header.nitrocode;
1035
+ h.decrypted = this.header.decrypted;
1036
+ h.trimmedRom = this.header.trimmedRom;
1037
+ h.dlp_signature = this.header.dlp_signature;
1038
+ const writer = new BinaryWriter(65536, this.maxOutputSize);
1039
+ const system = (name) => {
1040
+ const file = this.root.files.find((file2) => file2.name === name);
1041
+ if (!file) throw new Error(`Missing ROM system file: ${name}`);
1042
+ return file;
1043
+ };
1044
+ const replacedHeader = system("header.bin").replacement;
1045
+ if (replacedHeader) {
1046
+ const replacement = new Header(replacedHeader);
1047
+ h.gameTitle = replacement.gameTitle;
1048
+ h.gameCode = replacement.gameCode;
1049
+ }
1050
+ const placements = /* @__PURE__ */ new Map();
1051
+ const write = (file, padding = true) => {
1052
+ const start = writer.position;
1053
+ writer.write(this.fileBytes(file));
1054
+ if (file.id !== 65535) placements.set(file.id, { start, end: writer.position });
1055
+ if (padding) writer.pad(512);
1056
+ };
1057
+ const overlayIds = /* @__PURE__ */ new Set();
1058
+ const writeOverlays = (arm92) => {
1059
+ const originalTable = arm92 ? this.overlay9Table : this.overlay7Table;
1060
+ const name = arm92 ? "overarm9.bin" : "overarm7.bin";
1061
+ const entry = this.root.files.find((file) => file.name === name);
1062
+ if (!entry) return;
1063
+ const raw = entry.replacement ?? originalTable.toBuffer();
1064
+ const table = new OverlayTable(raw, 0, raw.length, arm92);
1065
+ const files = this.overlay.files.filter((file) => file.name.startsWith(arm92 ? "overlay_" : "overlay7_")).sort((a, b) => a.id - b.id);
1066
+ for (const file of files) {
1067
+ const item = table.entries.find((item2) => item2.fileId === file.id);
1068
+ if (!item) throw new Error(`Replacement overlay table is missing file id ${file.id}`);
1069
+ const size = this.fileBytes(file).length;
1070
+ if (item.reserved >>> 24 & 1 || !arm92 && item.reserved > 0)
1071
+ item.reserved = (item.reserved & 4278190080 | size & 16777215) >>> 0;
1072
+ overlayIds.add(file.id);
1073
+ }
1074
+ if (table.entries.length !== files.length)
1075
+ throw new Error("Changing overlay file count is unsupported");
1076
+ const encoded = table.toBuffer();
1077
+ if (arm92) {
1078
+ h.ARM9overlayOffset = writer.position;
1079
+ h.ARM9overlaySize = encoded.length;
1080
+ } else {
1081
+ h.ARM7overlayOffset = writer.position;
1082
+ h.ARM7overlaySize = encoded.length;
1083
+ }
1084
+ writer.write(encoded);
1085
+ writer.pad(512);
1086
+ for (const file of files) write(file);
1087
+ };
1088
+ writer.position = h.headerSize;
1089
+ h.ARM9romOffset = writer.position;
1090
+ const arm9 = system("arm9.bin");
1091
+ const arm9Bytes = this.fileBytes(arm9);
1092
+ if (arm9.replacement) {
1093
+ h.nitrocode = arm9Bytes.length >= 12 && arm9Bytes.readUInt32LE(arm9Bytes.length - 12) === 3737126433;
1094
+ }
1095
+ h.ARM9size = arm9Bytes.length - (h.nitrocode ? 12 : 0);
1096
+ write(arm9);
1097
+ h.ARM9overlayOffset = 0;
1098
+ writeOverlays(true);
1099
+ h.ARM7romOffset = writer.position;
1100
+ h.ARM7size = this.fileBytes(system("arm7.bin")).length;
1101
+ write(system("arm7.bin"));
1102
+ h.ARM7overlayOffset = 0;
1103
+ writeOverlays(false);
1104
+ h.FNToffset = writer.position;
1105
+ h.FNTsize = this.fileBytes(system("fnt.bin")).length;
1106
+ write(system("fnt.bin"));
1107
+ h.FAToffset = writer.position;
1108
+ h.FATsize = this.fatTable.entries.length * 8;
1109
+ writer.write(Buffer.alloc(h.FATsize));
1110
+ writer.pad(512);
1111
+ h.bannerOffset = writer.position;
1112
+ const bannerFile = system("banner.bin");
1113
+ const banner = bannerFile.replacement ? new Banner(bannerFile.replacement, 0, bannerFile.replacement.length) : this.banner;
1114
+ const bannerBytes = banner.toBuffer();
1115
+ if (h.unitCode & 2 && h.headerSize > 512) h.banner_size = bannerBytes.length;
1116
+ writer.write(bannerBytes);
1117
+ writer.pad(512);
1118
+ for (const [index, id] of this.fatTable.sortedIDs.entries()) {
1119
+ if (overlayIds.has(id)) continue;
1120
+ const file = FileNameTable.findFile(id, this.root);
1121
+ if (file) write(file, index < this.fatTable.sortedIDs.length - 1);
1122
+ else {
1123
+ const entry = this.fatTable.entries[id];
1124
+ if (entry.size > 0) {
1125
+ write({ ...entry, name: `FAT entry ${id}` }, index < this.fatTable.sortedIDs.length - 1);
1126
+ }
1127
+ }
1128
+ }
1129
+ const logicalEnd = writer.position;
1130
+ const fat = Buffer.alloc(h.FATsize);
1131
+ for (const [id, location] of placements) {
1132
+ fat.writeUInt32LE(location.start, id * 8);
1133
+ fat.writeUInt32LE(location.end, id * 8 + 4);
1134
+ }
1135
+ writer.position = h.FAToffset;
1136
+ writer.write(fat);
1137
+ writer.position = logicalEnd;
1138
+ h.decrypted = writer.length >= 16392 && writer.buffer.readBigUInt64LE(16384) === 0xe7ffdeffe7ffdeffn;
1139
+ if (this.twl) this.twl.writeTo(writer, h);
1140
+ h.ROMsize = writer.position;
1141
+ h.size = 2 ** Math.ceil(Math.log2(Math.max(h.ROMsize + h.dlp_signature.length, 131072)));
1142
+ const secure = writer.toBuffer().subarray(16384, 32768);
1143
+ h.secureCRC16 = crc16(h.decrypted ? encryptSecureArea(h.gameCode, secure) : secure);
1144
+ writer.position = 0;
1145
+ writer.write(h.toBuffer());
1146
+ writer.position = h.ROMsize;
1147
+ writer.write(h.dlp_signature);
1148
+ writer.fillTo(h.size);
1149
+ return writer.toBuffer();
1150
+ }
1151
+ async saveAs(path) {
1152
+ await writeFile(path, this.toBuffer());
1153
+ }
1154
+ };
1155
+
1156
+ export {
1157
+ hashDigest,
1158
+ readFile,
1159
+ inflateRawSync,
1160
+ bytes,
1161
+ BinaryWriter,
1162
+ crc16,
1163
+ crc32,
1164
+ aes128CtrCrypt,
1165
+ modcryptKey,
1166
+ encryptSecureArea,
1167
+ UnitCode,
1168
+ Header,
1169
+ Banner,
1170
+ FileAllocationTable,
1171
+ FileNameTable,
1172
+ OverlayTable,
1173
+ decompressBLZ,
1174
+ compressBLZ,
1175
+ decompressArm9,
1176
+ compressArm9,
1177
+ TWL,
1178
+ updateHeaderSignatures,
1179
+ NDSFile
1180
+ };
1181
+ //# sourceMappingURL=chunk-GFF67HX4.js.map