diff --git a/SabreTools.Models/SeparatedValue/MetadataFile.cs b/SabreTools.Models/SeparatedValue/MetadataFile.cs
new file mode 100644
index 00000000..cc9f81bd
--- /dev/null
+++ b/SabreTools.Models/SeparatedValue/MetadataFile.cs
@@ -0,0 +1,9 @@
+namespace SabreTools.Models.SeparatedValue
+{
+ public class MetadataFile
+ {
+ public string[] Header { get; set; }
+
+ public Row[]? Row { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/SabreTools.Models/SeparatedValue/Row.cs b/SabreTools.Models/SeparatedValue/Row.cs
new file mode 100644
index 00000000..ebce2bb6
--- /dev/null
+++ b/SabreTools.Models/SeparatedValue/Row.cs
@@ -0,0 +1,66 @@
+namespace SabreTools.Models.SeparatedValue
+{
+ ///
+ /// Standardized variant of a row
+ ///
+ public class Row
+ {
+ /// File Name
+ public string FileName { get; set; }
+
+ /// Internal Name
+ public string InternalName { get; set; }
+
+ /// Description
+ public string Description { get; set; }
+
+ /// Game Name
+ public string GameName { get; set; }
+
+ /// Game Description
+ public string GameDescription { get; set; }
+
+ /// Type
+ public string Type { get; set; }
+
+ /// Rom Name
+ public string RomName { get; set; }
+
+ /// Disk Name
+ public string DiskName { get; set; }
+
+ /// Size, Numeric
+ public string Size { get; set; }
+
+ /// CRC
+ public string CRC { get; set; }
+
+ /// MD5
+ public string MD5 { get; set; }
+
+ /// SHA1
+ public string SHA1 { get; set; }
+
+ /// SHA256
+ public string SHA256 { get; set; }
+
+ /// SHA384, Optional
+ public string SHA384 { get; set; }
+
+ /// SHA512, Optional
+ public string SHA512 { get; set; }
+
+ /// SpamSum, Optional
+ public string SpamSum { get; set; }
+
+ /// Nodump
+ public string Nodump { 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.Serialization/AttractMode.cs b/SabreTools.Serialization/AttractMode.cs
index 24743cf6..1636552c 100644
--- a/SabreTools.Serialization/AttractMode.cs
+++ b/SabreTools.Serialization/AttractMode.cs
@@ -73,7 +73,7 @@ namespace SabreTools.Serialization
break;
// Parse the line into a row
- Row row = null;
+ Row? row = null;
if (reader.Line.Count < HeaderWithRomnameCount)
{
row = new Row
diff --git a/SabreTools.Serialization/SeparatedValue.cs b/SabreTools.Serialization/SeparatedValue.cs
new file mode 100644
index 00000000..afaedbae
--- /dev/null
+++ b/SabreTools.Serialization/SeparatedValue.cs
@@ -0,0 +1,143 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using SabreTools.IO.Readers;
+using SabreTools.Models.SeparatedValue;
+
+namespace SabreTools.Serialization
+{
+ ///
+ /// Serializer for separated-value variants
+ ///
+ public class SeparatedValue
+ {
+ private const int HeaderWithoutExtendedHashesCount = 14;
+
+ private const int HeaderWithExtendedHashesCount = 17;
+
+ ///
+ /// Deserializes a separated-value variant to the defined type
+ ///
+ /// Path to the file to deserialize
+ /// Character delimiter between values
+ /// Deserialized data on success, null on failure
+ public static MetadataFile? Deserialize(string path, char delim)
+ {
+ try
+ {
+ using var stream = PathProcessor.OpenStream(path);
+ return Deserialize(stream, delim);
+ }
+ catch
+ {
+ // TODO: Handle logging the exception
+ return default;
+ }
+ }
+
+ ///
+ /// Deserializes a separated-value variant in a stream to the defined type
+ ///
+ /// Stream to deserialize
+ /// Character delimiter between values
+ /// Deserialized data on success, null on failure
+ public static MetadataFile? Deserialize(Stream? stream, char delim)
+ {
+ try
+ {
+ // If the stream is null
+ if (stream == null)
+ return default;
+
+ // Setup the reader and output
+ var reader = new SeparatedValueReader(stream, Encoding.UTF8)
+ {
+ Header = true,
+ Separator = delim,
+ VerifyFieldCount = false,
+ };
+ var dat = new MetadataFile();
+
+ // Read the header values first
+ if (!reader.ReadHeader())
+ return null;
+
+ dat.Header = reader.HeaderValues.ToArray();
+
+ // Loop through the rows and parse out values
+ var rows = new List();
+ while (!reader.EndOfStream)
+ {
+ // If we have no next line
+ if (!reader.ReadNextLine())
+ break;
+
+ // Parse the line into a row
+ Row? row = null;
+ if (reader.Line.Count < HeaderWithExtendedHashesCount)
+ {
+ row = new Row
+ {
+ FileName = reader.Line[0],
+ InternalName = reader.Line[1],
+ Description = reader.Line[2],
+ GameName = reader.Line[3],
+ GameDescription = reader.Line[4],
+ Type = reader.Line[5],
+ RomName = reader.Line[6],
+ DiskName = reader.Line[7],
+ Size = reader.Line[8],
+ CRC = reader.Line[9],
+ MD5 = reader.Line[10],
+ SHA1 = reader.Line[11],
+ SHA256 = reader.Line[12],
+ Nodump = reader.Line[13],
+ };
+
+ // If we have additional fields
+ if (reader.Line.Count > HeaderWithoutExtendedHashesCount)
+ row.ADDITIONAL_ELEMENTS = reader.Line.Skip(HeaderWithoutExtendedHashesCount).ToArray();
+ }
+ else
+ {
+ row = new Row
+ {
+ FileName = reader.Line[0],
+ InternalName = reader.Line[1],
+ Description = reader.Line[2],
+ GameName = reader.Line[3],
+ GameDescription = reader.Line[4],
+ Type = reader.Line[5],
+ RomName = reader.Line[6],
+ DiskName = reader.Line[7],
+ Size = reader.Line[8],
+ CRC = reader.Line[9],
+ MD5 = reader.Line[10],
+ SHA1 = reader.Line[11],
+ SHA256 = reader.Line[12],
+ SHA384 = reader.Line[13],
+ SHA512 = reader.Line[14],
+ SpamSum = reader.Line[15],
+ Nodump = reader.Line[16],
+ };
+
+ // If we have additional fields
+ if (reader.Line.Count > HeaderWithExtendedHashesCount)
+ row.ADDITIONAL_ELEMENTS = reader.Line.Skip(HeaderWithExtendedHashesCount).ToArray();
+ }
+ rows.Add(row);
+ }
+
+ // Assign the rows to the Dat and return
+ dat.Row = rows.ToArray();
+ return dat;
+ }
+ catch
+ {
+ // TODO: Handle logging the exception
+ return default;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SabreTools.Test/Serialization/DeserializationTests.cs b/SabreTools.Test/Serialization/DeserializationTests.cs
index a32042c9..ccc1ff5c 100644
--- a/SabreTools.Test/Serialization/DeserializationTests.cs
+++ b/SabreTools.Test/Serialization/DeserializationTests.cs
@@ -816,6 +816,32 @@ namespace SabreTools.Test.Parser
}
}
+ [Theory]
+ [InlineData("test-csv-files1.csv", ',', 2)]
+ [InlineData("test-csv-files2.csv", ',', 2)]
+ [InlineData("test-ssv-files1.ssv", ';', 2)]
+ [InlineData("test-ssv-files2.ssv", ';', 2)]
+ [InlineData("test-tsv-files2.tsv", '\t', 2)]
+ [InlineData("test-tsv-files1.tsv", '\t', 2)]
+ public void SeparatedValueDeserializeTest(string path, char delim, long count)
+ {
+ // Open the file for reading
+ string filename = System.IO.Path.Combine(Environment.CurrentDirectory, "TestData", path);
+
+ // Deserialize the file
+ var dat = Serialization.SeparatedValue.Deserialize(filename, delim);
+
+ // Validate the values
+ Assert.NotNull(dat);
+ Assert.Equal(count, dat.Row.Length);
+
+ // Validate we're not missing any attributes or elements
+ foreach (var rom in dat.Row ?? Array.Empty())
+ {
+ Assert.Null(rom.ADDITIONAL_ELEMENTS);
+ }
+ }
+
[Theory]
[InlineData("test-softwarelist-files1.xml", 4531)]
[InlineData("test-softwarelist-files2.xml", 2797)]
diff --git a/SabreTools.Test/TestData/test-csv-files1.csv b/SabreTools.Test/TestData/test-csv-files1.csv
new file mode 100644
index 00000000..a6208551
--- /dev/null
+++ b/SabreTools.Test/TestData/test-csv-files1.csv
@@ -0,0 +1,3 @@
+File Name,Internal Name,Description,Game Name,Game Description,Type,Rom Name,Disk Name,Size,CRC,MD5,SHA1,SHA256,Nodump
+Original DatFile Name.xml,Original DatFile Name,Original DatFile,Game,A cool game,Rom,file.bin,,12345,0,d41d8cd98f00b204e9800998ecf8427e,da39a3ee5e6b4b0d3255bfef95601890afd80709,ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad,No
+Original DatFile Name.xml,Original DatFile Name,Original DatFile,Game,A cool game,Disk,,file.chd,,,,da39a3ee5e6b4b0d3255bfef95601890afd80709,,No
diff --git a/SabreTools.Test/TestData/test-csv-files2.csv b/SabreTools.Test/TestData/test-csv-files2.csv
new file mode 100644
index 00000000..75d85260
--- /dev/null
+++ b/SabreTools.Test/TestData/test-csv-files2.csv
@@ -0,0 +1,3 @@
+File Name,Internal Name,Description,Game Name,Game Description,Type,Rom Name,Disk Name,Size,CRC,MD5,SHA1,SHA256,SHA384,SHA512,SpamSum,Nodump
+Original DatFile Name.xml,Original DatFile Name,Original DatFile,Game,A cool game,Rom,file.bin,,12345,0,d41d8cd98f00b204e9800998ecf8427e,da39a3ee5e6b4b0d3255bfef95601890afd80709,ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad,cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7,ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f,QXX,No
+Original DatFile Name.xml,Original DatFile Name,Original DatFile,Game,A cool game,Disk,,file.chd,,,,da39a3ee5e6b4b0d3255bfef95601890afd80709,,,,,No
diff --git a/SabreTools.Test/TestData/test-ssv-files1.ssv b/SabreTools.Test/TestData/test-ssv-files1.ssv
new file mode 100644
index 00000000..c7630cda
--- /dev/null
+++ b/SabreTools.Test/TestData/test-ssv-files1.ssv
@@ -0,0 +1,3 @@
+File Name;Internal Name;Description;Game Name;Game Description;Type;Rom Name;Disk Name;Size;CRC;MD5;SHA1;SHA256;Nodump
+Original DatFile Name.xml;Original DatFile Name;Original DatFile;Game;A cool game;Rom;file.bin;;12345;0;d41d8cd98f00b204e9800998ecf8427e;da39a3ee5e6b4b0d3255bfef95601890afd80709;ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad;No
+Original DatFile Name.xml;Original DatFile Name;Original DatFile;Game;A cool game;Disk;;file.chd;;;;da39a3ee5e6b4b0d3255bfef95601890afd80709;;No
diff --git a/SabreTools.Test/TestData/test-ssv-files2.ssv b/SabreTools.Test/TestData/test-ssv-files2.ssv
new file mode 100644
index 00000000..66ab0f3d
--- /dev/null
+++ b/SabreTools.Test/TestData/test-ssv-files2.ssv
@@ -0,0 +1,3 @@
+File Name;Internal Name;Description;Game Name;Game Description;Type;Rom Name;Disk Name;Size;CRC;MD5;SHA1;SHA256;SHA384;SHA512;SpamSum;Nodump
+Original DatFile Name.xml;Original DatFile Name;Original DatFile;Game;A cool game;Rom;file.bin;;12345;0;d41d8cd98f00b204e9800998ecf8427e;da39a3ee5e6b4b0d3255bfef95601890afd80709;ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad;cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7;ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f;QXX;No
+Original DatFile Name.xml;Original DatFile Name;Original DatFile;Game;A cool game;Disk;;file.chd;;;;da39a3ee5e6b4b0d3255bfef95601890afd80709;;;;;No
diff --git a/SabreTools.Test/TestData/test-tsv-files1.tsv b/SabreTools.Test/TestData/test-tsv-files1.tsv
new file mode 100644
index 00000000..6480dbbe
--- /dev/null
+++ b/SabreTools.Test/TestData/test-tsv-files1.tsv
@@ -0,0 +1,3 @@
+File Name Internal Name Description Game Name Game Description Type Rom Name Disk Name Size CRC MD5 SHA1 SHA256 Nodump
+Original DatFile Name.xml Original DatFile Name Original DatFile Game A cool game Rom file.bin 12345 0 d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad No
+Original DatFile Name.xml Original DatFile Name Original DatFile Game A cool game Disk file.chd da39a3ee5e6b4b0d3255bfef95601890afd80709 No
diff --git a/SabreTools.Test/TestData/test-tsv-files2.tsv b/SabreTools.Test/TestData/test-tsv-files2.tsv
new file mode 100644
index 00000000..2b0a9616
--- /dev/null
+++ b/SabreTools.Test/TestData/test-tsv-files2.tsv
@@ -0,0 +1,3 @@
+File Name Internal Name Description Game Name Game Description Type Rom Name Disk Name Size CRC MD5 SHA1 SHA256 SHA384 SHA512 SpamSum Nodump
+Original DatFile Name.xml Original DatFile Name Original DatFile Game A cool game Rom file.bin 12345 0 d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7 ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f QXX No
+Original DatFile Name.xml Original DatFile Name Original DatFile Game A cool game Disk file.chd da39a3ee5e6b4b0d3255bfef95601890afd80709 No