NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsImageComparer.cs
1using System.Globalization;
2
3namespace NdsForge;
4
6public static class NdsImageComparer
7{
13 public static async ValueTask<NdsImageDiff> CompareAsync(
14 NdsImage left,
15 NdsImage right,
16 CancellationToken cancellationToken = default)
17 {
18 ArgumentNullException.ThrowIfNull(left);
19 ArgumentNullException.ThrowIfNull(right);
20 NdsImageManifest leftManifest = await NdsImageManifest.CaptureAsync(left, cancellationToken).ConfigureAwait(false);
21 NdsImageManifest rightManifest = await NdsImageManifest.CaptureAsync(right, cancellationToken).ConfigureAwait(false);
22 return Compare(leftManifest, rightManifest);
23 }
24
30 {
31 ArgumentNullException.ThrowIfNull(left);
32 ArgumentNullException.ThrowIfNull(right);
33 left.Validate();
34 right.Validate();
35 var differences = new List<NdsSemanticDifference>();
36 CompareHeader(left.Header, right.Header, differences);
37 CompareDsi(left.Dsi, right.Dsi, differences);
38 ComparePrograms(left.Programs, right.Programs, differences);
39 CompareDirectories(left.Directories, right.Directories, differences);
40 CompareFiles(left.Files, right.Files, differences);
41 CompareAllocations(left.Allocations, right.Allocations, differences);
42 CompareOverlays(left.Overlays, right.Overlays, differences);
43 CompareBanner(left.Banner, right.Banner, differences);
44 AddValue(differences, "Image.PhysicalLength", left.PhysicalLength, right.PhysicalLength, NdsDifferenceKind.Relocated);
45 AddValue(differences, "Image.Sha256", left.ImageSha256, right.ImageSha256);
46 return new(left.ImageSha256, right.ImageSha256, differences);
47 }
48
50 private static void CompareDirectories(
51 IEnumerable<string> left,
52 IEnumerable<string> right,
53 List<NdsSemanticDifference> output)
54 {
55 var leftSet = left.ToHashSet(StringComparer.Ordinal);
56 var rightSet = right.ToHashSet(StringComparer.Ordinal);
57 output.AddRange(leftSet.Except(rightSet, StringComparer.Ordinal).Select(path =>
58 new NdsSemanticDifference("Directories[" + path + "]", NdsDifferenceKind.Removed, "present", null)));
59 output.AddRange(rightSet.Except(leftSet, StringComparer.Ordinal).Select(path =>
60 new NdsSemanticDifference("Directories[" + path + "]", NdsDifferenceKind.Added, null, "present")));
61 }
62
64 private static void CompareHeader(NdsManifestHeader left, NdsManifestHeader right, List<NdsSemanticDifference> output)
65 {
66 AddValue(output, "Header.Title", left.Title, right.Title);
67 AddValue(output, "Header.GameCode", left.GameCode, right.GameCode);
68 AddValue(output, "Header.MakerCode", left.MakerCode, right.MakerCode);
69 AddValue(output, "Header.Kind", left.Kind, right.Kind);
70 AddValue(output, "Header.Version", left.Version, right.Version);
71 AddValue(output, "Header.RegionCode", left.RegionCode, right.RegionCode);
72 AddValue(output, "Header.AutoStart", left.AutoStart, right.AutoStart);
73 AddValue(output, "Header.UsedImageSize", left.UsedImageSize, right.UsedImageSize, NdsDifferenceKind.Relocated);
74 AddValue(output, "Header.DeviceCapacityBytes", left.DeviceCapacityBytes, right.DeviceCapacityBytes, NdsDifferenceKind.Relocated);
75 AddValue(output, "Header.NormalCardControl", left.NormalCardControl, right.NormalCardControl);
76 AddValue(output, "Header.SecureCardControl", left.SecureCardControl, right.SecureCardControl);
77 AddValue(output, "Header.Sha256", left.Sha256, right.Sha256);
78 }
79
81 private static void CompareDsi(NdsManifestDsi? left, NdsManifestDsi? right, List<NdsSemanticDifference> output)
82 {
83 if (left is null || right is null)
84 {
85 AddPresence(output, "Dsi", left, right);
86 return;
87 }
88
89 AddValue(output, "Dsi.TitleId", left.TitleId, right.TitleId);
90 AddValue(output, "Dsi.TotalImageSize", left.TotalImageSize, right.TotalImageSize, NdsDifferenceKind.Relocated);
91 AddValue(output, "Dsi.RegionFlags", left.RegionFlags, right.RegionFlags);
92 AddValue(output, "Dsi.AccessControl", left.AccessControl, right.AccessControl);
93 AddValue(output, "Dsi.HasModcryptAreas", left.HasModcryptAreas, right.HasModcryptAreas);
94 AddValue(output, "Dsi.UsesInsecureModcryptKey", left.UsesInsecureModcryptKey, right.UsesInsecureModcryptKey);
95 CompareRegion("Dsi.ModcryptArea1", left.ModcryptArea1, right.ModcryptArea1, output);
96 CompareRegion("Dsi.ModcryptArea2", left.ModcryptArea2, right.ModcryptArea2, output);
97 }
98
100 private static void ComparePrograms(
101 IEnumerable<NdsManifestProgram> left,
102 IEnumerable<NdsManifestProgram> right,
103 List<NdsSemanticDifference> output)
104 {
105 Dictionary<NdsProcessor, NdsManifestProgram> leftMap = left.ToDictionary(static value => value.Processor);
106 Dictionary<NdsProcessor, NdsManifestProgram> rightMap = right.ToDictionary(static value => value.Processor);
107 foreach (NdsProcessor processor in leftMap.Keys.Union(rightMap.Keys).Order())
108 {
109 string path = $"Programs[{processor}]";
110 bool hasBefore = leftMap.TryGetValue(processor, out NdsManifestProgram? before);
111 bool hasAfter = rightMap.TryGetValue(processor, out NdsManifestProgram? after);
112 if (!hasBefore || !hasAfter)
113 {
114 AddPresence(output, path, before, after);
115 continue;
116 }
117
118 NdsManifestProgram leftProgram = before!;
119 NdsManifestProgram rightProgram = after!;
120 AddValue(output, path + ".Sha256", leftProgram.Sha256, rightProgram.Sha256);
121 AddValue(output, path + ".Offset", leftProgram.Offset, rightProgram.Offset, NdsDifferenceKind.Relocated);
122 AddValue(output, path + ".Length", leftProgram.Length, rightProgram.Length);
123 AddValue(output, path + ".LoadAddress", leftProgram.LoadAddress, rightProgram.LoadAddress);
124 AddValue(output, path + ".EntryAddress", leftProgram.EntryAddress, rightProgram.EntryAddress);
125 }
126 }
127
129 private static void CompareFiles(
130 IEnumerable<NdsManifestFile> left,
131 IEnumerable<NdsManifestFile> right,
132 List<NdsSemanticDifference> output)
133 {
134 Dictionary<string, NdsManifestFile> leftMap = left.ToDictionary(static value => value.Path, StringComparer.Ordinal);
135 Dictionary<string, NdsManifestFile> rightMap = right.ToDictionary(static value => value.Path, StringComparer.Ordinal);
136 foreach (string path in leftMap.Keys.Intersect(rightMap.Keys, StringComparer.Ordinal).Order(StringComparer.Ordinal))
137 {
138 CompareFile("Files[" + path + "]", leftMap[path], rightMap[path], output);
139 }
140
141 List<NdsManifestFile> removed = leftMap.Values.Where(file => !rightMap.ContainsKey(file.Path)).ToList();
142 List<NdsManifestFile> added = rightMap.Values.Where(file => !leftMap.ContainsKey(file.Path)).ToList();
143 foreach (NdsManifestFile before in removed.ToArray())
144 {
145 NdsManifestFile[] matches = added
146 .Where(after => after.Sha256 == before.Sha256 && after.Length == before.Length)
147 .ToArray();
148 if (matches.Length != 1 || removed.Count(candidate => candidate.Sha256 == before.Sha256 && candidate.Length == before.Length) != 1)
149 {
150 continue;
151 }
152
153 NdsManifestFile after = matches[0];
154 output.Add(new("Files.Path", NdsDifferenceKind.Moved, before.Path, after.Path));
155 CompareFile("Files[" + after.Path + "]", before, after, output);
156 removed.Remove(before);
157 added.Remove(after);
158 }
159
160 output.AddRange(removed.Select(file => new NdsSemanticDifference(
161 "Files[" + file.Path + "]", NdsDifferenceKind.Removed, file.Sha256, null)));
162 output.AddRange(added.Select(file => new NdsSemanticDifference(
163 "Files[" + file.Path + "]", NdsDifferenceKind.Added, null, file.Sha256)));
164 }
165
167 private static void CompareFile(
168 string path,
169 NdsManifestFile left,
170 NdsManifestFile right,
171 List<NdsSemanticDifference> output)
172 {
173 AddValue(output, path + ".Sha256", left.Sha256, right.Sha256);
174 AddValue(output, path + ".FileId", left.FileId, right.FileId, NdsDifferenceKind.Renumbered);
175 AddValue(output, path + ".Offset", left.Offset, right.Offset, NdsDifferenceKind.Relocated);
176 AddValue(output, path + ".Length", left.Length, right.Length);
177 }
178
180 private static void CompareAllocations(
181 IEnumerable<NdsManifestAllocation> left,
182 IEnumerable<NdsManifestAllocation> right,
183 List<NdsSemanticDifference> output)
184 {
185 Dictionary<int, NdsManifestAllocation> leftMap = left.ToDictionary(static value => value.FileId);
186 Dictionary<int, NdsManifestAllocation> rightMap = right.ToDictionary(static value => value.FileId);
187 foreach (int fileId in leftMap.Keys.Union(rightMap.Keys).Order())
188 {
189 string path = $"Allocations[{fileId}]";
190 bool hasBefore = leftMap.TryGetValue(fileId, out NdsManifestAllocation? before);
191 bool hasAfter = rightMap.TryGetValue(fileId, out NdsManifestAllocation? after);
192 if (!hasBefore || !hasAfter)
193 {
194 AddPresence(output, path, before, after);
195 continue;
196 }
197
198 NdsManifestAllocation leftAllocation = before!;
199 NdsManifestAllocation rightAllocation = after!;
200 AddValue(output, path + ".Sha256", leftAllocation.Sha256, rightAllocation.Sha256);
201 AddValue(output, path + ".Offset", leftAllocation.Offset, rightAllocation.Offset, NdsDifferenceKind.Relocated);
202 AddValue(output, path + ".Length", leftAllocation.Length, rightAllocation.Length);
203 }
204 }
205
207 private static void CompareOverlays(
208 IEnumerable<NdsManifestOverlay> left,
209 IEnumerable<NdsManifestOverlay> right,
210 List<NdsSemanticDifference> output)
211 {
212 Dictionary<string, NdsManifestOverlay> leftMap = left.ToDictionary(OverlayKey, StringComparer.Ordinal);
213 Dictionary<string, NdsManifestOverlay> rightMap = right.ToDictionary(OverlayKey, StringComparer.Ordinal);
214 foreach (string key in leftMap.Keys.Union(rightMap.Keys, StringComparer.Ordinal).Order(StringComparer.Ordinal))
215 {
216 string path = "Overlays[" + key + "]";
217 bool hasBefore = leftMap.TryGetValue(key, out NdsManifestOverlay? before);
218 bool hasAfter = rightMap.TryGetValue(key, out NdsManifestOverlay? after);
219 if (!hasBefore || !hasAfter)
220 {
221 AddPresence(output, path, before, after);
222 continue;
223 }
224
225 NdsManifestOverlay leftOverlay = before!;
226 NdsManifestOverlay rightOverlay = after!;
227 AddValue(output, path + ".Sha256", leftOverlay.Sha256, rightOverlay.Sha256);
228 AddValue(output, path + ".FileId", leftOverlay.FileId, rightOverlay.FileId, NdsDifferenceKind.Renumbered);
229 AddValue(output, path + ".FilePath", leftOverlay.FilePath, rightOverlay.FilePath, NdsDifferenceKind.Moved);
230 AddValue(output, path + ".Offset", leftOverlay.Offset, rightOverlay.Offset, NdsDifferenceKind.Relocated);
231 AddValue(output, path + ".Length", leftOverlay.Length, rightOverlay.Length);
232 AddValue(output, path + ".LoadAddress", leftOverlay.LoadAddress, rightOverlay.LoadAddress);
233 AddValue(output, path + ".RamSize", leftOverlay.RamSize, rightOverlay.RamSize);
234 AddValue(output, path + ".BssSize", leftOverlay.BssSize, rightOverlay.BssSize);
235 AddValue(output, path + ".StaticInitializerStart", leftOverlay.StaticInitializerStart, rightOverlay.StaticInitializerStart);
236 AddValue(output, path + ".StaticInitializerEnd", leftOverlay.StaticInitializerEnd, rightOverlay.StaticInitializerEnd);
237 AddValue(output, path + ".CompressedSize", leftOverlay.CompressedSize, rightOverlay.CompressedSize);
238 AddValue(output, path + ".Flags", leftOverlay.Flags, rightOverlay.Flags);
239 }
240 }
241
243 private static void CompareBanner(NdsManifestBanner? left, NdsManifestBanner? right, List<NdsSemanticDifference> output)
244 {
245 if (left is null || right is null)
246 {
247 AddPresence(output, "Banner", left, right);
248 return;
249 }
250
251 AddValue(output, "Banner.Sha256", left.Sha256, right.Sha256);
252 AddValue(output, "Banner.Offset", left.Offset, right.Offset, NdsDifferenceKind.Relocated);
253 AddValue(output, "Banner.Length", left.Length, right.Length);
254 AddValue(output, "Banner.Version", left.Version, right.Version);
255 AddValue(output, "Banner.IsAnimated", left.IsAnimated, right.IsAnimated);
256 foreach (string language in left.Titles.Keys.Union(right.Titles.Keys, StringComparer.Ordinal).Order(StringComparer.Ordinal))
257 {
258 left.Titles.TryGetValue(language, out string? before);
259 right.Titles.TryGetValue(language, out string? after);
260 AddValue(output, $"Banner.Titles[{language}]", before, after);
261 }
262 }
263
265 private static void CompareRegion(
266 string path,
267 NdsManifestRegion left,
268 NdsManifestRegion right,
269 List<NdsSemanticDifference> output)
270 {
271 AddValue(output, path + ".Offset", left.Offset, right.Offset, NdsDifferenceKind.Relocated);
272 AddValue(output, path + ".Length", left.Length, right.Length, NdsDifferenceKind.Relocated);
273 }
274
276 private static void AddPresence(List<NdsSemanticDifference> output, string path, object? left, object? right)
277 {
278 if (left is null && right is not null)
279 {
280 output.Add(new(path, NdsDifferenceKind.Added, null, "present"));
281 }
282 else if (left is not null && right is null)
283 {
284 output.Add(new(path, NdsDifferenceKind.Removed, "present", null));
285 }
286 }
287
289 private static void AddValue<T>(
290 List<NdsSemanticDifference> output,
291 string path,
292 T left,
293 T right,
294 NdsDifferenceKind kind = NdsDifferenceKind.Modified)
295 {
296 if (EqualityComparer<T>.Default.Equals(left, right))
297 {
298 return;
299 }
300
301 output.Add(new(path, kind, Format(left), Format(right)));
302 }
303
305 private static string? Format<T>(T value) => value switch
306 {
307 null => null,
308 IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
309 _ => value.ToString(),
310 };
311
313 private static string OverlayKey(NdsManifestOverlay overlay) => $"{overlay.Processor}:{overlay.OverlayId}";
314}
Compares hash-bearing manifests so tooling can distinguish content edits from identity and layout cha...
static NdsImageDiff Compare(NdsImageManifest left, NdsImageManifest right)
Compares detached manifests without reading ROM bytes, making review artifacts usable in offline CI s...
static async ValueTask< NdsImageDiff > CompareAsync(NdsImage left, NdsImage right, CancellationToken cancellationToken=default)
Captures and compares two live images without taking ownership of either source.
Collects deterministic semantic, identity, and layout differences between two manifest snapshots.
Provides a stable, content-addressed description of one parsed image for CI artifacts,...
IReadOnlyList< NdsManifestProgram > Programs
Contains executable snapshots in processor enumeration order.
IReadOnlyList< NdsManifestAllocation > Allocations
Contains every FAT record in numeric File ID order, including unnamed allocations.
long PhysicalLength
Records physical source bytes independently from header claims and nominal cartridge capacity.
static ValueTask< NdsImageManifest > CaptureAsync(NdsImage image, CancellationToken cancellationToken=default)
Captures hashes and structured metadata from a live image without taking ownership of it.
NdsManifestHeader Header
Contains common typed header values plus a hash covering reserved header bytes.
string ImageSha256
Hashes every physical image byte so padding-only changes remain detectable.
NdsManifestBanner? Banner
Contains native menu metadata, or remains absent when the image declares no supported banner.
IReadOnlyList< NdsManifestFile > Files
Contains every named NitroFS entry in canonical ordinal path order.
NdsManifestDsi? Dsi
Contains extended DSi metadata, or remains absent for an original DS image.
IReadOnlyList< NdsManifestOverlay > Overlays
Contains ARM9 then ARM7 Overlay records ordered by runtime Overlay ID.
IReadOnlyList< string > Directories
Contains every NitroFS directory path, including the root and explicitly empty nodes.
Provides structured, random-access inspection of a Nintendo DS-family image.
Definition NdsImage.cs:5
Describes one stable manifest path whose semantic value differs between two images.
NdsProcessor
Identifies a processor and execution mode.
NdsDifferenceKind
Classifies how one semantic or physical image value changed between manifest snapshots.