NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsImageManifest.cs
1using System.Text.Json;
2using System.Text.Json.Serialization;
3
4namespace NdsForge;
5
10public sealed class NdsImageManifest
11{
13 public const int CurrentSchemaVersion = 1;
15 private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions(indented: false);
16
18 public int SchemaVersion { get; init; } = CurrentSchemaVersion;
20 public long PhysicalLength { get; init; }
22 public string ImageSha256 { get; init; } = string.Empty;
24 public NdsManifestHeader Header { get; init; } = new();
26 public NdsManifestDsi? Dsi { get; init; }
28 public IReadOnlyList<NdsManifestProgram> Programs { get; init; } = [];
30 public IReadOnlyList<string> Directories { get; init; } = [];
32 public IReadOnlyList<NdsManifestFile> Files { get; init; } = [];
34 public IReadOnlyList<NdsManifestAllocation> Allocations { get; init; } = [];
36 public IReadOnlyList<NdsManifestOverlay> Overlays { get; init; } = [];
38 public NdsManifestBanner? Banner { get; init; }
39
44 public static ValueTask<NdsImageManifest> CaptureAsync(
45 NdsImage image,
46 CancellationToken cancellationToken = default) =>
47 NdsImageManifestCapture.CaptureAsync(image, cancellationToken);
48
52 public string ToJson(bool indented = true)
53 {
54 Validate();
55 return JsonSerializer.Serialize(this, CreateJsonOptions(indented));
56 }
57
61 public static NdsImageManifest ParseJson(string json)
62 {
63 ArgumentException.ThrowIfNullOrWhiteSpace(json);
64 NdsImageManifest manifest = JsonSerializer.Deserialize<NdsImageManifest>(json, JsonOptions) ??
65 throw new InvalidDataException("The JSON document did not contain an NDS image manifest.");
66 manifest.Validate();
67 return manifest;
68 }
69
75 public async ValueTask WriteJsonAsync(
76 Stream destination,
77 bool indented = true,
78 CancellationToken cancellationToken = default)
79 {
80 ArgumentNullException.ThrowIfNull(destination);
81 if (!destination.CanWrite)
82 {
83 throw new ArgumentException("The manifest destination must be writable.", nameof(destination));
84 }
85
86 Validate();
87 await JsonSerializer.SerializeAsync(
88 destination,
89 this,
90 CreateJsonOptions(indented),
91 cancellationToken).ConfigureAwait(false);
92 }
93
98 public static async ValueTask<NdsImageManifest> ReadJsonAsync(
99 Stream source,
100 CancellationToken cancellationToken = default)
101 {
102 ArgumentNullException.ThrowIfNull(source);
103 if (!source.CanRead)
104 {
105 throw new ArgumentException("The manifest source must be readable.", nameof(source));
106 }
107
108 NdsImageManifest manifest = await JsonSerializer.DeserializeAsync<NdsImageManifest>(
109 source,
110 JsonOptions,
111 cancellationToken).ConfigureAwait(false) ??
112 throw new InvalidDataException("The JSON document did not contain an NDS image manifest.");
113 manifest.Validate();
114 return manifest;
115 }
116
118 internal void Validate()
119 {
120 if (SchemaVersion != CurrentSchemaVersion || PhysicalLength < 0 || Header is null ||
121 Programs is null || Directories is null || Files is null || Allocations is null || Overlays is null)
122 {
123 throw new InvalidDataException("The NDS image manifest schema or required fields are invalid.");
124 }
125
126 ValidateHash(ImageSha256, nameof(ImageSha256));
127 ValidateHash(Header.Sha256, "Header.Sha256");
128 if (Header.Title is null || Header.GameCode is null || Header.MakerCode is null ||
129 !Enum.IsDefined(Header.Kind) ||
130 Programs.Any(static value => value is null) ||
131 Directories.Any(static value => value is null) ||
132 Files.Any(static value => value is null) ||
133 Allocations.Any(static value => value is null) ||
134 Overlays.Any(static value => value is null))
135 {
136 throw new InvalidDataException("The manifest contains an invalid image kind or null collection entry.");
137 }
138
139 if (Programs.Select(static value => value.Processor).Distinct().Count() != Programs.Count ||
140 Programs.Any(static value => !Enum.IsDefined(value.Processor) || value.Offset < 0 || value.Length < 0))
141 {
142 throw new InvalidDataException("Manifest Programs must have unique processors and non-negative regions.");
143 }
144
145 foreach (NdsManifestProgram program in Programs)
146 {
147 ValidateHash(program.Sha256, $"Programs[{program.Processor}].Sha256");
148 }
149
150 if (Directories.Distinct(StringComparer.Ordinal).Count() != Directories.Count ||
151 Directories.Any(static value => string.IsNullOrEmpty(value) || !value.StartsWith('/')))
152 {
153 throw new InvalidDataException("Manifest directories must be unique canonical absolute paths.");
154 }
155
156 if (Files.Select(static value => value.Path).Distinct(StringComparer.Ordinal).Count() != Files.Count ||
157 Files.Any(static value => string.IsNullOrWhiteSpace(value.Path) || value.FileId < 0 || value.Offset < 0 || value.Length < 0))
158 {
159 throw new InvalidDataException("Manifest files must have unique paths, valid File IDs, and non-negative regions.");
160 }
161
162 foreach (NdsManifestFile file in Files)
163 {
164 ValidateHash(file.Sha256, $"Files[{file.Path}].Sha256");
165 }
166
167 if (Allocations.Select(static value => value.FileId).Distinct().Count() != Allocations.Count ||
168 Allocations.Any(static value => value.FileId < 0 || value.Offset < 0 || value.Length < 0))
169 {
170 throw new InvalidDataException("Manifest allocations must have unique File IDs and non-negative regions.");
171 }
172
173 foreach (NdsManifestAllocation allocation in Allocations)
174 {
175 ValidateHash(allocation.Sha256, $"Allocations[{allocation.FileId}].Sha256");
176 }
177
178 if (Overlays.Select(static value => $"{value.Processor}:{value.OverlayId}").Distinct(StringComparer.Ordinal).Count() != Overlays.Count ||
179 Overlays.Any(static value =>
180 value.Processor is not NdsProcessor.Arm9 and not NdsProcessor.Arm7 ||
181 value.Offset.HasValue != value.Length.HasValue || value.Offset < 0 || value.Length < 0 ||
182 value.Offset.HasValue != (value.Sha256 is not null)))
183 {
184 throw new InvalidDataException("Manifest Overlays must have unique DS processor identities and coherent payload regions.");
185 }
186
187 foreach (NdsManifestOverlay overlay in Overlays.Where(static value => value.Sha256 is not null))
188 {
189 ValidateHash(overlay.Sha256!, $"Overlays[{overlay.Processor}:{overlay.OverlayId}].Sha256");
190 }
191
192 if (Banner is not null)
193 {
194 if (Banner.Offset < 0 || Banner.Length < 0 || Banner.Titles is null)
195 {
196 throw new InvalidDataException("The manifest Banner contains an invalid region or title map.");
197 }
198
199 ValidateHash(Banner.Sha256, "Banner.Sha256");
200 }
201
202 if (Dsi is not null &&
203 (Dsi.ModcryptArea1 is null || Dsi.ModcryptArea2 is null ||
204 Dsi.ModcryptArea1.Offset < 0 || Dsi.ModcryptArea1.Length < 0 ||
205 Dsi.ModcryptArea2.Offset < 0 || Dsi.ModcryptArea2.Length < 0))
206 {
207 throw new InvalidDataException("The manifest DSi modcrypt regions are incomplete or negative.");
208 }
209 }
210
214 private static void ValidateHash(string? value, string name)
215 {
216 if (value is null || value.Length != 64 ||
217 value.Any(static character => !char.IsAsciiHexDigit(character) || char.IsUpper(character)))
218 {
219 throw new InvalidDataException($"Manifest field {name} is not a canonical lowercase SHA-256 digest.");
220 }
221 }
222
226 private static JsonSerializerOptions CreateJsonOptions(bool indented)
227 {
228 var options = new JsonSerializerOptions
229 {
230 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
231 WriteIndented = indented,
232 UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
233 };
234 options.Converters.Add(new JsonStringEnumConverter());
235 return options;
236 }
237}
Provides a stable, content-addressed description of one parsed image for CI artifacts,...
const int CurrentSchemaVersion
Identifies the currently supported JSON contract and prevents silent interpretation of future shapes.
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.
int SchemaVersion
Records the contract version required to interpret every other field.
async ValueTask WriteJsonAsync(Stream destination, bool indented=true, CancellationToken cancellationToken=default)
Writes one UTF-8 JSON document at the destination's current position without closing the stream.
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.
string ToJson(bool indented=true)
Serializes this contract with enum names and deterministic property order suitable for source review.
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.
static async ValueTask< NdsImageManifest > ReadJsonAsync(Stream source, CancellationToken cancellationToken=default)
Reads one UTF-8 JSON document without closing the caller-owned source stream.
static NdsImageManifest ParseJson(string json)
Parses a complete JSON contract and rejects unknown schema versions or incomplete required identity.
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
Snapshots banner format, content, localized text, and physical placement without embedding rendered p...
Snapshots DSi-specific title, size, security-mode, and modcrypt layout metadata.
Snapshots common header identity, execution policy, and size claims in serialization-stable scalar fi...
string Title
Preserves the visible fixed-width cartridge title after format padding is removed.
string MakerCode
Preserves the two-character publisher identity without resolving an external company database.
string GameCode
Preserves the exact four-character product identity used by tools and cryptographic derivations.
string Sha256
Hashes every parsed common or extended header byte, including reserved fields, with SHA-256.
NdsImageKind Kind
Records the DS, DSi-enhanced, or DSi-exclusive unit-code interpretation.
NdsProcessor
Identifies a processor and execution mode.