diff --git a/SabreTools.DatFiles/Formats/Hashfile.Reader.cs b/SabreTools.DatFiles/Formats/Hashfile.Reader.cs
new file mode 100644
index 00000000..e52edaf6
--- /dev/null
+++ b/SabreTools.DatFiles/Formats/Hashfile.Reader.cs
@@ -0,0 +1,538 @@
+using System;
+using System.IO;
+using System.Linq;
+using SabreTools.Core;
+using SabreTools.DatItems;
+using SabreTools.DatItems.Formats;
+
+namespace SabreTools.DatFiles.Formats
+{
+ ///
+ /// Represents parsing of a hashfile such as an SFV, MD5, or SHA-1 file
+ ///
+ internal partial class Hashfile : DatFile
+ {
+ ///
+ public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false)
+ {
+ try
+ {
+ // Deserialize the input file
+ var hashfile = Serialization.Hashfile.Deserialize(filename, _hash);
+
+ // Convert items
+ switch (_hash)
+ {
+ case Hash.CRC:
+ ConvertSFV(hashfile?.SFV, filename, indexId, statsOnly);
+ break;
+ case Hash.MD5:
+ ConvertMD5(hashfile?.MD5, filename, indexId, statsOnly);
+ break;
+ case Hash.SHA1:
+ ConvertSHA1(hashfile?.SHA1, filename, indexId, statsOnly);
+ break;
+ case Hash.SHA256:
+ ConvertSHA256(hashfile?.SHA256, filename, indexId, statsOnly);
+ break;
+ case Hash.SHA384:
+ ConvertSHA384(hashfile?.SHA384, filename, indexId, statsOnly);
+ break;
+ case Hash.SHA512:
+ ConvertSHA512(hashfile?.SHA512, filename, indexId, statsOnly);
+ break;
+ case Hash.SpamSum:
+ ConvertSpamSum(hashfile?.SpamSum, filename, indexId, statsOnly);
+ break;
+ }
+ }
+ catch (Exception ex) when (!throwOnError)
+ {
+ string message = $"'{filename}' - An error occurred during parsing";
+ logger.Error(ex, message);
+ }
+ }
+
+ #region Converters
+
+ ///
+ /// Create a machine from the filename
+ ///
+ /// Filename to derive from
+ /// Filled machine and new filename on success, null on error
+ private static (Machine?, string?) DeriveMachine(string filename)
+ {
+ // If the filename is missing, we can't do anything
+ if (string.IsNullOrWhiteSpace(filename))
+ return (null, null);
+
+ string machineName = Path.GetFileNameWithoutExtension(filename);
+ if (filename.Contains('/'))
+ {
+ string[] split = filename.Split('/');
+ machineName = split[0];
+ filename = filename[(machineName.Length + 1)..];
+ }
+ else if (filename.Contains('\\'))
+ {
+ string[] split = filename.Split('\\');
+ machineName = split[0];
+ filename = filename[(machineName.Length + 1)..];
+ }
+
+ var machine = new Machine { Name = machineName };
+ return (machine, filename);
+ }
+
+ ///
+ /// Derive the item type from the filename
+ ///
+ /// Filename to derive from
+ /// ItemType representing the item (Rom by default), ItemType.NULL on error
+ private static ItemType DeriveItemType(string filename)
+ {
+ // If the filename is missing, we can't do anything
+ if (string.IsNullOrWhiteSpace(filename))
+ return ItemType.NULL;
+
+ // If we end in the CHD extension
+ if (filename.EndsWith(".chd", StringComparison.OrdinalIgnoreCase))
+ return ItemType.Disk;
+
+ // If we end in an Aaruformat extension
+ if (filename.EndsWith(".aaru", StringComparison.OrdinalIgnoreCase)
+ || filename.EndsWith(".aaruf", StringComparison.OrdinalIgnoreCase)
+ || filename.EndsWith(".aaruformat", StringComparison.OrdinalIgnoreCase)
+ || filename.EndsWith(".aif", StringComparison.OrdinalIgnoreCase)
+ || filename.EndsWith(".dicf", StringComparison.OrdinalIgnoreCase))
+ {
+ return ItemType.Media;
+ }
+
+ // Everything else is assumed to be a generic item
+ return ItemType.Rom;
+ }
+
+ ///
+ /// Convert SFV information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertSFV(Models.Hashfile.SFV[]? sfvs, string filename, int indexId, bool statsOnly)
+ {
+ // If the sfv array is missing, we can't do anything
+ if (sfvs == null || !sfvs.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var sfv in sfvs)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(sfv.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(sfv.File);
+ switch (itemType)
+ {
+ case ItemType.Disk: // Should not happen with CRC32 hashes
+ case ItemType.Media: // Should not happen with CRC32 hashes
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ CRC = sfv.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Convert MD5 information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertMD5(Models.Hashfile.MD5[]? md5s, string filename, int indexId, bool statsOnly)
+ {
+ // If the md5 array is missing, we can't do anything
+ if (md5s == null || !md5s.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var md5 in md5s)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(md5.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(md5.File);
+ switch (itemType)
+ {
+ case ItemType.Disk:
+ var disk = new Disk
+ {
+ Name = itemName,
+ MD5 = md5.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(disk, statsOnly);
+ break;
+
+ case ItemType.Media:
+ var media = new Media
+ {
+ Name = itemName,
+ MD5 = md5.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(media, statsOnly);
+ break;
+
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ MD5 = md5.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Convert SHA1 information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertSHA1(Models.Hashfile.SHA1[]? sha1s, string filename, int indexId, bool statsOnly)
+ {
+ // If the sha1 array is missing, we can't do anything
+ if (sha1s == null || !sha1s.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var sha1 in sha1s)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(sha1.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(sha1.File);
+ switch (itemType)
+ {
+ case ItemType.Disk:
+ var disk = new Disk
+ {
+ Name = itemName,
+ SHA1 = sha1.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(disk, statsOnly);
+ break;
+
+ case ItemType.Media:
+ var media = new Media
+ {
+ Name = itemName,
+ SHA1 = sha1.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(media, statsOnly);
+ break;
+
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ SHA1 = sha1.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Convert SHA256 information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertSHA256(Models.Hashfile.SHA256[]? sha256s, string filename, int indexId, bool statsOnly)
+ {
+ // If the sha256 array is missing, we can't do anything
+ if (sha256s == null || !sha256s.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var sha256 in sha256s)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(sha256.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(sha256.File);
+ switch (itemType)
+ {
+ case ItemType.Media:
+ var media = new Media
+ {
+ Name = itemName,
+ SHA256 = sha256.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(media, statsOnly);
+ break;
+
+ case ItemType.Disk: // Should not happen with SHA-256 hashes
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ SHA256 = sha256.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Convert SHA384 information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertSHA384(Models.Hashfile.SHA384[]? sha384s, string filename, int indexId, bool statsOnly)
+ {
+ // If the sha384 array is missing, we can't do anything
+ if (sha384s == null || !sha384s.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var sha384 in sha384s)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(sha384.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(sha384.File);
+ switch (itemType)
+ {
+ case ItemType.Disk: // Should not happen with SHA-384 hashes
+ case ItemType.Media: // Should not happen with SHA-384 hashes
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ SHA384 = sha384.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Convert SHA512 information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertSHA512(Models.Hashfile.SHA512[]? sha512s, string filename, int indexId, bool statsOnly)
+ {
+ // If the sha512 array is missing, we can't do anything
+ if (sha512s == null || !sha512s.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var sha512 in sha512s)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(sha512.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(sha512.File);
+ switch (itemType)
+ {
+ case ItemType.Disk: // Should not happen with SHA-512 hashes
+ case ItemType.Media: // Should not happen with SHA-512 hashes
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ SHA512 = sha512.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ ///
+ /// Convert SpamSum information
+ ///
+ /// Array of deserialized models to convert
+ /// Name of the file to be parsed
+ /// Index ID for the DAT
+ /// True to only add item statistics while parsing, false otherwise
+ private void ConvertSpamSum(Models.Hashfile.SpamSum[]? spamsums, string filename, int indexId, bool statsOnly)
+ {
+ // If the spamsum array is missing, we can't do anything
+ if (spamsums == null || !spamsums.Any())
+ return;
+
+ // Loop through and add the items
+ foreach (var spamsum in spamsums)
+ {
+ // Get the item type
+ ItemType itemType = DeriveItemType(spamsum.File);
+ if (itemType == ItemType.NULL)
+ continue;
+
+ (var machine, string itemName) = DeriveMachine(spamsum.File);
+ switch (itemType)
+ {
+ case ItemType.Media:
+ var media = new Media
+ {
+ Name = itemName,
+ SpamSum = spamsum.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(media, statsOnly);
+ break;
+
+ case ItemType.Disk: // Should not happen with SpamSum fuzzy hashes
+ case ItemType.Rom:
+ var rom = new Rom
+ {
+ Name = itemName,
+ Size = null,
+ SpamSum = spamsum.Hash,
+ Machine = machine,
+ Source = new Source
+ {
+ Index = indexId,
+ Name = filename,
+ },
+ };
+ ParseAddHelper(rom, statsOnly);
+ break;
+
+ default:
+ continue;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.DatFiles/Formats/Hashfile.Writer.cs b/SabreTools.DatFiles/Formats/Hashfile.Writer.cs
new file mode 100644
index 00000000..245e490e
--- /dev/null
+++ b/SabreTools.DatFiles/Formats/Hashfile.Writer.cs
@@ -0,0 +1,505 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using SabreTools.Core;
+using SabreTools.DatItems;
+using SabreTools.DatItems.Formats;
+
+namespace SabreTools.DatFiles.Formats
+{
+ ///
+ /// Represents writing a hashfile such as an SFV, MD5, or SHA-1 file
+ ///
+ internal partial class Hashfile : DatFile
+ {
+ ///
+ protected override ItemType[] GetSupportedTypes()
+ {
+ return new ItemType[]
+ {
+ ItemType.Disk,
+ ItemType.Media,
+ ItemType.Rom
+ };
+ }
+
+ ///
+ protected override List GetMissingRequiredFields(DatItem datItem)
+ {
+ List missingFields = new();
+
+ // Check item name
+ if (string.IsNullOrWhiteSpace(datItem.GetName()))
+ missingFields.Add(DatItemField.Name);
+
+ // Check hash linked to specific Hashfile type
+ switch (_hash)
+ {
+ case Hash.CRC:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.CRC))
+ missingFields.Add(DatItemField.CRC);
+ break;
+ default:
+ missingFields.Add(DatItemField.CRC);
+ break;
+ }
+ break;
+ case Hash.MD5:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Disk:
+ if (!string.IsNullOrEmpty((datItem as Disk)?.MD5))
+ missingFields.Add(DatItemField.MD5);
+ break;
+ case ItemType.Media:
+ if (!string.IsNullOrEmpty((datItem as Media)?.MD5))
+ missingFields.Add(DatItemField.MD5);
+ break;
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.MD5))
+ missingFields.Add(DatItemField.MD5);
+ break;
+ default:
+ missingFields.Add(DatItemField.MD5);
+ break;
+ }
+ break;
+ case Hash.SHA1:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Disk:
+ if (!string.IsNullOrEmpty((datItem as Disk)?.SHA1))
+ missingFields.Add(DatItemField.SHA1);
+ break;
+ case ItemType.Media:
+ if (!string.IsNullOrEmpty((datItem as Media)?.SHA1))
+ missingFields.Add(DatItemField.SHA1);
+ break;
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.SHA1))
+ missingFields.Add(DatItemField.SHA1);
+ break;
+ default:
+ missingFields.Add(DatItemField.SHA1);
+ break;
+ }
+ break;
+ case Hash.SHA256:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Media:
+ if (!string.IsNullOrEmpty((datItem as Media)?.SHA256))
+ missingFields.Add(DatItemField.SHA256);
+ break;
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.SHA256))
+ missingFields.Add(DatItemField.SHA256);
+ break;
+ default:
+ missingFields.Add(DatItemField.SHA256);
+ break;
+ }
+ break;
+ case Hash.SHA384:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.SHA384))
+ missingFields.Add(DatItemField.SHA384);
+ break;
+ default:
+ missingFields.Add(DatItemField.SHA384);
+ break;
+ }
+ break;
+ case Hash.SHA512:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.SHA512))
+ missingFields.Add(DatItemField.SHA512);
+ break;
+ default:
+ missingFields.Add(DatItemField.SHA512);
+ break;
+ }
+ break;
+ case Hash.SpamSum:
+ switch (datItem.ItemType)
+ {
+ case ItemType.Media:
+ if (!string.IsNullOrEmpty((datItem as Media)?.SpamSum))
+ missingFields.Add(DatItemField.SpamSum);
+ break;
+ case ItemType.Rom:
+ if (!string.IsNullOrEmpty((datItem as Rom)?.SpamSum))
+ missingFields.Add(DatItemField.SpamSum);
+ break;
+ default:
+ missingFields.Add(DatItemField.SpamSum);
+ break;
+ }
+ break;
+ }
+
+ return missingFields;
+ }
+
+ ///
+ public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
+ {
+ try
+ {
+ logger.User($"Writing to '{outfile}'...");
+
+ var hashfile = CreateHashFile();
+ if (!Serialization.Hashfile.SerializeToFile(hashfile, outfile, _hash))
+ {
+ logger.Warning($"File '{outfile}' could not be written! See the log for more details.");
+ return false;
+ }
+ }
+ catch (Exception ex) when (!throwOnError)
+ {
+ logger.Error(ex);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Create a Hashfile from the current internal information
+ ///
+ private Models.Hashfile.Hashfile CreateHashFile()
+ {
+ var hashfile = new Models.Hashfile.Hashfile();
+
+ switch (_hash)
+ {
+ case Hash.CRC:
+ hashfile.SFV = CreateSFV();
+ break;
+ case Hash.MD5:
+ hashfile.MD5 = CreateMD5();
+ break;
+ case Hash.SHA1:
+ hashfile.SHA1 = CreateSHA1();
+ break;
+ case Hash.SHA256:
+ hashfile.SHA256 = CreateSHA256();
+ break;
+ case Hash.SHA384:
+ hashfile.SHA384 = CreateSHA384();
+ break;
+ case Hash.SHA512:
+ hashfile.SHA512 = CreateSHA512();
+ break;
+ case Hash.SpamSum:
+ hashfile.SpamSum = CreateSpamSum();
+ break;
+ }
+
+ return hashfile;
+ }
+
+ ///
+ /// Create an array of SFV
+ ///
+ private Models.Hashfile.SFV[]? CreateSFV()
+ {
+ // Create a list of hold the SFVs
+ var sfvs = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ if (Header.GameName)
+ name = $"{item.Machine.Name}{Path.DirectorySeparatorChar}";
+
+ switch (item)
+ {
+ case Rom rom:
+ sfvs.Add(new Models.Hashfile.SFV
+ {
+ File = name + rom.Name,
+ Hash = rom.CRC,
+ });
+ break;
+ }
+ }
+ }
+
+ return sfvs.ToArray();
+ }
+
+ ///
+ /// Create an array of MD5
+ ///
+ private Models.Hashfile.MD5[]? CreateMD5()
+ {
+ // Create a list of hold the MD5s
+ var md5s = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ if (Header.GameName)
+ name = $"{item.Machine.Name}{Path.DirectorySeparatorChar}";
+
+ switch (item)
+ {
+ case Disk disk:
+ md5s.Add(new Models.Hashfile.MD5
+ {
+ Hash = disk.MD5,
+ File = name + disk.Name,
+ });
+ break;
+
+ case Media media:
+ md5s.Add(new Models.Hashfile.MD5
+ {
+ Hash = media.MD5,
+ File = name + media.Name,
+ });
+ break;
+
+ case Rom rom:
+ md5s.Add(new Models.Hashfile.MD5
+ {
+ Hash = rom.MD5,
+ File = name + rom.Name,
+ });
+ break;
+ }
+ }
+ }
+
+ return md5s.ToArray();
+ }
+
+ ///
+ /// Create an array of SHA1
+ ///
+ private Models.Hashfile.SHA1[]? CreateSHA1()
+ {
+ // Create a list of hold the SHA1s
+ var sha1s = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ if (Header.GameName)
+ name = $"{item.Machine.Name}{Path.DirectorySeparatorChar}";
+
+ switch (item)
+ {
+ case Disk disk:
+ sha1s.Add(new Models.Hashfile.SHA1
+ {
+ Hash = disk.SHA1,
+ File = name + disk.Name,
+ });
+ break;
+
+ case Media media:
+ sha1s.Add(new Models.Hashfile.SHA1
+ {
+ Hash = media.SHA1,
+ File = name + media.Name,
+ });
+ break;
+
+ case Rom rom:
+ sha1s.Add(new Models.Hashfile.SHA1
+ {
+ Hash = rom.SHA1,
+ File = name + rom.Name,
+ });
+ break;
+ }
+ }
+ }
+
+ return sha1s.ToArray();
+ }
+
+ ///
+ /// Create an array of SHA256
+ ///
+ private Models.Hashfile.SHA256[]? CreateSHA256()
+ {
+ // Create a list of hold the SHA256s
+ var sha256s = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ switch (item)
+ {
+ case Media media:
+ sha256s.Add(new Models.Hashfile.SHA256
+ {
+ Hash = media.SHA256,
+ File = name + media.Name,
+ });
+ break;
+
+ case Rom rom:
+ sha256s.Add(new Models.Hashfile.SHA256
+ {
+ Hash = rom.SHA256,
+ File = name + rom.Name,
+ });
+ break;
+ }
+ }
+ }
+
+ return sha256s.ToArray();
+ }
+
+ ///
+ /// Create an array of SHA384
+ ///
+ private Models.Hashfile.SHA384[]? CreateSHA384()
+ {
+ // Create a list of hold the SHA384s
+ var sha384s = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ switch (item)
+ {
+ case Rom rom:
+ sha384s.Add(new Models.Hashfile.SHA384
+ {
+ Hash = rom.SHA384,
+ File = name + rom.Name,
+ });
+ break;
+ }
+ }
+ }
+
+ return sha384s.ToArray();
+ }
+
+ ///
+ /// Create an array of SHA512
+ ///
+ private Models.Hashfile.SHA512[]? CreateSHA512()
+ {
+ // Create a list of hold the SHA512s
+ var sha512s = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ switch (item)
+ {
+ case Rom rom:
+ sha512s.Add(new Models.Hashfile.SHA512
+ {
+ Hash = rom.SHA512,
+ File = name + rom.Name,
+ });
+ break;
+ }
+ }
+ }
+
+ return sha512s.ToArray();
+ }
+
+ ///
+ /// Create an array of SpamSum
+ ///
+ private Models.Hashfile.SpamSum[]? CreateSpamSum()
+ {
+ // Create a list of hold the SpamSums
+ var spamsums = new List();
+
+ // Loop through the sorted items and create items for them
+ foreach (string key in Items.SortedKeys)
+ {
+ var items = Items.FilteredItems(key);
+ if (items == null || !items.Any())
+ continue;
+
+ foreach (var item in items)
+ {
+ string name = string.Empty;
+ switch (item)
+ {
+ case Media media:
+ spamsums.Add(new Models.Hashfile.SpamSum
+ {
+ Hash = media.SpamSum,
+ File = name + media.Name,
+ });
+ break;
+
+ case Rom rom:
+ spamsums.Add(new Models.Hashfile.SpamSum
+ {
+ Hash = rom.SpamSum,
+ File = name + rom.Name,
+ });
+ break;
+ }
+ }
+ }
+
+ return spamsums.ToArray();
+ }
+ }
+}
diff --git a/SabreTools.DatFiles/Formats/Hashfile.cs b/SabreTools.DatFiles/Formats/Hashfile.cs
index 863bab5a..6c5a9561 100644
--- a/SabreTools.DatFiles/Formats/Hashfile.cs
+++ b/SabreTools.DatFiles/Formats/Hashfile.cs
@@ -1,19 +1,11 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using SabreTools.Core;
-using SabreTools.DatItems;
-using SabreTools.DatItems.Formats;
-using SabreTools.IO;
-using SabreTools.IO.Writers;
+using SabreTools.Core;
namespace SabreTools.DatFiles.Formats
{
///
- /// Represents parsing and writing of a hashfile such as an SFV, MD5, or SHA-1 file
+ /// Represents a hashfile such as an SFV, MD5, or SHA-1 file
///
- internal class Hashfile : DatFile
+ internal partial class Hashfile : DatFile
{
// Private instance variables specific to Hashfile DATs
private readonly Hash _hash;
@@ -28,450 +20,5 @@ namespace SabreTools.DatFiles.Formats
{
_hash = hash;
}
-
- ///
- public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false)
- {
- // Open a file reader
- Encoding enc = filename.GetEncoding();
- StreamReader sr = new(System.IO.File.OpenRead(filename), enc);
-
- while (!sr.EndOfStream)
- {
- try
- {
- string line = sr.ReadLine();
-
- // Split the line and get the name and hash
- string[] split = line.Split(' ');
- string name = string.Empty;
- string hash = string.Empty;
-
- // If we have CRC, then it's an SFV file and the name is first
- if (_hash.HasFlag(Hash.CRC))
- {
- name = string.Join(" ", split[..^1]).Replace("*", String.Empty).Trim();
- hash = split[^1];
- }
- // Otherwise, the name is second
- else
- {
- name = string.Join(" ", split[1..]).Replace("*", String.Empty).Trim();
- hash = split[0];
- }
-
- // If the name contains a path, use that path as the machine
- string machine = Path.GetFileNameWithoutExtension(filename);
- if (name.Contains('/'))
- {
- split = name.Split('/');
- machine = split[0];
- name = name[(machine.Length + 1)..];
- }
- else if (name.Contains('\\'))
- {
- split = name.Split('\\');
- machine = split[0];
- name = name[(machine.Length + 1)..];
- }
-
- Rom rom = new()
- {
- Name = name,
- Size = null,
- CRC = (_hash.HasFlag(Hash.CRC) ? hash : null),
- MD5 = (_hash.HasFlag(Hash.MD5) ? hash : null),
- SHA1 = (_hash.HasFlag(Hash.SHA1) ? hash : null),
- SHA256 = (_hash.HasFlag(Hash.SHA256) ? hash : null),
- SHA384 = (_hash.HasFlag(Hash.SHA384) ? hash : null),
- SHA512 = (_hash.HasFlag(Hash.SHA512) ? hash : null),
- SpamSum = (_hash.HasFlag(Hash.SpamSum) ? hash : null),
- ItemStatus = ItemStatus.None,
-
- Machine = new Machine
- {
- Name = machine,
- },
-
- Source = new Source
- {
- Index = indexId,
- Name = filename,
- },
- };
-
- // Now process and add the rom
- ParseAddHelper(rom, statsOnly);
- }
- catch (Exception ex) when (!throwOnError)
- {
- string message = $"'{filename}' - There was an error parsing at position {sr.BaseStream.Position}";
- logger.Error(ex, message);
- }
- }
-
- sr.Dispose();
- }
-
- ///
- protected override ItemType[] GetSupportedTypes()
- {
- return new ItemType[] { ItemType.Disk, ItemType.Media, ItemType.Rom };
- }
-
- ///
- protected override List GetMissingRequiredFields(DatItem datItem)
- {
- List missingFields = new();
-
- // Check item name
- if (string.IsNullOrWhiteSpace(datItem.GetName()))
- missingFields.Add(DatItemField.Name);
-
- // Check hash linked to specific Hashfile type
- switch (_hash)
- {
- case Hash.CRC:
- switch (datItem.ItemType)
- {
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.CRC))
- missingFields.Add(DatItemField.CRC);
- break;
- default:
- missingFields.Add(DatItemField.CRC);
- break;
- }
- break;
- case Hash.MD5:
- switch (datItem.ItemType)
- {
- case ItemType.Disk:
- if (!string.IsNullOrEmpty((datItem as Disk)?.MD5))
- missingFields.Add(DatItemField.MD5);
- break;
- case ItemType.Media:
- if (!string.IsNullOrEmpty((datItem as Media)?.MD5))
- missingFields.Add(DatItemField.MD5);
- break;
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.MD5))
- missingFields.Add(DatItemField.MD5);
- break;
- default:
- missingFields.Add(DatItemField.MD5);
- break;
- }
- break;
- case Hash.SHA1:
- switch (datItem.ItemType)
- {
- case ItemType.Disk:
- if (!string.IsNullOrEmpty((datItem as Disk)?.SHA1))
- missingFields.Add(DatItemField.SHA1);
- break;
- case ItemType.Media:
- if (!string.IsNullOrEmpty((datItem as Media)?.SHA1))
- missingFields.Add(DatItemField.SHA1);
- break;
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.SHA1))
- missingFields.Add(DatItemField.SHA1);
- break;
- default:
- missingFields.Add(DatItemField.SHA1);
- break;
- }
- break;
- case Hash.SHA256:
- switch (datItem.ItemType)
- {
- case ItemType.Media:
- if (!string.IsNullOrEmpty((datItem as Media)?.SHA256))
- missingFields.Add(DatItemField.SHA256);
- break;
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.SHA256))
- missingFields.Add(DatItemField.SHA256);
- break;
- default:
- missingFields.Add(DatItemField.SHA256);
- break;
- }
- break;
- case Hash.SHA384:
- switch (datItem.ItemType)
- {
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.SHA384))
- missingFields.Add(DatItemField.SHA384);
- break;
- default:
- missingFields.Add(DatItemField.SHA384);
- break;
- }
- break;
- case Hash.SHA512:
- switch (datItem.ItemType)
- {
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.SHA512))
- missingFields.Add(DatItemField.SHA512);
- break;
- default:
- missingFields.Add(DatItemField.SHA512);
- break;
- }
- break;
- case Hash.SpamSum:
- switch (datItem.ItemType)
- {
- case ItemType.Media:
- if (!string.IsNullOrEmpty((datItem as Media)?.SpamSum))
- missingFields.Add(DatItemField.SpamSum);
- break;
- case ItemType.Rom:
- if (!string.IsNullOrEmpty((datItem as Rom)?.SpamSum))
- missingFields.Add(DatItemField.SpamSum);
- break;
- default:
- missingFields.Add(DatItemField.SpamSum);
- break;
- }
- break;
- }
-
- return missingFields;
- }
-
- ///
- public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
- {
- try
- {
- logger.User($"Writing to '{outfile}'...");
- FileStream fs = System.IO.File.Create(outfile);
-
- // If we get back null for some reason, just log and return
- if (fs == null)
- {
- logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
- return false;
- }
-
- SeparatedValueWriter svw = new(fs, new UTF8Encoding(false))
- {
- Quotes = false,
- Separator = ' ',
- VerifyFieldCount = true
- };
-
- // Use a sorted list of games to output
- foreach (string key in Items.SortedKeys)
- {
- ConcurrentList datItems = Items[key];
-
- // If this machine doesn't contain any writable items, skip
- if (!ContainsWritable(datItems))
- continue;
-
- // Resolve the names in the block
- datItems = DatItem.ResolveNames(datItems);
-
- for (int index = 0; index < datItems.Count; index++)
- {
- DatItem datItem = datItems[index];
-
- // Check for a "null" item
- datItem = ProcessNullifiedItem(datItem);
-
- // Write out the item if we're not ignoring
- if (!ShouldIgnore(datItem, ignoreblanks))
- WriteDatItem(svw, datItem);
- }
- }
-
- logger.User($"'{outfile}' written!{Environment.NewLine}");
- svw.Dispose();
- fs.Dispose();
- }
- catch (Exception ex) when (!throwOnError)
- {
- logger.Error(ex);
- return false;
- }
-
- return true;
- }
-
- ///
- /// Write out DatItem using the supplied SeparatedValueWriter
- ///
- /// SeparatedValueWriter to output to
- /// DatItem object to be output
- private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
- {
- // Build the state
- string[] fields = new string[2];
-
- // Get the name field
- string name = string.Empty;
- switch (datItem.ItemType)
- {
- case ItemType.Disk:
- var disk = datItem as Disk;
- if (Header.GameName)
- name = $"{disk.Machine.Name}{Path.DirectorySeparatorChar}";
-
- name += disk.Name;
- break;
-
- case ItemType.Media:
- var media = datItem as Media;
- if (Header.GameName)
- name = $"{media.Machine.Name}{Path.DirectorySeparatorChar}";
-
- name += media.Name;
- break;
-
- case ItemType.Rom:
- var rom = datItem as Rom;
- if (Header.GameName)
- name = $"{rom.Machine.Name}{Path.DirectorySeparatorChar}";
-
- name += rom.Name;
- break;
- }
-
- // Get the hash field and set final fields
- switch (_hash)
- {
- case Hash.CRC:
- switch (datItem.ItemType)
- {
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = name;
- fields[1] = rom.CRC;
- break;
- }
-
- break;
-
- case Hash.MD5:
- switch (datItem.ItemType)
- {
- case ItemType.Disk:
- var disk = datItem as Disk;
- fields[0] = disk.MD5;
- fields[1] = name;
- break;
-
- case ItemType.Media:
- var media = datItem as Media;
- fields[0] = media.MD5;
- fields[1] = name;
- break;
-
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = rom.MD5;
- fields[1] = name;
- break;
- }
-
- break;
-
- case Hash.SHA1:
- switch (datItem.ItemType)
- {
- case ItemType.Disk:
- var disk = datItem as Disk;
- fields[0] = disk.SHA1;
- fields[1] = name;
- break;
-
- case ItemType.Media:
- var media = datItem as Media;
- fields[0] = media.SHA1;
- fields[1] = name;
- break;
-
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = rom.SHA1;
- fields[1] = name;
- break;
- }
-
- break;
-
- case Hash.SHA256:
- switch (datItem.ItemType)
- {
- case ItemType.Media:
- var media = datItem as Media;
- fields[0] = media.SHA256;
- fields[1] = name;
- break;
-
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = rom.SHA256;
- fields[1] = name;
- break;
- }
-
- break;
-
- case Hash.SHA384:
- switch (datItem.ItemType)
- {
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = rom.SHA384;
- fields[1] = name;
- break;
- }
-
- break;
-
- case Hash.SHA512:
- switch (datItem.ItemType)
- {
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = rom.SHA512;
- fields[1] = name;
- break;
- }
-
- break;
-
- case Hash.SpamSum:
- switch (datItem.ItemType)
- {
- case ItemType.Media:
- var media = datItem as Media;
- fields[0] = media.SpamSum;
- fields[1] = name;
- break;
-
- case ItemType.Rom:
- var rom = datItem as Rom;
- fields[0] = rom.SpamSum;
- fields[1] = name;
- break;
- }
-
- break;
- }
-
- // If we had at least one field filled in
- if (!string.IsNullOrEmpty(fields[0]) || !string.IsNullOrEmpty(fields[1]))
- svw.WriteValues(fields);
-
- svw.Flush();
- }
}
}
diff --git a/SabreTools.Serialization/ClrMamePro.Serializer.cs b/SabreTools.Serialization/ClrMamePro.Serializer.cs
index 77de8195..52fa24b4 100644
--- a/SabreTools.Serialization/ClrMamePro.Serializer.cs
+++ b/SabreTools.Serialization/ClrMamePro.Serializer.cs
@@ -76,7 +76,7 @@ namespace SabreTools.Serialization
/// Write header information to the current writer
///
/// ClrMamePro representing the header information
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteHeader(Models.ClrMamePro.ClrMamePro? header, ClrMameProWriter writer)
{
// If the header information is missing, we can't do anything
@@ -109,7 +109,7 @@ namespace SabreTools.Serialization
/// Write games information to the current writer
///
/// Array of GameBase objects representing the games information
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteGames(GameBase[]? games, ClrMameProWriter writer)
{
// If the games information is missing, we can't do anything
@@ -128,7 +128,7 @@ namespace SabreTools.Serialization
/// Write game information to the current writer
///
/// GameBase object representing the game information
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteGame(GameBase game, ClrMameProWriter writer)
{
// If the game information is missing, we can't do anything
@@ -183,7 +183,7 @@ namespace SabreTools.Serialization
/// Write releases information to the current writer
///
/// Array of Release objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteReleases(Release[]? releases, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -206,7 +206,7 @@ namespace SabreTools.Serialization
/// Write biossets information to the current writer
///
/// Array of BiosSet objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteBiosSets(BiosSet[]? biossets, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -227,7 +227,7 @@ namespace SabreTools.Serialization
/// Write roms information to the current writer
///
/// Array of Rom objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteRoms(Rom[]? roms, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -266,7 +266,7 @@ namespace SabreTools.Serialization
/// Write disks information to the current writer
///
/// Array of Disk objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteDisks(Disk[]? disks, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -290,7 +290,7 @@ namespace SabreTools.Serialization
/// Write medias information to the current writer
///
/// Array of Media objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteMedia(Media[]? medias, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -313,7 +313,7 @@ namespace SabreTools.Serialization
/// Write samples information to the current writer
///
/// Array of Sample objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteSamples(Sample[]? samples, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -332,7 +332,7 @@ namespace SabreTools.Serialization
/// Write archives information to the current writer
///
/// Array of Archive objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteArchives(Archive[]? archives, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -351,7 +351,7 @@ namespace SabreTools.Serialization
/// Write chips information to the current writer
///
/// Array of Chip objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteChips(Chip[]? chips, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -373,7 +373,7 @@ namespace SabreTools.Serialization
/// Write video information to the current writer
///
/// Video object to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteVideo(Video? video, ClrMameProWriter writer)
{
// If the item is missing, we can't do anything
@@ -395,7 +395,7 @@ namespace SabreTools.Serialization
/// Write sound information to the current writer
///
/// Sound object to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteSound(Sound? sound, ClrMameProWriter writer)
{
// If the item is missing, we can't do anything
@@ -411,7 +411,7 @@ namespace SabreTools.Serialization
/// Write input information to the current writer
///
/// Input object to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteInput(Input? input, ClrMameProWriter writer)
{
// If the item is missing, we can't do anything
@@ -432,7 +432,7 @@ namespace SabreTools.Serialization
/// Write dipswitches information to the current writer
///
/// Array of DipSwitch objects to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteDipSwitches(DipSwitch[]? dipswitches, ClrMameProWriter writer)
{
// If the array is missing, we can't do anything
@@ -456,7 +456,7 @@ namespace SabreTools.Serialization
/// Write driver information to the current writer
///
/// Driver object to write
- /// ClrMameProReader representing the metadata file
+ /// ClrMameProWriter representing the output
private static void WriteDriver(Driver? driver, ClrMameProWriter writer)
{
// If the item is missing, we can't do anything
diff --git a/SabreTools.Serialization/Hashfile.cs b/SabreTools.Serialization/Hashfile.Deserializer.cs
similarity index 98%
rename from SabreTools.Serialization/Hashfile.cs
rename to SabreTools.Serialization/Hashfile.Deserializer.cs
index e438e04d..a2e2900e 100644
--- a/SabreTools.Serialization/Hashfile.cs
+++ b/SabreTools.Serialization/Hashfile.Deserializer.cs
@@ -7,9 +7,9 @@ using SabreTools.Core;
namespace SabreTools.Serialization
{
///
- /// Serializer for hashfile variants
+ /// Deserializer for hashfile variants
///
- public class Hashfile
+ public partial class Hashfile
{
///
/// Deserializes a hashfile variant to the defined type
diff --git a/SabreTools.Serialization/Hashfile.Serializer.cs b/SabreTools.Serialization/Hashfile.Serializer.cs
new file mode 100644
index 00000000..6f66f3d1
--- /dev/null
+++ b/SabreTools.Serialization/Hashfile.Serializer.cs
@@ -0,0 +1,232 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using SabreTools.Core;
+using SabreTools.IO.Writers;
+using SabreTools.Models.Hashfile;
+
+namespace SabreTools.Serialization
+{
+ ///
+ /// Serializer for hashfile variants
+ ///
+ public partial class Hashfile
+ {
+ ///
+ /// Serializes the defined type to a hashfile variant file
+ ///
+ /// Data to serialize
+ /// Path to the file to serialize to
+ /// Hash corresponding to the hashfile variant
+ /// True on successful serialization, false otherwise
+ public static bool SerializeToFile(Models.Hashfile.Hashfile? hashfile, string path, Hash hash)
+ {
+ try
+ {
+ using var stream = SerializeToStream(hashfile, hash);
+ if (stream == null)
+ return false;
+
+ using var fs = File.OpenWrite(path);
+ stream.Seek(0, SeekOrigin.Begin);
+ stream.CopyTo(fs);
+ return true;
+ }
+ catch
+ {
+ // TODO: Handle logging the exception
+ return false;
+ }
+ }
+
+ ///
+ /// Serializes the defined type to a stream
+ ///
+ /// Data to serialize
+ /// Hash corresponding to the hashfile variant
+ /// Stream containing serialized data on success, null otherwise
+ public static Stream? SerializeToStream(Models.Hashfile.Hashfile? hashfile, Hash hash)
+ {
+ try
+ {
+ // If the metadata file is null
+ if (hashfile == null)
+ return null;
+
+ // Setup the writer and output
+ var stream = new MemoryStream();
+ var writer = new SeparatedValueWriter(stream, Encoding.UTF8) { Separator = ' ', Quotes = false };
+
+ // Write out the items, if they exist
+ switch (hash)
+ {
+ case Hash.CRC:
+ WriteSFV(hashfile.SFV, writer);
+ break;
+ case Hash.MD5:
+ WriteMD5(hashfile.MD5, writer);
+ break;
+ case Hash.SHA1:
+ WriteSHA1(hashfile.SHA1, writer);
+ break;
+ case Hash.SHA256:
+ WriteSHA256(hashfile.SHA256, writer);
+ break;
+ case Hash.SHA384:
+ WriteSHA384(hashfile.SHA384, writer);
+ break;
+ case Hash.SHA512:
+ WriteSHA512(hashfile.SHA512, writer);
+ break;
+ case Hash.SpamSum:
+ WriteSpamSum(hashfile.SpamSum, writer);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(hash));
+ }
+
+ // Return the stream
+ return stream;
+ }
+ catch
+ {
+ // TODO: Handle logging the exception
+ return null;
+ }
+ }
+
+ ///
+ /// Write SFV information to the current writer
+ ///
+ /// Array of SFV objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteSFV(SFV[]? sfvs, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (sfvs == null || !sfvs.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var sfv in sfvs)
+ {
+ writer.WriteValues(new string[] { sfv.File, sfv.Hash });
+ writer.Flush();
+ }
+ }
+
+ ///
+ /// Write MD5 information to the current writer
+ ///
+ /// Array of MD5 objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteMD5(MD5[]? md5s, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (md5s == null || !md5s.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var md5 in md5s)
+ {
+ writer.WriteValues(new string[] { md5.Hash, md5.File });
+ writer.Flush();
+ }
+ }
+
+ ///
+ /// Write SHA1 information to the current writer
+ ///
+ /// Array of SHA1 objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteSHA1(SHA1[]? sha1s, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (sha1s == null || !sha1s.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var sha1 in sha1s)
+ {
+ writer.WriteValues(new string[] { sha1.Hash, sha1.File });
+ writer.Flush();
+ }
+ }
+
+ ///
+ /// Write SHA256 information to the current writer
+ ///
+ /// Array of SHA256 objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteSHA256(SHA256[]? sha256s, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (sha256s == null || !sha256s.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var sha256 in sha256s)
+ {
+ writer.WriteValues(new string[] { sha256.Hash, sha256.File });
+ writer.Flush();
+ }
+ }
+
+ ///
+ /// Write SHA384 information to the current writer
+ ///
+ /// Array of SHA384 objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteSHA384(SHA384[]? sha384s, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (sha384s == null || !sha384s.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var sha384 in sha384s)
+ {
+ writer.WriteValues(new string[] { sha384.Hash, sha384.File });
+ writer.Flush();
+ }
+ }
+
+ ///
+ /// Write SHA512 information to the current writer
+ ///
+ /// Array of SHA512 objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteSHA512(SHA512[]? sha512s, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (sha512s == null || !sha512s.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var sha512 in sha512s)
+ {
+ writer.WriteValues(new string[] { sha512.Hash, sha512.File });
+ writer.Flush();
+ }
+ }
+
+ ///
+ /// Write SpamSum information to the current writer
+ ///
+ /// Array of SpamSum objects representing the files
+ /// SeparatedValueWriter representing the output
+ private static void WriteSpamSum(SpamSum[]? spamsums, SeparatedValueWriter writer)
+ {
+ // If the item information is missing, we can't do anything
+ if (spamsums == null || !spamsums.Any())
+ return;
+
+ // Loop through and write out the items
+ foreach (var spamsum in spamsums)
+ {
+ writer.WriteValues(new string[] { spamsum.Hash, spamsum.File });
+ writer.Flush();
+ }
+ }
+ }
+}
\ No newline at end of file