NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsLegacyArm7Hook.cs
1using System.Buffers.Binary;
2
3namespace NdsForge;
4
9public static class NdsLegacyArm7Hook
10{
19 public static NdsLegacyArm7HookResult Apply(ReadOnlySpan<byte> image, ReadOnlySpan<byte> hook)
20 {
21 if (image.Length < 0x200)
22 {
23 throw new InvalidDataException("A legacy ARM7 hook requires at least a complete 512-byte DS header.");
24 }
25
26 if (hook.IsEmpty)
27 {
28 throw new ArgumentException("A legacy ARM7 hook payload cannot be empty.", nameof(hook));
29 }
30
31 byte[] originalHeader = image[..0x200].ToArray();
32 RestoreHeaderBackupIfPresent(image, originalHeader);
33 uint arm7Offset = ReadUInt32(originalHeader, 0x30);
34 uint arm7Entry = ReadUInt32(originalHeader, 0x34);
35 uint arm7Ram = ReadUInt32(originalHeader, 0x38);
36 uint arm7Size = ReadUInt32(originalHeader, 0x3C);
37 uint usedSize = ReadUInt32(originalHeader, 0x80);
38 if ((ulong)arm7Offset + arm7Size > (ulong)image.Length || usedSize > image.Length)
39 {
40 throw new InvalidDataException("The source ARM7 region or declared used size lies outside the image.");
41 }
42
43 int alignedHookSize = checked((hook.Length + 3) & ~3);
44 uint newArm7Offset = Align(checked(usedSize + 0x100u), 0x200);
45 uint hookOffset = checked(newArm7Offset + arm7Size);
46 uint headerBackupOffset = checked(hookOffset + (uint)alignedHookSize);
47 uint newUsedSize = checked(headerBackupOffset + 0x200u);
48 int outputLength = checked((int)Math.Max(image.Length, (long)newUsedSize + 4));
49 var output = new byte[outputLength];
50 image.CopyTo(output);
51 byte[] relocatedArm7 = image.Slice(checked((int)arm7Offset), checked((int)arm7Size)).ToArray();
52 var alignedHook = new byte[alignedHookSize];
53 hook.CopyTo(alignedHook);
54 int logicalLength = image.Length;
55
56 NdsLegacyCrc32.ReplacePreservingCrc(output, ref logicalLength, checked((int)newArm7Offset), relocatedArm7);
57 NdsLegacyCrc32.ReplacePreservingCrc(output, ref logicalLength, checked((int)hookOffset), alignedHook);
58 NdsLegacyCrc32.ReplacePreservingCrc(output, ref logicalLength, checked((int)headerBackupOffset), originalHeader);
59 byte[] patchedHeader = CreatePatchedHeader(
60 originalHeader,
61 headerBackupOffset,
62 checked(arm7Ram + arm7Size + (uint)alignedHookSize),
63 checked(arm7Entry + arm7Size),
64 newArm7Offset,
65 checked(arm7Size + (uint)alignedHookSize + 0x200u),
66 newUsedSize);
67 NdsLegacyCrc32.ReplacePreservingCrc(output, ref logicalLength, 0, patchedHeader, checked((int)newUsedSize));
68 uint resultCrc = NdsLegacyCrc32.Calculate(output);
69
70 return new(
71 output,
72 new(newArm7Offset, checked(arm7Size + (uint)alignedHookSize + 0x200u)),
73 new(hookOffset, alignedHookSize),
74 new(headerBackupOffset, 0x200),
75 resultCrc);
76 }
77
81 private static void RestoreHeaderBackupIfPresent(ReadOnlySpan<byte> image, Span<byte> header)
82 {
83 if (ReadUInt32(header, 0x160) == 0)
84 {
85 return;
86 }
87
88 uint backupOffset = ReadUInt32(header, 0x78);
89 if ((ulong)backupOffset + 0x200 > (ulong)image.Length)
90 {
91 throw new InvalidDataException("A previously hooked image points its original header backup outside the file.");
92 }
93
94 ReadOnlySpan<byte> backup = image.Slice(checked((int)backupOffset), 0x200);
95 if (backup.Slice(0x0C, 4).SequenceEqual(header.Slice(0x0C, 4)))
96 {
97 backup.CopyTo(header);
98 }
99 }
100
110 private static byte[] CreatePatchedHeader(
111 ReadOnlySpan<byte> original,
112 uint backupOffset,
113 uint backupRamAddress,
114 uint arm7Entry,
115 uint arm7Offset,
116 uint arm7Size,
117 uint usedSize)
118 {
119 byte[] header = original.ToArray();
120 WriteUInt32(header, 0x78, backupOffset);
121 WriteUInt32(header, 0x7C, backupRamAddress);
122 WriteUInt32(header, 0x34, arm7Entry);
123 WriteUInt32(header, 0x24, 0x027F_FE18);
124 WriteUInt32(header, 0x18, 0xE59F_F004);
125 WriteUInt32(header, 0x30, arm7Offset);
126 WriteUInt32(header, 0x3C, arm7Size);
127 WriteUInt32(header, 0x80, usedSize);
128 BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(0x15E), NdsChecksums.ComputeCrc16(header.AsSpan(0, 0x15E)));
129 return header;
130 }
131
133 private static uint ReadUInt32(ReadOnlySpan<byte> data, int offset) => BinaryPrimitives.ReadUInt32LittleEndian(data[offset..]);
134
136 private static void WriteUInt32(Span<byte> data, int offset, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(data[offset..], value);
137
139 private static uint Align(uint value, uint alignment) => checked((value + alignment - 1) & ~(alignment - 1));
140}
Reports a detached legacy ARM7 hook image and the regions introduced by the compatibility transform.
Reproduces ndstool's historical trainer layout by relocating ARM7, appending caller code and a header...
static NdsLegacyArm7HookResult Apply(ReadOnlySpan< byte > image, ReadOnlySpan< byte > hook)
Applies the compatibility transform to a complete DS-family image without mutating caller memory.