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