NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsImageBuildVerifier.cs
1namespace NdsForge;
2
4internal static class NdsImageBuildVerifier
5{
11 public static async ValueTask VerifyAsync(
12 Stream destination,
13 NdsImageBuilder builder,
14 NdsFileSystemBuildSnapshot fileSystem,
15 CancellationToken cancellationToken)
16 {
17 destination.Position = 0;
18 using NdsImage image = await NdsImage.OpenAsync(
19 destination,
20 leaveOpen: true,
21 cancellationToken: cancellationToken).ConfigureAwait(false);
22 var validationOptions = new NdsValidationOptions();
23 if (builder.DsiMetadata?.Integrity.HmacKey.IsEmpty == false)
24 {
25 validationOptions.SetDsiHmacKey(builder.DsiMetadata.Integrity.HmacKey.Span);
26 }
27
28 NdsValidationResult validation = image.Validate(validationOptions);
29 if (!validation.IsValid)
30 {
31 throw new InvalidDataException(
32 $"Generated image verification failed: {string.Join("; ", validation.Diagnostics.Select(static item => item.Message))}");
33 }
34
35 foreach (NdsBuildFile expectedFile in fileSystem.FilesInIdOrder)
36 {
37 NdsFile actualFile = image.FileSystem.GetFile(expectedFile.Path);
38 byte[] actual = await actualFile.ReadAllBytesAsync(cancellationToken).ConfigureAwait(false);
39 if (!actual.AsSpan().SequenceEqual(expectedFile.Contents.Span))
40 {
41 throw new InvalidDataException(
42 $"Generated image payload verification failed for '{expectedFile.Path}' at File ID {actualFile.Id}.");
43 }
44 }
45
46 if (builder.Arm9i is not null)
47 {
48 await VerifyProgramAsync(image, image.Header.Arm9i, builder.Arm9i, cancellationToken).ConfigureAwait(false);
49 }
50
51 if (builder.Arm7i is not null)
52 {
53 await VerifyProgramAsync(image, image.Header.Arm7i, builder.Arm7i, cancellationToken).ConfigureAwait(false);
54 }
55
56 await VerifyOverlaysAsync(image, image.Arm9Overlays, builder.Arm9Overlays, cancellationToken).ConfigureAwait(false);
57 await VerifyOverlaysAsync(image, image.Arm7Overlays, builder.Arm7Overlays, cancellationToken).ConfigureAwait(false);
58 }
59
65 private static async ValueTask VerifyProgramAsync(
66 NdsImage image,
67 NdsProgram? actual,
68 NdsProgramDefinition expected,
69 CancellationToken cancellationToken)
70 {
71 if (actual is null || actual.Processor != expected.Processor || actual.LoadAddress != expected.LoadAddress)
72 {
73 throw new InvalidDataException($"Generated {expected.Processor} Program metadata did not round-trip.");
74 }
75
76 using Stream stream = image.OpenRead(actual.Data);
77 byte[] contents = new byte[expected.Contents.Length];
78 await stream.ReadExactlyAsync(contents, cancellationToken).ConfigureAwait(false);
79 if (!contents.AsSpan().SequenceEqual(expected.Contents.Span))
80 {
81 throw new InvalidDataException($"Generated {expected.Processor} Program payload did not round-trip.");
82 }
83 }
84
90 private static async ValueTask VerifyOverlaysAsync(
91 NdsImage image,
92 IReadOnlyList<NdsOverlay> actual,
93 IReadOnlyList<NdsOverlayDefinition> expected,
94 CancellationToken cancellationToken)
95 {
96 if (actual.Count != expected.Count)
97 {
98 throw new InvalidDataException("Generated Overlay table entry count did not round-trip.");
99 }
100
101 for (int index = 0; index < actual.Count; index++)
102 {
103 if (actual[index].Id != expected[index].Id || actual[index].Data is null)
104 {
105 throw new InvalidDataException($"Generated Overlay record {index} did not round-trip.");
106 }
107
108 string? linkedFilePath = expected[index].EffectiveLinkedFilePath;
109 if (linkedFilePath is not null)
110 {
111 if (actual[index].File?.FullPath != linkedFilePath)
112 {
113 throw new InvalidDataException($"Generated Overlay file link {index} did not round-trip.");
114 }
115
116 continue;
117 }
118
119 using Stream stream = image.OpenRead(actual[index].Data!.Value);
120 byte[] contents = new byte[expected[index].Contents.Length];
121 await stream.ReadExactlyAsync(contents, cancellationToken).ConfigureAwait(false);
122 if (!contents.AsSpan().SequenceEqual(expected[index].Contents.Span))
123 {
124 throw new InvalidDataException($"Generated Overlay payload {index} did not round-trip.");
125 }
126 }
127 }
128}