nitro-patcher 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,16 @@
1
1
  // Port of NitroHelper/Banner.cs. Copyright Xzonn and upstream contributors, GPL-3.0.
2
- import { bytes, range } from './binary';
3
- import { crc16 } from './crc';
2
+ import { bytes, range } from "./binary";
3
+ import { crc16 } from "./crc";
4
4
 
5
5
  const titles = [
6
- 'japaneseTitle',
7
- 'englishTitle',
8
- 'frenchTitle',
9
- 'germanTitle',
10
- 'italianTitle',
11
- 'spanishTitle',
12
- 'chineseTitle',
13
- 'koreanTitle',
6
+ "japaneseTitle",
7
+ "englishTitle",
8
+ "frenchTitle",
9
+ "germanTitle",
10
+ "italianTitle",
11
+ "spanishTitle",
12
+ "chineseTitle",
13
+ "koreanTitle",
14
14
  ] as const;
15
15
  export class Banner {
16
16
  version: number;
@@ -18,21 +18,20 @@ export class Banner {
18
18
  bannerCRC: boolean;
19
19
  tileData: Buffer;
20
20
  palette: Buffer;
21
- japaneseTitle = '';
22
- englishTitle = '';
23
- frenchTitle = '';
24
- germanTitle = '';
25
- italianTitle = '';
26
- spanishTitle = '';
27
- chineseTitle = '';
28
- koreanTitle = '';
21
+ japaneseTitle = "";
22
+ englishTitle = "";
23
+ frenchTitle = "";
24
+ germanTitle = "";
25
+ italianTitle = "";
26
+ spanishTitle = "";
27
+ chineseTitle = "";
28
+ koreanTitle = "";
29
29
  constructor(input: Uint8Array, offset = 0, hardSize = 0) {
30
- const data = bytes(input, 'banner');
31
- this.version = range(data, offset, 2, 'banner').readUInt16LE();
30
+ const data = bytes(input, "banner");
31
+ this.version = range(data, offset, 2, "banner").readUInt16LE();
32
32
  let size = this.getDefSize(hardSize);
33
- if (this.version >>> 8 === 1)
34
- size = hardSize > 0 && hardSize !== 0xffffffff ? Math.min(hardSize, 0x23c0) : 0x23c0;
35
- this.raw = Buffer.from(range(data, offset, size, 'banner'));
33
+ if (this.version >>> 8 === 1) size = hardSize > 0 && hardSize !== 0xffffffff ? Math.min(hardSize, 0x23c0) : 0x23c0;
34
+ this.raw = Buffer.from(range(data, offset, size, "banner"));
36
35
  this.bannerCRC = crc16(this.raw.subarray(0x20, 0x840)) === this.raw.readUInt16LE(2);
37
36
  this.tileData = Buffer.from(this.raw.subarray(0x20, 0x220));
38
37
  this.palette = Buffer.from(this.raw.subarray(0x220, 0x240));
@@ -41,9 +40,9 @@ export class Banner {
41
40
  i < this.titleCount
42
41
  ? this.raw
43
42
  .subarray(0x240 + i * 0x100, 0x340 + i * 0x100)
44
- .toString('utf16le')
45
- .split('\0')[0]
46
- : '';
43
+ .toString("utf16le")
44
+ .split("\0")[0]
45
+ : "";
47
46
  }
48
47
  get titleCount() {
49
48
  return this.version >= 3 ? 8 : this.version >= 2 ? 7 : 6;
@@ -57,14 +56,12 @@ export class Banner {
57
56
  toBuffer() {
58
57
  const output = Buffer.from(this.raw);
59
58
  output.writeUInt16LE(this.version);
60
- if (this.tileData.length !== 512 || this.palette.length !== 32)
61
- throw new RangeError('Invalid banner icon size');
59
+ if (this.tileData.length !== 512 || this.palette.length !== 32) throw new RangeError("Invalid banner icon size");
62
60
  this.tileData.copy(output, 0x20);
63
61
  this.palette.copy(output, 0x220);
64
62
  for (let i = 0; i < this.titleCount; i++) {
65
- const text = Buffer.from(this[titles[i]], 'utf16le');
66
- if (text.length > 256)
67
- throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
63
+ const text = Buffer.from(this[titles[i]], "utf16le");
64
+ if (text.length > 256) throw new RangeError(`Banner ${titles[i]} exceeds 128 UTF-16 code units`);
68
65
  output.fill(0, 0x240 + i * 256, 0x340 + i * 256);
69
66
  text.copy(output, 0x240 + i * 256);
70
67
  }
@@ -1,16 +1,11 @@
1
1
  export const DEFAULT_MAX_SIZE = 0x40000000;
2
2
 
3
- export const bytes = (value: Uint8Array, name = 'input'): Buffer => {
3
+ export const bytes = (value: Uint8Array, name = "input"): Buffer => {
4
4
  if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Buffer or Uint8Array`);
5
5
  return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
6
6
  };
7
7
 
8
- export const range = (
9
- buffer: Buffer,
10
- offset: number,
11
- size: number,
12
- name = 'binary data',
13
- ): Buffer => {
8
+ export const range = (buffer: Buffer, offset: number, size: number, name = "binary data"): Buffer => {
14
9
  if (
15
10
  !Number.isSafeInteger(offset) ||
16
11
  !Number.isSafeInteger(size) ||
@@ -18,16 +13,12 @@ export const range = (
18
13
  size < 0 ||
19
14
  offset + size > buffer.length
20
15
  ) {
21
- throw new RangeError(
22
- `Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`,
23
- );
16
+ throw new RangeError(`Invalid ${name} range: offset=${offset}, size=${size}, available=${buffer.length}`);
24
17
  }
25
18
  return buffer.subarray(offset, offset + size);
26
19
  };
27
20
 
28
- export const align = (value: number, alignment = 0x200): number => {
29
- return Math.ceil(value / alignment) * alignment;
30
- };
21
+ export const align = (value: number, alignment = 0x200): number => Math.ceil(value / alignment) * alignment;
31
22
 
32
23
  /** Seekable, bounded in-memory writer; unwritten gaps have MemoryStream's zero fill. */
33
24
  export class BinaryWriter {
@@ -37,7 +28,7 @@ export class BinaryWriter {
37
28
  length: number;
38
29
  constructor(initialSize = 65536, maxSize = DEFAULT_MAX_SIZE) {
39
30
  if (!Number.isSafeInteger(maxSize) || maxSize < 1 || maxSize > 0xffffffff)
40
- throw new RangeError('Invalid output size limit');
31
+ throw new RangeError("Invalid output size limit");
41
32
  this.maxSize = maxSize;
42
33
  this.buffer = Buffer.alloc(Math.min(initialSize, maxSize));
43
34
  this.position = 0;
@@ -55,8 +46,7 @@ export class BinaryWriter {
55
46
 
56
47
  write(data: Uint8Array) {
57
48
  data = bytes(data);
58
- if (!Number.isSafeInteger(this.position) || this.position < 0)
59
- throw new RangeError('Invalid writer position');
49
+ if (!Number.isSafeInteger(this.position) || this.position < 0) throw new RangeError("Invalid writer position");
60
50
  this.ensure(this.position + data.length);
61
51
  bytes(data).copy(this.buffer, this.position);
62
52
  this.position += data.length;
package/src/nitro/blz.ts CHANGED
@@ -7,7 +7,7 @@ export interface Arm9Header {
7
7
  // Native port of Xzonn/BlzHelper BLZ.cs (MIT), including overwrite-safe prefix.
8
8
  // Compression returns an empty Buffer when no smaller representation exists.
9
9
  export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
10
- if (input.length < 8) throw new Error('BLZ footer is truncated');
10
+ if (input.length < 8) throw new Error("BLZ footer is truncated");
11
11
  const diff = input.readUInt32LE(input.length - 4);
12
12
  const packed = input.readUInt32LE(input.length - 8),
13
13
  header = packed >>> 24,
@@ -15,7 +15,7 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
15
15
  const size = input.length + diff,
16
16
  prefix = input.length - length;
17
17
  if (header < 8 || header > length || length > input.length || size > maxSize)
18
- throw new Error('Invalid BLZ size or footer');
18
+ throw new Error("Invalid BLZ size or footer");
19
19
  const out = Buffer.alloc(size);
20
20
  input.copy(out, 0, 0, prefix);
21
21
  let src = input.length - header,
@@ -24,16 +24,15 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
24
24
  const flags = input[--src];
25
25
  for (let bit = 128; bit && src > prefix; bit >>= 1) {
26
26
  if (!(flags & bit)) {
27
- if (dst <= prefix) throw new Error('BLZ output overflow');
27
+ if (dst <= prefix) throw new Error("BLZ output overflow");
28
28
  out[--dst] = input[--src];
29
29
  } else {
30
- if (src - 2 < prefix) throw new Error('Truncated BLZ reference');
30
+ if (src - 2 < prefix) throw new Error("Truncated BLZ reference");
31
31
  const high = input[--src],
32
32
  low = input[--src];
33
33
  const count = (high >>> 4) + 3,
34
34
  offset = (((high & 15) << 8) | low) + 3;
35
- if (dst - count < prefix || dst - 1 + offset >= size)
36
- throw new Error('Invalid BLZ reference');
35
+ if (dst - count < prefix || dst - 1 + offset >= size) throw new Error("Invalid BLZ reference");
37
36
  for (let j = 0; j < count; j++) {
38
37
  --dst;
39
38
  out[dst] = out[dst + offset];
@@ -41,12 +40,12 @@ export const decompressBLZ = (input: Buffer, maxSize = 0x10000000): Buffer => {
41
40
  }
42
41
  }
43
42
  }
44
- if (dst !== prefix) throw new Error('BLZ decompressed size mismatch');
43
+ if (dst !== prefix) throw new Error("BLZ decompressed size mismatch");
45
44
  return out;
46
45
  };
47
46
 
48
47
  export const compressBLZ = (input: Buffer): Buffer => {
49
- if (input.length > 0xffffff) throw new Error('BLZ input exceeds 24-bit format limit');
48
+ if (input.length > 0xffffff) throw new Error("BLZ input exceeds 24-bit format limit");
50
49
  const temp = Buffer.alloc(input.length);
51
50
  let src = input.length,
52
51
  dst = input.length;
@@ -131,31 +130,24 @@ export const compressBLZ = (input: Buffer): Buffer => {
131
130
  return out;
132
131
  };
133
132
 
134
- export const decompressArm9 = (
135
- data: Buffer,
136
- header: Arm9Header,
137
- ): { compressed: boolean; data: Buffer } => {
138
- if (data.length < 0x18) throw new Error('ARM9 data too short');
133
+ export const decompressArm9 = (data: Buffer, header: Arm9Header): { compressed: boolean; data: Buffer } => {
134
+ if (data.length < 0x18) throw new Error("ARM9 data too short");
139
135
  const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
140
136
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
141
137
  const ptr = init ? data.readUInt32LE(init + 0x14) : header.ARM9ramAddress + header.ARM9size;
142
- const compressed =
143
- ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
138
+ const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
144
139
  return {
145
140
  compressed,
146
141
  data: compressed
147
- ? Buffer.concat([
148
- decompressBLZ(data.subarray(0, data.length - nitro)),
149
- data.subarray(data.length - nitro),
150
- ])
142
+ ? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)])
151
143
  : Buffer.from(data),
152
144
  };
153
145
  };
154
146
  export const compressArm9 = (data: Buffer, header: Arm9Header, postSize = 0): Buffer => {
155
- if (data.length < 0x4000) throw new Error('ARM9 secure area is truncated');
147
+ if (data.length < 0x4000) throw new Error("ARM9 secure area is truncated");
156
148
  const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
157
149
  const packed = compressBLZ(data.subarray(0x4000, data.length - nitro));
158
- if (!packed.length) throw new Error('ARM9 BLZ compression produced no smaller representation');
150
+ if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
159
151
  const out = Buffer.concat([data.subarray(0, 0x4000), packed, data.subarray(data.length - nitro)]);
160
152
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
161
153
  if (init) out.writeUInt32LE(out.length - postSize + header.ARM9ramAddress, init + 0x14);
@@ -1,4 +1,4 @@
1
- import { createAesBlockCipher } from '#nitro-runtime';
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
- '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',
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 !== 'string' || header.gameCode.length !== 4)
101
- throw new Error('Expected four-character game code');
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, 'ascii');
105
- key.write(header.gameCode, 12, 4, 'ascii');
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
- throw new Error('Missing ARM9i HMAC for modcrypt key');
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 !== 'string' || gameCode.length !== 4)
127
- throw new Error('Expected four-character game code');
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, 'ascii').readUInt32LE();
152
+ const code = Buffer.from(gameCode, "ascii").readUInt32LE();
159
153
  args = [code, code >>> 1, (code << 1) >>> 0];
160
154
  init2();
161
155
  init2();
@@ -1,5 +1,5 @@
1
1
  // Based on NitroHelper FNT/FAT/OverlayTable, GPL-3.0.
2
- import { bytes, range } from './binary';
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, 'FAT');
33
- if (size % 8 || size / 8 > 65535) throw new Error('Invalid FAT entry count');
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, 'FNT');
52
- range(table, 0, 8, 'FNT root directory');
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('Invalid FNT directory count');
55
- range(table, 0, count * 8, 'FNT directory table');
56
- const decoder = new TextDecoder('shift_jis');
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('FNT directory names overlap directory table');
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, 'FNT name')[0];
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('Empty FNT directory name');
78
- const filename = decoder.decode(range(table, cursor, length, 'FNT filename'));
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('\0') ||
84
- filename === '.' ||
85
- filename === '..'
78
+ filename.includes("/") ||
79
+ filename.includes("\\") ||
80
+ filename.includes("\0") ||
81
+ filename === "." ||
82
+ filename === ".."
86
83
  )
87
- throw new Error('Invalid FNT path component');
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, 'FNT directory id').readUInt16LE();
89
+ const id = range(table, cursor, 2, "FNT directory id").readUInt16LE();
93
90
  cursor += 2;
94
- if (id < 0xf000) throw new Error('Invalid FNT directory id');
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('FNT file id is outside FAT');
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: 'root', id: 0xffff, files: [], folders: [parse(0, 'data', 0)] };
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
- 'overlayId',
132
- 'ramAddress',
133
- 'ramSize',
134
- 'bssSize',
135
- 'staticInitialiserStartAddress',
136
- 'staticInitialiserEndAddress',
137
- 'fileId',
138
- 'reserved',
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, 'overlay table');
151
- if (size % 32) throw new Error('Invalid overlay table size');
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('Overlay file id is outside FAT');
166
+ if (!file) throw new Error("Overlay file id is outside FAT");
170
167
  return {
171
168
  ...file,
172
- name: `overlay${this.isArm9 ? '' : '7'}_${String(item.overlayId).padStart(4, '0')}.bin`,
169
+ name: `overlay${this.isArm9 ? "" : "7"}_${String(item.overlayId).padStart(4, "0")}.bin`,
173
170
  };
174
171
  });
175
172
  }