2using System.Text.Json.Serialization;
15 private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions(indented:
false);
28 public IReadOnlyList<NdsManifestProgram>
Programs {
get; init; } = [];
32 public IReadOnlyList<NdsManifestFile>
Files {
get; init; } = [];
34 public IReadOnlyList<NdsManifestAllocation>
Allocations {
get; init; } = [];
36 public IReadOnlyList<NdsManifestOverlay>
Overlays {
get; init; } = [];
46 CancellationToken cancellationToken =
default) =>
47 NdsImageManifestCapture.CaptureAsync(image, cancellationToken);
52 public string ToJson(
bool indented =
true)
55 return JsonSerializer.Serialize(
this, CreateJsonOptions(indented));
63 ArgumentException.ThrowIfNullOrWhiteSpace(json);
65 throw new InvalidDataException(
"The JSON document did not contain an NDS image manifest.");
78 CancellationToken cancellationToken =
default)
80 ArgumentNullException.ThrowIfNull(destination);
81 if (!destination.CanWrite)
83 throw new ArgumentException(
"The manifest destination must be writable.", nameof(destination));
87 await JsonSerializer.SerializeAsync(
90 CreateJsonOptions(indented),
91 cancellationToken).ConfigureAwait(
false);
100 CancellationToken cancellationToken =
default)
102 ArgumentNullException.ThrowIfNull(source);
105 throw new ArgumentException(
"The manifest source must be readable.", nameof(source));
111 cancellationToken).ConfigureAwait(
false) ??
112 throw new InvalidDataException(
"The JSON document did not contain an NDS image manifest.");
118 internal void Validate()
123 throw new InvalidDataException(
"The NDS image manifest schema or required fields are invalid.");
130 Programs.Any(
static value => value is
null) ||
132 Files.Any(
static value => value is
null) ||
134 Overlays.Any(
static value => value is
null))
136 throw new InvalidDataException(
"The manifest contains an invalid image kind or null collection entry.");
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))
142 throw new InvalidDataException(
"Manifest Programs must have unique processors and non-negative regions.");
145 foreach (NdsManifestProgram program
in Programs)
147 ValidateHash(program.Sha256, $
"Programs[{program.Processor}].Sha256");
151 Directories.Any(
static value =>
string.IsNullOrEmpty(value) || !value.StartsWith(
'/')))
153 throw new InvalidDataException(
"Manifest directories must be unique canonical absolute paths.");
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))
159 throw new InvalidDataException(
"Manifest files must have unique paths, valid File IDs, and non-negative regions.");
162 foreach (NdsManifestFile file
in Files)
164 ValidateHash(file.Sha256, $
"Files[{file.Path}].Sha256");
168 Allocations.Any(
static value => value.FileId < 0 || value.Offset < 0 || value.Length < 0))
170 throw new InvalidDataException(
"Manifest allocations must have unique File IDs and non-negative regions.");
173 foreach (NdsManifestAllocation allocation
in Allocations)
175 ValidateHash(allocation.Sha256, $
"Allocations[{allocation.FileId}].Sha256");
178 if (
Overlays.Select(
static value => $
"{value.Processor}:{value.OverlayId}").Distinct(StringComparer.Ordinal).Count() !=
Overlays.Count ||
181 value.Offset.HasValue != value.Length.HasValue || value.Offset < 0 || value.Length < 0 ||
182 value.Offset.HasValue != (value.Sha256 is not
null)))
184 throw new InvalidDataException(
"Manifest Overlays must have unique DS processor identities and coherent payload regions.");
187 foreach (NdsManifestOverlay overlay
in Overlays.Where(
static value => value.Sha256 is not
null))
189 ValidateHash(overlay.Sha256!, $
"Overlays[{overlay.Processor}:{overlay.OverlayId}].Sha256");
196 throw new InvalidDataException(
"The manifest Banner contains an invalid region or title map.");
199 ValidateHash(
Banner.Sha256,
"Banner.Sha256");
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))
207 throw new InvalidDataException(
"The manifest DSi modcrypt regions are incomplete or negative.");
214 private static void ValidateHash(
string? value,
string name)
216 if (value is
null || value.Length != 64 ||
217 value.Any(
static character => !
char.IsAsciiHexDigit(character) ||
char.IsUpper(character)))
219 throw new InvalidDataException($
"Manifest field {name} is not a canonical lowercase SHA-256 digest.");
226 private static JsonSerializerOptions CreateJsonOptions(
bool indented)
228 var options =
new JsonSerializerOptions
230 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
231 WriteIndented = indented,
232 UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
234 options.Converters.Add(
new JsonStringEnumConverter());
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.
Snapshots banner format, content, localized text, and physical placement without embedding rendered p...
Snapshots DSi-specific title, size, security-mode, and modcrypt layout metadata.
NdsProcessor
Identifies a processor and execution mode.