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