From 1cec6fa9182747e6aac5e0e9bf45e474e71b5bf3 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 31 Mar 2026 22:09:32 -0400 Subject: [PATCH] Add No-Intro database export reader and writer --- .../NoIntroDatabaseTests.cs | 480 +++++++++++++++++ .../NoIntroDatabase.cs | 491 ++++++++++++++++++ .../NoIntroDatabaseTests.cs | 24 + .../NoIntroDatabase.cs | 350 +++++++++++++ 4 files changed, 1345 insertions(+) create mode 100644 SabreTools.Serialization.Readers.Test/NoIntroDatabaseTests.cs create mode 100644 SabreTools.Serialization.Readers/NoIntroDatabase.cs create mode 100644 SabreTools.Serialization.Writers.Test/NoIntroDatabaseTests.cs create mode 100644 SabreTools.Serialization.Writers/NoIntroDatabase.cs diff --git a/SabreTools.Serialization.Readers.Test/NoIntroDatabaseTests.cs b/SabreTools.Serialization.Readers.Test/NoIntroDatabaseTests.cs new file mode 100644 index 00000000..58c29bf5 --- /dev/null +++ b/SabreTools.Serialization.Readers.Test/NoIntroDatabaseTests.cs @@ -0,0 +1,480 @@ +using System.IO; +using System.Linq; +using Xunit; + +namespace SabreTools.Serialization.Readers.Test +{ + public class NoIntroDatabaseTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var deserializer = new NoIntroDatabase(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var deserializer = new NoIntroDatabase(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void InvalidArray_Null() + { + byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; + int offset = 0; + var deserializer = new NoIntroDatabase(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var deserializer = new NoIntroDatabase(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var deserializer = new NoIntroDatabase(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var deserializer = new NoIntroDatabase(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void RoundTripTest() + { + // Get the serializer and deserializer + var deserializer = new NoIntroDatabase(); + var serializer = new Writers.NoIntroDatabase(); + + // Build the data + Data.Models.NoIntroDatabase.Datafile df = Build(); + + // Serialize to stream + Stream? metadata = serializer.SerializeStream(df); + Assert.NotNull(metadata); + + // Serialize back to original model + Data.Models.NoIntroDatabase.Datafile? newDf = deserializer.Deserialize(metadata); + + // Validate the data + Assert.NotNull(newDf); + + Assert.NotNull(newDf.Game); + var newGame = Assert.Single(newDf.Game); + Validate(newGame); + } + + /// + /// Build model for serialization and deserialization + /// + private static Data.Models.NoIntroDatabase.Datafile Build() + { + var archive = new Data.Models.NoIntroDatabase.Archive + { + Number = "XXXXXX", + Clone = "XXXXXX", + RegParent = "XXXXXX", + MergeOf = "XXXXXX", + MergeName = "XXXXXX", + Name = "XXXXXX", + NameAlt = "XXXXXX", + Region = "XXXXXX", + Languages = "XXXXXX", + ShowLang = "XXXXXX", + LangChecked = "XXXXXX", + Version1 = "XXXXXX", + Version2 = "XXXXXX", + DevStatus = "XXXXXX", + Additional = "XXXXXX", + Special1 = "XXXXXX", + Special2 = "XXXXXX", + Alt = "XXXXXX", + GameId1 = "XXXXXX", + GameId2 = "XXXXXX", + Description = "XXXXXX", + Bios = "XXXXXX", + Licensed = "XXXXXX", + Pirate = "XXXXXX", + Physical = "XXXXXX", + Complete = "XXXXXX", + Adult = "XXXXXX", + Dat = "XXXXXX", + Listed = "XXXXXX", + Private = "XXXXXX", + StickyNote = "XXXXXX", + DatterNote = "XXXXXX", + Categories = "XXXXXX", + }; + + var media = new Data.Models.NoIntroDatabase.Media(); + + var sourceDetails = new Data.Models.NoIntroDatabase.SourceDetails + { + Id = "XXXXXX", + AppendToNumber = "XXXXXX", + Section = "XXXXXX", + RomInfo = "XXXXXX", + DumpDate = "XXXXXX", + DumpDateInfo = "XXXXXX", + ReleaseDate = "XXXXXX", + ReleaseDateInfo = "XXXXXX", + Dumper = "XXXXXX", + Project = "XXXXXX", + OriginalFormat = "XXXXXX", + Nodump = "XXXXXX", + Tool = "XXXXXX", + Origin = "XXXXXX", + Comment1 = "XXXXXX", + Comment2 = "XXXXXX", + Link1 = "XXXXXX", + Link1Public = "XXXXXX", + Link2 = "XXXXXX", + Link2Public = "XXXXXX", + Link3 = "XXXXXX", + Link3Public = "XXXXXX", + Region = "XXXXXX", + MediaTitle = "XXXXXX", + }; + + var serials = new Data.Models.NoIntroDatabase.Serials + { + MediaSerial1 = "XXXXXX", + MediaSerial2 = "XXXXXX", + MediaSerial3 = "XXXXXX", + PCBSerial = "XXXXXX", + RomChipSerial1 = "XXXXXX", + RomChipSerial2 = "XXXXXX", + LockoutSerial = "XXXXXX", + SaveChipSerial = "XXXXXX", + ChipSerial = "XXXXXX", + BoxSerial = "XXXXXX", + MediaStamp = "XXXXXX", + BoxBarcode = "XXXXXX", + DigitalSerial1 = "XXXXXX", + DigitalSerial2 = "XXXXXX", + }; + + var file = new Data.Models.NoIntroDatabase.File + { + Id = "XXXXXX", + AppendToSourceId = "XXXXXX", + ForceName = "XXXXXX", + ForceSceneName = "XXXXXX", + EmptyDir = "XXXXXX", + Extension = "XXXXXX", + Item = "XXXXXX", + Date = "XXXXXX", + Format = "XXXXXX", + Note = "XXXXXX", + Filter = "XXXXXX", + Version = "XXXXXX", + UpdateType = "XXXXXX", + Size = "XXXXXX", + CRC32 = "XXXXXX", + MD5 = "XXXXXX", + SHA1 = "XXXXXX", + SHA256 = "XXXXXX", + Serial = "XXXXXX", + Header = "XXXXXX", + Bad = "XXXXXX", + MIA = "XXXXXX", + Unique = "XXXXXX", + MergeName = "XXXXXX", + UniqueAttachment = "XXXXXX", + }; + + var source = new Data.Models.NoIntroDatabase.Source + { + Details = sourceDetails, + Serials = serials, + File = [file], + }; + + var releaseDetails = new Data.Models.NoIntroDatabase.ReleaseDetails + { + Id = "XXXXXX", + AppendToNumber = "XXXXXX", + Date = "XXXXXX", + OriginalFormat = "XXXXXX", + Group = "XXXXXX", + DirName = "XXXXXX", + NfoName = "XXXXXX", + NfoSize = "XXXXXX", + NfoCRC = "XXXXXX", + ArchiveName = "XXXXXX", + RomInfo = "XXXXXX", + Category = "XXXXXX", + Comment = "XXXXXX", + Tool = "XXXXXX", + Region = "XXXXXX", + Origin = "XXXXXX", + }; + + var release = new Data.Models.NoIntroDatabase.Release + { + Details = releaseDetails, + Serials = serials, + File = [file], + }; + + var game = new Data.Models.NoIntroDatabase.Game + { + Name = "XXXXXX", + Archive = archive, + Media = [media], + Source = [source], + Release = [release], + }; + + return new Data.Models.NoIntroDatabase.Datafile + { + Game = [game], + }; + } + + /// + /// Validate a Game + /// + private static void Validate(Data.Models.NoIntroDatabase.Game? game) + { + Assert.NotNull(game); + Assert.Equal("XXXXXX", game.Name); + + Validate(game.Archive); + + Assert.NotNull(game.Media); + Data.Models.NoIntroDatabase.Media media = Assert.Single(game.Media); + Validate(media); + + Assert.NotNull(game.Source); + Data.Models.NoIntroDatabase.Source source = Assert.Single(game.Source); + Validate(source); + + Assert.NotNull(game.Release); + Data.Models.NoIntroDatabase.Release release = Assert.Single(game.Release); + Validate(release); + } + + /// + /// Validate a Archive + /// + private static void Validate(Data.Models.NoIntroDatabase.Archive? archive) + { + Assert.NotNull(archive); + Assert.Equal("XXXXXX", archive.Number); + Assert.Equal("XXXXXX", archive.Clone); + Assert.Equal("XXXXXX", archive.RegParent); + Assert.Equal("XXXXXX", archive.MergeOf); + Assert.Equal("XXXXXX", archive.MergeName); + Assert.Equal("XXXXXX", archive.Name); + Assert.Equal("XXXXXX", archive.NameAlt); + Assert.Equal("XXXXXX", archive.Region); + Assert.Equal("XXXXXX", archive.Languages); + Assert.Equal("XXXXXX", archive.ShowLang); + Assert.Equal("XXXXXX", archive.LangChecked); + Assert.Equal("XXXXXX", archive.Version1); + Assert.Equal("XXXXXX", archive.Version2); + Assert.Equal("XXXXXX", archive.DevStatus); + Assert.Equal("XXXXXX", archive.Additional); + Assert.Equal("XXXXXX", archive.Special1); + Assert.Equal("XXXXXX", archive.Special2); + Assert.Equal("XXXXXX", archive.Alt); + Assert.Equal("XXXXXX", archive.GameId1); + Assert.Equal("XXXXXX", archive.GameId2); + Assert.Equal("XXXXXX", archive.Description); + Assert.Equal("XXXXXX", archive.Bios); + Assert.Equal("XXXXXX", archive.Licensed); + Assert.Equal("XXXXXX", archive.Pirate); + Assert.Equal("XXXXXX", archive.Physical); + Assert.Equal("XXXXXX", archive.Complete); + Assert.Equal("XXXXXX", archive.Adult); + Assert.Equal("XXXXXX", archive.Dat); + Assert.Equal("XXXXXX", archive.Listed); + Assert.Equal("XXXXXX", archive.Private); + Assert.Equal("XXXXXX", archive.StickyNote); + Assert.Equal("XXXXXX", archive.DatterNote); + Assert.Equal("XXXXXX", archive.Categories); + } + + /// + /// Validate a Media + /// + private static void Validate(Data.Models.NoIntroDatabase.Media? media) + { + Assert.NotNull(media); + } + + /// + /// Validate a Source + /// + private static void Validate(Data.Models.NoIntroDatabase.Source? source) + { + Assert.NotNull(source); + + Validate(source.Details); + + Validate(source.Serials); + + Assert.NotNull(source.File); + Data.Models.NoIntroDatabase.File file = Assert.Single(source.File); + Validate(file); + } + + /// + /// Validate a Rom + /// + private static void Validate(Data.Models.NoIntroDatabase.SourceDetails? sourceDetails) + { + Assert.NotNull(sourceDetails); + Assert.Equal("XXXXXX", sourceDetails.Id); + Assert.Equal("XXXXXX", sourceDetails.AppendToNumber); + Assert.Equal("XXXXXX", sourceDetails.Section); + Assert.Equal("XXXXXX", sourceDetails.RomInfo); + Assert.Equal("XXXXXX", sourceDetails.DumpDate); + Assert.Equal("XXXXXX", sourceDetails.DumpDateInfo); + Assert.Equal("XXXXXX", sourceDetails.ReleaseDate); + Assert.Equal("XXXXXX", sourceDetails.ReleaseDateInfo); + Assert.Equal("XXXXXX", sourceDetails.Dumper); + Assert.Equal("XXXXXX", sourceDetails.Project); + Assert.Equal("XXXXXX", sourceDetails.OriginalFormat); + Assert.Equal("XXXXXX", sourceDetails.Nodump); + Assert.Equal("XXXXXX", sourceDetails.Tool); + Assert.Equal("XXXXXX", sourceDetails.Origin); + Assert.Equal("XXXXXX", sourceDetails.Comment1); + Assert.Equal("XXXXXX", sourceDetails.Comment2); + Assert.Equal("XXXXXX", sourceDetails.Link1); + Assert.Equal("XXXXXX", sourceDetails.Link1Public); + Assert.Equal("XXXXXX", sourceDetails.Link2); + Assert.Equal("XXXXXX", sourceDetails.Link2Public); + Assert.Equal("XXXXXX", sourceDetails.Link3); + Assert.Equal("XXXXXX", sourceDetails.Link3Public); + Assert.Equal("XXXXXX", sourceDetails.Region); + Assert.Equal("XXXXXX", sourceDetails.MediaTitle); + } + + /// + /// Validate a Serials + /// + private static void Validate(Data.Models.NoIntroDatabase.Serials? serials) + { + Assert.NotNull(serials); + Assert.Equal("XXXXXX", serials.MediaSerial1); + Assert.Equal("XXXXXX", serials.MediaSerial2); + Assert.Equal("XXXXXX", serials.MediaSerial3); + Assert.Equal("XXXXXX", serials.PCBSerial); + Assert.Equal("XXXXXX", serials.RomChipSerial1); + Assert.Equal("XXXXXX", serials.RomChipSerial2); + Assert.Equal("XXXXXX", serials.LockoutSerial); + Assert.Equal("XXXXXX", serials.SaveChipSerial); + Assert.Equal("XXXXXX", serials.ChipSerial); + Assert.Equal("XXXXXX", serials.BoxSerial); + Assert.Equal("XXXXXX", serials.MediaStamp); + Assert.Equal("XXXXXX", serials.BoxBarcode); + Assert.Equal("XXXXXX", serials.DigitalSerial1); + Assert.Equal("XXXXXX", serials.DigitalSerial2); + } + + /// + /// Validate a File + /// + private static void Validate(Data.Models.NoIntroDatabase.File? file) + { + Assert.NotNull(file); + Assert.Equal("XXXXXX", file.Id); + Assert.Equal("XXXXXX", file.AppendToSourceId); + Assert.Equal("XXXXXX", file.ForceName); + Assert.Equal("XXXXXX", file.ForceSceneName); + Assert.Equal("XXXXXX", file.EmptyDir); + Assert.Equal("XXXXXX", file.Extension); + Assert.Equal("XXXXXX", file.Item); + Assert.Equal("XXXXXX", file.Date); + Assert.Equal("XXXXXX", file.Format); + Assert.Equal("XXXXXX", file.Note); + Assert.Equal("XXXXXX", file.Filter); + Assert.Equal("XXXXXX", file.Version); + Assert.Equal("XXXXXX", file.UpdateType); + Assert.Equal("XXXXXX", file.Size); + Assert.Equal("XXXXXX", file.CRC32); + Assert.Equal("XXXXXX", file.MD5); + Assert.Equal("XXXXXX", file.SHA1); + Assert.Equal("XXXXXX", file.SHA256); + Assert.Equal("XXXXXX", file.Serial); + Assert.Equal("XXXXXX", file.Header); + Assert.Equal("XXXXXX", file.Bad); + Assert.Equal("XXXXXX", file.MIA); + Assert.Equal("XXXXXX", file.Unique); + Assert.Equal("XXXXXX", file.MergeName); + Assert.Equal("XXXXXX", file.UniqueAttachment); + } + + /// + /// Validate a Release + /// + private static void Validate(Data.Models.NoIntroDatabase.Release? release) + { + Assert.NotNull(release); + + Validate(release.Details); + + Validate(release.Serials); + + Assert.NotNull(release.File); + Data.Models.NoIntroDatabase.File file = Assert.Single(release.File); + Validate(file); + } + + /// + /// Validate a ReleaseDetails + /// + private static void Validate(Data.Models.NoIntroDatabase.ReleaseDetails? releaseDetails) + { + Assert.NotNull(releaseDetails); + Assert.Equal("XXXXXX", releaseDetails.Id); + Assert.Equal("XXXXXX", releaseDetails.AppendToNumber); + Assert.Equal("XXXXXX", releaseDetails.Date); + Assert.Equal("XXXXXX", releaseDetails.OriginalFormat); + Assert.Equal("XXXXXX", releaseDetails.Group); + Assert.Equal("XXXXXX", releaseDetails.DirName); + Assert.Equal("XXXXXX", releaseDetails.NfoName); + Assert.Equal("XXXXXX", releaseDetails.NfoSize); + Assert.Equal("XXXXXX", releaseDetails.NfoCRC); + Assert.Equal("XXXXXX", releaseDetails.ArchiveName); + Assert.Equal("XXXXXX", releaseDetails.RomInfo); + Assert.Equal("XXXXXX", releaseDetails.Category); + Assert.Equal("XXXXXX", releaseDetails.Comment); + Assert.Equal("XXXXXX", releaseDetails.Tool); + Assert.Equal("XXXXXX", releaseDetails.Region); + Assert.Equal("XXXXXX", releaseDetails.Origin); + } + } +} diff --git a/SabreTools.Serialization.Readers/NoIntroDatabase.cs b/SabreTools.Serialization.Readers/NoIntroDatabase.cs new file mode 100644 index 00000000..1109170c --- /dev/null +++ b/SabreTools.Serialization.Readers/NoIntroDatabase.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using SabreTools.Data.Models.NoIntroDatabase; + +namespace SabreTools.Serialization.Readers +{ + public class NoIntroDatabase : BaseBinaryReader + { + /// + public override Datafile? Deserialize(Stream? data) + { + // If the data is invalid + if (data is null || !data.CanRead) + return null; + + try + { + // Cache the current offset + long initialOffset = data.Position; + + // Create the XmlTextReader + var reader = new XmlTextReader(data); + reader.WhitespaceHandling = WhitespaceHandling.None; + + // Parse the XML, if possible + Datafile? datafile = null; + while (reader.Read()) + { + // An ending element means exit + if (reader.NodeType == XmlNodeType.EndElement) + break; + + // Only process starting elements + if (!reader.IsStartElement()) + continue; + + switch (reader.Name) + { + case "datafile": + if (datafile is not null && Debug) + Console.WriteLine($"'{reader.Name}' element already found, overwriting"); + + datafile = ParseDatafile(reader); + break; + + default: + if (Debug) Console.Error.WriteLine($"Element '{reader.Name}' is not recognized"); + break; + } + } + + return datafile; + } + catch + { + // Ignore the actual error + return null; + } + } + + /// + /// Parse from an XmlTextReader into a Datafile + /// + /// XmlTextReader to read from + /// Filled Datafile on success, null on error + public Datafile ParseDatafile(XmlTextReader reader) + { + var obj = new Datafile(); + + List games = []; + while (reader.Read()) + { + // An ending element means exit + if (reader.NodeType == XmlNodeType.EndElement) + break; + + // Only process starting elements + if (!reader.IsStartElement()) + continue; + + switch (reader.Name) + { + case "game": + var game = ParseGame(reader); + if (game is not null) + games.Add(game); + + break; + + default: + if (Debug) Console.Error.WriteLine($"Element '{reader.Name}' is not recognized"); + break; + } + } + + if (games.Count > 0) + obj.Game = [.. games]; + + return obj; + } + + #region Items + + /// + /// Parse from an XmlTextReader into a Archive + /// + /// XmlTextReader to read from + /// Filled Archive on success, null on error + public Archive ParseArchive(XmlTextReader reader) + { + var obj = new Archive(); + + obj.Number = reader.GetAttribute("number"); + obj.Clone = reader.GetAttribute("clone"); + obj.RegParent = reader.GetAttribute("regparent"); + obj.MergeOf = reader.GetAttribute("mergeof"); + obj.MergeName = reader.GetAttribute("mergename"); + obj.Name = reader.GetAttribute("name"); + obj.NameAlt = reader.GetAttribute("name_alt"); + obj.Region = reader.GetAttribute("region"); + obj.Languages = reader.GetAttribute("languages"); + obj.ShowLang = reader.GetAttribute("showlang"); + obj.LangChecked = reader.GetAttribute("langchecked"); + obj.Version1 = reader.GetAttribute("version1"); + obj.Version2 = reader.GetAttribute("version2"); + obj.DevStatus = reader.GetAttribute("devstatus"); + obj.Additional = reader.GetAttribute("additional"); + obj.Special1 = reader.GetAttribute("special1"); + obj.Special2 = reader.GetAttribute("special2"); + obj.Alt = reader.GetAttribute("alt"); + obj.GameId1 = reader.GetAttribute("gameid1"); + obj.GameId2 = reader.GetAttribute("gameid2"); + obj.Description = reader.GetAttribute("description"); + obj.Bios = reader.GetAttribute("bios"); + obj.Licensed = reader.GetAttribute("licensed"); + obj.Pirate = reader.GetAttribute("pirate"); + obj.Physical = reader.GetAttribute("physical"); + obj.Complete = reader.GetAttribute("complete"); + obj.Adult = reader.GetAttribute("adult"); + obj.Dat = reader.GetAttribute("dat"); + obj.Listed = reader.GetAttribute("listed"); + obj.Private = reader.GetAttribute("private"); + obj.StickyNote = reader.GetAttribute("sticky_note"); + obj.DatterNote = reader.GetAttribute("datter_note"); + obj.Categories = reader.GetAttribute("categories"); + + return obj; + } + + /// + /// Parse from an XmlTextReader into a File + /// + /// XmlTextReader to read from + /// Filled File on success, null on error + public Data.Models.NoIntroDatabase.File ParseFile(XmlTextReader reader) + { + var obj = new Data.Models.NoIntroDatabase.File(); + + obj.Id = reader.GetAttribute("id"); + obj.AppendToSourceId = reader.GetAttribute("append_to_source_id"); + obj.ForceName = reader.GetAttribute("forcename"); + obj.ForceSceneName = reader.GetAttribute("forcescenename"); + obj.EmptyDir = reader.GetAttribute("emptydir"); + obj.Extension = reader.GetAttribute("extension"); + obj.Item = reader.GetAttribute("item"); + obj.Date = reader.GetAttribute("date"); + obj.Format = reader.GetAttribute("format"); + obj.Note = reader.GetAttribute("note"); + obj.Filter = reader.GetAttribute("filter"); + obj.Version = reader.GetAttribute("version"); + obj.UpdateType = reader.GetAttribute("update_type"); + obj.Size = reader.GetAttribute("size"); + obj.CRC32 = reader.GetAttribute("crc32"); + obj.MD5 = reader.GetAttribute("md5"); + obj.SHA1 = reader.GetAttribute("sha1"); + obj.SHA256 = reader.GetAttribute("sha256"); + obj.Serial = reader.GetAttribute("serial"); + obj.Header = reader.GetAttribute("header"); + obj.Bad = reader.GetAttribute("bad"); + obj.MIA = reader.GetAttribute("mia"); + obj.Unique = reader.GetAttribute("unique"); + obj.MergeName = reader.GetAttribute("mergename"); + obj.UniqueAttachment = reader.GetAttribute("unique_attachment"); + + return obj; + } + + /// + /// Parse from an XmlTextReader into a Game + /// + /// XmlTextReader to read from + /// Filled Game on success, null on error + public Game ParseGame(XmlTextReader reader) + { + var obj = new Game(); + + obj.Name = reader.GetAttribute("name"); + + List medias = []; + List sources = []; + List releases = []; + + reader.Read(); + while (!reader.EOF) + { + // An ending element means exit + if (reader.NodeType == XmlNodeType.EndElement) + break; + + // Only process starting elements + if (!reader.IsStartElement()) + continue; + + switch (reader.Name) + { + case "archive": + if (obj.Archive is not null && Debug) + Console.WriteLine($"'{reader.Name}' element already found, overwriting"); + + obj.Archive = ParseArchive(reader); + reader.Skip(); + break; + case "media": + var media = ParseMedia(reader); + if (media is not null) + medias.Add(media); + + reader.Skip(); + break; + case "source": + var source = ParseSource(reader); + if (source is not null) + sources.Add(source); + + reader.Skip(); + break; + case "release": + var release = ParseRelease(reader); + if (release is not null) + releases.Add(release); + + reader.Skip(); + break; + + default: + if (Debug) Console.Error.WriteLine($"Element '{reader.Name}' is not recognized"); + reader.Skip(); + break; + } + } + + if (medias.Count > 0) + obj.Media = [.. medias]; + if (sources.Count > 0) + obj.Source = [.. sources]; + if (releases.Count > 0) + obj.Release = [.. releases]; + + return obj; + } + + /// + /// Parse from an XmlTextReader into a Media + /// + /// XmlTextReader to read from + /// Filled Media on success, null on error + public Media ParseMedia(XmlTextReader reader) + { + var obj = new Media(); + + // This item is empty + + return obj; + } + + /// + /// Parse from an XmlTextReader into a Release + /// + /// XmlTextReader to read from + /// Filled Release on success, null on error + public Release ParseRelease(XmlTextReader reader) + { + var obj = new Release(); + + List files = []; + + reader.Read(); + while (!reader.EOF) + { + // An ending element means exit + if (reader.NodeType == XmlNodeType.EndElement) + break; + + // Only process starting elements + if (!reader.IsStartElement()) + continue; + + switch (reader.Name) + { + case "details": + if (obj.Details is not null && Debug) + Console.WriteLine($"'{reader.Name}' element already found, overwriting"); + + obj.Details = ParseReleaseDetails(reader); + reader.Skip(); + break; + case "serials": + if (obj.Serials is not null && Debug) + Console.WriteLine($"'{reader.Name}' element already found, overwriting"); + + obj.Serials = ParseSerials(reader); + reader.Skip(); + break; + case "file": + var file = ParseFile(reader); + if (file is not null) + files.Add(file); + + reader.Skip(); + break; + + default: + if (Debug) Console.Error.WriteLine($"Element '{reader.Name}' is not recognized"); + reader.Skip(); + break; + } + } + + if (files.Count > 0) + obj.File = [.. files]; + + return obj; + } + + /// + /// Parse from an XmlTextReader into a ReleaseDetails + /// + /// XmlTextReader to read from + /// Filled ReleaseDetails on success, null on error + public ReleaseDetails ParseReleaseDetails(XmlTextReader reader) + { + var obj = new ReleaseDetails(); + + obj.Id = reader.GetAttribute("id"); + obj.AppendToNumber = reader.GetAttribute("append_to_number"); + obj.Date = reader.GetAttribute("date"); + obj.OriginalFormat = reader.GetAttribute("originalformat"); + obj.Group = reader.GetAttribute("group"); + obj.DirName = reader.GetAttribute("dirname"); + obj.NfoName = reader.GetAttribute("nfoname"); + obj.NfoSize = reader.GetAttribute("nfosize"); + obj.NfoCRC = reader.GetAttribute("nfocrc"); + obj.ArchiveName = reader.GetAttribute("archivename"); + obj.RomInfo = reader.GetAttribute("rominfo"); + obj.Category = reader.GetAttribute("category"); + obj.Comment = reader.GetAttribute("comment"); + obj.Tool = reader.GetAttribute("tool"); + obj.Region = reader.GetAttribute("region"); + obj.Origin = reader.GetAttribute("origin"); + + return obj; + } + + /// + /// Parse from an XmlTextReader into a Serials + /// + /// XmlTextReader to read from + /// Filled Serials on success, null on error + public Serials ParseSerials(XmlTextReader reader) + { + var obj = new Serials(); + + obj.MediaSerial1 = reader.GetAttribute("media_serial1"); + obj.MediaSerial2 = reader.GetAttribute("media_serial2"); + obj.MediaSerial3 = reader.GetAttribute("media_serial3"); + obj.PCBSerial = reader.GetAttribute("pcb_serial"); + obj.RomChipSerial1 = reader.GetAttribute("romchip_serial1"); + obj.RomChipSerial2 = reader.GetAttribute("romchip_serial2"); + obj.LockoutSerial = reader.GetAttribute("lockout_serial"); + obj.SaveChipSerial = reader.GetAttribute("savechip_serial"); + obj.ChipSerial = reader.GetAttribute("chip_serial"); + obj.BoxSerial = reader.GetAttribute("box_serial"); + obj.MediaStamp = reader.GetAttribute("mediastamp"); + obj.BoxBarcode = reader.GetAttribute("box_barcode"); + obj.DigitalSerial1 = reader.GetAttribute("digital_serial1"); + obj.DigitalSerial2 = reader.GetAttribute("digital_serial2"); + + return obj; + } + + /// + /// Parse from an XmlTextReader into a Source + /// + /// XmlTextReader to read from + /// Filled Source on success, null on error + public Source ParseSource(XmlTextReader reader) + { + var obj = new Source(); + + List files = []; + + reader.Read(); + while (!reader.EOF) + { + // An ending element means exit + if (reader.NodeType == XmlNodeType.EndElement) + break; + + // Only process starting elements + if (!reader.IsStartElement()) + continue; + + switch (reader.Name) + { + case "details": + if (obj.Details is not null && Debug) + Console.WriteLine($"'{reader.Name}' element already found, overwriting"); + + obj.Details = ParseSourceDetails(reader); + reader.Skip(); + break; + case "serials": + if (obj.Serials is not null && Debug) + Console.WriteLine($"'{reader.Name}' element already found, overwriting"); + + obj.Serials = ParseSerials(reader); + reader.Skip(); + break; + case "file": + var file = ParseFile(reader); + if (file is not null) + files.Add(file); + + reader.Skip(); + break; + + default: + if (Debug) Console.Error.WriteLine($"Element '{reader.Name}' is not recognized"); + reader.Skip(); + break; + } + } + + if (files.Count > 0) + obj.File = [.. files]; + + return obj; + } + + /// + /// Parse from an XmlTextReader into a SourceDetails + /// + /// XmlTextReader to read from + /// Filled SourceDetails on success, null on error + public SourceDetails ParseSourceDetails(XmlTextReader reader) + { + var obj = new SourceDetails(); + + obj.Id = reader.GetAttribute("id"); + obj.AppendToNumber = reader.GetAttribute("append_to_number"); + obj.Section = reader.GetAttribute("section"); + obj.RomInfo = reader.GetAttribute("rominfo"); + obj.DumpDate = reader.GetAttribute("d_date"); + obj.DumpDateInfo = reader.GetAttribute("d_date_info"); + obj.ReleaseDate = reader.GetAttribute("r_date"); + obj.ReleaseDateInfo = reader.GetAttribute("r_date_info"); + obj.Dumper = reader.GetAttribute("dumper"); + obj.Project = reader.GetAttribute("project"); + obj.OriginalFormat = reader.GetAttribute("originalformat"); + obj.Nodump = reader.GetAttribute("nodump"); + obj.Tool = reader.GetAttribute("tool"); + obj.Origin = reader.GetAttribute("origin"); + obj.Comment1 = reader.GetAttribute("comment1"); + obj.Comment2 = reader.GetAttribute("comment2"); + obj.Link1 = reader.GetAttribute("link1"); + obj.Link1Public = reader.GetAttribute("link1_public"); + obj.Link2 = reader.GetAttribute("link2"); + obj.Link2Public = reader.GetAttribute("link2_public"); + obj.Link3 = reader.GetAttribute("link3"); + obj.Link3Public = reader.GetAttribute("link3_public"); + obj.Region = reader.GetAttribute("region"); + obj.MediaTitle = reader.GetAttribute("media_title"); + + return obj; + } + + #endregion + } +} diff --git a/SabreTools.Serialization.Writers.Test/NoIntroDatabaseTests.cs b/SabreTools.Serialization.Writers.Test/NoIntroDatabaseTests.cs new file mode 100644 index 00000000..1cc17575 --- /dev/null +++ b/SabreTools.Serialization.Writers.Test/NoIntroDatabaseTests.cs @@ -0,0 +1,24 @@ +using System.IO; +using Xunit; + +namespace SabreTools.Serialization.Writers.Test +{ + public class NoIntroDatabaseTests + { + [Fact] + public void SerializeArray_Null_Null() + { + var serializer = new NoIntroDatabase(); + byte[]? actual = serializer.SerializeArray(null); + Assert.Null(actual); + } + + [Fact] + public void SerializeStream_Null_Null() + { + var serializer = new NoIntroDatabase(); + Stream? actual = serializer.SerializeStream(null); + Assert.Null(actual); + } + } +} diff --git a/SabreTools.Serialization.Writers/NoIntroDatabase.cs b/SabreTools.Serialization.Writers/NoIntroDatabase.cs new file mode 100644 index 00000000..1c391fd8 --- /dev/null +++ b/SabreTools.Serialization.Writers/NoIntroDatabase.cs @@ -0,0 +1,350 @@ +using System.IO; +using System.Text; +using System.Xml; +using SabreTools.Data.Models.NoIntroDatabase; +using SabreTools.IO.Extensions; +using SabreTools.Text.Extensions; + +namespace SabreTools.Serialization.Writers +{ + public class NoIntroDatabase : BaseBinaryWriter + { + /// + public override Stream? SerializeStream(Datafile? obj) + { + // If the metadata file is null + if (obj is null) + return null; + + // Setup the writer and output + var stream = new MemoryStream(); + var writer = new XmlTextWriter(stream, Encoding.UTF8) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + writer.Settings?.CheckCharacters = false; + writer.Settings?.NewLineChars = "\n"; + + // Write document start + writer.WriteStartDocument(); + + // Write the SoftwareDb, if it exists + WriteDatafile(obj, writer); + writer.Flush(); + + // Return the stream + stream.SeekIfPossible(0, SeekOrigin.Begin); + return stream; + } + + /// + /// Write a Datafile to an XmlTextWriter + /// + /// Datafile to write + /// XmlTextReader to write to + private static void WriteDatafile(Datafile obj, XmlTextWriter writer) + { + writer.WriteStartElement("datafile"); + + if (obj.Game is not null && obj.Game.Length > 0) + { + foreach (var game in obj.Game) + { + WriteGame(game, writer); + } + } + + writer.WriteEndElement(); + } + + #region Items + + /// + /// Write a Archive to an XmlTextWriter + /// + /// Archive to write + /// XmlTextReader to write to + private static void WriteArchive(Archive obj, XmlTextWriter writer) + { + writer.WriteStartElement("archive"); + + writer.WriteOptionalAttributeString("number", obj.Number); + writer.WriteOptionalAttributeString("clone", obj.Clone); + writer.WriteOptionalAttributeString("regparent", obj.RegParent); + writer.WriteOptionalAttributeString("mergeof", obj.MergeOf); + writer.WriteOptionalAttributeString("mergename", obj.MergeName); + writer.WriteOptionalAttributeString("name", obj.Name); + writer.WriteOptionalAttributeString("name_alt", obj.NameAlt); + writer.WriteOptionalAttributeString("region", obj.Region); + writer.WriteOptionalAttributeString("languages", obj.Languages); + writer.WriteOptionalAttributeString("showlang", obj.ShowLang); + writer.WriteOptionalAttributeString("langchecked", obj.LangChecked); + writer.WriteOptionalAttributeString("version1", obj.Version1); + writer.WriteOptionalAttributeString("version2", obj.Version2); + writer.WriteOptionalAttributeString("devstatus", obj.DevStatus); + writer.WriteOptionalAttributeString("additional", obj.Additional); + writer.WriteOptionalAttributeString("special1", obj.Special1); + writer.WriteOptionalAttributeString("special2", obj.Special2); + writer.WriteOptionalAttributeString("alt", obj.Alt); + writer.WriteOptionalAttributeString("gameid1", obj.GameId1); + writer.WriteOptionalAttributeString("gameid2", obj.GameId2); + writer.WriteOptionalAttributeString("description", obj.Description); + writer.WriteOptionalAttributeString("bios", obj.Bios); + writer.WriteOptionalAttributeString("licensed", obj.Licensed); + writer.WriteOptionalAttributeString("pirate", obj.Pirate); + writer.WriteOptionalAttributeString("physical", obj.Physical); + writer.WriteOptionalAttributeString("complete", obj.Complete); + writer.WriteOptionalAttributeString("adult", obj.Adult); + writer.WriteOptionalAttributeString("dat", obj.Dat); + writer.WriteOptionalAttributeString("listed", obj.Listed); + writer.WriteOptionalAttributeString("private", obj.Private); + writer.WriteOptionalAttributeString("sticky_note", obj.StickyNote); + writer.WriteOptionalAttributeString("datter_note", obj.DatterNote); + writer.WriteOptionalAttributeString("categories", obj.Categories); + + writer.WriteEndElement(); + } + + /// + /// Write a File to an XmlTextWriter + /// + /// File to write + /// XmlTextReader to write to + private static void WriteFile(Data.Models.NoIntroDatabase.File obj, XmlTextWriter writer) + { + writer.WriteStartElement("file"); + + writer.WriteOptionalAttributeString("id", obj.Id); + writer.WriteOptionalAttributeString("append_to_source_id", obj.AppendToSourceId); + writer.WriteOptionalAttributeString("forcename", obj.ForceName); + writer.WriteOptionalAttributeString("forcescenename", obj.ForceSceneName); + writer.WriteOptionalAttributeString("emptydir", obj.EmptyDir); + writer.WriteOptionalAttributeString("extension", obj.Extension); + writer.WriteOptionalAttributeString("item", obj.Item); + writer.WriteOptionalAttributeString("date", obj.Date); + writer.WriteOptionalAttributeString("format", obj.Format); + writer.WriteOptionalAttributeString("note", obj.Note); + writer.WriteOptionalAttributeString("filter", obj.Filter); + writer.WriteOptionalAttributeString("version", obj.Version); + writer.WriteOptionalAttributeString("update_type", obj.UpdateType); + writer.WriteOptionalAttributeString("size", obj.Size); + writer.WriteOptionalAttributeString("crc32", obj.CRC32); + writer.WriteOptionalAttributeString("md5", obj.MD5); + writer.WriteOptionalAttributeString("sha1", obj.SHA1); + writer.WriteOptionalAttributeString("sha256", obj.SHA256); + writer.WriteOptionalAttributeString("serial", obj.Serial); + writer.WriteOptionalAttributeString("header", obj.Header); + writer.WriteOptionalAttributeString("bad", obj.Bad); + writer.WriteOptionalAttributeString("mia", obj.MIA); + writer.WriteOptionalAttributeString("unique", obj.Unique); + writer.WriteOptionalAttributeString("mergename", obj.MergeName); + writer.WriteOptionalAttributeString("unique_attachment", obj.UniqueAttachment); + + writer.WriteEndElement(); + } + + /// + /// Write a Game to an XmlTextWriter + /// + /// Game to write + /// XmlTextReader to write to + private static void WriteGame(Game obj, XmlTextWriter writer) + { + writer.WriteStartElement("game"); + + writer.WriteOptionalAttributeString("name", obj.Name); + + if (obj.Archive is not null) + WriteArchive(obj.Archive, writer); + + if (obj.Media is not null && obj.Media.Length > 0) + { + foreach (var media in obj.Media) + { + WriteMedia(media, writer); + } + } + + if (obj.Source is not null && obj.Source.Length > 0) + { + foreach (var source in obj.Source) + { + WriteSource(source, writer); + } + } + + if (obj.Release is not null && obj.Release.Length > 0) + { + foreach (var release in obj.Release) + { + WriteRelease(release, writer); + } + } + + writer.WriteEndElement(); + } + + /// + /// Write a Media to an XmlTextWriter + /// + /// Media to write + /// XmlTextReader to write to + private static void WriteMedia(Media obj, XmlTextWriter writer) + { + writer.WriteStartElement("media"); + + // This item is empty + + writer.WriteEndElement(); + } + + /// + /// Write a Release to an XmlTextWriter + /// + /// Release to write + /// XmlTextReader to write to + private static void WriteRelease(Release obj, XmlTextWriter writer) + { + writer.WriteStartElement("release"); + + if (obj.Details is not null) + WriteReleaseDetails(obj.Details, writer); + + if (obj.Serials is not null) + WriteSerials(obj.Serials, writer); + + if (obj.File is not null && obj.File.Length > 0) + { + foreach (var file in obj.File) + { + WriteFile(file, writer); + } + } + + writer.WriteEndElement(); + } + + /// + /// Write a ReleaseDetails to an XmlTextWriter + /// + /// ReleaseDetails to write + /// XmlTextReader to write to + private static void WriteReleaseDetails(ReleaseDetails obj, XmlTextWriter writer) + { + writer.WriteStartElement("details"); + + writer.WriteOptionalAttributeString("id", obj.Id); + writer.WriteOptionalAttributeString("append_to_number", obj.AppendToNumber); + writer.WriteOptionalAttributeString("date", obj.Date); + writer.WriteOptionalAttributeString("originalformat", obj.OriginalFormat); + writer.WriteOptionalAttributeString("group", obj.Group); + writer.WriteOptionalAttributeString("dirname", obj.DirName); + writer.WriteOptionalAttributeString("nfoname", obj.NfoName); + writer.WriteOptionalAttributeString("nfosize", obj.NfoSize); + writer.WriteOptionalAttributeString("nfocrc", obj.NfoCRC); + writer.WriteOptionalAttributeString("archivename", obj.ArchiveName); + writer.WriteOptionalAttributeString("rominfo", obj.RomInfo); + writer.WriteOptionalAttributeString("category", obj.Category); + writer.WriteOptionalAttributeString("comment", obj.Comment); + writer.WriteOptionalAttributeString("tool", obj.Tool); + writer.WriteOptionalAttributeString("region", obj.Region); + writer.WriteOptionalAttributeString("origin", obj.Origin); + + writer.WriteEndElement(); + } + + /// + /// Write a Serials to an XmlTextWriter + /// + /// Serials to write + /// XmlTextReader to write to + private static void WriteSerials(Serials obj, XmlTextWriter writer) + { + writer.WriteStartElement("serials"); + + writer.WriteOptionalAttributeString("media_serial1", obj.MediaSerial1); + writer.WriteOptionalAttributeString("media_serial2", obj.MediaSerial2); + writer.WriteOptionalAttributeString("media_serial3", obj.MediaSerial3); + writer.WriteOptionalAttributeString("pcb_serial", obj.PCBSerial); + writer.WriteOptionalAttributeString("romchip_serial1", obj.RomChipSerial1); + writer.WriteOptionalAttributeString("romchip_serial2", obj.RomChipSerial2); + writer.WriteOptionalAttributeString("lockout_serial", obj.LockoutSerial); + writer.WriteOptionalAttributeString("savechip_serial", obj.SaveChipSerial); + writer.WriteOptionalAttributeString("chip_serial", obj.ChipSerial); + writer.WriteOptionalAttributeString("box_serial", obj.BoxSerial); + writer.WriteOptionalAttributeString("mediastamp", obj.MediaStamp); + writer.WriteOptionalAttributeString("box_barcode", obj.BoxBarcode); + writer.WriteOptionalAttributeString("digital_serial1", obj.DigitalSerial1); + writer.WriteOptionalAttributeString("digital_serial2", obj.DigitalSerial2); + + writer.WriteEndElement(); + } + + /// + /// Write a Source to an XmlTextWriter + /// + /// Source to write + /// XmlTextReader to write to + private static void WriteSource(Source obj, XmlTextWriter writer) + { + writer.WriteStartElement("source"); + + if (obj.Details is not null) + WriteSourceDetails(obj.Details, writer); + + if (obj.Serials is not null) + WriteSerials(obj.Serials, writer); + + if (obj.File is not null && obj.File.Length > 0) + { + foreach (var file in obj.File) + { + WriteFile(file, writer); + } + } + + writer.WriteEndElement(); + } + + /// + /// Write a SourceDetails to an XmlTextWriter + /// + /// SourceDetails to write + /// XmlTextReader to write to + private static void WriteSourceDetails(SourceDetails obj, XmlTextWriter writer) + { + writer.WriteStartElement("details"); + + writer.WriteOptionalAttributeString("id", obj.Id); + writer.WriteOptionalAttributeString("append_to_number", obj.AppendToNumber); + writer.WriteOptionalAttributeString("section", obj.Section); + writer.WriteOptionalAttributeString("rominfo", obj.RomInfo); + writer.WriteOptionalAttributeString("d_date", obj.DumpDate); + writer.WriteOptionalAttributeString("d_date_info", obj.DumpDateInfo); + writer.WriteOptionalAttributeString("r_date", obj.ReleaseDate); + writer.WriteOptionalAttributeString("r_date_info", obj.ReleaseDateInfo); + writer.WriteOptionalAttributeString("dumper", obj.Dumper); + writer.WriteOptionalAttributeString("project", obj.Project); + writer.WriteOptionalAttributeString("originalformat", obj.OriginalFormat); + writer.WriteOptionalAttributeString("nodump", obj.Nodump); + writer.WriteOptionalAttributeString("tool", obj.Tool); + writer.WriteOptionalAttributeString("origin", obj.Origin); + writer.WriteOptionalAttributeString("comment1", obj.Comment1); + writer.WriteOptionalAttributeString("comment2", obj.Comment2); + writer.WriteOptionalAttributeString("link1", obj.Link1); + writer.WriteOptionalAttributeString("link1_public", obj.Link1Public); + writer.WriteOptionalAttributeString("link2", obj.Link2); + writer.WriteOptionalAttributeString("link2_public", obj.Link2Public); + writer.WriteOptionalAttributeString("link3", obj.Link3); + writer.WriteOptionalAttributeString("link3_public", obj.Link3Public); + writer.WriteOptionalAttributeString("region", obj.Region); + writer.WriteOptionalAttributeString("media_title", obj.MediaTitle); + + writer.WriteEndElement(); + } + + #endregion + } +}