diff --git a/SabreTools.DatFiles/Formats/Listrom.cs b/SabreTools.DatFiles/Formats/Listrom.cs index fb60984f..4d96f63d 100644 --- a/SabreTools.DatFiles/Formats/Listrom.cs +++ b/SabreTools.DatFiles/Formats/Listrom.cs @@ -34,9 +34,9 @@ namespace SabreTools.DatFiles.Formats /// abcd.bin 1024 CRC(00000000) SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) /// efgh.bin 1024 BAD CRC(00000000) SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP /// ijkl.bin 1024 NO GOOD DUMP KNOWN - /// abcd.chd SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) - /// efgh.chd BAD (da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP - /// ijkl.chd NO GOOD DUMP KNOWN + /// abcd SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) + /// efgh BAD (da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP + /// ijkl NO GOOD DUMP KNOWN /// public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false) { diff --git a/SabreTools.Models/Listrom/Dat.cs b/SabreTools.Models/Listrom/Dat.cs new file mode 100644 index 00000000..4ac38211 --- /dev/null +++ b/SabreTools.Models/Listrom/Dat.cs @@ -0,0 +1,14 @@ +namespace SabreTools.Models.Listrom +{ + public class Dat + { + public Set[]? Set { get; set; } + + #region DO NOT USE IN PRODUCTION + + /// Should be empty + public string[]? ADDITIONAL_ELEMENTS { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/SabreTools.Models/Listrom/Row.cs b/SabreTools.Models/Listrom/Row.cs index 202022c9..3cadd445 100644 --- a/SabreTools.Models/Listrom/Row.cs +++ b/SabreTools.Models/Listrom/Row.cs @@ -6,15 +6,15 @@ namespace SabreTools.Models.Listrom /// abcd.bin 1024 CRC(00000000) SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) /// efgh.bin 1024 BAD CRC(00000000) SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP /// ijkl.bin 1024 NO GOOD DUMP KNOWN - /// abcd.chd SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) - /// efgh.chd BAD (da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP - /// ijkl.chd NO GOOD DUMP KNOWN + /// abcd SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) + /// efgh BAD SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP + /// ijkl NO GOOD DUMP KNOWN /// public class Row { public string Name { get; set; } - public long? Size { get; set; } + public string? Size { get; set; } public bool Bad { get; set; } diff --git a/SabreTools.Models/Listrom/Set.cs b/SabreTools.Models/Listrom/Set.cs new file mode 100644 index 00000000..2d5eaa9b --- /dev/null +++ b/SabreTools.Models/Listrom/Set.cs @@ -0,0 +1,21 @@ +namespace SabreTools.Models.Listrom +{ + /// + /// ROMs required for driver "testdriver". + /// Name Size Checksum + /// abcd.bin 1024 CRC(00000000) SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) + /// efgh.bin 1024 BAD CRC(00000000) SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP + /// ijkl.bin 1024 NO GOOD DUMP KNOWN + /// abcd SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) + /// efgh BAD SHA1(da39a3ee5e6b4b0d3255bfef95601890afd80709) BAD_DUMP + /// ijkl NO GOOD DUMP KNOWN + /// + public class Set + { + public string? Driver { get; set; } + + public string? Device { get; set; } + + public Row[]? Row { get; set; } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Listrom.cs b/SabreTools.Serialization/Listrom.cs new file mode 100644 index 00000000..82f9654e --- /dev/null +++ b/SabreTools.Serialization/Listrom.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.Models.Listrom; + +namespace SabreTools.Serialization +{ + /// + /// Serializer for MAME listrom files + /// + public class Listrom + { + /// + /// Deserializes a MAME listrom file to the defined type + /// + /// Path to the file to deserialize + /// Deserialized data on success, null on failure + public static Dat? Deserialize(string path) + { + try + { + using var stream = PathProcessor.OpenStream(path); + return Deserialize(stream); + } + catch + { + // TODO: Handle logging the exception + return default; + } + } + + /// + /// Deserializes a MAME listrom file in a stream to the defined type + /// + /// Stream to deserialize + /// Deserialized data on success, null on failure + public static Dat? Deserialize(Stream? stream) + { + try + { + // If the stream is null + if (stream == null) + return default; + + // Setup the reader and output + var reader = new StreamReader(stream, Encoding.UTF8); + var dat = new Dat(); + + Set? set = null; + var sets = new List(); + var rows = new List(); + + var additional = new List(); + while (!reader.EndOfStream) + { + // Read the line and don't split yet + string? line = reader.ReadLine(); + if (string.IsNullOrWhiteSpace(line)) + { + // If we have a set to process + if (set != null) + { + set.Row = rows.ToArray(); + sets.Add(set); + set = null; + rows.Clear(); + } + + continue; + } + + // Set lines are unique + if (line.StartsWith("ROMs required for driver")) + { + string driver = line["ROMs required for driver".Length..].Trim('"', ' ', '.'); + set = new Set { Driver = driver }; + continue; + } + else if (line.StartsWith("No ROMs required for driver")) + { + string driver = line["No ROMs required for driver".Length..].Trim('"', ' ', '.'); + set = new Set { Driver = driver }; + continue; + } + else if (line.StartsWith("ROMs required for device")) + { + string device = line["ROMs required for device".Length..].Trim('"', ' ', '.'); + set = new Set { Device = device }; + continue; + } + else if (line.StartsWith("No ROMs required for device")) + { + string device = line["No ROMs required for device".Length..].Trim('"', ' ', '.'); + set = new Set { Device = device }; + continue; + } + else if (line.Equals("Name Size Checksum", StringComparison.OrdinalIgnoreCase)) + { + // No-op + continue; + } + + // Split the line for the name iteratively + string[]? lineParts = line?.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (lineParts?.Length == 1) + lineParts = line?.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (lineParts?.Length == 1) + lineParts = line?.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (lineParts?.Length == 1) + lineParts = line?.Split(" ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // Read the name and set the rest of the line for processing + string name = lineParts[0]; + string trimmedLine = line[name.Length..]; + + lineParts = trimmedLine?.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // The number of items in the row explains what type of row it is + var row = new Row(); + switch (lineParts.Length) + { + // Normal CHD (Name, SHA1) + case 1: + row.Name = name; + row.SHA1 = lineParts[0]["SHA1".Length..].Trim('(', ')'); + break; + + // Normal ROM (Name, Size, CRC, SHA1) + case 3 when line.Contains("CRC"): + row.Name = name; + row.Size = lineParts[0]; + row.CRC = lineParts[1]["CRC".Length..].Trim('(', ')'); + row.SHA1 = lineParts[2]["SHA1".Length..].Trim('(', ')'); + break; + + // Bad CHD (Name, BAD, SHA1, BAD_DUMP) + case 3 when line.Contains("BAD_DUMP"): + row.Name = name; + row.Bad = true; + row.SHA1 = lineParts[1]["SHA1".Length..].Trim('(', ')'); + break; + + // Nodump CHD (Name, NO GOOD DUMP KNOWN) + case 4 when line.Contains("NO GOOD DUMP KNOWN"): + row.Name = name; + row.NoGoodDumpKnown = true; + break; + + // Bad ROM (Name, Size, BAD, CRC, SHA1, BAD_DUMP) + case 5 when line.Contains("BAD_DUMP"): + row.Name = name; + row.Size = lineParts[0]; + row.Bad = true; + row.CRC = lineParts[2]["CRC".Length..].Trim('(', ')'); + row.SHA1 = lineParts[3]["SHA1".Length..].Trim('(', ')'); + break; + + // Nodump ROM (Name, Size, NO GOOD DUMP KNOWN) + case 5 when line.Contains("NO GOOD DUMP KNOWN"): + row.Name = name; + row.Size = lineParts[0]; + row.NoGoodDumpKnown = true; + break; + + default: + row = null; + additional.Add(line); + break; + } + + if (row != null) + rows.Add(row); + } + + // If we have a set to process + if (set != null) + { + set.Row = rows.ToArray(); + sets.Add(set); + set = null; + rows.Clear(); + } + + // Add extra pieces and return + dat.Set = sets.ToArray(); + dat.ADDITIONAL_ELEMENTS = additional.ToArray(); + return dat; + } + catch + { + // TODO: Handle logging the exception + return default; + } + } + } +} \ No newline at end of file diff --git a/SabreTools.Test/Parser/SerializationTests.cs b/SabreTools.Test/Parser/SerializationTests.cs index 0b14a2e7..49663e66 100644 --- a/SabreTools.Test/Parser/SerializationTests.cs +++ b/SabreTools.Test/Parser/SerializationTests.cs @@ -98,7 +98,7 @@ namespace SabreTools.Test.Parser [Theory] [InlineData("test-sfv.sfv", Hash.CRC)] [InlineData("test-md5.md5", Hash.MD5)] - [InlineData("test-sha1.sha1", Hash.SHA1)] + [InlineData("test-sha1.sha1", Hash.SHA1)] [InlineData("test-sha256.sha256", Hash.SHA256)] [InlineData("test-sha384.sha384", Hash.SHA384)] [InlineData("test-sha512.sha512", Hash.SHA512)] @@ -142,6 +142,23 @@ namespace SabreTools.Test.Parser } } + [Fact] + public void ListromDeserializeTest() + { + // Open the file for reading + string filename = System.IO.Path.Combine(Environment.CurrentDirectory, "TestData", "test-listrom-files.txt.gz"); + + // Deserialize the file + var dat = Serialization.Listrom.Deserialize(filename); + + // Validate the values + Assert.NotNull(dat?.Set); + Assert.Equal(45861, dat.Set.Length); + + // Validate we're not missing any attributes or elements + Assert.Empty(dat.ADDITIONAL_ELEMENTS); + } + [Fact] public void ListxmlDeserializeTest() { diff --git a/SabreTools.Test/TestData/test-listrom-files.txt.gz b/SabreTools.Test/TestData/test-listrom-files.txt.gz new file mode 100644 index 00000000..6bc58d5c Binary files /dev/null and b/SabreTools.Test/TestData/test-listrom-files.txt.gz differ