using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Serialization; using Newtonsoft.Json; using SabreTools.Core; using SabreTools.Core.Filter; using SabreTools.Core.Tools; using SabreTools.DatFiles.Formats; using SabreTools.DatItems; using SabreTools.DatItems.Formats; using SabreTools.Hashing; using SabreTools.Logging; namespace SabreTools.DatFiles { /// /// Represents a format-agnostic DAT /// [JsonObject("datfile"), XmlRoot("datfile")] public abstract partial class DatFile { #region Fields /// /// Header values /// [JsonProperty("header"), XmlElement("header")] public DatHeader Header { get; private set; } = new DatHeader(); /// /// DatItems and related statistics /// [JsonProperty("items"), XmlElement("items")] public ItemDictionary Items { get; private set; } = []; /// /// DatItems and related statistics /// [JsonProperty("items"), XmlElement("items")] public ItemDictionaryDB ItemsDB { get; private set; } = new ItemDictionaryDB(); #endregion #region Logging /// /// Logging object /// [JsonIgnore, XmlIgnore] protected Logger logger; #endregion #region Constructors /// /// Create a new DatFile from an existing one /// /// DatFile to get the values from public DatFile(DatFile? datFile) { logger = new Logger(this); if (datFile != null) { Header = datFile.Header; Items = datFile.Items; ItemsDB = datFile.ItemsDB; } } /// /// Create a specific type of DatFile to be used based on a format and a base DAT /// /// Format of the DAT to be created /// DatFile containing the information to use in specific operations /// For relevant types, assume the usage of quotes /// DatFile of the specific internal type that corresponds to the inputs public static DatFile Create(DatFormat? datFormat = null, DatFile? baseDat = null, bool quotes = true) { return datFormat switch { DatFormat.ArchiveDotOrg => new ArchiveDotOrg(baseDat), DatFormat.AttractMode => new AttractMode(baseDat), DatFormat.ClrMamePro => new ClrMamePro(baseDat, quotes), DatFormat.CSV => new CommaSeparatedValue(baseDat), DatFormat.DOSCenter => new DosCenter(baseDat), DatFormat.EverdriveSMDB => new EverdriveSMDB(baseDat), DatFormat.Listrom => new Listrom(baseDat), DatFormat.Listxml => new Listxml(baseDat), DatFormat.Logiqx => new Logiqx(baseDat, false), DatFormat.LogiqxDeprecated => new Logiqx(baseDat, true), DatFormat.MissFile => new Missfile(baseDat), DatFormat.OfflineList => new OfflineList(baseDat), DatFormat.OpenMSX => new OpenMSX(baseDat), DatFormat.RedumpMD5 => new Md5File(baseDat), DatFormat.RedumpSFV => new SfvFile(baseDat), DatFormat.RedumpSHA1 => new Sha1File(baseDat), DatFormat.RedumpSHA256 => new Sha256File(baseDat), DatFormat.RedumpSHA384 => new Sha384File(baseDat), DatFormat.RedumpSHA512 => new Sha512File(baseDat), DatFormat.RedumpSpamSum => new SpamSumFile(baseDat), DatFormat.RomCenter => new RomCenter(baseDat), DatFormat.SabreJSON => new SabreJSON(baseDat), DatFormat.SabreXML => new SabreXML(baseDat), DatFormat.SoftwareList => new Formats.SoftwareList(baseDat), DatFormat.SSV => new SemicolonSeparatedValue(baseDat), DatFormat.TSV => new TabSeparatedValue(baseDat), // We use new-style Logiqx as a backup for generic DatFile _ => new Logiqx(baseDat, false), }; } /// /// Create a new DatFile from an existing DatHeader /// /// DatHeader to get the values from public static DatFile Create(DatHeader datHeader) { DatFormat format = datHeader.GetFieldValue(DatHeader.DatFormatKey); DatFile datFile = Create(format); datFile.Header = (DatHeader)datHeader.Clone(); return datFile; } /// /// Fill the header values based on existing Header and path /// /// Path used for creating a name, if necessary /// True if the date should be omitted from name and description, false otherwise public void FillHeaderFromPath(string path, bool bare) { // Get the header strings string? name = Header.GetStringFieldValue(Models.Metadata.Header.NameKey); string? description = Header.GetStringFieldValue(Models.Metadata.Header.DescriptionKey); string? date = Header.GetStringFieldValue(Models.Metadata.Header.DateKey); // If the description is defined but not the name, set the name from the description if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(description)) { Header.SetFieldValue(Models.Metadata.Header.NameKey, description); } // If the name is defined but not the description, set the description from the name else if (!string.IsNullOrEmpty(name) && string.IsNullOrEmpty(description)) { Header.SetFieldValue(Models.Metadata.Header.DescriptionKey, name + (bare ? string.Empty : $" ({date})")); } // If neither the name or description are defined, set them from the automatic values else if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(description)) { string[] splitpath = path.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); #if NETFRAMEWORK Header.SetFieldValue(Models.Metadata.Header.NameKey, splitpath[splitpath.Length - 1]); Header.SetFieldValue(Models.Metadata.Header.DescriptionKey, splitpath[splitpath.Length - 1] + (bare ? string.Empty : $" ({date})")); #else Header.SetFieldValue(Models.Metadata.Header.NameKey, splitpath[^1]); Header.SetFieldValue(Models.Metadata.Header.DescriptionKey, splitpath[^1] + (bare ? string.Empty : $" ({date})")); #endif } } #endregion #region Accessors /// /// Reset the internal item dictionary /// public void ResetDictionary() { Items = []; ItemsDB = new ItemDictionaryDB(); } #endregion #region Filtering /// /// Execute all filters in a filter runner on the items in the dictionary /// /// Preconfigured filter runner to use public void ExecuteFilters(FilterRunner filterRunner) { Items.ExecuteFilters(filterRunner); ItemsDB.ExecuteFilters(filterRunner); } #endregion #region Parsing /// /// Parse DatFile and return all found games and roms within /// /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) /// True to only add item statistics while parsing, false otherwise /// True if the error that is thrown should be thrown back to the caller, false otherwise public abstract void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false); /// /// Add a DatItem to the dictionary after checking /// /// Item data to check against /// True to only add item statistics while parsing, false otherwise /// The key for the item protected string ParseAddHelper(DatItem item, bool statsOnly) => Items.AddItem(item, statsOnly); /// /// Add a DatItem to the dictionary after checking /// /// Item data to check against /// Index of the machine to map the DatItem to /// Index of the source to map the DatItem to /// True to only add item statistics while parsing, false otherwise /// The key for the item protected long ParseAddHelper(DatItem item, long machineIndex, long sourceIndex, bool statsOnly) => ItemsDB.AddItem(item, machineIndex, sourceIndex, statsOnly); #endregion #region Writing /// /// Create and open an output file for writing direct from a dictionary /// /// Name of the file to write to /// True if blank roms should be skipped on output, false otherwise (default) /// True if the error that is thrown should be thrown back to the caller, false otherwise /// True if the DAT was written correctly, false otherwise public abstract bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false); /// /// Create and open an output file for writing direct from a dictionary /// /// Name of the file to write to /// True if blank roms should be skipped on output, false otherwise (default) /// True if the error that is thrown should be thrown back to the caller, false otherwise /// True if the DAT was written correctly, false otherwise public abstract bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false); /// /// Create a prefix or postfix from inputs /// /// DatItem to create a prefix/postfix for /// True for prefix, false for postfix /// Sanitized string representing the postfix or prefix protected string CreatePrefixPostfix(DatItem item, bool prefix) { // Get machine for the item var machine = item.GetFieldValue(DatItem.MachineKey); if (machine == null) return string.Empty; // Initialize strings string? type = item.GetStringFieldValue(Models.Metadata.DatItem.TypeKey); string fix, game = machine.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, manufacturer = machine.GetStringFieldValue(Models.Metadata.Machine.ManufacturerKey) ?? string.Empty, publisher = machine.GetStringFieldValue(Models.Metadata.Machine.PublisherKey) ?? string.Empty, category = machine.GetStringFieldValue(Models.Metadata.Machine.CategoryKey) ?? string.Empty, name = item.GetName() ?? type.AsEnumValue().AsStringValue() ?? string.Empty, crc = string.Empty, md5 = string.Empty, sha1 = string.Empty, sha256 = string.Empty, sha384 = string.Empty, sha512 = string.Empty, size = string.Empty, spamsum = string.Empty; // Check for quotes bool? quotes = Header.GetBoolFieldValue(DatHeader.QuotesKey); // If we have a prefix if (prefix) { string? prefixString = Header.GetStringFieldValue(DatHeader.PrefixKey); fix = prefixString + (quotes == true ? "\"" : string.Empty); } // If we have a postfix else { string? postfixString = Header.GetStringFieldValue(DatHeader.PostfixKey); fix = (quotes == true ? "\"" : string.Empty) + postfixString; } // Ensure we have the proper values for replacement if (item is Disk disk) { md5 = disk.GetStringFieldValue(Models.Metadata.Disk.MD5Key) ?? string.Empty; sha1 = disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key) ?? string.Empty; } else if (item is Media media) { md5 = media.GetStringFieldValue(Models.Metadata.Media.MD5Key) ?? string.Empty; sha1 = media.GetStringFieldValue(Models.Metadata.Media.SHA1Key) ?? string.Empty; sha256 = media.GetStringFieldValue(Models.Metadata.Media.SHA256Key) ?? string.Empty; spamsum = media.GetStringFieldValue(Models.Metadata.Media.SpamSumKey) ?? string.Empty; } else if (item is Rom rom) { crc = rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) ?? string.Empty; md5 = rom.GetStringFieldValue(Models.Metadata.Rom.MD5Key) ?? string.Empty; sha1 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key) ?? string.Empty; sha256 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA256Key) ?? string.Empty; sha384 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA384Key) ?? string.Empty; sha512 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA512Key) ?? string.Empty; size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey).ToString() ?? string.Empty; spamsum = rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) ?? string.Empty; } // Now do bulk replacement where possible fix = fix .Replace("%game%", game) .Replace("%machine%", game) .Replace("%name%", name) .Replace("%manufacturer%", manufacturer) .Replace("%publisher%", publisher) .Replace("%category%", category) .Replace("%crc%", crc) .Replace("%md5%", md5) .Replace("%sha1%", sha1) .Replace("%sha256%", sha256) .Replace("%sha384%", sha384) .Replace("%sha512%", sha512) .Replace("%size%", size) .Replace("%spamsum%", spamsum); return fix; } /// /// Create a prefix or postfix from inputs /// /// DatItem to create a prefix/postfix for /// True for prefix, false for postfix /// Sanitized string representing the postfix or prefix protected string CreatePrefixPostfixDB((long, DatItem) item, bool prefix) { // Get machine for the item var machine = ItemsDB.GetMachineForItem(item.Item1); if (machine.Item2 == null) return string.Empty; // Initialize strings string? type = item.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey); string fix, game = machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, manufacturer = machine.Item2.GetStringFieldValue(Models.Metadata.Machine.ManufacturerKey) ?? string.Empty, publisher = machine.Item2.GetStringFieldValue(Models.Metadata.Machine.PublisherKey) ?? string.Empty, category = machine.Item2.GetStringFieldValue(Models.Metadata.Machine.CategoryKey) ?? string.Empty, name = item.Item2.GetName() ?? type.AsEnumValue().AsStringValue() ?? string.Empty, crc = string.Empty, md5 = string.Empty, sha1 = string.Empty, sha256 = string.Empty, sha384 = string.Empty, sha512 = string.Empty, size = string.Empty, spamsum = string.Empty; // Check for quotes bool? quotes = Header.GetBoolFieldValue(DatHeader.QuotesKey); // If we have a prefix if (prefix) { string? prefixString = Header.GetStringFieldValue(DatHeader.PrefixKey); fix = prefixString + (quotes == true ? "\"" : string.Empty); } // If we have a postfix else { string? postfixString = Header.GetStringFieldValue(DatHeader.PostfixKey); fix = (quotes == true ? "\"" : string.Empty) + postfixString; } // Ensure we have the proper values for replacement if (item.Item2 is Disk disk) { md5 = disk.GetStringFieldValue(Models.Metadata.Disk.MD5Key) ?? string.Empty; sha1 = disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key) ?? string.Empty; } else if (item.Item2 is Media media) { md5 = media.GetStringFieldValue(Models.Metadata.Media.MD5Key) ?? string.Empty; sha1 = media.GetStringFieldValue(Models.Metadata.Media.SHA1Key) ?? string.Empty; sha256 = media.GetStringFieldValue(Models.Metadata.Media.SHA256Key) ?? string.Empty; spamsum = media.GetStringFieldValue(Models.Metadata.Media.SpamSumKey) ?? string.Empty; } else if (item.Item2 is Rom rom) { crc = rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) ?? string.Empty; md5 = rom.GetStringFieldValue(Models.Metadata.Rom.MD5Key) ?? string.Empty; sha1 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key) ?? string.Empty; sha256 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA256Key) ?? string.Empty; sha384 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA384Key) ?? string.Empty; sha512 = rom.GetStringFieldValue(Models.Metadata.Rom.SHA512Key) ?? string.Empty; size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey).ToString() ?? string.Empty; spamsum = rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) ?? string.Empty; } // Now do bulk replacement where possible fix = fix .Replace("%game%", game) .Replace("%machine%", game) .Replace("%name%", name) .Replace("%manufacturer%", manufacturer) .Replace("%publisher%", publisher) .Replace("%category%", category) .Replace("%crc%", crc) .Replace("%md5%", md5) .Replace("%sha1%", sha1) .Replace("%sha256%", sha256) .Replace("%sha384%", sha384) .Replace("%sha512%", sha512) .Replace("%size%", size) .Replace("%spamsum%", spamsum); return fix; } /// /// Process an item and correctly set the item name /// /// DatItem to update /// True if the Quotes flag should be ignored, false otherwise /// True if the UseRomName should be always on (default), false otherwise protected void ProcessItemName(DatItem item, bool forceRemoveQuotes, bool forceRomName = true) { // Backup relevant values and set new ones accordingly bool? quotesBackup = Header.GetBoolFieldValue(DatHeader.QuotesKey); bool? useRomNameBackup = Header.GetBoolFieldValue(DatHeader.UseRomNameKey); if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, false); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, true); // Get the machine var machine = item.GetFieldValue(DatItem.MachineKey); if (machine == null) return; // Get the name to update string? name = (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true ? item.GetName() : machine.GetStringFieldValue(Models.Metadata.Machine.NameKey)) ?? string.Empty; // Create the proper Prefix and Postfix string pre = CreatePrefixPostfix(item, true); string post = CreatePrefixPostfix(item, false); // If we're in Depot mode, take care of that instead var outputDepot = Header.GetFieldValue(DatHeader.OutputDepotKey); if (outputDepot?.IsActive == true) { if (item is Disk disk) { // We can only write out if there's a SHA-1 if (!string.IsNullOrEmpty(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key))) { name = Utilities.GetDepotPath(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key), outputDepot.Depth)?.Replace('\\', '/'); item.SetName($"{pre}{name}{post}"); } } else if (item is Media media) { // We can only write out if there's a SHA-1 if (!string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key))) { name = Utilities.GetDepotPath(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key), outputDepot.Depth)?.Replace('\\', '/'); item.SetName($"{pre}{name}{post}"); } } else if (item is Rom rom) { // We can only write out if there's a SHA-1 if (!string.IsNullOrEmpty(rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key))) { name = Utilities.GetDepotPath(rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key), outputDepot.Depth)?.Replace('\\', '/'); item.SetName($"{pre}{name}{post}"); } } return; } string? replaceExtension = Header.GetStringFieldValue(DatHeader.ReplaceExtensionKey); bool? removeExtension = Header.GetBoolFieldValue(DatHeader.RemoveExtensionKey); if (!string.IsNullOrEmpty(replaceExtension) || removeExtension == true) { if (removeExtension == true) Header.SetFieldValue(DatHeader.ReplaceExtensionKey, string.Empty); string? dir = Path.GetDirectoryName(name); if (dir != null) { dir = dir.TrimStart(Path.DirectorySeparatorChar); name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + replaceExtension); } } string? addExtension = Header.GetStringFieldValue(DatHeader.AddExtensionKey); if (!string.IsNullOrEmpty(addExtension)) name += addExtension; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true && Header.GetBoolFieldValue(DatHeader.GameNameKey) == true) name = Path.Combine(machine.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, name); // Now assign back the formatted name name = $"{pre}{name}{post}"; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true) item.SetName(name); else machine.SetFieldValue(Models.Metadata.Machine.NameKey, name); // Restore all relevant values if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, quotesBackup); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, useRomNameBackup); } /// /// Process an item and correctly set the item name /// /// DatItem to update /// True if the Quotes flag should be ignored, false otherwise /// True if the UseRomName should be always on (default), false otherwise protected void ProcessItemNameDB((long, DatItem) item, bool forceRemoveQuotes, bool forceRomName = true) { // Backup relevant values and set new ones accordingly bool? quotesBackup = Header.GetBoolFieldValue(DatHeader.QuotesKey); bool? useRomNameBackup = Header.GetBoolFieldValue(DatHeader.UseRomNameKey); if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, false); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, true); // Get machine for the item var machine = ItemsDB.GetMachineForItem(item.Item1); // Get the name to update string? name = (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true ? item.Item2.GetName() : machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey)) ?? string.Empty; // Create the proper Prefix and Postfix string pre = CreatePrefixPostfixDB(item, true); string post = CreatePrefixPostfixDB(item, false); // If we're in Depot mode, take care of that instead var outputDepot = Header.GetFieldValue(DatHeader.OutputDepotKey); if (outputDepot?.IsActive == true) { if (item.Item2 is Disk disk) { // We can only write out if there's a SHA-1 if (!string.IsNullOrEmpty(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key))) { name = Utilities.GetDepotPath(disk.GetStringFieldValue(Models.Metadata.Disk.SHA1Key), outputDepot.Depth)?.Replace('\\', '/'); item.Item2.SetName($"{pre}{name}{post}"); } } else if (item.Item2 is Media media) { // We can only write out if there's a SHA-1 if (!string.IsNullOrEmpty(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key))) { name = Utilities.GetDepotPath(media.GetStringFieldValue(Models.Metadata.Media.SHA1Key), outputDepot.Depth)?.Replace('\\', '/'); item.Item2.SetName($"{pre}{name}{post}"); } } else if (item.Item2 is Rom rom) { // We can only write out if there's a SHA-1 if (!string.IsNullOrEmpty(rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key))) { name = Utilities.GetDepotPath(rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key), outputDepot.Depth)?.Replace('\\', '/'); item.Item2.SetName($"{pre}{name}{post}"); } } return; } string? replaceExtension = Header.GetStringFieldValue(DatHeader.ReplaceExtensionKey); bool? removeExtension = Header.GetBoolFieldValue(DatHeader.RemoveExtensionKey); if (!string.IsNullOrEmpty(replaceExtension) || removeExtension == true) { if (removeExtension == true) Header.SetFieldValue(DatHeader.ReplaceExtensionKey, string.Empty); string? dir = Path.GetDirectoryName(name); if (dir != null) { dir = dir.TrimStart(Path.DirectorySeparatorChar); name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + replaceExtension); } } string? addExtension = Header.GetStringFieldValue(DatHeader.AddExtensionKey); if (!string.IsNullOrEmpty(addExtension)) name += addExtension; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true && Header.GetBoolFieldValue(DatHeader.GameNameKey) == true) name = Path.Combine(machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey) ?? string.Empty, name); // Now assign back the formatted name name = $"{pre}{name}{post}"; if (Header.GetBoolFieldValue(DatHeader.UseRomNameKey) == true) item.Item2.SetName(name); else if (machine.Item2 != null) machine.Item2.SetFieldValue(Models.Metadata.Machine.NameKey, name); // Restore all relevant values if (forceRemoveQuotes) Header.SetFieldValue(DatHeader.QuotesKey, quotesBackup); if (forceRomName) Header.SetFieldValue(DatHeader.UseRomNameKey, useRomNameBackup); } /// /// Process any DatItems that are "null", usually created from directory population /// /// DatItem to check for "null" status /// Cleaned DatItem protected DatItem ProcessNullifiedItem(DatItem item) { // If we don't have a Rom, we can ignore it if (item is not Rom rom) return item; // Get machine for the item var machine = item.GetFieldValue(DatItem.MachineKey); if (machine == null) return item; // If the Rom has "null" characteristics, ensure all fields if (rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey) == null && rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) == "null") { logger.Verbose($"Empty folder found: {machine.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); rom.SetName(rom.GetName() == "null" ? "-" : rom.GetName()); rom.SetFieldValue(Models.Metadata.Rom.SizeKey, Constants.SizeZero.ToString()); rom.SetFieldValue(Models.Metadata.Rom.CRCKey, rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) == "null" ? Constants.CRCZero : null); rom.SetFieldValue(Models.Metadata.Rom.MD5Key, rom.GetStringFieldValue(Models.Metadata.Rom.MD5Key) == "null" ? Constants.MD5Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA1Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key) == "null" ? Constants.SHA1Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA256Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA256Key) == "null" ? Constants.SHA256Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA384Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA384Key) == "null" ? Constants.SHA384Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA512Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA512Key) == "null" ? Constants.SHA512Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SpamSumKey, rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) == "null" ? Constants.SpamSumZero : null); } return rom; } /// /// Process any DatItems that are "null", usually created from directory population /// /// DatItem to check for "null" status /// Cleaned DatItem protected (long, DatItem) ProcessNullifiedItem((long, DatItem) item) { // If we don't have a Rom, we can ignore it if (item.Item2 is not Rom rom) return item; // Get machine for the item var machine = ItemsDB.GetMachineForItem(item.Item1); var machineObj = machine.Item2; if (machineObj == null) return item; // If the Rom has "null" characteristics, ensure all fields if (rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey) == null && rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) == "null") { logger.Verbose($"Empty folder found: {machineObj.GetStringFieldValue(Models.Metadata.Machine.NameKey)}"); rom.SetName(rom.GetName() == "null" ? "-" : rom.GetName()); rom.SetFieldValue(Models.Metadata.Rom.SizeKey, Constants.SizeZero.ToString()); rom.SetFieldValue(Models.Metadata.Rom.CRCKey, rom.GetStringFieldValue(Models.Metadata.Rom.CRCKey) == "null" ? Constants.CRCZero : null); rom.SetFieldValue(Models.Metadata.Rom.MD5Key, rom.GetStringFieldValue(Models.Metadata.Rom.MD5Key) == "null" ? Constants.MD5Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA1Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA1Key) == "null" ? Constants.SHA1Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA256Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA256Key) == "null" ? Constants.SHA256Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA384Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA384Key) == "null" ? Constants.SHA384Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SHA512Key, rom.GetStringFieldValue(Models.Metadata.Rom.SHA512Key) == "null" ? Constants.SHA512Zero : null); rom.SetFieldValue(Models.Metadata.Rom.SpamSumKey, rom.GetStringFieldValue(Models.Metadata.Rom.SpamSumKey) == "null" ? Constants.SpamSumZero : null); } return (item.Item1, rom); } /// /// Get supported types for write /// /// List of supported types for writing protected virtual ItemType[] GetSupportedTypes() { return Enum.GetValues(typeof(ItemType)) as ItemType[] ?? []; } /// /// Return list of required fields missing from a DatItem /// /// List of missing required fields, null or empty if none were found protected virtual List? GetMissingRequiredFields(DatItem datItem) => null; /// /// Get if a machine contains any writable items /// /// DatItems to check /// True if the machine contains at least one writable item, false otherwise /// Empty machines are kept with this protected bool ContainsWritable(ConcurrentList datItems) { // Empty machines are considered writable if (datItems == null || datItems.Count == 0) return true; foreach (DatItem datItem in datItems) { ItemType itemType = datItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); if (GetSupportedTypes().Contains(itemType)) return true; } return false; } /// /// Get if a machine contains any writable items /// /// DatItems to check /// True if the machine contains at least one writable item, false otherwise /// Empty machines are kept with this protected bool ContainsWritable((long, DatItem)[]? datItems) { // Empty machines are considered writable if (datItems == null || datItems.Length == 0) return true; foreach ((long, DatItem) datItem in datItems) { ItemType itemType = datItem.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); if (GetSupportedTypes().Contains(itemType)) return true; } return false; } /// /// Get if an item should be ignored on write /// /// DatItem to check /// True if blank roms should be skipped on output, false otherwise /// True if the item should be skipped on write, false otherwise protected bool ShouldIgnore(DatItem? datItem, bool ignoreBlanks) { // If this is invoked with a null DatItem, we ignore if (datItem == null) { logger?.Verbose($"Item was skipped because it was null"); return true; } // If the item is supposed to be removed, we ignore if (datItem.GetBoolFieldValue(DatItem.RemoveKey) == true) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it was marked for removal"); return true; } // If we have the Blank dat item, we ignore if (datItem is Blank) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it was of type 'Blank'"); return true; } // If we're ignoring blanks and we have a Rom if (ignoreBlanks && datItem is Rom rom) { // If we have a 0-size or blank rom, then we ignore long? size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey); if (size == 0 || size == null) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it had an invalid size"); return true; } } // If we have an item type not in the list of supported values string datFormat = Header?.GetFieldValue(DatHeader.DatFormatKey).ToString() ?? "Unknown Format"; if (!GetSupportedTypes().Contains(datItem.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue())) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it was not supported in {datFormat}"); return true; } // If we have an item with missing required fields List? missingFields = GetMissingRequiredFields(datItem); if (missingFields != null && missingFields.Count != 0) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); #if NET20 || NET35 logger?.Verbose($"Item '{itemString}' was skipped because it was missing required fields for {datFormat}: {string.Join(", ", [.. missingFields])}"); #else logger?.Verbose($"Item '{itemString}' was skipped because it was missing required fields for {datFormat}: {string.Join(", ", missingFields)}"); #endif return true; } return false; } /// /// Get if an item should be ignored on write /// /// DatItem to check /// True if blank roms should be skipped on output, false otherwise /// True if the item should be skipped on write, false otherwise protected bool ShouldIgnore((long, DatItem?) datItem, bool ignoreBlanks) { // If this is invoked with a null DatItem, we ignore if (datItem.Item1 < 0 || datItem.Item2 == null) { logger?.Verbose($"Item was skipped because it was null"); return true; } // If the item is supposed to be removed, we ignore if (datItem.Item2.GetBoolFieldValue(DatItem.RemoveKey) == true) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it was marked for removal"); return true; } // If we have the Blank dat item, we ignore if (datItem.Item2 is Blank) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it was of type 'Blank'"); return true; } // If we're ignoring blanks and we have a Rom if (ignoreBlanks && datItem.Item2 is Rom rom) { // If we have a 0-size or blank rom, then we ignore long? size = rom.GetInt64FieldValue(Models.Metadata.Rom.SizeKey); if (size == 0 || size == null) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it had an invalid size"); return true; } } // If we have an item type not in the list of supported values string datFormat = Header?.GetFieldValue(DatHeader.DatFormatKey).ToString() ?? "Unknown Format"; if (!GetSupportedTypes().Contains(datItem.Item2.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue())) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); logger?.Verbose($"Item '{itemString}' was skipped because it was not supported in {datFormat}"); return true; } // If we have an item with missing required fields List? missingFields = GetMissingRequiredFields(datItem.Item2); if (missingFields != null && missingFields.Count != 0) { string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); #if NET20 || NET35 logger?.Verbose($"Item '{itemString}' was skipped because it was missing required fields for {datFormat}: {string.Join(", ", [.. missingFields])}"); #else logger?.Verbose($"Item '{itemString}' was skipped because it was missing required fields for {datFormat}: {string.Join(", ", missingFields)}"); #endif return true; } return false; } #endregion } }