1using System.Buffers.Binary;
6internal static class NdsImageBuildWriter
14 public static async ValueTask<NdsImageBuildResult> WriteAsync(
15 NdsImageBuilder builder,
17 NdsImageBuildOptions options,
18 CancellationToken cancellationToken)
20 ArgumentNullException.ThrowIfNull(destination);
21 if (!destination.CanRead || !destination.CanWrite || !destination.CanSeek)
23 throw new ArgumentException(
"The build destination must be readable, writable, and seekable.", nameof(destination));
27 ValidateRecipe(builder);
28 NdsImageBuildContent content = NdsImageBuildContentPreparer.Prepare(builder, options);
29 NdsImageBuildLayout layout = NdsImageLayoutPlanner.Plan(builder, content, options);
30 byte[] fat = BuildFat(layout.FileRegions);
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)
42 layout.Arm9Footer.Value.Offset,
43 content.Arm9TrailingData,
45 cancellationToken).ConfigureAwait(
false);
49 layout.Arm9OverlayTable.Offset,
50 content.Arm9OverlayTable,
52 cancellationToken).ConfigureAwait(
false);
53 await WriteAtAsync(destination, layout.Arm7.Offset, content.Arm7Data, paddingByte, cancellationToken)
54 .ConfigureAwait(
false);
57 layout.Arm7OverlayTable.Offset,
58 content.Arm7OverlayTable,
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)
69 layout.Banner.Value.Offset,
70 builder.Banner!.RawData,
72 cancellationToken).ConfigureAwait(
false);
75 for (
int fileId = 0; fileId < content.Allocations.Length; fileId++)
79 layout.FileRegions[fileId].Offset,
80 content.Allocations[fileId],
82 cancellationToken).ConfigureAwait(
false);
85 if (layout.Arm9i is not
null)
89 layout.Arm9i.Value.Offset,
92 cancellationToken).ConfigureAwait(
false);
95 if (layout.Arm7i is not
null)
99 layout.Arm7i.Value.Offset,
102 cancellationToken).ConfigureAwait(
false);
105 NdsDsiDigestBuildResult? digestResult =
null;
106 if (builder.DsiMetadata?.Digests is not
null)
108 digestResult = await NdsDsiDigestBuilder.BuildAsync(
111 builder.DsiMetadata.Digests,
112 builder.DsiMetadata.Integrity.HmacKey,
113 cancellationToken).ConfigureAwait(
false);
116 layout.SectorHashTable.Offset,
117 digestResult.SectorHashes,
119 cancellationToken).ConfigureAwait(
false);
122 layout.BlockHashTable.Offset,
123 digestResult.BlockHashes,
125 cancellationToken).ConfigureAwait(
false);
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)
136 await NdsImageBuildVerifier.VerifyAsync(destination, builder, content.FileSystem, cancellationToken).ConfigureAwait(
false);
139 destination.Position = layout.PhysicalSize;
145 layout.Arm9OverlayTable,
147 layout.Arm7OverlayTable,
148 layout.FileNameTable,
149 layout.FileAllocationTable,
153 layout.SectorHashTable,
154 layout.BlockHashTable,
155 content.FileSystem.FilesInIdOrder.Count,
156 layout.FileRegions.Count);
161 private static void ValidateRecipe(NdsImageBuilder builder)
163 if (builder.Arm9 is
null || builder.Arm9.Processor !=
NdsProcessor.Arm9 || builder.Arm9.Contents.IsEmpty)
165 throw new InvalidDataException(
"A non-empty ARM9 definition with the ARM9 processor identity is required.");
168 if (builder.Arm7 is
null || builder.Arm7.Processor !=
NdsProcessor.Arm7 || builder.Arm7.Contents.IsEmpty)
170 throw new InvalidDataException(
"A non-empty ARM7 definition with the ARM7 processor identity is required.");
174 if (!isDsi && (builder.Arm9i is not
null || builder.Arm7i is not
null || builder.DsiMetadata is not
null))
176 throw new InvalidDataException(
"DS recipes cannot contain DSi Programs or extended metadata; select a DSi image kind explicitly.");
181 ValidateDsiRecipe(builder);
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));
191 private static void ValidateDsiRecipe(NdsImageBuilder builder)
193 if (builder.DsiMetadata is
null)
195 throw new InvalidDataException(
"A DSi recipe requires explicit extended metadata and integrity policy.");
198 if (builder.Arm9i is
null || builder.Arm9i.Processor !=
NdsProcessor.Arm9i || builder.Arm9i.Contents.IsEmpty ||
199 builder.Arm9i.EntryAddress != builder.Arm9i.LoadAddress)
201 throw new InvalidDataException(
"A DSi recipe requires non-empty ARM9i data whose entry and load addresses match.");
204 if (builder.Arm7i is
null || builder.Arm7i.Processor !=
NdsProcessor.Arm7i || builder.Arm7i.Contents.IsEmpty ||
205 builder.Arm7i.EntryAddress != builder.Arm7i.LoadAddress)
207 throw new InvalidDataException(
"A DSi recipe requires non-empty ARM7i data whose entry and load addresses match.");
210 if (builder.DsiMetadata.Integrity is
null)
212 throw new InvalidDataException(
"A DSi recipe must name how authentication fields are populated.");
215 if (builder.DsiMetadata.Digests is not
null)
217 builder.DsiMetadata.Digests.Validate();
218 if (!builder.DsiMetadata.Integrity.ComputesHmacSha1)
220 throw new InvalidDataException(
"DSi digest tables require an explicit HMAC-SHA1 key policy.");
228 private static byte[] BuildFat(IReadOnlyList<NdsRegion> regions)
230 byte[] fat =
new byte[checked(regions.Count * 8)];
231 for (
int fileId = 0; fileId < regions.Count; fileId++)
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));
245 private static void ValidateAscii(
string value,
int minimum,
int maximum,
string name)
247 ArgumentNullException.ThrowIfNull(value);
248 if (value.Length < minimum || value.Length > maximum || value.Any(
static character => character is <
' ' or >
'~'))
250 throw new InvalidDataException($
"{name} must contain {minimum} through {maximum} printable ASCII characters.");
260 private static async ValueTask WriteAtAsync(
263 ReadOnlyMemory<byte> data,
265 CancellationToken cancellationToken)
267 await FillToAsync(destination, offset, paddingByte, cancellationToken).ConfigureAwait(
false);
268 destination.Position = offset;
269 await destination.WriteAsync(data, cancellationToken).ConfigureAwait(
false);
277 private static async ValueTask FillToAsync(
281 CancellationToken cancellationToken)
283 if (destination.Length >= targetOffset)
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)
294 int count = (int)Math.Min(buffer.Length, remaining);
295 await destination.WriteAsync(buffer.AsMemory(0, count), cancellationToken).ConfigureAwait(
false);
NdsProcessor
Identifies a processor and execution mode.
NdsImageKind
Identifies the hardware family targeted by an image.