NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsImageBuildWriter.cs
1using System.Buffers.Binary;
2
3namespace NdsForge;
4
6internal static class NdsImageBuildWriter
7{
14 public static async ValueTask<NdsImageBuildResult> WriteAsync(
15 NdsImageBuilder builder,
16 Stream destination,
17 NdsImageBuildOptions options,
18 CancellationToken cancellationToken)
19 {
20 ArgumentNullException.ThrowIfNull(destination);
21 if (!destination.CanRead || !destination.CanWrite || !destination.CanSeek)
22 {
23 throw new ArgumentException("The build destination must be readable, writable, and seekable.", nameof(destination));
24 }
25
26 options.Validate();
27 ValidateRecipe(builder);
28 NdsImageBuildContent content = NdsImageBuildContentPreparer.Prepare(builder, options);
29 NdsImageBuildLayout layout = NdsImageLayoutPlanner.Plan(builder, content, options);
30 byte[] fat = BuildFat(layout.FileRegions);
31
32 destination.Position = 0;
33 destination.SetLength(0);
34 byte paddingByte = options.Profile == NdsImageBuildProfile.Ndstool1503 ? (byte)0 : options.PaddingByte;
35 await destination.WriteAsync(new byte[options.HeaderSize], cancellationToken).ConfigureAwait(false);
36 await WriteAtAsync(destination, layout.Arm9.Offset, content.Arm9Data, paddingByte, cancellationToken)
37 .ConfigureAwait(false);
38 if (layout.Arm9Footer is not null)
39 {
40 await WriteAtAsync(
41 destination,
42 layout.Arm9Footer.Value.Offset,
43 content.Arm9TrailingData,
44 paddingByte,
45 cancellationToken).ConfigureAwait(false);
46 }
47 await WriteAtAsync(
48 destination,
49 layout.Arm9OverlayTable.Offset,
50 content.Arm9OverlayTable,
51 paddingByte,
52 cancellationToken).ConfigureAwait(false);
53 await WriteAtAsync(destination, layout.Arm7.Offset, content.Arm7Data, paddingByte, cancellationToken)
54 .ConfigureAwait(false);
55 await WriteAtAsync(
56 destination,
57 layout.Arm7OverlayTable.Offset,
58 content.Arm7OverlayTable,
59 paddingByte,
60 cancellationToken).ConfigureAwait(false);
61 await WriteAtAsync(destination, layout.FileNameTable.Offset, content.FileSystem.FileNameTable, paddingByte, cancellationToken)
62 .ConfigureAwait(false);
63 await WriteAtAsync(destination, layout.FileAllocationTable.Offset, fat, paddingByte, cancellationToken)
64 .ConfigureAwait(false);
65 if (layout.Banner is not null)
66 {
67 await WriteAtAsync(
68 destination,
69 layout.Banner.Value.Offset,
70 builder.Banner!.RawData,
71 paddingByte,
72 cancellationToken).ConfigureAwait(false);
73 }
74
75 for (int fileId = 0; fileId < content.Allocations.Length; fileId++)
76 {
77 await WriteAtAsync(
78 destination,
79 layout.FileRegions[fileId].Offset,
80 content.Allocations[fileId],
81 paddingByte,
82 cancellationToken).ConfigureAwait(false);
83 }
84
85 if (layout.Arm9i is not null)
86 {
87 await WriteAtAsync(
88 destination,
89 layout.Arm9i.Value.Offset,
90 content.Arm9iData,
91 paddingByte,
92 cancellationToken).ConfigureAwait(false);
93 }
94
95 if (layout.Arm7i is not null)
96 {
97 await WriteAtAsync(
98 destination,
99 layout.Arm7i.Value.Offset,
100 content.Arm7iData,
101 paddingByte,
102 cancellationToken).ConfigureAwait(false);
103 }
104
105 NdsDsiDigestBuildResult? digestResult = null;
106 if (builder.DsiMetadata?.Digests is not null)
107 {
108 digestResult = await NdsDsiDigestBuilder.BuildAsync(
109 destination,
110 layout,
111 builder.DsiMetadata.Digests,
112 builder.DsiMetadata.Integrity.HmacKey,
113 cancellationToken).ConfigureAwait(false);
114 await WriteAtAsync(
115 destination,
116 layout.SectorHashTable.Offset,
117 digestResult.SectorHashes,
118 paddingByte,
119 cancellationToken).ConfigureAwait(false);
120 await WriteAtAsync(
121 destination,
122 layout.BlockHashTable.Offset,
123 digestResult.BlockHashes,
124 paddingByte,
125 cancellationToken).ConfigureAwait(false);
126 }
127
128 await FillToAsync(destination, layout.PhysicalSize, paddingByte, cancellationToken).ConfigureAwait(false);
129 destination.SetLength(layout.PhysicalSize);
130 byte[] header = NdsImageHeaderWriter.Write(builder, layout, content, options, digestResult);
131 destination.Position = 0;
132 await destination.WriteAsync(header, cancellationToken).ConfigureAwait(false);
133 await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
134 if (options.VerifyOutput)
135 {
136 await NdsImageBuildVerifier.VerifyAsync(destination, builder, content.FileSystem, cancellationToken).ConfigureAwait(false);
137 }
138
139 destination.Position = layout.PhysicalSize;
140 return new(
141 layout.UsedSize,
142 layout.PhysicalSize,
143 layout.Arm9,
144 layout.Arm9Footer,
145 layout.Arm9OverlayTable,
146 layout.Arm7,
147 layout.Arm7OverlayTable,
148 layout.FileNameTable,
149 layout.FileAllocationTable,
150 layout.Banner,
151 layout.Arm9i,
152 layout.Arm7i,
153 layout.SectorHashTable,
154 layout.BlockHashTable,
155 content.FileSystem.FilesInIdOrder.Count,
156 layout.FileRegions.Count);
157 }
158
161 private static void ValidateRecipe(NdsImageBuilder builder)
162 {
163 if (builder.Arm9 is null || builder.Arm9.Processor != NdsProcessor.Arm9 || builder.Arm9.Contents.IsEmpty)
164 {
165 throw new InvalidDataException("A non-empty ARM9 definition with the ARM9 processor identity is required.");
166 }
167
168 if (builder.Arm7 is null || builder.Arm7.Processor != NdsProcessor.Arm7 || builder.Arm7.Contents.IsEmpty)
169 {
170 throw new InvalidDataException("A non-empty ARM7 definition with the ARM7 processor identity is required.");
171 }
172
173 bool isDsi = builder.Kind != NdsImageKind.NintendoDs;
174 if (!isDsi && (builder.Arm9i is not null || builder.Arm7i is not null || builder.DsiMetadata is not null))
175 {
176 throw new InvalidDataException("DS recipes cannot contain DSi Programs or extended metadata; select a DSi image kind explicitly.");
177 }
178
179 if (isDsi)
180 {
181 ValidateDsiRecipe(builder);
182 }
183
184 ValidateAscii(builder.Title, 0, 12, nameof(builder.Title));
185 ValidateAscii(builder.GameCode, 4, 4, nameof(builder.GameCode));
186 ValidateAscii(builder.MakerCode, 2, 2, nameof(builder.MakerCode));
187 }
188
191 private static void ValidateDsiRecipe(NdsImageBuilder builder)
192 {
193 if (builder.DsiMetadata is null)
194 {
195 throw new InvalidDataException("A DSi recipe requires explicit extended metadata and integrity policy.");
196 }
197
198 if (builder.Arm9i is null || builder.Arm9i.Processor != NdsProcessor.Arm9i || builder.Arm9i.Contents.IsEmpty ||
199 builder.Arm9i.EntryAddress != builder.Arm9i.LoadAddress)
200 {
201 throw new InvalidDataException("A DSi recipe requires non-empty ARM9i data whose entry and load addresses match.");
202 }
203
204 if (builder.Arm7i is null || builder.Arm7i.Processor != NdsProcessor.Arm7i || builder.Arm7i.Contents.IsEmpty ||
205 builder.Arm7i.EntryAddress != builder.Arm7i.LoadAddress)
206 {
207 throw new InvalidDataException("A DSi recipe requires non-empty ARM7i data whose entry and load addresses match.");
208 }
209
210 if (builder.DsiMetadata.Integrity is null)
211 {
212 throw new InvalidDataException("A DSi recipe must name how authentication fields are populated.");
213 }
214
215 if (builder.DsiMetadata.Digests is not null)
216 {
217 builder.DsiMetadata.Digests.Validate();
218 if (!builder.DsiMetadata.Integrity.ComputesHmacSha1)
219 {
220 throw new InvalidDataException("DSi digest tables require an explicit HMAC-SHA1 key policy.");
221 }
222 }
223 }
224
228 private static byte[] BuildFat(IReadOnlyList<NdsRegion> regions)
229 {
230 byte[] fat = new byte[checked(regions.Count * 8)];
231 for (int fileId = 0; fileId < regions.Count; fileId++)
232 {
233 BinaryPrimitives.WriteUInt32LittleEndian(fat.AsSpan(fileId * 8), checked((uint)regions[fileId].Offset));
234 BinaryPrimitives.WriteUInt32LittleEndian(fat.AsSpan((fileId * 8) + 4), checked((uint)regions[fileId].End));
235 }
236
237 return fat;
238 }
239
245 private static void ValidateAscii(string value, int minimum, int maximum, string name)
246 {
247 ArgumentNullException.ThrowIfNull(value);
248 if (value.Length < minimum || value.Length > maximum || value.Any(static character => character is < ' ' or > '~'))
249 {
250 throw new InvalidDataException($"{name} must contain {minimum} through {maximum} printable ASCII characters.");
251 }
252 }
253
260 private static async ValueTask WriteAtAsync(
261 Stream destination,
262 long offset,
263 ReadOnlyMemory<byte> data,
264 byte paddingByte,
265 CancellationToken cancellationToken)
266 {
267 await FillToAsync(destination, offset, paddingByte, cancellationToken).ConfigureAwait(false);
268 destination.Position = offset;
269 await destination.WriteAsync(data, cancellationToken).ConfigureAwait(false);
270 }
271
277 private static async ValueTask FillToAsync(
278 Stream destination,
279 long targetOffset,
280 byte paddingByte,
281 CancellationToken cancellationToken)
282 {
283 if (destination.Length >= targetOffset)
284 {
285 return;
286 }
287
288 destination.Position = destination.Length;
289 byte[] buffer = new byte[64 * 1024];
290 buffer.AsSpan().Fill(paddingByte);
291 long remaining = targetOffset - destination.Length;
292 while (remaining > 0)
293 {
294 int count = (int)Math.Min(buffer.Length, remaining);
295 await destination.WriteAsync(buffer.AsMemory(0, count), cancellationToken).ConfigureAwait(false);
296 remaining -= count;
297 }
298 }
299
300}
NdsProcessor
Identifies a processor and execution mode.
NdsImageKind
Identifies the hardware family targeted by an image.