From e11a08b5877484dff89d2c36cea1be7dcd4663ba Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 24 Mar 2026 18:03:01 -0400 Subject: [PATCH] Port metadata functionality from ST --- .../ConvertersTests.cs | 23 + .../DatFileTests.Filtering.cs | 304 +++ .../DatFileTests.FromMetadata.cs | 1564 +++++++++++ .../DatFileTests.Splitting.cs | 847 ++++++ .../DatFileTests.ToMetadata.cs | 1166 ++++++++ .../DatFileTests.cs | 2343 +++++++++++++++++ .../DatHeaderTests.cs | 211 ++ .../DatStatisticsTests.cs | 326 +++ .../ExtensionsTests.cs | 114 + .../FormatsTests.cs | 1647 ++++++++++++ .../ItemDictionaryDBTests.cs | 1025 +++++++ .../ItemDictionaryTests.cs | 818 ++++++ .../SabreTools.Metadata.DatFiles.Test.csproj | 24 + .../DatFile.Filtering.cs | 801 ++++++ .../DatFile.FromMetadata.cs | 715 +++++ .../DatFile.Splitting.cs | 1506 +++++++++++ .../DatFile.ToMetadata.cs | 1107 ++++++++ SabreTools.Metadata.DatFiles/DatFile.cs | 1252 +++++++++ SabreTools.Metadata.DatFiles/DatHeader.cs | 229 ++ SabreTools.Metadata.DatFiles/DatModifiers.cs | 88 + SabreTools.Metadata.DatFiles/DatStatistics.cs | 725 +++++ .../DepotInformation.cs | 66 + SabreTools.Metadata.DatFiles/Enums.cs | 267 ++ SabreTools.Metadata.DatFiles/Extensions.cs | 172 ++ .../Formats/ArchiveDotOrg.cs | 25 + .../Formats/AttractMode.cs | 38 + .../Formats/ClrMamePro.cs | 203 ++ .../Formats/DosCenter.cs | 55 + .../Formats/EverdriveSmdb.cs | 55 + .../Formats/Hashfile.cs | 601 +++++ .../Formats/Listrom.cs | 63 + .../Formats/Listxml.cs | 397 +++ .../Formats/Logiqx.cs | 404 +++ .../Formats/Missfile.cs | 214 ++ .../Formats/OfflineList.cs | 47 + .../Formats/OpenMSX.cs | 88 + .../Formats/RomCenter.cs | 51 + .../Formats/SabreJSON.cs | 719 +++++ .../Formats/SabreXML.cs | 522 ++++ .../Formats/SeparatedValue.cs | 191 ++ .../Formats/SerializableDatFile.cs | 121 + .../Formats/SoftwareList.cs | 213 ++ .../ItemDictionary.cs | 931 +++++++ .../ItemDictionaryDB.cs | 1263 +++++++++ SabreTools.Metadata.DatFiles/ItemMappings.cs | 12 + .../SabreTools.Metadata.DatFiles.csproj | 53 + .../ConvertersTests.cs | 37 + .../DatItemTests.cs | 539 ++++ .../ExtensionsTests.cs | 590 +++++ .../Formats/DiskTests.cs | 269 ++ .../Formats/FileTests.cs | 360 +++ .../Formats/MediaTests.cs | 323 +++ .../Formats/RomTests.cs | 672 +++++ .../MachineTests.cs | 87 + .../SabreTools.Metadata.DatItems.Test.csproj | 22 + .../SourceTests.cs | 23 + SabreTools.Metadata.DatItems/DatItem.cs | 346 +++ SabreTools.Metadata.DatItems/DatItemT.cs | 112 + SabreTools.Metadata.DatItems/Enums.cs | 685 +++++ SabreTools.Metadata.DatItems/Extensions.cs | 788 ++++++ .../Formats/Adjuster.cs | 70 + .../Formats/Analog.cs | 33 + .../Formats/Archive.cs | 100 + .../Formats/BiosSet.cs | 39 + SabreTools.Metadata.DatItems/Formats/Blank.cs | 95 + SabreTools.Metadata.DatItems/Formats/Chip.cs | 41 + .../Formats/Condition.cs | 38 + .../Formats/ConfLocation.cs | 39 + .../Formats/ConfSetting.cs | 71 + .../Formats/Configuration.cs | 115 + .../Formats/Control.cs | 55 + .../Formats/DataArea.cs | 43 + .../Formats/Device.cs | 98 + .../Formats/DeviceRef.cs | 33 + .../Formats/DipLocation.cs | 39 + .../Formats/DipSwitch.cs | 141 + .../Formats/DipValue.cs | 71 + SabreTools.Metadata.DatItems/Formats/Disk.cs | 184 ++ .../Formats/DiskArea.cs | 34 + .../Formats/Display.cs | 106 + .../Formats/Driver.cs | 59 + .../Formats/Extension.cs | 33 + .../Formats/Feature.cs | 42 + SabreTools.Metadata.DatItems/Formats/File.cs | 323 +++ SabreTools.Metadata.DatItems/Formats/Info.cs | 33 + SabreTools.Metadata.DatItems/Formats/Input.cs | 86 + .../Formats/Instance.cs | 33 + SabreTools.Metadata.DatItems/Formats/Media.cs | 136 + .../Formats/Original.cs | 32 + SabreTools.Metadata.DatItems/Formats/Part.cs | 44 + .../Formats/PartFeature.cs | 51 + SabreTools.Metadata.DatItems/Formats/Port.cs | 73 + .../Formats/RamOption.cs | 39 + .../Formats/Release.cs | 39 + .../Formats/ReleaseDetails.cs | 189 ++ SabreTools.Metadata.DatItems/Formats/Rom.cs | 312 +++ .../Formats/Sample.cs | 33 + .../Formats/Serials.cs | 180 ++ .../Formats/SharedFeat.cs | 33 + SabreTools.Metadata.DatItems/Formats/Slot.cs | 73 + .../Formats/SlotOption.cs | 39 + .../Formats/SoftwareList.cs | 41 + SabreTools.Metadata.DatItems/Formats/Sound.cs | 38 + .../Formats/SourceDetails.cs | 230 ++ SabreTools.Metadata.DatItems/Machine.cs | 156 ++ .../SabreTools.Metadata.DatItems.csproj | 45 + SabreTools.Metadata.DatItems/Source.cs | 41 + SabreTools.Metadata.DatItems/Trurip.cs | 146 + .../ExtraIniItemTests.cs | 39 + .../FilterKeyTests.cs | 81 + .../FilterObjectTests.cs | 476 ++++ .../FilterRunnerTests.cs | 219 ++ .../SabreTools.Metadata.Filter.Test.csproj | 32 + .../TestData/extra.ini | 12 + SabreTools.Metadata.Filter/Enums.cs | 41 + SabreTools.Metadata.Filter/ExtraIniItem.cs | 126 + SabreTools.Metadata.Filter/FilterGroup.cs | 198 ++ SabreTools.Metadata.Filter/FilterKey.cs | 214 ++ SabreTools.Metadata.Filter/FilterObject.cs | 404 +++ SabreTools.Metadata.Filter/FilterRunner.cs | 105 + .../SabreTools.Metadata.Filter.csproj | 41 + .../DictionaryBaseExtensionsTests.cs | 983 +++++++ .../ModelBackedItemTests.cs | 317 +++ .../SabreTools.Metadata.Test.csproj | 21 + .../Tools/ConvertersTests.cs | 38 + .../Tools/UtilitiesTests.cs | 70 + .../DictionaryBaseExtensions.cs | 291 ++ SabreTools.Metadata/MappingAttribute.cs | 16 + SabreTools.Metadata/ModelBackedItem.cs | 17 + SabreTools.Metadata/ModelBackedItemT.cs | 228 ++ .../SabreTools.Metadata.csproj | 42 + SabreTools.Metadata/Tools/AttributeHelper.cs | 47 + SabreTools.Metadata/Tools/Converters.cs | 132 + SabreTools.Metadata/Tools/NumberHelper.cs | 264 ++ SabreTools.Metadata/Tools/TextHelper.cs | 302 +++ SabreTools.Metadata/Tools/TypeHelper.cs | 137 + SabreTools.Metadata/Tools/Utilities.cs | 97 + SabreTools.Serialization.sln | 112 + 138 files changed, 37585 insertions(+) create mode 100644 SabreTools.Metadata.DatFiles.Test/ConvertersTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatFileTests.Filtering.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatFileTests.FromMetadata.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatFileTests.Splitting.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatFileTests.ToMetadata.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatFileTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatHeaderTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/DatStatisticsTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/ExtensionsTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/FormatsTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/ItemDictionaryDBTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/ItemDictionaryTests.cs create mode 100644 SabreTools.Metadata.DatFiles.Test/SabreTools.Metadata.DatFiles.Test.csproj create mode 100644 SabreTools.Metadata.DatFiles/DatFile.Filtering.cs create mode 100644 SabreTools.Metadata.DatFiles/DatFile.FromMetadata.cs create mode 100644 SabreTools.Metadata.DatFiles/DatFile.Splitting.cs create mode 100644 SabreTools.Metadata.DatFiles/DatFile.ToMetadata.cs create mode 100644 SabreTools.Metadata.DatFiles/DatFile.cs create mode 100644 SabreTools.Metadata.DatFiles/DatHeader.cs create mode 100644 SabreTools.Metadata.DatFiles/DatModifiers.cs create mode 100644 SabreTools.Metadata.DatFiles/DatStatistics.cs create mode 100644 SabreTools.Metadata.DatFiles/DepotInformation.cs create mode 100644 SabreTools.Metadata.DatFiles/Enums.cs create mode 100644 SabreTools.Metadata.DatFiles/Extensions.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/ArchiveDotOrg.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/AttractMode.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/ClrMamePro.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/DosCenter.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/EverdriveSmdb.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/Hashfile.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/Listrom.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/Listxml.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/Logiqx.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/Missfile.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/OfflineList.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/OpenMSX.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/RomCenter.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/SabreJSON.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/SabreXML.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/SeparatedValue.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/SerializableDatFile.cs create mode 100644 SabreTools.Metadata.DatFiles/Formats/SoftwareList.cs create mode 100644 SabreTools.Metadata.DatFiles/ItemDictionary.cs create mode 100644 SabreTools.Metadata.DatFiles/ItemDictionaryDB.cs create mode 100644 SabreTools.Metadata.DatFiles/ItemMappings.cs create mode 100644 SabreTools.Metadata.DatFiles/SabreTools.Metadata.DatFiles.csproj create mode 100644 SabreTools.Metadata.DatItems.Test/ConvertersTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/DatItemTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/ExtensionsTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/Formats/DiskTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/Formats/FileTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/Formats/MediaTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/Formats/RomTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/MachineTests.cs create mode 100644 SabreTools.Metadata.DatItems.Test/SabreTools.Metadata.DatItems.Test.csproj create mode 100644 SabreTools.Metadata.DatItems.Test/SourceTests.cs create mode 100644 SabreTools.Metadata.DatItems/DatItem.cs create mode 100644 SabreTools.Metadata.DatItems/DatItemT.cs create mode 100644 SabreTools.Metadata.DatItems/Enums.cs create mode 100644 SabreTools.Metadata.DatItems/Extensions.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Adjuster.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Analog.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Archive.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/BiosSet.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Blank.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Chip.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Condition.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/ConfLocation.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/ConfSetting.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Configuration.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Control.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/DataArea.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Device.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/DeviceRef.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/DipLocation.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/DipSwitch.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/DipValue.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Disk.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/DiskArea.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Display.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Driver.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Extension.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Feature.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/File.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Info.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Input.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Instance.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Media.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Original.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Part.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/PartFeature.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Port.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/RamOption.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Release.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/ReleaseDetails.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Rom.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Sample.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Serials.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/SharedFeat.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Slot.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/SlotOption.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/SoftwareList.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/Sound.cs create mode 100644 SabreTools.Metadata.DatItems/Formats/SourceDetails.cs create mode 100644 SabreTools.Metadata.DatItems/Machine.cs create mode 100644 SabreTools.Metadata.DatItems/SabreTools.Metadata.DatItems.csproj create mode 100644 SabreTools.Metadata.DatItems/Source.cs create mode 100644 SabreTools.Metadata.DatItems/Trurip.cs create mode 100644 SabreTools.Metadata.Filter.Test/ExtraIniItemTests.cs create mode 100644 SabreTools.Metadata.Filter.Test/FilterKeyTests.cs create mode 100644 SabreTools.Metadata.Filter.Test/FilterObjectTests.cs create mode 100644 SabreTools.Metadata.Filter.Test/FilterRunnerTests.cs create mode 100644 SabreTools.Metadata.Filter.Test/SabreTools.Metadata.Filter.Test.csproj create mode 100644 SabreTools.Metadata.Filter.Test/TestData/extra.ini create mode 100644 SabreTools.Metadata.Filter/Enums.cs create mode 100644 SabreTools.Metadata.Filter/ExtraIniItem.cs create mode 100644 SabreTools.Metadata.Filter/FilterGroup.cs create mode 100644 SabreTools.Metadata.Filter/FilterKey.cs create mode 100644 SabreTools.Metadata.Filter/FilterObject.cs create mode 100644 SabreTools.Metadata.Filter/FilterRunner.cs create mode 100644 SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj create mode 100644 SabreTools.Metadata.Test/DictionaryBaseExtensionsTests.cs create mode 100644 SabreTools.Metadata.Test/ModelBackedItemTests.cs create mode 100644 SabreTools.Metadata.Test/SabreTools.Metadata.Test.csproj create mode 100644 SabreTools.Metadata.Test/Tools/ConvertersTests.cs create mode 100644 SabreTools.Metadata.Test/Tools/UtilitiesTests.cs create mode 100644 SabreTools.Metadata/DictionaryBaseExtensions.cs create mode 100644 SabreTools.Metadata/MappingAttribute.cs create mode 100644 SabreTools.Metadata/ModelBackedItem.cs create mode 100644 SabreTools.Metadata/ModelBackedItemT.cs create mode 100644 SabreTools.Metadata/SabreTools.Metadata.csproj create mode 100644 SabreTools.Metadata/Tools/AttributeHelper.cs create mode 100644 SabreTools.Metadata/Tools/Converters.cs create mode 100644 SabreTools.Metadata/Tools/NumberHelper.cs create mode 100644 SabreTools.Metadata/Tools/TextHelper.cs create mode 100644 SabreTools.Metadata/Tools/TypeHelper.cs create mode 100644 SabreTools.Metadata/Tools/Utilities.cs diff --git a/SabreTools.Metadata.DatFiles.Test/ConvertersTests.cs b/SabreTools.Metadata.DatFiles.Test/ConvertersTests.cs new file mode 100644 index 00000000..95827ede --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/ConvertersTests.cs @@ -0,0 +1,23 @@ +using SabreTools.Metadata.Tools; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public class ConvertersTests + { + #region Generators + + [Theory] + [InlineData(MergingFlag.None, 12)] + [InlineData(NodumpFlag.None, 4)] + [InlineData(PackingFlag.None, 8)] + public void GenerateToEnumTest(T value, int expected) + { + var actual = Converters.GenerateToEnum(); + Assert.Equal(default, value); + Assert.Equal(expected, actual.Keys.Count); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatFileTests.Filtering.cs b/SabreTools.Metadata.DatFiles.Test/DatFileTests.Filtering.cs new file mode 100644 index 00000000..e0583167 --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatFileTests.Filtering.cs @@ -0,0 +1,304 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatFiles.Formats; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using SabreTools.Metadata.Filter; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public partial class DatFileTests + { + #region ExecuteFilters + + [Fact] + public void ExecuteFilters_Items() + { + FilterObject filterObject = new FilterObject("rom.crc", "deadbeef", Operation.NotEquals); + FilterRunner filterRunner = new FilterRunner([filterObject]); + + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + DatItem datItem = new Rom(); + datItem.SetName("rom.bin"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + datItem.SetFieldValue(DatItem.MachineKey, machine); + datItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(datItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.ExecuteFilters(filterRunner); + + var actualDatItems = datFile.GetItemsForBucket("machine"); + DatItem actualRom = Assert.Single(actualDatItems); + Assert.Equal(true, actualRom.GetBoolFieldValue(DatItem.RemoveKey)); + } + + [Fact] + public void ExecuteFilters_ItemsDB() + { + FilterObject filterObject = new FilterObject("rom.crc", "deadbeef", Operation.NotEquals); + FilterRunner filterRunner = new FilterRunner([filterObject]); + + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + DatItem datItem = new Rom(); + datItem.SetName("rom.bin"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + datItem.SetFieldValue(DatItem.MachineKey, machine); + datItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long sourceIndex = datFile.AddSourceDB(source); + long machineIndex = datFile.AddMachineDB(machine); + _ = datFile.AddItemDB(datItem, machineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.ExecuteFilters(filterRunner); + + var actualDatItems = datFile.GetItemsForBucketDB("machine"); + DatItem actualRom = Assert.Single(actualDatItems).Value; + Assert.Equal(true, actualRom.GetBoolFieldValue(DatItem.RemoveKey)); + } + + #endregion + + #region MachineDescriptionToName + + [Fact] + public void MachineDescriptionToName_Items() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.DescriptionKey, "description"); + + DatItem datItem = new Rom(); + datItem.SetFieldValue(DatItem.MachineKey, machine); + datItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(datItem, statsOnly: false); + + datFile.MachineDescriptionToName(); + + // The name of the bucket is not expected to change + DatItem actual = Assert.Single(datFile.GetItemsForBucket("machine")); + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("description", actualMachine.GetName()); + Assert.Equal("description", actualMachine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey)); + } + + [Fact] + public void MachineDescriptionToName_ItemsDB() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.DescriptionKey, "description"); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + _ = datFile.AddMachineDB(machine); + + datFile.MachineDescriptionToName(); + + Machine actualMachine = Assert.Single(datFile.GetMachinesDB()).Value; + Assert.Equal("description", actualMachine.GetName()); + Assert.Equal("description", actualMachine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey)); + } + + #endregion + + #region SetOneRomPerGame + + [Fact] + public void SetOneRomPerGame_Items() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + DatItem rom = new Rom(); + rom.SetName("rom.bin"); + rom.SetFieldValue(DatItem.MachineKey, machine); + rom.SetFieldValue(DatItem.SourceKey, source); + + DatItem disk = new Disk(); + disk.SetName("disk"); + disk.SetFieldValue(DatItem.MachineKey, machine); + disk.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(rom, statsOnly: false); + datFile.AddItem(disk, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.SetOneRomPerGame(); + + var actualDatItems = datFile.GetItemsForBucket("machine"); + Assert.Equal(2, actualDatItems.Count); + + DatItem actualRom = Assert.Single(actualDatItems.FindAll(i => i is Rom)); + Machine? actualRomMachine = actualRom.GetMachine(); + Assert.NotNull(actualRomMachine); + Assert.Equal("machine/rom", actualRomMachine.GetName()); + + DatItem actualDisk = Assert.Single(actualDatItems.FindAll(i => i is Disk)); + Machine? actualDiskMachine = actualDisk.GetMachine(); + Assert.NotNull(actualDiskMachine); + Assert.Equal("machine/disk", actualDiskMachine.GetName()); + } + + [Fact] + public void SetOneRomPerGame_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + DatItem rom = new Rom(); + rom.SetName("rom.bin"); + + DatItem disk = new Disk(); + disk.SetName("disk"); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long sourceIndex = datFile.AddSourceDB(source); + long machineIndex = datFile.AddMachineDB(machine); + _ = datFile.AddItemDB(rom, machineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(disk, machineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.SetOneRomPerGame(); + + var actualDatItems = datFile.GetItemsForBucketDB("machine"); + Assert.Equal(2, actualDatItems.Count); + + var actualRom = Assert.Single(actualDatItems, i => i.Value is Rom); + var actualRomMachine = datFile.GetMachineForItemDB(actualRom.Key); + Assert.NotNull(actualRomMachine.Value); + Assert.Equal("machine/rom", actualRomMachine.Value.GetName()); + + var actualDisk = Assert.Single(actualDatItems, i => i.Value is Disk); + var actualDiskMachine = datFile.GetMachineForItemDB(actualDisk.Key); + Assert.NotNull(actualDiskMachine.Value); + Assert.Equal("machine/disk", actualDiskMachine.Value.GetName()); + } + + #endregion + + #region SetOneGamePerRegion + + [Fact] + public void SetOneGamePerRegion_Items() + { + Machine nowhereMachine = new Machine(); + nowhereMachine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine (Nowhere)"); + + Machine worldMachine = new Machine(); + worldMachine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine (World)"); + worldMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "machine (Nowhere)"); + + DatItem nowhereRom = new Rom(); + nowhereRom.SetName("rom.bin"); + nowhereRom.SetFieldValue(DatItem.MachineKey, nowhereMachine); + + DatItem worldRom = new Rom(); + worldRom.SetName("rom.nib"); + worldRom.SetFieldValue(DatItem.MachineKey, worldMachine); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(nowhereRom, statsOnly: false); + datFile.AddItem(worldRom, statsOnly: false); + + List regions = ["World", "Nowhere"]; + datFile.SetOneGamePerRegion(regions); + + Assert.Empty(datFile.GetItemsForBucket("machine (nowhere)")); + + var actualDatItems = datFile.GetItemsForBucket("machine (world)"); + DatItem actualWorldRom = Assert.Single(actualDatItems); + Machine? actualWorldMachine = actualWorldRom.GetMachine(); + Assert.NotNull(actualWorldMachine); + Assert.Equal("machine (World)", actualWorldMachine.GetName()); + } + + [Fact] + public void SetOneGamePerRegion_ItemsDB() + { + Machine nowhereMachine = new Machine(); + nowhereMachine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine (Nowhere)"); + + Machine worldMachine = new Machine(); + worldMachine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine (World)"); + worldMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "machine (Nowhere)"); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + _ = datFile.AddMachineDB(nowhereMachine); + _ = datFile.AddMachineDB(worldMachine); + + List regions = ["World", "Nowhere"]; + datFile.SetOneGamePerRegion(regions); + + var actualWorldMachine = Assert.Single(datFile.GetMachinesDB()); + Assert.NotNull(actualWorldMachine.Value); + Assert.Equal("machine (World)", actualWorldMachine.Value.GetName()); + } + + #endregion + + #region StripSceneDatesFromItems + + [Fact] + public void StripSceneDatesFromItems_Items() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "10.10.10-machine-name"); + + DatItem datItem = new Rom(); + datItem.SetFieldValue(DatItem.MachineKey, machine); + datItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(datItem, statsOnly: false); + + datFile.StripSceneDatesFromItems(); + + // The name of the bucket is not expected to change + DatItem actual = Assert.Single(datFile.GetItemsForBucket("10.10.10-machine-name")); + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("machine-name", actualMachine.GetName()); + } + + [Fact] + public void StripSceneDatesFromItems_ItemsDB() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "10.10.10-machine-name"); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + _ = datFile.AddMachineDB(machine); + + datFile.StripSceneDatesFromItems(); + + Machine actualMachine = Assert.Single(datFile.GetMachinesDB()).Value; + Assert.Equal("machine-name", actualMachine.GetName()); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatFileTests.FromMetadata.cs b/SabreTools.Metadata.DatFiles.Test/DatFileTests.FromMetadata.cs new file mode 100644 index 00000000..346e1abf --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatFileTests.FromMetadata.cs @@ -0,0 +1,1564 @@ +using System; +using System.Linq; +using SabreTools.Hashing; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public partial class DatFileTests + { + #region ConvertFromMetadata + + [Fact] + public void ConvertFromMetadata_Null() + { + Data.Models.Metadata.MetadataFile? item = null; + + DatFile datFile = new Formats.Logiqx(null, useGame: false); + datFile.ConvertFromMetadata(item, "filename", indexId: 0, keep: true, statsOnly: false, filterRunner: null); + + Assert.Equal(0, datFile.Items.DatStatistics.TotalCount); + Assert.Equal(0, datFile.ItemsDB.DatStatistics.TotalCount); + } + + [Fact] + public void ConvertFromMetadata_Empty() + { + Data.Models.Metadata.MetadataFile? item = []; + + DatFile datFile = new Formats.Logiqx(null, useGame: false); + datFile.ConvertFromMetadata(item, "filename", indexId: 0, keep: true, statsOnly: false, filterRunner: null); + + Assert.Equal(0, datFile.Items.DatStatistics.TotalCount); + Assert.Equal(0, datFile.ItemsDB.DatStatistics.TotalCount); + } + + [Fact] + public void ConvertFromMetadata_FilledHeader() + { + Data.Models.Metadata.Header? header = CreateMetadataHeader(); + Data.Models.Metadata.Machine[]? machines = null; + Data.Models.Metadata.MetadataFile? item = new Data.Models.Metadata.MetadataFile + { + [Data.Models.Metadata.MetadataFile.HeaderKey] = header, + [Data.Models.Metadata.MetadataFile.MachineKey] = machines, + }; + + DatFile datFile = new Formats.Logiqx(null, useGame: false); + datFile.ConvertFromMetadata(item, "filename", indexId: 0, keep: true, statsOnly: false, filterRunner: null); + + ValidateHeader(datFile.Header); + } + + [Fact] + public void ConvertFromMetadata_FilledMachine() + { + Data.Models.Metadata.Header? header = null; + Data.Models.Metadata.Machine machine = CreateMetadataMachine(); + Data.Models.Metadata.Machine[]? machines = [machine]; + Data.Models.Metadata.MetadataFile? item = new Data.Models.Metadata.MetadataFile + { + [Data.Models.Metadata.MetadataFile.HeaderKey] = header, + [Data.Models.Metadata.MetadataFile.MachineKey] = machines, + }; + + DatFile datFile = new Formats.Logiqx(null, useGame: false); + datFile.ConvertFromMetadata(item, "filename", indexId: 0, keep: true, statsOnly: false, filterRunner: null); + + // TODO: Reenable when ItemsDB is used again + // DatItems.Machine actualMachine = Assert.Single(datFile.ItemsDB.GetMachines()).Value; + // ValidateMachine(actualMachine); + + // Aggregate for easier validation + DatItems.DatItem[] datItems = [.. datFile.Items.SortedKeys + .SelectMany(key => datFile.GetItemsForBucket(key))]; + + Adjuster? adjuster = Array.Find(datItems, item => item is Adjuster) as Adjuster; + ValidateAdjuster(adjuster); + + Archive? archive = Array.Find(datItems, item => item is Archive) as Archive; + ValidateArchive(archive); + + BiosSet? biosSet = Array.Find(datItems, item => item is BiosSet) as BiosSet; + ValidateBiosSet(biosSet); + + Chip? chip = Array.Find(datItems, item => item is Chip) as Chip; + ValidateChip(chip); + + Configuration? configuration = Array.Find(datItems, item => item is Configuration) as Configuration; + ValidateConfiguration(configuration); + + Device? device = Array.Find(datItems, item => item is Device) as Device; + ValidateDevice(device); + + DeviceRef? deviceRef = Array.Find(datItems, item => item is DeviceRef) as DeviceRef; + ValidateDeviceRef(deviceRef); + + DipSwitch? dipSwitch = Array.Find(datItems, item => item is DipSwitch dipSwitch && !dipSwitch.PartSpecified) as DipSwitch; + ValidateDipSwitch(dipSwitch); + + Disk? disk = Array.Find(datItems, item => item is Disk disk && !disk.DiskAreaSpecified && !disk.PartSpecified) as Disk; + ValidateDisk(disk); + + Display? display = Array.Find(datItems, item => item is Display display && display.GetInt64FieldValue(Data.Models.Metadata.Video.AspectXKey) is null) as Display; + ValidateDisplay(display); + + Driver? driver = Array.Find(datItems, item => item is Driver) as Driver; + ValidateDriver(driver); + + // All other fields are tested separately + Rom? dump = Array.Find(datItems, item => item is Rom rom && rom.GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType) is not null) as Rom; + Assert.NotNull(dump); + Assert.Equal("rom", dump.GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType)); + + Feature? feature = Array.Find(datItems, item => item is Feature) as Feature; + ValidateFeature(feature); + + Info? info = Array.Find(datItems, item => item is Info) as Info; + ValidateInfo(info); + + Input? input = Array.Find(datItems, item => item is Input) as Input; + ValidateInput(input); + + Media? media = Array.Find(datItems, item => item is Media) as Media; + ValidateMedia(media); + + // All other fields are tested separately + DipSwitch? partDipSwitch = Array.Find(datItems, item => item is DipSwitch dipSwitch && dipSwitch.PartSpecified) as DipSwitch; + Assert.NotNull(partDipSwitch); + Part? dipSwitchPart = partDipSwitch.GetFieldValue(DipSwitch.PartKey); + ValidatePart(dipSwitchPart); + + // All other fields are tested separately + Disk? partDisk = Array.Find(datItems, item => item is Disk disk && disk.DiskAreaSpecified && disk.PartSpecified) as Disk; + Assert.NotNull(partDisk); + DiskArea? diskDiskArea = partDisk.GetFieldValue(Disk.DiskAreaKey); + ValidateDiskArea(diskDiskArea); + Part? diskPart = partDisk.GetFieldValue(Disk.PartKey); + ValidatePart(diskPart); + + PartFeature? partFeature = Array.Find(datItems, item => item is PartFeature) as PartFeature; + ValidatePartFeature(partFeature); + + // All other fields are tested separately + Rom? partRom = Array.Find(datItems, item => item is Rom rom && rom.DataAreaSpecified && rom.PartSpecified) as Rom; + Assert.NotNull(partRom); + DataArea? romDataArea = partRom.GetFieldValue(Rom.DataAreaKey); + ValidateDataArea(romDataArea); + Part? romPart = partRom.GetFieldValue(Rom.PartKey); + ValidatePart(romPart); + + Port? port = Array.Find(datItems, item => item is Port) as Port; + ValidatePort(port); + + RamOption? ramOption = Array.Find(datItems, item => item is RamOption) as RamOption; + ValidateRamOption(ramOption); + + Release? release = Array.Find(datItems, item => item is Release) as Release; + ValidateRelease(release); + + Rom? rom = Array.Find(datItems, item => item is Rom rom && !rom.DataAreaSpecified && !rom.PartSpecified && rom.GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType) is null) as Rom; + ValidateRom(rom); + + Sample? sample = Array.Find(datItems, item => item is Sample) as Sample; + ValidateSample(sample); + + SharedFeat? sharedFeat = Array.Find(datItems, item => item is SharedFeat) as SharedFeat; + ValidateSharedFeat(sharedFeat); + + Slot? slot = Array.Find(datItems, item => item is Slot) as Slot; + ValidateSlot(slot); + + SoftwareList? softwareList = Array.Find(datItems, item => item is SoftwareList) as SoftwareList; + ValidateSoftwareList(softwareList); + + Sound? sound = Array.Find(datItems, item => item is Sound) as Sound; + ValidateSound(sound); + + Display? video = Array.Find(datItems, item => item is Display display && display.GetInt64FieldValue(Data.Models.Metadata.Video.AspectXKey) is not null) as Display; + ValidateVideo(video); + } + + #endregion + + #region Creation Helpers + + private static Data.Models.Metadata.Header CreateMetadataHeader() + { + Data.Models.OfflineList.CanOpen canOpen = new Data.Models.OfflineList.CanOpen + { + Extension = ["ext"], + }; + + Data.Models.OfflineList.Images images = new Data.Models.OfflineList.Images(); + + Data.Models.OfflineList.Infos infos = new Data.Models.OfflineList.Infos(); + + Data.Models.OfflineList.NewDat newDat = new Data.Models.OfflineList.NewDat(); + + Data.Models.OfflineList.Search search = new Data.Models.OfflineList.Search(); + + return new Data.Models.Metadata.Header + { + [Data.Models.Metadata.Header.AuthorKey] = "author", + [Data.Models.Metadata.Header.BiosModeKey] = "merged", + [Data.Models.Metadata.Header.BuildKey] = "build", + [Data.Models.Metadata.Header.CanOpenKey] = canOpen, + [Data.Models.Metadata.Header.CategoryKey] = "category", + [Data.Models.Metadata.Header.CommentKey] = "comment", + [Data.Models.Metadata.Header.DateKey] = "date", + [Data.Models.Metadata.Header.DatVersionKey] = "datversion", + [Data.Models.Metadata.Header.DebugKey] = "yes", + [Data.Models.Metadata.Header.DescriptionKey] = "description", + [Data.Models.Metadata.Header.EmailKey] = "email", + [Data.Models.Metadata.Header.EmulatorVersionKey] = "emulatorversion", + [Data.Models.Metadata.Header.ForceMergingKey] = "merged", + [Data.Models.Metadata.Header.ForceNodumpKey] = "required", + [Data.Models.Metadata.Header.ForcePackingKey] = "zip", + [Data.Models.Metadata.Header.ForceZippingKey] = "yes", + [Data.Models.Metadata.Header.HeaderKey] = "header", + [Data.Models.Metadata.Header.HomepageKey] = "homepage", + [Data.Models.Metadata.Header.IdKey] = "id", + [Data.Models.Metadata.Header.ImagesKey] = images, + [Data.Models.Metadata.Header.ImFolderKey] = "imfolder", + [Data.Models.Metadata.Header.InfosKey] = infos, + [Data.Models.Metadata.Header.LockBiosModeKey] = "yes", + [Data.Models.Metadata.Header.LockRomModeKey] = "yes", + [Data.Models.Metadata.Header.LockSampleModeKey] = "yes", + [Data.Models.Metadata.Header.MameConfigKey] = "mameconfig", + [Data.Models.Metadata.Header.NameKey] = "name", + [Data.Models.Metadata.Header.NewDatKey] = newDat, + [Data.Models.Metadata.Header.NotesKey] = "notes", + [Data.Models.Metadata.Header.PluginKey] = "plugin", + [Data.Models.Metadata.Header.RefNameKey] = "refname", + [Data.Models.Metadata.Header.RomModeKey] = "merged", + [Data.Models.Metadata.Header.RomTitleKey] = "romtitle", + [Data.Models.Metadata.Header.RootDirKey] = "rootdir", + [Data.Models.Metadata.Header.SampleModeKey] = "merged", + [Data.Models.Metadata.Header.SchemaLocationKey] = "schemalocation", + [Data.Models.Metadata.Header.ScreenshotsHeightKey] = "screenshotsheight", + [Data.Models.Metadata.Header.ScreenshotsWidthKey] = "screenshotsWidth", + [Data.Models.Metadata.Header.SearchKey] = search, + [Data.Models.Metadata.Header.SystemKey] = "system", + [Data.Models.Metadata.Header.TimestampKey] = "timestamp", + [Data.Models.Metadata.Header.TypeKey] = "type", + [Data.Models.Metadata.Header.UrlKey] = "url", + [Data.Models.Metadata.Header.VersionKey] = "version", + }; + } + + private static Data.Models.Metadata.Machine CreateMetadataMachine() + { + return new Data.Models.Metadata.Machine + { + [Data.Models.Metadata.Machine.AdjusterKey] = new Data.Models.Metadata.Adjuster[] { CreateMetadataAdjuster() }, + [Data.Models.Metadata.Machine.ArchiveKey] = new Data.Models.Metadata.Archive[] { CreateMetadataArchive() }, + [Data.Models.Metadata.Machine.BiosSetKey] = new Data.Models.Metadata.BiosSet[] { CreateMetadataBiosSet() }, + [Data.Models.Metadata.Machine.BoardKey] = "board", + [Data.Models.Metadata.Machine.ButtonsKey] = "buttons", + [Data.Models.Metadata.Machine.CategoryKey] = "category", + [Data.Models.Metadata.Machine.ChipKey] = new Data.Models.Metadata.Chip[] { CreateMetadataChip() }, + [Data.Models.Metadata.Machine.CloneOfKey] = "cloneof", + [Data.Models.Metadata.Machine.CloneOfIdKey] = "cloneofid", + [Data.Models.Metadata.Machine.CommentKey] = "comment", + [Data.Models.Metadata.Machine.CompanyKey] = "company", + [Data.Models.Metadata.Machine.ConfigurationKey] = new Data.Models.Metadata.Configuration[] { CreateMetadataConfiguration() }, + [Data.Models.Metadata.Machine.ControlKey] = "control", + [Data.Models.Metadata.Machine.CountryKey] = "country", + [Data.Models.Metadata.Machine.DescriptionKey] = "description", + [Data.Models.Metadata.Machine.DeviceKey] = new Data.Models.Metadata.Device[] { CreateMetadataDevice() }, + [Data.Models.Metadata.Machine.DeviceRefKey] = new Data.Models.Metadata.DeviceRef[] { CreateMetadataDeviceRef() }, + [Data.Models.Metadata.Machine.DipSwitchKey] = new Data.Models.Metadata.DipSwitch[] { CreateMetadataDipSwitch() }, + [Data.Models.Metadata.Machine.DirNameKey] = "dirname", + [Data.Models.Metadata.Machine.DiskKey] = new Data.Models.Metadata.Disk[] { CreateMetadataDisk() }, + [Data.Models.Metadata.Machine.DisplayCountKey] = "displaycount", + [Data.Models.Metadata.Machine.DisplayKey] = new Data.Models.Metadata.Display[] { CreateMetadataDisplay() }, + [Data.Models.Metadata.Machine.DisplayTypeKey] = "displaytype", + [Data.Models.Metadata.Machine.DriverKey] = CreateMetadataDriver(), + [Data.Models.Metadata.Machine.DumpKey] = new Data.Models.Metadata.Dump[] { CreateMetadataDump() }, + [Data.Models.Metadata.Machine.DuplicateIDKey] = "duplicateid", + [Data.Models.Metadata.Machine.EmulatorKey] = "emulator", + [Data.Models.Metadata.Machine.ExtraKey] = "extra", + [Data.Models.Metadata.Machine.FavoriteKey] = "favorite", + [Data.Models.Metadata.Machine.FeatureKey] = new Data.Models.Metadata.Feature[] { CreateMetadataFeature() }, + [Data.Models.Metadata.Machine.GenMSXIDKey] = "genmsxid", + [Data.Models.Metadata.Machine.HistoryKey] = "history", + [Data.Models.Metadata.Machine.IdKey] = "id", + [Data.Models.Metadata.Machine.Im1CRCKey] = HashType.CRC32.ZeroString, + [Data.Models.Metadata.Machine.Im2CRCKey] = HashType.CRC32.ZeroString, + [Data.Models.Metadata.Machine.ImageNumberKey] = "imagenumber", + [Data.Models.Metadata.Machine.InfoKey] = new Data.Models.Metadata.Info[] { CreateMetadataInfo() }, + [Data.Models.Metadata.Machine.InputKey] = CreateMetadataInput(), + [Data.Models.Metadata.Machine.IsBiosKey] = "yes", + [Data.Models.Metadata.Machine.IsDeviceKey] = "yes", + [Data.Models.Metadata.Machine.IsMechanicalKey] = "yes", + [Data.Models.Metadata.Machine.LanguageKey] = "language", + [Data.Models.Metadata.Machine.LocationKey] = "location", + [Data.Models.Metadata.Machine.ManufacturerKey] = "manufacturer", + [Data.Models.Metadata.Machine.MediaKey] = new Data.Models.Metadata.Media[] { CreateMetadataMedia() }, + [Data.Models.Metadata.Machine.NameKey] = "name", + [Data.Models.Metadata.Machine.NotesKey] = "notes", + [Data.Models.Metadata.Machine.PartKey] = new Data.Models.Metadata.Part[] { CreateMetadataPart() }, + [Data.Models.Metadata.Machine.PlayedCountKey] = "playedcount", + [Data.Models.Metadata.Machine.PlayedTimeKey] = "playedtime", + [Data.Models.Metadata.Machine.PlayersKey] = "players", + [Data.Models.Metadata.Machine.PortKey] = new Data.Models.Metadata.Port[] { CreateMetadataPort() }, + [Data.Models.Metadata.Machine.PublisherKey] = "publisher", + [Data.Models.Metadata.Machine.RamOptionKey] = new Data.Models.Metadata.RamOption[] { CreateMetadataRamOption() }, + [Data.Models.Metadata.Machine.RebuildToKey] = "rebuildto", + [Data.Models.Metadata.Machine.ReleaseKey] = new Data.Models.Metadata.Release[] { CreateMetadataRelease() }, + [Data.Models.Metadata.Machine.ReleaseNumberKey] = "releasenumber", + [Data.Models.Metadata.Machine.RomKey] = new Data.Models.Metadata.Rom[] { CreateMetadataRom() }, + [Data.Models.Metadata.Machine.RomOfKey] = "romof", + [Data.Models.Metadata.Machine.RotationKey] = "rotation", + [Data.Models.Metadata.Machine.RunnableKey] = "yes", + [Data.Models.Metadata.Machine.SampleKey] = new Data.Models.Metadata.Sample[] { CreateMetadataSample() }, + [Data.Models.Metadata.Machine.SampleOfKey] = "sampleof", + [Data.Models.Metadata.Machine.SaveTypeKey] = "savetype", + [Data.Models.Metadata.Machine.SharedFeatKey] = new Data.Models.Metadata.SharedFeat[] { CreateMetadataSharedFeat() }, + [Data.Models.Metadata.Machine.SlotKey] = new Data.Models.Metadata.Slot[] { CreateMetadataSlot() }, + [Data.Models.Metadata.Machine.SoftwareListKey] = new Data.Models.Metadata.SoftwareList[] { CreateMetadataSoftwareList() }, + [Data.Models.Metadata.Machine.SoundKey] = CreateMetadataSound(), + [Data.Models.Metadata.Machine.SourceFileKey] = "sourcefile", + [Data.Models.Metadata.Machine.SourceRomKey] = "sourcerom", + [Data.Models.Metadata.Machine.StatusKey] = "status", + [Data.Models.Metadata.Machine.SupportedKey] = "yes", + [Data.Models.Metadata.Machine.SystemKey] = "system", + [Data.Models.Metadata.Machine.TagsKey] = "tags", + [Data.Models.Metadata.Machine.TruripKey] = CreateMetadataTrurip(), + [Data.Models.Metadata.Machine.VideoKey] = new Data.Models.Metadata.Video[] { CreateMetadataVideo() }, + [Data.Models.Metadata.Machine.YearKey] = "year", + }; + } + + private static Data.Models.Metadata.Adjuster CreateMetadataAdjuster() + { + return new Data.Models.Metadata.Adjuster + { + [Data.Models.Metadata.Adjuster.ConditionKey] = CreateMetadataCondition(), + [Data.Models.Metadata.Adjuster.DefaultKey] = true, + [Data.Models.Metadata.Adjuster.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Analog CreateMetadataAnalog() + { + return new Data.Models.Metadata.Analog + { + [Data.Models.Metadata.Analog.MaskKey] = "mask", + }; + } + + private static Data.Models.Metadata.Archive CreateMetadataArchive() + { + return new Data.Models.Metadata.Archive + { + [Data.Models.Metadata.Archive.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.BiosSet CreateMetadataBiosSet() + { + return new Data.Models.Metadata.BiosSet + { + [Data.Models.Metadata.BiosSet.DefaultKey] = true, + [Data.Models.Metadata.BiosSet.DescriptionKey] = "description", + [Data.Models.Metadata.BiosSet.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Chip CreateMetadataChip() + { + return new Data.Models.Metadata.Chip + { + [Data.Models.Metadata.Chip.ClockKey] = 12345L, + [Data.Models.Metadata.Chip.FlagsKey] = "flags", + [Data.Models.Metadata.Chip.NameKey] = "name", + [Data.Models.Metadata.Chip.SoundOnlyKey] = "yes", + [Data.Models.Metadata.Chip.TagKey] = "tag", + [Data.Models.Metadata.Chip.ChipTypeKey] = "cpu", + }; + } + + private static Data.Models.Metadata.Configuration CreateMetadataConfiguration() + { + return new Data.Models.Metadata.Configuration + { + [Data.Models.Metadata.Configuration.ConditionKey] = CreateMetadataCondition(), + [Data.Models.Metadata.Configuration.ConfLocationKey] = new Data.Models.Metadata.ConfLocation[] { CreateMetadataConfLocation() }, + [Data.Models.Metadata.Configuration.ConfSettingKey] = new Data.Models.Metadata.ConfSetting[] { CreateMetadataConfSetting() }, + [Data.Models.Metadata.Configuration.MaskKey] = "mask", + [Data.Models.Metadata.Configuration.NameKey] = "name", + [Data.Models.Metadata.Configuration.TagKey] = "tag", + }; + } + + private static Data.Models.Metadata.Condition CreateMetadataCondition() + { + return new Data.Models.Metadata.Condition + { + [Data.Models.Metadata.Condition.ValueKey] = "value", + [Data.Models.Metadata.Condition.MaskKey] = "mask", + [Data.Models.Metadata.Condition.RelationKey] = "eq", + [Data.Models.Metadata.Condition.TagKey] = "tag", + }; + } + + private static Data.Models.Metadata.ConfLocation CreateMetadataConfLocation() + { + return new Data.Models.Metadata.ConfLocation + { + [Data.Models.Metadata.ConfLocation.InvertedKey] = "yes", + [Data.Models.Metadata.ConfLocation.NameKey] = "name", + [Data.Models.Metadata.ConfLocation.NumberKey] = "number", + }; + } + + private static Data.Models.Metadata.ConfSetting CreateMetadataConfSetting() + { + return new Data.Models.Metadata.ConfSetting + { + [Data.Models.Metadata.ConfSetting.ConditionKey] = CreateMetadataCondition(), + [Data.Models.Metadata.ConfSetting.DefaultKey] = "yes", + [Data.Models.Metadata.ConfSetting.NameKey] = "name", + [Data.Models.Metadata.ConfSetting.ValueKey] = "value", + }; + } + + private static Data.Models.Metadata.Control CreateMetadataControl() + { + return new Data.Models.Metadata.Control + { + [Data.Models.Metadata.Control.ButtonsKey] = 12345L, + [Data.Models.Metadata.Control.KeyDeltaKey] = 12345L, + [Data.Models.Metadata.Control.MaximumKey] = 12345L, + [Data.Models.Metadata.Control.MinimumKey] = 12345L, + [Data.Models.Metadata.Control.PlayerKey] = 12345L, + [Data.Models.Metadata.Control.ReqButtonsKey] = 12345L, + [Data.Models.Metadata.Control.ReverseKey] = "yes", + [Data.Models.Metadata.Control.SensitivityKey] = 12345L, + [Data.Models.Metadata.Control.ControlTypeKey] = "lightgun", + [Data.Models.Metadata.Control.WaysKey] = "ways", + [Data.Models.Metadata.Control.Ways2Key] = "ways2", + [Data.Models.Metadata.Control.Ways3Key] = "ways3", + }; + } + + private static Data.Models.Metadata.Device CreateMetadataDevice() + { + return new Data.Models.Metadata.Device + { + [Data.Models.Metadata.Device.ExtensionKey] = new Data.Models.Metadata.Extension[] { CreateMetadataExtension() }, + [Data.Models.Metadata.Device.FixedImageKey] = "fixedimage", + [Data.Models.Metadata.Device.InstanceKey] = CreateMetadataInstance(), + [Data.Models.Metadata.Device.InterfaceKey] = "interface", + [Data.Models.Metadata.Device.MandatoryKey] = 1L, + [Data.Models.Metadata.Device.TagKey] = "tag", + [Data.Models.Metadata.Device.DeviceTypeKey] = "punchtape", + }; + } + + private static Data.Models.Metadata.DeviceRef CreateMetadataDeviceRef() + { + return new Data.Models.Metadata.DeviceRef + { + [Data.Models.Metadata.DeviceRef.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.DipLocation CreateMetadataDipLocation() + { + return new Data.Models.Metadata.DipLocation + { + [Data.Models.Metadata.DipLocation.InvertedKey] = "yes", + [Data.Models.Metadata.DipLocation.NameKey] = "name", + [Data.Models.Metadata.DipLocation.NumberKey] = "number", + }; + } + + private static Data.Models.Metadata.DipSwitch CreateMetadataDipSwitch() + { + return new Data.Models.Metadata.DipSwitch + { + [Data.Models.Metadata.DipSwitch.ConditionKey] = CreateMetadataCondition(), + [Data.Models.Metadata.DipSwitch.DefaultKey] = "yes", + [Data.Models.Metadata.DipSwitch.DipLocationKey] = new Data.Models.Metadata.DipLocation[] { CreateMetadataDipLocation() }, + [Data.Models.Metadata.DipSwitch.DipValueKey] = new Data.Models.Metadata.DipValue[] { CreateMetadataDipValue() }, + [Data.Models.Metadata.DipSwitch.EntryKey] = new string[] { "entry" }, + [Data.Models.Metadata.DipSwitch.MaskKey] = "mask", + [Data.Models.Metadata.DipSwitch.NameKey] = "name", + [Data.Models.Metadata.DipSwitch.TagKey] = "tag", + }; + } + + private static Data.Models.Metadata.DipValue CreateMetadataDipValue() + { + return new Data.Models.Metadata.DipValue + { + [Data.Models.Metadata.DipValue.ConditionKey] = CreateMetadataCondition(), + [Data.Models.Metadata.DipValue.DefaultKey] = "yes", + [Data.Models.Metadata.DipValue.NameKey] = "name", + [Data.Models.Metadata.DipValue.ValueKey] = "value", + }; + } + + private static Data.Models.Metadata.DataArea CreateMetadataDataArea() + { + return new Data.Models.Metadata.DataArea + { + [Data.Models.Metadata.DataArea.EndiannessKey] = "big", + [Data.Models.Metadata.DataArea.NameKey] = "name", + [Data.Models.Metadata.DataArea.RomKey] = new Data.Models.Metadata.Rom[] { [] }, + [Data.Models.Metadata.DataArea.SizeKey] = 12345L, + [Data.Models.Metadata.DataArea.WidthKey] = 64, + }; + } + + private static Data.Models.Metadata.Disk CreateMetadataDisk() + { + return new Data.Models.Metadata.Disk + { + [Data.Models.Metadata.Disk.FlagsKey] = "flags", + [Data.Models.Metadata.Disk.IndexKey] = "index", + [Data.Models.Metadata.Disk.MD5Key] = HashType.MD5.ZeroString, + [Data.Models.Metadata.Disk.MergeKey] = "merge", + [Data.Models.Metadata.Disk.NameKey] = "name", + [Data.Models.Metadata.Disk.OptionalKey] = "yes", + [Data.Models.Metadata.Disk.RegionKey] = "region", + [Data.Models.Metadata.Disk.SHA1Key] = HashType.SHA1.ZeroString, + [Data.Models.Metadata.Disk.WritableKey] = "yes", + }; + } + + private static Data.Models.Metadata.DiskArea CreateMetadataDiskArea() + { + return new Data.Models.Metadata.DiskArea + { + [Data.Models.Metadata.DiskArea.DiskKey] = new Data.Models.Metadata.Disk[] { [] }, + [Data.Models.Metadata.DiskArea.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Display CreateMetadataDisplay() + { + return new Data.Models.Metadata.Display + { + [Data.Models.Metadata.Display.FlipXKey] = "yes", + [Data.Models.Metadata.Display.HBEndKey] = 12345L, + [Data.Models.Metadata.Display.HBStartKey] = 12345L, + [Data.Models.Metadata.Display.HeightKey] = 12345L, + [Data.Models.Metadata.Display.HTotalKey] = 12345L, + [Data.Models.Metadata.Display.PixClockKey] = 12345L, + [Data.Models.Metadata.Display.RefreshKey] = 12345L, + [Data.Models.Metadata.Display.RotateKey] = 90, + [Data.Models.Metadata.Display.TagKey] = "tag", + [Data.Models.Metadata.Display.DisplayTypeKey] = "vector", + [Data.Models.Metadata.Display.VBEndKey] = 12345L, + [Data.Models.Metadata.Display.VBStartKey] = 12345L, + [Data.Models.Metadata.Display.VTotalKey] = 12345L, + [Data.Models.Metadata.Display.WidthKey] = 12345L, + }; + } + + private static Data.Models.Metadata.Driver CreateMetadataDriver() + { + return new Data.Models.Metadata.Driver + { + [Data.Models.Metadata.Driver.BlitKey] = "plain", + [Data.Models.Metadata.Driver.CocktailKey] = "good", + [Data.Models.Metadata.Driver.ColorKey] = "good", + [Data.Models.Metadata.Driver.EmulationKey] = "good", + [Data.Models.Metadata.Driver.IncompleteKey] = "yes", + [Data.Models.Metadata.Driver.NoSoundHardwareKey] = "yes", + [Data.Models.Metadata.Driver.PaletteSizeKey] = "pallettesize", + [Data.Models.Metadata.Driver.RequiresArtworkKey] = "yes", + [Data.Models.Metadata.Driver.SaveStateKey] = "supported", + [Data.Models.Metadata.Driver.SoundKey] = "good", + [Data.Models.Metadata.Driver.StatusKey] = "good", + [Data.Models.Metadata.Driver.UnofficialKey] = "yes", + }; + } + + private static Data.Models.Metadata.Dump CreateMetadataDump() + { + return new Data.Models.Metadata.Dump + { + [Data.Models.Metadata.Dump.OriginalKey] = CreateMetadataOriginal(), + + // The following are searched for in order + // For the purposes of this test, only RomKey will be populated + // The only difference is what OpenMSXSubType value is applied + [Data.Models.Metadata.Dump.RomKey] = new Data.Models.Metadata.Rom(), + [Data.Models.Metadata.Dump.MegaRomKey] = null, + [Data.Models.Metadata.Dump.SCCPlusCartKey] = null, + }; + } + + private static Data.Models.Metadata.Extension CreateMetadataExtension() + { + return new Data.Models.Metadata.Extension + { + [Data.Models.Metadata.Extension.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Feature CreateMetadataFeature() + { + return new Data.Models.Metadata.Feature + { + [Data.Models.Metadata.Feature.NameKey] = "name", + [Data.Models.Metadata.Feature.OverallKey] = "imperfect", + [Data.Models.Metadata.Feature.StatusKey] = "imperfect", + [Data.Models.Metadata.Feature.FeatureTypeKey] = "protection", + [Data.Models.Metadata.Feature.ValueKey] = "value", + }; + } + + private static Data.Models.Metadata.Info CreateMetadataInfo() + { + return new Data.Models.Metadata.Info + { + [Data.Models.Metadata.Info.NameKey] = "name", + [Data.Models.Metadata.Info.ValueKey] = "value", + }; + } + + private static Data.Models.Metadata.Input CreateMetadataInput() + { + return new Data.Models.Metadata.Input + { + [Data.Models.Metadata.Input.ButtonsKey] = 12345L, + [Data.Models.Metadata.Input.CoinsKey] = 12345L, + [Data.Models.Metadata.Input.ControlKey] = new Data.Models.Metadata.Control[] { CreateMetadataControl() }, + [Data.Models.Metadata.Input.PlayersKey] = 12345L, + [Data.Models.Metadata.Input.ServiceKey] = "yes", + [Data.Models.Metadata.Input.TiltKey] = "yes", + }; + } + + private static Data.Models.Metadata.Instance CreateMetadataInstance() + { + return new Data.Models.Metadata.Instance + { + [Data.Models.Metadata.Instance.BriefNameKey] = "briefname", + [Data.Models.Metadata.Instance.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Media CreateMetadataMedia() + { + return new Data.Models.Metadata.Media + { + [Data.Models.Metadata.Media.MD5Key] = HashType.MD5.ZeroString, + [Data.Models.Metadata.Media.NameKey] = "name", + [Data.Models.Metadata.Media.SHA1Key] = HashType.SHA1.ZeroString, + [Data.Models.Metadata.Media.SHA256Key] = HashType.SHA256.ZeroString, + [Data.Models.Metadata.Media.SpamSumKey] = HashType.SpamSum.ZeroString, + }; + } + + private static Data.Models.Metadata.Original CreateMetadataOriginal() + { + return new Data.Models.Metadata.Original + { + [Data.Models.Metadata.Original.ContentKey] = "content", + [Data.Models.Metadata.Original.ValueKey] = true, + }; + } + + private static Data.Models.Metadata.Part CreateMetadataPart() + { + return new Data.Models.Metadata.Part + { + [Data.Models.Metadata.Part.DataAreaKey] = new Data.Models.Metadata.DataArea[] { CreateMetadataDataArea() }, + [Data.Models.Metadata.Part.DiskAreaKey] = new Data.Models.Metadata.DiskArea[] { CreateMetadataDiskArea() }, + [Data.Models.Metadata.Part.DipSwitchKey] = new Data.Models.Metadata.DipSwitch[] { [] }, + [Data.Models.Metadata.Part.FeatureKey] = new Data.Models.Metadata.Feature[] { CreateMetadataFeature() }, + [Data.Models.Metadata.Part.InterfaceKey] = "interface", + [Data.Models.Metadata.Part.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Port CreateMetadataPort() + { + return new Data.Models.Metadata.Port + { + [Data.Models.Metadata.Port.AnalogKey] = new Data.Models.Metadata.Analog[] { CreateMetadataAnalog() }, + [Data.Models.Metadata.Port.TagKey] = "tag", + }; + } + + private static Data.Models.Metadata.RamOption CreateMetadataRamOption() + { + return new Data.Models.Metadata.RamOption + { + [Data.Models.Metadata.RamOption.ContentKey] = "content", + [Data.Models.Metadata.RamOption.DefaultKey] = "yes", + [Data.Models.Metadata.RamOption.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Release CreateMetadataRelease() + { + return new Data.Models.Metadata.Release + { + [Data.Models.Metadata.Release.DateKey] = "date", + [Data.Models.Metadata.Release.DefaultKey] = "yes", + [Data.Models.Metadata.Release.LanguageKey] = "language", + [Data.Models.Metadata.Release.NameKey] = "name", + [Data.Models.Metadata.Release.RegionKey] = "region", + }; + } + + private static Data.Models.Metadata.Rom CreateMetadataRom() + { + return new Data.Models.Metadata.Rom + { + [Data.Models.Metadata.Rom.AlbumKey] = "album", + [Data.Models.Metadata.Rom.AltRomnameKey] = "alt_romname", + [Data.Models.Metadata.Rom.AltTitleKey] = "alt_title", + [Data.Models.Metadata.Rom.ArtistKey] = "artist", + [Data.Models.Metadata.Rom.ASRDetectedLangKey] = "asr_detected_lang", + [Data.Models.Metadata.Rom.ASRDetectedLangConfKey] = "asr_detected_lang_conf", + [Data.Models.Metadata.Rom.ASRTranscribedLangKey] = "asr_transcribed_lang", + [Data.Models.Metadata.Rom.BiosKey] = "bios", + [Data.Models.Metadata.Rom.BitrateKey] = "bitrate", + [Data.Models.Metadata.Rom.BitTorrentMagnetHashKey] = "btih", + [Data.Models.Metadata.Rom.ClothCoverDetectionModuleVersionKey] = "cloth_cover_detection_module_version", + [Data.Models.Metadata.Rom.CollectionCatalogNumberKey] = "collection-catalog-number", + [Data.Models.Metadata.Rom.CommentKey] = "comment", + [Data.Models.Metadata.Rom.CRCKey] = HashType.CRC32.ZeroString, + [Data.Models.Metadata.Rom.CreatorKey] = "creator", + [Data.Models.Metadata.Rom.DateKey] = "date", + [Data.Models.Metadata.Rom.DisposeKey] = "yes", + [Data.Models.Metadata.Rom.ExtensionKey] = "extension", + [Data.Models.Metadata.Rom.FileCountKey] = 12345L, + [Data.Models.Metadata.Rom.FileIsAvailableKey] = true, + [Data.Models.Metadata.Rom.FlagsKey] = "flags", + [Data.Models.Metadata.Rom.FormatKey] = "format", + [Data.Models.Metadata.Rom.HeaderKey] = "header", + [Data.Models.Metadata.Rom.HeightKey] = "height", + [Data.Models.Metadata.Rom.hOCRCharToWordhOCRVersionKey] = "hocr_char_to_word_hocr_version", + [Data.Models.Metadata.Rom.hOCRCharToWordModuleVersionKey] = "hocr_char_to_word_module_version", + [Data.Models.Metadata.Rom.hOCRFtsTexthOCRVersionKey] = "hocr_fts_text_hocr_version", + [Data.Models.Metadata.Rom.hOCRFtsTextModuleVersionKey] = "hocr_fts_text_module_version", + [Data.Models.Metadata.Rom.hOCRPageIndexhOCRVersionKey] = "hocr_pageindex_hocr_version", + [Data.Models.Metadata.Rom.hOCRPageIndexModuleVersionKey] = "hocr_pageindex_module_version", + [Data.Models.Metadata.Rom.InvertedKey] = "yes", + [Data.Models.Metadata.Rom.LastModifiedTimeKey] = "mtime", + [Data.Models.Metadata.Rom.LengthKey] = "length", + [Data.Models.Metadata.Rom.LoadFlagKey] = "load16_byte", + [Data.Models.Metadata.Rom.MatrixNumberKey] = "matrix_number", + [Data.Models.Metadata.Rom.MD2Key] = HashType.MD2.ZeroString, + [Data.Models.Metadata.Rom.MD4Key] = HashType.MD4.ZeroString, + [Data.Models.Metadata.Rom.MD5Key] = HashType.MD5.ZeroString, + // [Data.Models.Metadata.Rom.OpenMSXMediaType] = null, // Omit due to other test + [Data.Models.Metadata.Rom.MergeKey] = "merge", + [Data.Models.Metadata.Rom.MIAKey] = "yes", + [Data.Models.Metadata.Rom.NameKey] = "name", + [Data.Models.Metadata.Rom.TesseractOCRKey] = "ocr", + [Data.Models.Metadata.Rom.TesseractOCRConvertedKey] = "ocr_converted", + [Data.Models.Metadata.Rom.TesseractOCRDetectedLangKey] = "ocr_detected_lang", + [Data.Models.Metadata.Rom.TesseractOCRDetectedLangConfKey] = "ocr_detected_lang_conf", + [Data.Models.Metadata.Rom.TesseractOCRDetectedScriptKey] = "ocr_detected_script", + [Data.Models.Metadata.Rom.TesseractOCRDetectedScriptConfKey] = "ocr_detected_script_conf", + [Data.Models.Metadata.Rom.TesseractOCRModuleVersionKey] = "ocr_module_version", + [Data.Models.Metadata.Rom.TesseractOCRParametersKey] = "ocr_parameters", + [Data.Models.Metadata.Rom.OffsetKey] = "offset", + [Data.Models.Metadata.Rom.OptionalKey] = "yes", + [Data.Models.Metadata.Rom.OriginalKey] = "original", + [Data.Models.Metadata.Rom.PDFModuleVersionKey] = "pdf_module_version", + [Data.Models.Metadata.Rom.PreviewImageKey] = "preview-image", + [Data.Models.Metadata.Rom.PublisherKey] = "publisher", + [Data.Models.Metadata.Rom.RegionKey] = "region", + [Data.Models.Metadata.Rom.RemarkKey] = "remark", + [Data.Models.Metadata.Rom.RIPEMD128Key] = HashType.RIPEMD128.ZeroString, + [Data.Models.Metadata.Rom.RIPEMD160Key] = HashType.RIPEMD160.ZeroString, + [Data.Models.Metadata.Rom.RotationKey] = "rotation", + [Data.Models.Metadata.Rom.SerialKey] = "serial", + [Data.Models.Metadata.Rom.SHA1Key] = HashType.SHA1.ZeroString, + [Data.Models.Metadata.Rom.SHA256Key] = HashType.SHA256.ZeroString, + [Data.Models.Metadata.Rom.SHA384Key] = HashType.SHA384.ZeroString, + [Data.Models.Metadata.Rom.SHA512Key] = HashType.SHA512.ZeroString, + [Data.Models.Metadata.Rom.SizeKey] = 12345L, + [Data.Models.Metadata.Rom.SoundOnlyKey] = "yes", + [Data.Models.Metadata.Rom.SourceKey] = "source", + [Data.Models.Metadata.Rom.SpamSumKey] = HashType.SpamSum.ZeroString, + [Data.Models.Metadata.Rom.StartKey] = "start", + [Data.Models.Metadata.Rom.StatusKey] = "good", + [Data.Models.Metadata.Rom.SummationKey] = "summation", + [Data.Models.Metadata.Rom.TitleKey] = "title", + [Data.Models.Metadata.Rom.TrackKey] = "track", + [Data.Models.Metadata.Rom.OpenMSXType] = "type", + [Data.Models.Metadata.Rom.ValueKey] = "value", + [Data.Models.Metadata.Rom.WhisperASRModuleVersionKey] = "whisper_asr_module_version", + [Data.Models.Metadata.Rom.WhisperModelHashKey] = "whisper_model_hash", + [Data.Models.Metadata.Rom.WhisperModelNameKey] = "whisper_model_name", + [Data.Models.Metadata.Rom.WhisperVersionKey] = "whisper_version", + [Data.Models.Metadata.Rom.WidthKey] = "width", + [Data.Models.Metadata.Rom.WordConfidenceInterval0To10Key] = "word_conf_0_10", + [Data.Models.Metadata.Rom.WordConfidenceInterval11To20Key] = "word_conf_11_20", + [Data.Models.Metadata.Rom.WordConfidenceInterval21To30Key] = "word_conf_21_30", + [Data.Models.Metadata.Rom.WordConfidenceInterval31To40Key] = "word_conf_31_40", + [Data.Models.Metadata.Rom.WordConfidenceInterval41To50Key] = "word_conf_41_50", + [Data.Models.Metadata.Rom.WordConfidenceInterval51To60Key] = "word_conf_51_60", + [Data.Models.Metadata.Rom.WordConfidenceInterval61To70Key] = "word_conf_61_70", + [Data.Models.Metadata.Rom.WordConfidenceInterval71To80Key] = "word_conf_71_80", + [Data.Models.Metadata.Rom.WordConfidenceInterval81To90Key] = "word_conf_81_90", + [Data.Models.Metadata.Rom.WordConfidenceInterval91To100Key] = "word_conf_91_100", + [Data.Models.Metadata.Rom.xxHash364Key] = HashType.XxHash3.ZeroString, + [Data.Models.Metadata.Rom.xxHash3128Key] = HashType.XxHash128.ZeroString, + }; + } + + private static Data.Models.Metadata.Sample CreateMetadataSample() + { + return new Data.Models.Metadata.Sample + { + [Data.Models.Metadata.Sample.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.SharedFeat CreateMetadataSharedFeat() + { + return new Data.Models.Metadata.SharedFeat + { + [Data.Models.Metadata.SharedFeat.NameKey] = "name", + [Data.Models.Metadata.SharedFeat.ValueKey] = "value", + }; + } + + private static Data.Models.Metadata.Slot CreateMetadataSlot() + { + return new Data.Models.Metadata.Slot + { + [Data.Models.Metadata.Slot.NameKey] = "name", + [Data.Models.Metadata.Slot.SlotOptionKey] = new Data.Models.Metadata.SlotOption[] { CreateMetadataSlotOption() }, + }; + } + + private static Data.Models.Metadata.SlotOption CreateMetadataSlotOption() + { + return new Data.Models.Metadata.SlotOption + { + [Data.Models.Metadata.SlotOption.DefaultKey] = "yes", + [Data.Models.Metadata.SlotOption.DevNameKey] = "devname", + [Data.Models.Metadata.SlotOption.NameKey] = "name", + }; + } + + private static Data.Models.Metadata.Software CreateMetadataSoftware() + { + return new Data.Models.Metadata.Software + { + [Data.Models.Metadata.Software.CloneOfKey] = "cloneof", + [Data.Models.Metadata.Software.DescriptionKey] = "description", + [Data.Models.Metadata.Software.InfoKey] = new Data.Models.Metadata.Info[] { CreateMetadataInfo() }, + [Data.Models.Metadata.Software.NameKey] = "name", + [Data.Models.Metadata.Software.NotesKey] = "notes", + [Data.Models.Metadata.Software.PartKey] = new Data.Models.Metadata.Part[] { CreateMetadataPart() }, + [Data.Models.Metadata.Software.PublisherKey] = "publisher", + [Data.Models.Metadata.Software.SharedFeatKey] = new Data.Models.Metadata.SharedFeat[] { CreateMetadataSharedFeat() }, + [Data.Models.Metadata.Software.SupportedKey] = "yes", + [Data.Models.Metadata.Software.YearKey] = "year", + }; + } + + private static Data.Models.Metadata.SoftwareList CreateMetadataSoftwareList() + { + return new Data.Models.Metadata.SoftwareList + { + [Data.Models.Metadata.SoftwareList.DescriptionKey] = "description", + [Data.Models.Metadata.SoftwareList.FilterKey] = "filter", + [Data.Models.Metadata.SoftwareList.NameKey] = "name", + [Data.Models.Metadata.SoftwareList.NotesKey] = "notes", + [Data.Models.Metadata.SoftwareList.SoftwareKey] = new Data.Models.Metadata.Software[] { CreateMetadataSoftware() }, + [Data.Models.Metadata.SoftwareList.StatusKey] = "original", + [Data.Models.Metadata.SoftwareList.TagKey] = "tag", + }; + } + + private static Data.Models.Metadata.Sound CreateMetadataSound() + { + return new Data.Models.Metadata.Sound + { + [Data.Models.Metadata.Sound.ChannelsKey] = 12345L, + }; + } + + private static Data.Models.Logiqx.Trurip CreateMetadataTrurip() + { + return new Data.Models.Logiqx.Trurip + { + TitleID = "titleid", + Publisher = "publisher", + Developer = "developer", + Year = "year", + Genre = "genre", + Subgenre = "subgenre", + Ratings = "ratings", + Score = "score", + Players = "players", + Enabled = "enabled", + CRC = "true", + Source = "source", + CloneOf = "cloneof", + RelatedTo = "relatedto", + }; + } + + private static Data.Models.Metadata.Video CreateMetadataVideo() + { + return new Data.Models.Metadata.Video + { + [Data.Models.Metadata.Video.AspectXKey] = 12345L, + [Data.Models.Metadata.Video.AspectYKey] = 12345L, + [Data.Models.Metadata.Video.HeightKey] = 12345L, + [Data.Models.Metadata.Video.OrientationKey] = "vertical", + [Data.Models.Metadata.Video.RefreshKey] = 12345L, + [Data.Models.Metadata.Video.ScreenKey] = "vector", + [Data.Models.Metadata.Video.WidthKey] = 12345L, + }; + } + + #endregion + + #region Validation Helpers + + private static void ValidateHeader(DatHeader datHeader) + { + Assert.Equal("author", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.AuthorKey)); + Assert.Equal("merged", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.BiosModeKey)); + Assert.Equal("build", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.BuildKey)); + Assert.Equal("ext", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.CanOpenKey)); + Assert.Equal("category", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.CategoryKey)); + Assert.Equal("comment", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.CommentKey)); + Assert.Equal("date", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.DateKey)); + Assert.Equal("datversion", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.DatVersionKey)); + Assert.True(datHeader.GetBoolFieldValue(Data.Models.Metadata.Header.DebugKey)); + Assert.Equal("description", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + Assert.Equal("email", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.EmailKey)); + Assert.Equal("emulatorversion", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.EmulatorVersionKey)); + Assert.Equal("merged", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ForceMergingKey)); + Assert.Equal("required", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ForceNodumpKey)); + Assert.Equal("zip", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ForcePackingKey)); + Assert.True(datHeader.GetBoolFieldValue(Data.Models.Metadata.Header.ForceZippingKey)); + Assert.Equal("header", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.HeaderKey)); + Assert.Equal("homepage", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.HomepageKey)); + Assert.Equal("id", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.IdKey)); + Assert.NotNull(datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ImagesKey)); + Assert.Equal("imfolder", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ImFolderKey)); + Assert.NotNull(datHeader.GetStringFieldValue(Data.Models.Metadata.Header.InfosKey)); + Assert.True(datHeader.GetBoolFieldValue(Data.Models.Metadata.Header.LockBiosModeKey)); + Assert.True(datHeader.GetBoolFieldValue(Data.Models.Metadata.Header.LockRomModeKey)); + Assert.True(datHeader.GetBoolFieldValue(Data.Models.Metadata.Header.LockSampleModeKey)); + Assert.Equal("mameconfig", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.MameConfigKey)); + Assert.Equal("name", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.NotNull(datHeader.GetStringFieldValue(Data.Models.Metadata.Header.NewDatKey)); + Assert.Equal("notes", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.NotesKey)); + Assert.Equal("plugin", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.PluginKey)); + Assert.Equal("refname", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.RefNameKey)); + Assert.Equal("merged", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.RomModeKey)); + Assert.Equal("romtitle", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.RomTitleKey)); + Assert.Equal("rootdir", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.RootDirKey)); + Assert.Equal("merged", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.SampleModeKey)); + Assert.Equal("schemalocation", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.SchemaLocationKey)); + Assert.Equal("screenshotsheight", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsHeightKey)); + Assert.Equal("screenshotsWidth", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsWidthKey)); + Assert.NotNull(datHeader.GetStringFieldValue(Data.Models.Metadata.Header.SearchKey)); + Assert.Equal("system", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.SystemKey)); + Assert.Equal("timestamp", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.TimestampKey)); + Assert.Equal("type", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey)); + Assert.Equal("url", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.UrlKey)); + Assert.Equal("version", datHeader.GetStringFieldValue(Data.Models.Metadata.Header.VersionKey)); + } + +#pragma warning disable IDE0051 + private static void ValidateMachine(DatItems.Machine machine) + { + Assert.Equal("board", machine.GetStringFieldValue(Data.Models.Metadata.Machine.BoardKey)); + Assert.Equal("buttons", machine.GetStringFieldValue(Data.Models.Metadata.Machine.ButtonsKey)); + Assert.Equal("category", machine.GetStringFieldValue(Data.Models.Metadata.Machine.CategoryKey)); + Assert.Equal("cloneof", machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)); + Assert.Equal("cloneofid", machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfIdKey)); + Assert.Equal("comment", machine.GetStringFieldValue(Data.Models.Metadata.Machine.CommentKey)); + Assert.Equal("company", machine.GetStringFieldValue(Data.Models.Metadata.Machine.CompanyKey)); + Assert.Equal("control", machine.GetStringFieldValue(Data.Models.Metadata.Machine.ControlKey)); + Assert.Equal("country", machine.GetStringFieldValue(Data.Models.Metadata.Machine.CountryKey)); + Assert.Equal("description", machine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey)); + Assert.Equal("dirname", machine.GetStringFieldValue(Data.Models.Metadata.Machine.DirNameKey)); + Assert.Equal("displaycount", machine.GetStringFieldValue(Data.Models.Metadata.Machine.DisplayCountKey)); + Assert.Equal("displaytype", machine.GetStringFieldValue(Data.Models.Metadata.Machine.DisplayTypeKey)); + Assert.Equal("duplicateid", machine.GetStringFieldValue(Data.Models.Metadata.Machine.DuplicateIDKey)); + Assert.Equal("emulator", machine.GetStringFieldValue(Data.Models.Metadata.Machine.EmulatorKey)); + Assert.Equal("extra", machine.GetStringFieldValue(Data.Models.Metadata.Machine.ExtraKey)); + Assert.Equal("favorite", machine.GetStringFieldValue(Data.Models.Metadata.Machine.FavoriteKey)); + Assert.Equal("genmsxid", machine.GetStringFieldValue(Data.Models.Metadata.Machine.GenMSXIDKey)); + Assert.Equal("history", machine.GetStringFieldValue(Data.Models.Metadata.Machine.HistoryKey)); + Assert.Equal("id", machine.GetStringFieldValue(Data.Models.Metadata.Machine.IdKey)); + Assert.Equal(HashType.CRC32.ZeroString, machine.GetStringFieldValue(Data.Models.Metadata.Machine.Im1CRCKey)); + Assert.Equal(HashType.CRC32.ZeroString, machine.GetStringFieldValue(Data.Models.Metadata.Machine.Im2CRCKey)); + Assert.Equal("imagenumber", machine.GetStringFieldValue(Data.Models.Metadata.Machine.ImageNumberKey)); + Assert.Equal("yes", machine.GetStringFieldValue(Data.Models.Metadata.Machine.IsBiosKey)); + Assert.Equal("yes", machine.GetStringFieldValue(Data.Models.Metadata.Machine.IsDeviceKey)); + Assert.Equal("yes", machine.GetStringFieldValue(Data.Models.Metadata.Machine.IsMechanicalKey)); + Assert.Equal("language", machine.GetStringFieldValue(Data.Models.Metadata.Machine.LanguageKey)); + Assert.Equal("location", machine.GetStringFieldValue(Data.Models.Metadata.Machine.LocationKey)); + Assert.Equal("manufacturer", machine.GetStringFieldValue(Data.Models.Metadata.Machine.ManufacturerKey)); + Assert.Equal("name", machine.GetName()); + Assert.Equal("notes", machine.GetStringFieldValue(Data.Models.Metadata.Machine.NotesKey)); + Assert.Equal("playedcount", machine.GetStringFieldValue(Data.Models.Metadata.Machine.PlayedCountKey)); + Assert.Equal("playedtime", machine.GetStringFieldValue(Data.Models.Metadata.Machine.PlayedTimeKey)); + Assert.Equal("players", machine.GetStringFieldValue(Data.Models.Metadata.Machine.PlayersKey)); + Assert.Equal("publisher", machine.GetStringFieldValue(Data.Models.Metadata.Machine.PublisherKey)); + Assert.Equal("rebuildto", machine.GetStringFieldValue(Data.Models.Metadata.Machine.RebuildToKey)); + Assert.Equal("releasenumber", machine.GetStringFieldValue(Data.Models.Metadata.Machine.ReleaseNumberKey)); + Assert.Equal("romof", machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)); + Assert.Equal("rotation", machine.GetStringFieldValue(Data.Models.Metadata.Machine.RotationKey)); + Assert.Equal("yes", machine.GetStringFieldValue(Data.Models.Metadata.Machine.RunnableKey)); + Assert.Equal("sampleof", machine.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey)); + Assert.Equal("savetype", machine.GetStringFieldValue(Data.Models.Metadata.Machine.SaveTypeKey)); + Assert.Equal("sourcefile", machine.GetStringFieldValue(Data.Models.Metadata.Machine.SourceFileKey)); + Assert.Equal("sourcerom", machine.GetStringFieldValue(Data.Models.Metadata.Machine.SourceRomKey)); + Assert.Equal("status", machine.GetStringFieldValue(Data.Models.Metadata.Machine.StatusKey)); + Assert.Equal("yes", machine.GetStringFieldValue(Data.Models.Metadata.Machine.SupportedKey)); + Assert.Equal("system", machine.GetStringFieldValue(Data.Models.Metadata.Machine.SystemKey)); + Assert.Equal("tags", machine.GetStringFieldValue(Data.Models.Metadata.Machine.TagsKey)); + Assert.Equal("year", machine.GetStringFieldValue(Data.Models.Metadata.Machine.YearKey)); + + DatItems.Trurip? trurip = machine.GetFieldValue(Data.Models.Metadata.Machine.TruripKey); + ValidateTrurip(trurip); + } +#pragma warning restore IDE0051 + + private static void ValidateAdjuster(Adjuster? adjuster) + { + Assert.NotNull(adjuster); + Assert.True(adjuster.GetBoolFieldValue(Data.Models.Metadata.Adjuster.DefaultKey)); + Assert.Equal("name", adjuster.GetStringFieldValue(Data.Models.Metadata.Adjuster.NameKey)); + + Condition? condition = adjuster.GetFieldValue(Data.Models.Metadata.Adjuster.ConditionKey); + ValidateCondition(condition); + } + + private static void ValidateAnalog(Analog? analog) + { + Assert.NotNull(analog); + Assert.Equal("mask", analog.GetStringFieldValue(Data.Models.Metadata.Analog.MaskKey)); + } + + private static void ValidateArchive(Archive? archive) + { + Assert.NotNull(archive); + Assert.Equal("name", archive.GetStringFieldValue(Data.Models.Metadata.Archive.NameKey)); + } + + private static void ValidateBiosSet(BiosSet? biosSet) + { + Assert.NotNull(biosSet); + Assert.True(biosSet.GetBoolFieldValue(Data.Models.Metadata.BiosSet.DefaultKey)); + Assert.Equal("description", biosSet.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey)); + Assert.Equal("name", biosSet.GetStringFieldValue(Data.Models.Metadata.BiosSet.NameKey)); + } + + private static void ValidateChip(Chip? chip) + { + Assert.NotNull(chip); + Assert.Equal(12345L, chip.GetInt64FieldValue(Data.Models.Metadata.Chip.ClockKey)); + Assert.Equal("flags", chip.GetStringFieldValue(Data.Models.Metadata.Chip.FlagsKey)); + Assert.Equal("name", chip.GetStringFieldValue(Data.Models.Metadata.Chip.NameKey)); + Assert.True(chip.GetBoolFieldValue(Data.Models.Metadata.Chip.SoundOnlyKey)); + Assert.Equal("tag", chip.GetStringFieldValue(Data.Models.Metadata.Chip.TagKey)); + Assert.Equal("cpu", chip.GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey)); + } + + private static void ValidateCondition(Condition? condition) + { + Assert.NotNull(condition); + Assert.Equal("value", condition.GetStringFieldValue(Data.Models.Metadata.Condition.ValueKey)); + Assert.Equal("mask", condition.GetStringFieldValue(Data.Models.Metadata.Condition.MaskKey)); + Assert.Equal("eq", condition.GetStringFieldValue(Data.Models.Metadata.Condition.RelationKey)); + Assert.Equal("tag", condition.GetStringFieldValue(Data.Models.Metadata.Condition.TagKey)); + } + + private static void ValidateConfiguration(Configuration? configuration) + { + Assert.NotNull(configuration); + Assert.Equal("mask", configuration.GetStringFieldValue(Data.Models.Metadata.Configuration.MaskKey)); + Assert.Equal("name", configuration.GetStringFieldValue(Data.Models.Metadata.Configuration.NameKey)); + Assert.Equal("tag", configuration.GetStringFieldValue(Data.Models.Metadata.Configuration.TagKey)); + + Condition? condition = configuration.GetFieldValue(Data.Models.Metadata.Configuration.ConditionKey); + ValidateCondition(condition); + + ConfLocation[]? confLocations = configuration.GetFieldValue(Data.Models.Metadata.Configuration.ConfLocationKey); + Assert.NotNull(confLocations); + ConfLocation? confLocation = Assert.Single(confLocations); + ValidateConfLocation(confLocation); + + ConfSetting[]? confSettings = configuration.GetFieldValue(Data.Models.Metadata.Configuration.ConfSettingKey); + Assert.NotNull(confSettings); + ConfSetting? confSetting = Assert.Single(confSettings); + ValidateConfSetting(confSetting); + } + + private static void ValidateConfLocation(ConfLocation? confLocation) + { + Assert.NotNull(confLocation); + Assert.True(confLocation.GetBoolFieldValue(Data.Models.Metadata.ConfLocation.InvertedKey)); + Assert.Equal("name", confLocation.GetStringFieldValue(Data.Models.Metadata.ConfLocation.NameKey)); + Assert.Equal("number", confLocation.GetStringFieldValue(Data.Models.Metadata.ConfLocation.NumberKey)); + } + + private static void ValidateConfSetting(ConfSetting? confSetting) + { + Assert.NotNull(confSetting); + Assert.True(confSetting.GetBoolFieldValue(Data.Models.Metadata.ConfSetting.DefaultKey)); + Assert.Equal("name", confSetting.GetStringFieldValue(Data.Models.Metadata.ConfSetting.NameKey)); + Assert.Equal("value", confSetting.GetStringFieldValue(Data.Models.Metadata.ConfSetting.ValueKey)); + + Condition? condition = confSetting.GetFieldValue(Data.Models.Metadata.ConfSetting.ConditionKey); + ValidateCondition(condition); + } + + private static void ValidateControl(Control? control) + { + Assert.NotNull(control); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.ButtonsKey)); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.KeyDeltaKey)); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.MaximumKey)); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.MinimumKey)); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.PlayerKey)); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.ReqButtonsKey)); + Assert.True(control.GetBoolFieldValue(Data.Models.Metadata.Control.ReverseKey)); + Assert.Equal(12345L, control.GetInt64FieldValue(Data.Models.Metadata.Control.SensitivityKey)); + Assert.Equal("lightgun", control.GetStringFieldValue(Data.Models.Metadata.Control.ControlTypeKey)); + Assert.Equal("ways", control.GetStringFieldValue(Data.Models.Metadata.Control.WaysKey)); + Assert.Equal("ways2", control.GetStringFieldValue(Data.Models.Metadata.Control.Ways2Key)); + Assert.Equal("ways3", control.GetStringFieldValue(Data.Models.Metadata.Control.Ways3Key)); + } + + private static void ValidateDataArea(DataArea? dataArea) + { + Assert.NotNull(dataArea); + Assert.Equal("big", dataArea.GetStringFieldValue(Data.Models.Metadata.DataArea.EndiannessKey)); + Assert.Equal("name", dataArea.GetStringFieldValue(Data.Models.Metadata.DataArea.NameKey)); + Assert.Equal(12345L, dataArea.GetInt64FieldValue(Data.Models.Metadata.DataArea.SizeKey)); + Assert.Equal(64, dataArea.GetInt64FieldValue(Data.Models.Metadata.DataArea.WidthKey)); + } + + private static void ValidateDevice(Device? device) + { + Assert.NotNull(device); + Assert.Equal("fixedimage", device.GetStringFieldValue(Data.Models.Metadata.Device.FixedImageKey)); + Assert.Equal("interface", device.GetStringFieldValue(Data.Models.Metadata.Device.InterfaceKey)); + Assert.Equal(1, device.GetInt64FieldValue(Data.Models.Metadata.Device.MandatoryKey)); + Assert.Equal("tag", device.GetStringFieldValue(Data.Models.Metadata.Device.TagKey)); + Assert.Equal("punchtape", device.GetStringFieldValue(Data.Models.Metadata.Device.DeviceTypeKey)); + + Extension[]? extensions = device.GetFieldValue(Data.Models.Metadata.Device.ExtensionKey); + Assert.NotNull(extensions); + Extension? extension = Assert.Single(extensions); + ValidateExtension(extension); + + Instance? instance = device.GetFieldValue(Data.Models.Metadata.Device.InstanceKey); + ValidateInstance(instance); + } + + private static void ValidateDeviceRef(DeviceRef? deviceRef) + { + Assert.NotNull(deviceRef); + Assert.Equal("name", deviceRef.GetStringFieldValue(Data.Models.Metadata.DeviceRef.NameKey)); + } + + private static void ValidateDipLocation(DipLocation? dipLocation) + { + Assert.NotNull(dipLocation); + Assert.True(dipLocation.GetBoolFieldValue(Data.Models.Metadata.DipLocation.InvertedKey)); + Assert.Equal("name", dipLocation.GetStringFieldValue(Data.Models.Metadata.DipLocation.NameKey)); + Assert.Equal("number", dipLocation.GetStringFieldValue(Data.Models.Metadata.DipLocation.NumberKey)); + } + + private static void ValidateDipSwitch(DipSwitch? dipSwitch) + { + Assert.NotNull(dipSwitch); + Assert.True(dipSwitch.GetBoolFieldValue(Data.Models.Metadata.DipSwitch.DefaultKey)); + Assert.Equal("mask", dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.MaskKey)); + Assert.Equal("name", dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.NameKey)); + Assert.Equal("tag", dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.TagKey)); + + Condition? condition = dipSwitch.GetFieldValue(Data.Models.Metadata.DipSwitch.ConditionKey); + ValidateCondition(condition); + + DipLocation[]? dipLocations = dipSwitch.GetFieldValue(Data.Models.Metadata.DipSwitch.DipLocationKey); + Assert.NotNull(dipLocations); + DipLocation? dipLocation = Assert.Single(dipLocations); + ValidateDipLocation(dipLocation); + + DipValue[]? dipValues = dipSwitch.GetFieldValue(Data.Models.Metadata.DipSwitch.DipValueKey); + Assert.NotNull(dipValues); + DipValue? dipValue = Assert.Single(dipValues); + ValidateDipValue(dipValue); + + string[]? entries = dipSwitch.GetStringArrayFieldValue(Data.Models.Metadata.DipSwitch.EntryKey); + Assert.NotNull(entries); + string entry = Assert.Single(entries); + Assert.Equal("entry", entry); + } + + private static void ValidateDipValue(DipValue? dipValue) + { + Assert.NotNull(dipValue); + Assert.True(dipValue.GetBoolFieldValue(Data.Models.Metadata.DipValue.DefaultKey)); + Assert.Equal("name", dipValue.GetStringFieldValue(Data.Models.Metadata.DipValue.NameKey)); + Assert.Equal("value", dipValue.GetStringFieldValue(Data.Models.Metadata.DipValue.ValueKey)); + + Condition? condition = dipValue.GetFieldValue(Data.Models.Metadata.DipValue.ConditionKey); + ValidateCondition(condition); + } + + private static void ValidateDisk(Disk? disk) + { + Assert.NotNull(disk); + Assert.Equal("flags", disk.GetStringFieldValue(Data.Models.Metadata.Disk.FlagsKey)); + Assert.Equal("index", disk.GetStringFieldValue(Data.Models.Metadata.Disk.IndexKey)); + Assert.Equal(HashType.MD5.ZeroString, disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)); + Assert.Equal("merge", disk.GetStringFieldValue(Data.Models.Metadata.Disk.MergeKey)); + Assert.Equal("name", disk.GetStringFieldValue(Data.Models.Metadata.Disk.NameKey)); + Assert.True(disk.GetBoolFieldValue(Data.Models.Metadata.Disk.OptionalKey)); + Assert.Equal("region", disk.GetStringFieldValue(Data.Models.Metadata.Disk.RegionKey)); + Assert.Equal(HashType.SHA1.ZeroString, disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)); + Assert.True(disk.GetBoolFieldValue(Data.Models.Metadata.Disk.WritableKey)); + } + + private static void ValidateDiskArea(DiskArea? diskArea) + { + Assert.NotNull(diskArea); + Assert.Equal("name", diskArea.GetStringFieldValue(Data.Models.Metadata.DiskArea.NameKey)); + } + + private static void ValidateDisplay(Display? display) + { + Assert.NotNull(display); + Assert.True(display.GetBoolFieldValue(Data.Models.Metadata.Display.FlipXKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.HBEndKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.HBStartKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.HeightKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.HTotalKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.PixClockKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.RefreshKey)); + Assert.Equal(90, display.GetInt64FieldValue(Data.Models.Metadata.Display.RotateKey)); + Assert.Equal("tag", display.GetStringFieldValue(Data.Models.Metadata.Display.TagKey)); + Assert.Equal("vector", display.GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.VBEndKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.VBStartKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.VTotalKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.WidthKey)); + } + + private static void ValidateDriver(Driver? driver) + { + Assert.NotNull(driver); + Assert.Equal("plain", driver.GetStringFieldValue(Data.Models.Metadata.Driver.BlitKey)); + Assert.Equal("good", driver.GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey)); + Assert.Equal("good", driver.GetStringFieldValue(Data.Models.Metadata.Driver.ColorKey)); + Assert.Equal("good", driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey)); + Assert.True(driver.GetBoolFieldValue(Data.Models.Metadata.Driver.IncompleteKey)); + Assert.True(driver.GetBoolFieldValue(Data.Models.Metadata.Driver.NoSoundHardwareKey)); + Assert.Equal("pallettesize", driver.GetStringFieldValue(Data.Models.Metadata.Driver.PaletteSizeKey)); + Assert.True(driver.GetBoolFieldValue(Data.Models.Metadata.Driver.RequiresArtworkKey)); + Assert.Equal("supported", driver.GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey)); + Assert.Equal("good", driver.GetStringFieldValue(Data.Models.Metadata.Driver.SoundKey)); + Assert.Equal("good", driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey)); + Assert.True(driver.GetBoolFieldValue(Data.Models.Metadata.Driver.UnofficialKey)); + } + + private static void ValidateExtension(Extension? extension) + { + Assert.NotNull(extension); + Assert.Equal("name", extension.GetStringFieldValue(Data.Models.Metadata.Extension.NameKey)); + } + + private static void ValidateFeature(Feature? feature) + { + Assert.NotNull(feature); + Assert.Equal("name", feature.GetStringFieldValue(Data.Models.Metadata.Feature.NameKey)); + Assert.Equal("imperfect", feature.GetStringFieldValue(Data.Models.Metadata.Feature.OverallKey)); + Assert.Equal("imperfect", feature.GetStringFieldValue(Data.Models.Metadata.Feature.StatusKey)); + Assert.Equal("protection", feature.GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey)); + Assert.Equal("value", feature.GetStringFieldValue(Data.Models.Metadata.Feature.ValueKey)); + } + + private static void ValidateInfo(Info? info) + { + Assert.NotNull(info); + Assert.Equal("name", info.GetStringFieldValue(Data.Models.Metadata.Info.NameKey)); + Assert.Equal("value", info.GetStringFieldValue(Data.Models.Metadata.Info.ValueKey)); + } + + private static void ValidateInput(Input? input) + { + Assert.NotNull(input); + Assert.Equal(12345L, input.GetInt64FieldValue(Data.Models.Metadata.Input.ButtonsKey)); + Assert.Equal(12345L, input.GetInt64FieldValue(Data.Models.Metadata.Input.CoinsKey)); + Assert.Equal(12345L, input.GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey)); + Assert.True(input.GetBoolFieldValue(Data.Models.Metadata.Input.ServiceKey)); + Assert.True(input.GetBoolFieldValue(Data.Models.Metadata.Input.TiltKey)); + + Control[]? controls = input.GetFieldValue(Data.Models.Metadata.Input.ControlKey); + Assert.NotNull(controls); + Control? control = Assert.Single(controls); + ValidateControl(control); + } + + private static void ValidateInstance(Instance? instance) + { + Assert.NotNull(instance); + Assert.Equal("briefname", instance.GetStringFieldValue(Data.Models.Metadata.Instance.BriefNameKey)); + Assert.Equal("name", instance.GetStringFieldValue(Data.Models.Metadata.Instance.NameKey)); + } + + private static void ValidateMedia(Media? media) + { + Assert.NotNull(media); + Assert.Equal(HashType.MD5.ZeroString, media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)); + Assert.Equal("name", media.GetStringFieldValue(Data.Models.Metadata.Media.NameKey)); + Assert.Equal(HashType.SHA1.ZeroString, media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)); + Assert.Equal(HashType.SHA256.ZeroString, media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)); + Assert.Equal(HashType.SpamSum.ZeroString, media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)); + } + + private static void ValidatePart(Part? part) + { + Assert.NotNull(part); + Assert.Equal("interface", part.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey)); + Assert.Equal("name", part.GetStringFieldValue(Data.Models.Metadata.Part.NameKey)); + } + + private static void ValidatePartFeature(PartFeature? partFeature) + { + Assert.NotNull(partFeature); + Assert.Equal("name", partFeature.GetStringFieldValue(Data.Models.Metadata.Feature.NameKey)); + Assert.Equal("imperfect", partFeature.GetStringFieldValue(Data.Models.Metadata.Feature.OverallKey)); + Assert.Equal("imperfect", partFeature.GetStringFieldValue(Data.Models.Metadata.Feature.StatusKey)); + Assert.Equal("protection", partFeature.GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey)); + Assert.Equal("value", partFeature.GetStringFieldValue(Data.Models.Metadata.Feature.ValueKey)); + + Part? part = partFeature.GetFieldValue(PartFeature.PartKey); + ValidatePart(part); + } + + private static void ValidatePort(Port? port) + { + Assert.NotNull(port); + Assert.Equal("tag", port.GetStringFieldValue(Data.Models.Metadata.Port.TagKey)); + + Analog[]? dipValues = port.GetFieldValue(Data.Models.Metadata.Port.AnalogKey); + Assert.NotNull(dipValues); + Analog? dipValue = Assert.Single(dipValues); + ValidateAnalog(dipValue); + } + + private static void ValidateRamOption(RamOption? ramOption) + { + Assert.NotNull(ramOption); + Assert.Equal("content", ramOption.GetStringFieldValue(Data.Models.Metadata.RamOption.ContentKey)); + Assert.True(ramOption.GetBoolFieldValue(Data.Models.Metadata.RamOption.DefaultKey)); + Assert.Equal("name", ramOption.GetStringFieldValue(Data.Models.Metadata.RamOption.NameKey)); + } + + private static void ValidateRelease(Release? release) + { + Assert.NotNull(release); + Assert.Equal("date", release.GetStringFieldValue(Data.Models.Metadata.Release.DateKey)); + Assert.True(release.GetBoolFieldValue(Data.Models.Metadata.Release.DefaultKey)); + Assert.Equal("language", release.GetStringFieldValue(Data.Models.Metadata.Release.LanguageKey)); + Assert.Equal("name", release.GetStringFieldValue(Data.Models.Metadata.Release.NameKey)); + Assert.Equal("region", release.GetStringFieldValue(Data.Models.Metadata.Release.RegionKey)); + } + + private static void ValidateRom(Rom? rom) + { + Assert.NotNull(rom); + Assert.Equal("album", rom.GetStringFieldValue(Data.Models.Metadata.Rom.AlbumKey)); + Assert.Equal("alt_romname", rom.GetStringFieldValue(Data.Models.Metadata.Rom.AltRomnameKey)); + Assert.Equal("alt_title", rom.GetStringFieldValue(Data.Models.Metadata.Rom.AltTitleKey)); + Assert.Equal("artist", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ArtistKey)); + Assert.Equal("asr_detected_lang", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ASRDetectedLangKey)); + Assert.Equal("asr_detected_lang_conf", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ASRDetectedLangConfKey)); + Assert.Equal("asr_transcribed_lang", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ASRTranscribedLangKey)); + Assert.Equal("bios", rom.GetStringFieldValue(Data.Models.Metadata.Rom.BiosKey)); + Assert.Equal("bitrate", rom.GetStringFieldValue(Data.Models.Metadata.Rom.BitrateKey)); + Assert.Equal("btih", rom.GetStringFieldValue(Data.Models.Metadata.Rom.BitTorrentMagnetHashKey)); + Assert.Equal("cloth_cover_detection_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ClothCoverDetectionModuleVersionKey)); + Assert.Equal("collection-catalog-number", rom.GetStringFieldValue(Data.Models.Metadata.Rom.CollectionCatalogNumberKey)); + Assert.Equal("comment", rom.GetStringFieldValue(Data.Models.Metadata.Rom.CommentKey)); + Assert.Equal(HashType.CRC32.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Equal("creator", rom.GetStringFieldValue(Data.Models.Metadata.Rom.CreatorKey)); + Assert.Equal("date", rom.GetStringFieldValue(Data.Models.Metadata.Rom.DateKey)); + Assert.True(rom.GetBoolFieldValue(Data.Models.Metadata.Rom.DisposeKey)); + Assert.Equal("extension", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ExtensionKey)); + Assert.Equal(12345L, rom.GetInt64FieldValue(Data.Models.Metadata.Rom.FileCountKey)); + Assert.True(rom.GetBoolFieldValue(Data.Models.Metadata.Rom.FileIsAvailableKey)); + Assert.Equal("flags", rom.GetStringFieldValue(Data.Models.Metadata.Rom.FlagsKey)); + Assert.Equal("format", rom.GetStringFieldValue(Data.Models.Metadata.Rom.FormatKey)); + Assert.Equal("header", rom.GetStringFieldValue(Data.Models.Metadata.Rom.HeaderKey)); + Assert.Equal("height", rom.GetStringFieldValue(Data.Models.Metadata.Rom.HeightKey)); + Assert.Equal("hocr_char_to_word_hocr_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.hOCRCharToWordhOCRVersionKey)); + Assert.Equal("hocr_char_to_word_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.hOCRCharToWordModuleVersionKey)); + Assert.Equal("hocr_fts_text_hocr_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.hOCRFtsTexthOCRVersionKey)); + Assert.Equal("hocr_fts_text_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.hOCRFtsTextModuleVersionKey)); + Assert.Equal("hocr_pageindex_hocr_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.hOCRPageIndexhOCRVersionKey)); + Assert.Equal("hocr_pageindex_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.hOCRPageIndexModuleVersionKey)); + Assert.True(rom.GetBoolFieldValue(Data.Models.Metadata.Rom.InvertedKey)); + Assert.Equal("mtime", rom.GetStringFieldValue(Data.Models.Metadata.Rom.LastModifiedTimeKey)); + Assert.Equal("length", rom.GetStringFieldValue(Data.Models.Metadata.Rom.LengthKey)); + Assert.Equal("load16_byte", rom.GetStringFieldValue(Data.Models.Metadata.Rom.LoadFlagKey)); + Assert.Equal("matrix_number", rom.GetStringFieldValue(Data.Models.Metadata.Rom.MatrixNumberKey)); + Assert.Equal(HashType.MD2.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Equal(HashType.MD4.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Equal(HashType.MD5.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType)); // Omit due to other test + Assert.Equal("merge", rom.GetStringFieldValue(Data.Models.Metadata.Rom.MergeKey)); + Assert.True(rom.GetBoolFieldValue(Data.Models.Metadata.Rom.MIAKey)); + Assert.Equal("name", rom.GetStringFieldValue(Data.Models.Metadata.Rom.NameKey)); + Assert.Equal("ocr", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRKey)); + Assert.Equal("ocr_converted", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRConvertedKey)); + Assert.Equal("ocr_detected_lang", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRDetectedLangKey)); + Assert.Equal("ocr_detected_lang_conf", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRDetectedLangConfKey)); + Assert.Equal("ocr_detected_script", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRDetectedScriptKey)); + Assert.Equal("ocr_detected_script_conf", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRDetectedScriptConfKey)); + Assert.Equal("ocr_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRModuleVersionKey)); + Assert.Equal("ocr_parameters", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TesseractOCRParametersKey)); + Assert.Equal("offset", rom.GetStringFieldValue(Data.Models.Metadata.Rom.OffsetKey)); + Assert.True(rom.GetBoolFieldValue(Data.Models.Metadata.Rom.OptionalKey)); + Assert.Equal("original", rom.GetStringFieldValue(Data.Models.Metadata.Rom.OriginalKey)); + Assert.Equal("pdf_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.PDFModuleVersionKey)); + Assert.Equal("preview-image", rom.GetStringFieldValue(Data.Models.Metadata.Rom.PreviewImageKey)); + Assert.Equal("publisher", rom.GetStringFieldValue(Data.Models.Metadata.Rom.PublisherKey)); + Assert.Equal("region", rom.GetStringFieldValue(Data.Models.Metadata.Rom.RegionKey)); + Assert.Equal("remark", rom.GetStringFieldValue(Data.Models.Metadata.Rom.RemarkKey)); + Assert.Equal(HashType.RIPEMD128.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Equal(HashType.RIPEMD160.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Equal("rotation", rom.GetStringFieldValue(Data.Models.Metadata.Rom.RotationKey)); + Assert.Equal("serial", rom.GetStringFieldValue(Data.Models.Metadata.Rom.SerialKey)); + Assert.Equal(HashType.SHA1.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal(HashType.SHA256.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Equal(HashType.SHA384.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Equal(HashType.SHA512.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Equal(12345L, rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.True(rom.GetBoolFieldValue(Data.Models.Metadata.Rom.SoundOnlyKey)); + Assert.Equal("source", rom.GetStringFieldValue(Data.Models.Metadata.Rom.SourceKey)); + Assert.Equal(HashType.SpamSum.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + Assert.Equal("start", rom.GetStringFieldValue(Data.Models.Metadata.Rom.StartKey)); + Assert.Equal("good", rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + Assert.Equal("summation", rom.GetStringFieldValue(Data.Models.Metadata.Rom.SummationKey)); + Assert.Equal("title", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TitleKey)); + Assert.Equal("track", rom.GetStringFieldValue(Data.Models.Metadata.Rom.TrackKey)); + Assert.Equal("type", rom.GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXType)); + Assert.Equal("value", rom.GetStringFieldValue(Data.Models.Metadata.Rom.ValueKey)); + Assert.Equal("whisper_asr_module_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WhisperASRModuleVersionKey)); + Assert.Equal("whisper_model_hash", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WhisperModelHashKey)); + Assert.Equal("whisper_model_name", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WhisperModelNameKey)); + Assert.Equal("whisper_version", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WhisperVersionKey)); + Assert.Equal("width", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WidthKey)); + Assert.Equal("word_conf_0_10", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval0To10Key)); + Assert.Equal("word_conf_11_20", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval11To20Key)); + Assert.Equal("word_conf_21_30", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval21To30Key)); + Assert.Equal("word_conf_31_40", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval31To40Key)); + Assert.Equal("word_conf_41_50", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval41To50Key)); + Assert.Equal("word_conf_51_60", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval51To60Key)); + Assert.Equal("word_conf_61_70", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval61To70Key)); + Assert.Equal("word_conf_71_80", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval71To80Key)); + Assert.Equal("word_conf_81_90", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval81To90Key)); + Assert.Equal("word_conf_91_100", rom.GetStringFieldValue(Data.Models.Metadata.Rom.WordConfidenceInterval91To100Key)); + Assert.Equal(HashType.XxHash3.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.xxHash364Key)); + Assert.Equal(HashType.XxHash128.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.xxHash3128Key)); + } + + private static void ValidateSample(Sample? sample) + { + Assert.NotNull(sample); + Assert.Equal("name", sample.GetStringFieldValue(Data.Models.Metadata.Sample.NameKey)); + } + + private static void ValidateSharedFeat(SharedFeat? sharedFeat) + { + Assert.NotNull(sharedFeat); + Assert.Equal("name", sharedFeat.GetStringFieldValue(Data.Models.Metadata.SharedFeat.NameKey)); + Assert.Equal("value", sharedFeat.GetStringFieldValue(Data.Models.Metadata.SharedFeat.ValueKey)); + } + + private static void ValidateSlot(Slot? slot) + { + Assert.NotNull(slot); + Assert.Equal("name", slot.GetStringFieldValue(Data.Models.Metadata.Slot.NameKey)); + + SlotOption[]? slotOptions = slot.GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey); + Assert.NotNull(slotOptions); + SlotOption? slotOption = Assert.Single(slotOptions); + ValidateSlotOption(slotOption); + } + + private static void ValidateSlotOption(SlotOption? slotOption) + { + Assert.NotNull(slotOption); + Assert.True(slotOption.GetBoolFieldValue(Data.Models.Metadata.SlotOption.DefaultKey)); + Assert.Equal("devname", slotOption.GetStringFieldValue(Data.Models.Metadata.SlotOption.DevNameKey)); + Assert.Equal("name", slotOption.GetStringFieldValue(Data.Models.Metadata.SlotOption.NameKey)); + } + + private static void ValidateSoftwareList(SoftwareList? softwareList) + { + Assert.NotNull(softwareList); + Assert.Equal("description", softwareList.GetStringFieldValue(Data.Models.Metadata.SoftwareList.DescriptionKey)); + Assert.Equal("filter", softwareList.GetStringFieldValue(Data.Models.Metadata.SoftwareList.FilterKey)); + Assert.Equal("name", softwareList.GetStringFieldValue(Data.Models.Metadata.SoftwareList.NameKey)); + Assert.Equal("notes", softwareList.GetStringFieldValue(Data.Models.Metadata.SoftwareList.NotesKey)); + // TODO: Figure out why Data.Models.Metadata.SoftwareList.SoftwareKey doesn't get processed + Assert.Equal("original", softwareList.GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey)); + Assert.Equal("tag", softwareList.GetStringFieldValue(Data.Models.Metadata.SoftwareList.TagKey)); + } + + private static void ValidateSound(Sound? sound) + { + Assert.NotNull(sound); + Assert.Equal(12345L, sound.GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey)); + } + + // TODO: Figure out why so many fields are omitted + private static void ValidateTrurip(DatItems.Trurip? trurip) + { + Assert.NotNull(trurip); + Assert.Equal("titleid", trurip.TitleID); + //Assert.Equal("publisher", trurip.Publisher); // Omitted from conversion + Assert.Equal("developer", trurip.Developer); + // Assert.Equal("year", trurip.Year); // Omitted from conversion + Assert.Equal("genre", trurip.Genre); + Assert.Equal("subgenre", trurip.Subgenre); + Assert.Equal("ratings", trurip.Ratings); + Assert.Equal("score", trurip.Score); + // Assert.Equal("players", trurip.Players); // Omitted from conversion + Assert.Equal("enabled", trurip.Enabled); + Assert.True(trurip.Crc); + // Assert.Equal("source", trurip.Source); // Omitted from conversion + // Assert.Equal("cloneof", trurip.CloneOf); // Omitted from conversion + Assert.Equal("relatedto", trurip.RelatedTo); + } + + private static void ValidateVideo(Display? display) + { + Assert.NotNull(display); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Video.AspectXKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Video.AspectYKey)); + Assert.Equal("vector", display.GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.HeightKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.RefreshKey)); + Assert.Equal(12345L, display.GetInt64FieldValue(Data.Models.Metadata.Display.WidthKey)); + Assert.Equal(90, display.GetInt64FieldValue(Data.Models.Metadata.Display.RotateKey)); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatFileTests.Splitting.cs b/SabreTools.Metadata.DatFiles.Test/DatFileTests.Splitting.cs new file mode 100644 index 00000000..bd86abc5 --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatFileTests.Splitting.cs @@ -0,0 +1,847 @@ +using SabreTools.Metadata.DatFiles.Formats; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public partial class DatFileTests + { + #region AddItemsFromChildren + + [Fact] + public void AddItemsFromChildren_Items_Dedup() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(parentItem, statsOnly: false); + datFile.AddItem(matchChildItem, statsOnly: false); + datFile.AddItem(noMatchChildItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromChildren(subfolder: true, skipDedup: false); + + Assert.Equal(2, datFile.GetItemsForBucket("parent").Count); + } + + [Fact] + public void AddItemsFromChildren_Items_SkipDedup() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(parentItem, statsOnly: false); + datFile.AddItem(matchChildItem, statsOnly: false); + datFile.AddItem(noMatchChildItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromChildren(subfolder: true, skipDedup: true); + + Assert.Equal(3, datFile.GetItemsForBucket("parent").Count); + } + + [Fact] + public void AddItemsFromChildren_ItemsDB_Dedup() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(parentMachine); + long deviceMachineIndex = datFile.AddMachineDB(childMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(parentItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(matchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(noMatchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromChildren(subfolder: true, skipDedup: false); + + Assert.Equal(2, datFile.GetItemsForBucketDB("parent").Count); + } + + [Fact] + public void AddItemsFromChildren_ItemsDB_SkipDedup() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(parentMachine); + long deviceMachineIndex = datFile.AddMachineDB(childMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(parentItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(matchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(noMatchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromChildren(subfolder: true, skipDedup: true); + + Assert.Equal(3, datFile.GetItemsForBucketDB("parent").Count); + } + + #endregion + + #region AddItemsFromCloneOfParent + + [Fact] + public void AddItemsFromCloneOfParent_Items() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(parentItem, statsOnly: false); + datFile.AddItem(matchChildItem, statsOnly: false); + datFile.AddItem(noMatchChildItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromCloneOfParent(); + + Assert.Equal(2, datFile.GetItemsForBucket("child").Count); + } + + [Fact] + public void AddItemsFromCloneOfParent_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(parentMachine); + long deviceMachineIndex = datFile.AddMachineDB(childMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(parentItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(matchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(noMatchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromCloneOfParent(); + + Assert.Equal(2, datFile.GetItemsForBucketDB("child").Count); + } + + #endregion + + #region AddItemsFromDevices + + [Theory] + [InlineData(false, false, 4)] + [InlineData(false, true, 4)] + [InlineData(true, false, 3)] + [InlineData(true, true, 3)] + public void AddItemsFromDevices_Items(bool deviceOnly, bool useSlotOptions, int expected) + { + Source source = new Source(0, source: null); + + Machine deviceMachine = new Machine(); + deviceMachine.SetName("device"); + deviceMachine.SetFieldValue(Data.Models.Metadata.Machine.IsDeviceKey, true); + + Machine slotOptionMachine = new Machine(); + slotOptionMachine.SetName("slotoption"); + + Machine itemMachine = new Machine(); + itemMachine.SetName("machine"); + + DatItem deviceItem = new Sample(); + deviceItem.SetName("device_item"); + deviceItem.SetFieldValue(DatItem.MachineKey, deviceMachine); + deviceItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem slotOptionItem = new Sample(); + slotOptionItem.SetName("slot_option_item"); + slotOptionItem.SetFieldValue(DatItem.MachineKey, slotOptionMachine); + slotOptionItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem datItem = new Rom(); + datItem.SetName("rom"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + datItem.SetFieldValue(DatItem.MachineKey, itemMachine); + datItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem deviceRef = new DeviceRef(); + deviceRef.SetName("device"); + deviceRef.SetFieldValue(DatItem.MachineKey, itemMachine); + deviceRef.SetFieldValue(DatItem.SourceKey, source); + + DatItem slotOption = new SlotOption(); + slotOption.SetName("slotoption"); + slotOption.SetFieldValue(DatItem.MachineKey, itemMachine); + slotOption.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(deviceItem, statsOnly: false); + datFile.AddItem(slotOptionItem, statsOnly: false); + datFile.AddItem(datItem, statsOnly: false); + datFile.AddItem(deviceRef, statsOnly: false); + datFile.AddItem(slotOption, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromDevices(deviceOnly, useSlotOptions); + + Assert.Equal(expected, datFile.GetItemsForBucket("machine").Count); + } + + [Theory] + [InlineData(false, false, 4)] + [InlineData(false, true, 4)] + [InlineData(true, false, 3)] + [InlineData(true, true, 3)] + public void AddItemsFromDevices_ItemsDB(bool deviceOnly, bool useSlotOptions, int expected) + { + Source source = new Source(0, source: null); + + Machine deviceMachine = new Machine(); + deviceMachine.SetName("device"); + deviceMachine.SetFieldValue(Data.Models.Metadata.Machine.IsDeviceKey, true); + + Machine slotOptionMachine = new Machine(); + slotOptionMachine.SetName("slotoption"); + + Machine itemMachine = new Machine(); + itemMachine.SetName("machine"); + + DatItem deviceItem = new Sample(); + deviceItem.SetName("device_item"); + + DatItem slotOptionItem = new Sample(); + slotOptionItem.SetName("slot_option_item"); + + DatItem datItem = new Rom(); + datItem.SetName("rom"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + + DatItem deviceRef = new DeviceRef(); + deviceRef.SetName("device"); + + DatItem slotOption = new SlotOption(); + slotOption.SetName("slotoption"); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long deviceMachineIndex = datFile.AddMachineDB(deviceMachine); + long slotOptionMachineIndex = datFile.AddMachineDB(slotOptionMachine); + long itemMachineIndex = datFile.AddMachineDB(itemMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(deviceItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(slotOptionItem, slotOptionMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(datItem, itemMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(deviceRef, itemMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(slotOption, itemMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromDevices(deviceOnly, useSlotOptions); + + Assert.Equal(expected, datFile.GetItemsForBucketDB("machine").Count); + } + + #endregion + + #region AddItemsFromRomOfParent + + [Fact] + public void AddItemsFromRomOfParent_Items() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(parentItem, statsOnly: false); + datFile.AddItem(matchChildItem, statsOnly: false); + datFile.AddItem(noMatchChildItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromRomOfParent(); + + Assert.Equal(2, datFile.GetItemsForBucket("child").Count); + } + + [Fact] + public void AddItemsFromRomOfParent_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(parentMachine); + long deviceMachineIndex = datFile.AddMachineDB(childMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(parentItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(matchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(noMatchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.AddItemsFromRomOfParent(); + + Assert.Equal(2, datFile.GetItemsForBucketDB("child").Count); + } + + #endregion + + #region RemoveBiosAndDeviceSets + + [Fact] + public void RemoveBiosAndDeviceSets_Items() + { + Source source = new Source(0, source: null); + + Machine biosMachine = new Machine(); + biosMachine.SetName("bios"); + biosMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + Machine deviceMachine = new Machine(); + deviceMachine.SetName("device"); + deviceMachine.SetFieldValue(Data.Models.Metadata.Machine.IsDeviceKey, true); + + DatItem biosItem = new Rom(); + biosItem.SetFieldValue(DatItem.MachineKey, biosMachine); + biosItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem deviceItem = new Rom(); + deviceItem.SetFieldValue(DatItem.MachineKey, deviceMachine); + deviceItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(biosItem, statsOnly: false); + datFile.AddItem(deviceItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveBiosAndDeviceSets(); + + Assert.Empty(datFile.GetItemsForBucket("bios")); + Assert.Empty(datFile.GetItemsForBucket("device")); + } + + [Fact] + public void RemoveBiosAndDeviceSets_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine biosMachine = new Machine(); + biosMachine.SetName("bios"); + biosMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + Machine deviceMachine = new Machine(); + deviceMachine.SetName("device"); + deviceMachine.SetFieldValue(Data.Models.Metadata.Machine.IsDeviceKey, true); + + DatItem biosItem = new Rom(); + DatItem deviceItem = new Rom(); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(biosMachine); + long deviceMachineIndex = datFile.AddMachineDB(deviceMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(biosItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(deviceItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveBiosAndDeviceSets(); + + Assert.Empty(datFile.GetMachinesDB()); + } + + #endregion + + #region RemoveItemsFromCloneOfChild + + [Fact] + public void RemoveItemsFromCloneOfChild_Items() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + parentMachine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "romof"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(parentItem, statsOnly: false); + datFile.AddItem(matchChildItem, statsOnly: false); + datFile.AddItem(noMatchChildItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveItemsFromCloneOfChild(); + + Assert.Single(datFile.GetItemsForBucket("parent")); + DatItem actual = Assert.Single(datFile.GetItemsForBucket("child")); + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("child", actualMachine.GetName()); + Assert.Equal("romof", actualMachine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)); + } + + [Fact] + public void RemoveItemsFromCloneOfChild_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + parentMachine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "romof"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "parent"); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(parentMachine); + long deviceMachineIndex = datFile.AddMachineDB(childMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(parentItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(matchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(noMatchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveItemsFromCloneOfChild(); + + Assert.Single(datFile.GetItemsForBucketDB("parent")); + long actual = Assert.Single(datFile.GetItemsForBucketDB("child")).Key; + Machine? actualMachine = datFile.GetMachineForItemDB(actual).Value; + Assert.NotNull(actualMachine); + Assert.Equal("child", actualMachine.GetName()); + Assert.Equal("romof", actualMachine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)); + } + + #endregion + + #region RemoveItemsFromRomOfChild + + [Fact] + public void RemoveItemsFromRomOfChild_Items() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(parentItem, statsOnly: false); + datFile.AddItem(matchChildItem, statsOnly: false); + datFile.AddItem(noMatchChildItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveItemsFromRomOfChild(); + + Assert.Single(datFile.GetItemsForBucket("parent")); + DatItem actual = Assert.Single(datFile.GetItemsForBucket("child")); + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("child", actualMachine.GetName()); + } + + [Fact] + public void RemoveItemsFromRomOfChild_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine parentMachine = new Machine(); + parentMachine.SetName("parent"); + + Machine childMachine = new Machine(); + childMachine.SetName("child"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "parent"); + childMachine.SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, true); + + DatItem parentItem = new Rom(); + parentItem.SetName("parent_rom"); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + parentItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + parentItem.SetFieldValue(DatItem.MachineKey, parentMachine); + parentItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem matchChildItem = new Rom(); + matchChildItem.SetName("match_child_rom"); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + matchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + matchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + matchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatItem noMatchChildItem = new Rom(); + noMatchChildItem.SetName("no_match_child_rom"); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + noMatchChildItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "beefdead"); + noMatchChildItem.SetFieldValue(DatItem.MachineKey, childMachine); + noMatchChildItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long biosMachineIndex = datFile.AddMachineDB(parentMachine); + long deviceMachineIndex = datFile.AddMachineDB(childMachine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(parentItem, biosMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(matchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + _ = datFile.AddItemDB(noMatchChildItem, deviceMachineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveItemsFromRomOfChild(); + + Assert.Single(datFile.GetItemsForBucketDB("parent")); + long actual = Assert.Single(datFile.GetItemsForBucketDB("child")).Key; + Machine? actualMachine = datFile.GetMachineForItemDB(actual).Value; + Assert.NotNull(actualMachine); + Assert.Equal("child", actualMachine.GetName()); + } + + #endregion + + #region RemoveMachineRelationshipTags + + [Fact] + public void RemoveMachineRelationshipTags_Items() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "XXXXXX"); + machine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "XXXXXX"); + machine.SetFieldValue(Data.Models.Metadata.Machine.SampleOfKey, "XXXXXX"); + + DatItem datItem = new Rom(); + datItem.SetFieldValue(DatItem.MachineKey, machine); + datItem.SetFieldValue(DatItem.SourceKey, source); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(datItem, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveMachineRelationshipTags(); + + DatItem actualItem = Assert.Single(datFile.GetItemsForBucket("machine")); + Machine? actual = actualItem.GetMachine(); + Assert.NotNull(actual); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey)); + } + + [Fact] + public void RemoveMachineRelationshipTags_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, "XXXXXX"); + machine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, "XXXXXX"); + machine.SetFieldValue(Data.Models.Metadata.Machine.SampleOfKey, "XXXXXX"); + + DatItem datItem = new Rom(); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long machineIndex = datFile.AddMachineDB(machine); + long sourceIndex = datFile.AddSourceDB(source); + _ = datFile.AddItemDB(datItem, machineIndex, sourceIndex, statsOnly: false); + + datFile.BucketBy(ItemKey.Machine); + datFile.RemoveMachineRelationshipTags(); + + Machine actual = Assert.Single(datFile.GetMachinesDB()).Value; + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey)); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatFileTests.ToMetadata.cs b/SabreTools.Metadata.DatFiles.Test/DatFileTests.ToMetadata.cs new file mode 100644 index 00000000..717bcf85 --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatFileTests.ToMetadata.cs @@ -0,0 +1,1166 @@ +using System; +using System.Collections.Generic; +using SabreTools.Data.Extensions; +using SabreTools.Hashing; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public partial class DatFileTests + { + #region ConvertToMetadata + + [Fact] + public void ConvertToMetadata_Empty() + { + DatFile datFile = new Formats.Logiqx(null, useGame: false); + + Data.Models.Metadata.MetadataFile? actual = datFile.ConvertToMetadata(); + Assert.Null(actual); + } + + [Fact] + public void ConvertToMetadata_FilledHeader() + { + DatHeader header = CreateHeader(); + + DatFile datFile = new Formats.Logiqx(null, useGame: false); + datFile.SetHeader(header); + datFile.AddItem(new Rom(), statsOnly: false); + + Data.Models.Metadata.MetadataFile? actual = datFile.ConvertToMetadata(); + Assert.NotNull(actual); + + Data.Models.Metadata.Header? actualHeader = actual.Read(Data.Models.Metadata.MetadataFile.HeaderKey); + ValidateMetadataHeader(actualHeader); + } + + [Fact] + public void ConvertToMetadata_FilledMachine() + { + Machine machine = CreateMachine(); + + List datItems = + [ + CreateAdjuster(machine), + CreateArchive(machine), + CreateBiosSet(machine), + CreateChip(machine), + CreateConfiguration(machine), + CreateDevice(machine), + CreateDeviceRef(machine), + CreateDipSwitch(machine), + CreateDipSwitchWithPart(machine), + CreateDisk(machine), + CreateDiskWithDiskAreaPart(machine), + CreateDisplay(machine), + CreateDriver(machine), + CreateFeature(machine), + CreateInfo(machine), + CreateInput(machine), + CreateMedia(machine), + CreatePartFeature(machine), + CreatePort(machine), + CreateRamOption(machine), + CreateRelease(machine), + CreateRom(machine), + CreateRomWithDiskAreaPart(machine), + CreateSample(machine), + CreateSharedFeat(machine), + CreateSlot(machine), + CreateSoftwareList(machine), + CreateSound(machine), + CreateVideo(machine), + ]; + + DatFile datFile = new Formats.SabreJSON(null); + datItems.ForEach(item => datFile.AddItem(item, statsOnly: false)); + + Data.Models.Metadata.MetadataFile? actual = datFile.ConvertToMetadata(); + Assert.NotNull(actual); + + Data.Models.Metadata.Machine[]? machines = actual.ReadItemArray(Data.Models.Metadata.MetadataFile.MachineKey); + Assert.NotNull(machines); + Data.Models.Metadata.Machine actualMachine = Assert.Single(machines); + ValidateMetadataMachine(actualMachine); + } + + #endregion + + #region Creation Helpers + + private static DatHeader CreateHeader() + { + DatHeader item = new DatHeader(CreateMetadataHeader()); + + item.SetFieldValue(Data.Models.Metadata.Header.CanOpenKey, ["ext"]); + item.SetFieldValue(Data.Models.Metadata.Header.ImagesKey, + new Data.Models.OfflineList.Images() { Height = "height" }); + item.SetFieldValue(Data.Models.Metadata.Header.InfosKey, + new Data.Models.OfflineList.Infos() { Comment = new Data.Models.OfflineList.Comment() }); + item.SetFieldValue(Data.Models.Metadata.Header.NewDatKey, + new Data.Models.OfflineList.NewDat() { DatUrl = new Data.Models.OfflineList.DatUrl() }); + item.SetFieldValue(Data.Models.Metadata.Header.SearchKey, + new Data.Models.OfflineList.Search() { To = [] }); + + return item; + } + + private static Machine CreateMachine() + { + Machine item = new Machine(CreateMetadataMachine()); + item.SetFieldValue(Data.Models.Metadata.Machine.TruripKey, CreateTrurip()); + return item; + } + + private static Adjuster CreateAdjuster(Machine machine) + { + Adjuster item = new Adjuster(CreateMetadataAdjuster()); + item.CopyMachineInformation(machine); + return item; + } + + private static Archive CreateArchive(Machine machine) + { + Archive item = new Archive(CreateMetadataArchive()); + item.CopyMachineInformation(machine); + return item; + } + + private static BiosSet CreateBiosSet(Machine machine) + { + BiosSet item = new BiosSet(CreateMetadataBiosSet()); + item.CopyMachineInformation(machine); + return item; + } + + private static Chip CreateChip(Machine machine) + { + Chip item = new Chip(CreateMetadataChip()); + item.CopyMachineInformation(machine); + return item; + } + + private static Configuration CreateConfiguration(Machine machine) + { + Configuration item = new Configuration(CreateMetadataConfiguration()); + item.CopyMachineInformation(machine); + return item; + } + + private static Device CreateDevice(Machine machine) + { + Device item = new Device(CreateMetadataDevice()); + item.CopyMachineInformation(machine); + return item; + } + + private static DataArea CreateDataArea(Machine machine) + { + DataArea item = new DataArea(CreateMetadataDataArea()); + item.CopyMachineInformation(machine); + return item; + } + + private static DeviceRef CreateDeviceRef(Machine machine) + { + DeviceRef item = new DeviceRef(CreateMetadataDeviceRef()); + item.CopyMachineInformation(machine); + return item; + } + + private static DipSwitch CreateDipSwitch(Machine machine) + { + DipSwitch item = new DipSwitch(CreateMetadataDipSwitch()); + item.CopyMachineInformation(machine); + return item; + } + + private static DipSwitch CreateDipSwitchWithPart(Machine machine) + { + DipSwitch item = new DipSwitch(CreateMetadataDipSwitch()); + item.CopyMachineInformation(machine); + item.SetFieldValue(DipSwitch.PartKey, CreatePart(machine)); + return item; + } + + private static Disk CreateDisk(Machine machine) + { + Disk item = new Disk(CreateMetadataDisk()); + item.CopyMachineInformation(machine); + return item; + } + + private static Disk CreateDiskWithDiskAreaPart(Machine machine) + { + Disk item = new Disk(CreateMetadataDisk()); + item.CopyMachineInformation(machine); + item.SetFieldValue(Disk.DiskAreaKey, CreateDiskArea(machine)); + item.SetFieldValue(Disk.PartKey, CreatePart(machine)); + return item; + } + + private static DiskArea CreateDiskArea(Machine machine) + { + DiskArea item = new DiskArea(CreateMetadataDiskArea()); + item.CopyMachineInformation(machine); + return item; + } + + private static Display CreateDisplay(Machine machine) + { + Display item = new Display(CreateMetadataDisplay()); + item.CopyMachineInformation(machine); + return item; + } + + private static Driver CreateDriver(Machine machine) + { + Driver item = new Driver(CreateMetadataDriver()); + item.CopyMachineInformation(machine); + return item; + } + + private static Feature CreateFeature(Machine machine) + { + Feature item = new Feature(CreateMetadataFeature()); + item.CopyMachineInformation(machine); + return item; + } + + private static Info CreateInfo(Machine machine) + { + Info item = new Info(CreateMetadataInfo()); + item.CopyMachineInformation(machine); + return item; + } + + private static Input CreateInput(Machine machine) + { + Input item = new Input(CreateMetadataInput()); + item.CopyMachineInformation(machine); + return item; + } + + private static Media CreateMedia(Machine machine) + { + Media item = new Media(CreateMetadataMedia()); + item.CopyMachineInformation(machine); + return item; + } + + private static Part CreatePart(Machine machine) + { + Part item = new Part(CreateMetadataPart()); + item.CopyMachineInformation(machine); + return item; + } + + private static PartFeature CreatePartFeature(Machine machine) + { + PartFeature item = new PartFeature(CreateMetadataFeature()); + item.CopyMachineInformation(machine); + item.SetFieldValue(PartFeature.PartKey, CreatePart(machine)); + return item; + } + + private static Port CreatePort(Machine machine) + { + Port item = new Port(CreateMetadataPort()); + item.CopyMachineInformation(machine); + return item; + } + + private static RamOption CreateRamOption(Machine machine) + { + RamOption item = new RamOption(CreateMetadataRamOption()); + item.CopyMachineInformation(machine); + return item; + } + + private static Release CreateRelease(Machine machine) + { + Release item = new Release(CreateMetadataRelease()); + item.CopyMachineInformation(machine); + return item; + } + + // TODO: Create variant that results in a Dump + private static Rom CreateRom(Machine machine) + { + Rom item = new Rom(CreateMetadataRom()); + item.CopyMachineInformation(machine); + return item; + } + + private static Rom CreateRomWithDiskAreaPart(Machine machine) + { + Rom item = new Rom(CreateMetadataRom()); + item.CopyMachineInformation(machine); + item.SetFieldValue(Rom.DataAreaKey, CreateDataArea(machine)); + item.SetFieldValue(Rom.PartKey, CreatePart(machine)); + return item; + } + + private static Sample CreateSample(Machine machine) + { + Sample item = new Sample(CreateMetadataSample()); + item.CopyMachineInformation(machine); + return item; + } + + private static SharedFeat CreateSharedFeat(Machine machine) + { + SharedFeat item = new SharedFeat(CreateMetadataSharedFeat()); + item.CopyMachineInformation(machine); + return item; + } + + private static Slot CreateSlot(Machine machine) + { + Slot item = new Slot(CreateMetadataSlot()); + item.CopyMachineInformation(machine); + return item; + } + + private static SoftwareList CreateSoftwareList(Machine machine) + { + SoftwareList item = new SoftwareList(CreateMetadataSoftwareList()); + item.CopyMachineInformation(machine); + return item; + } + + private static Sound CreateSound(Machine machine) + { + Sound item = new Sound(CreateMetadataSound()); + item.CopyMachineInformation(machine); + return item; + } + + private static Trurip CreateTrurip() + { + return new Trurip + { + TitleID = "titleid", + // Publisher = "publisher", + Developer = "developer", + // Year = "year", + Genre = "genre", + Subgenre = "subgenre", + Ratings = "ratings", + Score = "score", + // Players = "players", + Enabled = "enabled", + Crc = true, + // Source = "source", + // CloneOf = "cloneof", + RelatedTo = "relatedto", + }; + } + + private static Display CreateVideo(Machine machine) + { + Display item = new Display(CreateMetadataVideo()); + item.CopyMachineInformation(machine); + return item; + } + + #endregion + + #region Validation Helpers + + private static void ValidateMetadataHeader(Data.Models.Metadata.Header? header) + { + Assert.NotNull(header); + Assert.Equal("author", header.ReadString(Data.Models.Metadata.Header.AuthorKey)); + Assert.Equal("merged", header.ReadString(Data.Models.Metadata.Header.BiosModeKey)); + Assert.Equal("build", header.ReadString(Data.Models.Metadata.Header.BuildKey)); + Assert.NotNull(header.Read(Data.Models.Metadata.Header.CanOpenKey)); + Assert.Equal("category", header.ReadString(Data.Models.Metadata.Header.CategoryKey)); + Assert.Equal("comment", header.ReadString(Data.Models.Metadata.Header.CommentKey)); + Assert.Equal("date", header.ReadString(Data.Models.Metadata.Header.DateKey)); + Assert.Equal("datversion", header.ReadString(Data.Models.Metadata.Header.DatVersionKey)); + Assert.True(header.ReadBool(Data.Models.Metadata.Header.DebugKey)); + Assert.Equal("description", header.ReadString(Data.Models.Metadata.Header.DescriptionKey)); + Assert.Equal("email", header.ReadString(Data.Models.Metadata.Header.EmailKey)); + Assert.Equal("emulatorversion", header.ReadString(Data.Models.Metadata.Header.EmulatorVersionKey)); + Assert.Equal("merged", header.ReadString(Data.Models.Metadata.Header.ForceMergingKey)); + Assert.Equal("required", header.ReadString(Data.Models.Metadata.Header.ForceNodumpKey)); + Assert.Equal("zip", header.ReadString(Data.Models.Metadata.Header.ForcePackingKey)); + Assert.True(header.ReadBool(Data.Models.Metadata.Header.ForceZippingKey)); + Assert.Equal("header", header.ReadString(Data.Models.Metadata.Header.HeaderKey)); + Assert.Equal("homepage", header.ReadString(Data.Models.Metadata.Header.HomepageKey)); + Assert.Equal("id", header.ReadString(Data.Models.Metadata.Header.IdKey)); + Assert.NotNull(header.Read(Data.Models.Metadata.Header.ImagesKey)); + Assert.Equal("imfolder", header.ReadString(Data.Models.Metadata.Header.ImFolderKey)); + Assert.NotNull(header.Read(Data.Models.Metadata.Header.InfosKey)); + Assert.True(header.ReadBool(Data.Models.Metadata.Header.LockBiosModeKey)); + Assert.True(header.ReadBool(Data.Models.Metadata.Header.LockRomModeKey)); + Assert.True(header.ReadBool(Data.Models.Metadata.Header.LockSampleModeKey)); + Assert.Equal("mameconfig", header.ReadString(Data.Models.Metadata.Header.MameConfigKey)); + Assert.Equal("name", header.ReadString(Data.Models.Metadata.Header.NameKey)); + Assert.NotNull(header.Read(Data.Models.Metadata.Header.NewDatKey)); + Assert.Equal("notes", header.ReadString(Data.Models.Metadata.Header.NotesKey)); + Assert.Equal("plugin", header.ReadString(Data.Models.Metadata.Header.PluginKey)); + Assert.Equal("refname", header.ReadString(Data.Models.Metadata.Header.RefNameKey)); + Assert.Equal("merged", header.ReadString(Data.Models.Metadata.Header.RomModeKey)); + Assert.Equal("romtitle", header.ReadString(Data.Models.Metadata.Header.RomTitleKey)); + Assert.Equal("rootdir", header.ReadString(Data.Models.Metadata.Header.RootDirKey)); + Assert.Equal("merged", header.ReadString(Data.Models.Metadata.Header.SampleModeKey)); + Assert.Equal("schemalocation", header.ReadString(Data.Models.Metadata.Header.SchemaLocationKey)); + Assert.Equal("screenshotsheight", header.ReadString(Data.Models.Metadata.Header.ScreenshotsHeightKey)); + Assert.Equal("screenshotsWidth", header.ReadString(Data.Models.Metadata.Header.ScreenshotsWidthKey)); + Assert.NotNull(header.Read(Data.Models.Metadata.Header.SearchKey)); + Assert.Equal("system", header.ReadString(Data.Models.Metadata.Header.SystemKey)); + Assert.Equal("timestamp", header.ReadString(Data.Models.Metadata.Header.TimestampKey)); + Assert.Equal("type", header.ReadString(Data.Models.Metadata.Header.TypeKey)); + Assert.Equal("url", header.ReadString(Data.Models.Metadata.Header.UrlKey)); + Assert.Equal("version", header.ReadString(Data.Models.Metadata.Header.VersionKey)); + } + + private static void ValidateMetadataMachine(Data.Models.Metadata.Machine machine) + { + Assert.Equal("board", machine.ReadString(Data.Models.Metadata.Machine.BoardKey)); + Assert.Equal("buttons", machine.ReadString(Data.Models.Metadata.Machine.ButtonsKey)); + Assert.Equal("category", machine.ReadString(Data.Models.Metadata.Machine.CategoryKey)); + Assert.Equal("cloneof", machine.ReadString(Data.Models.Metadata.Machine.CloneOfKey)); + Assert.Equal("cloneofid", machine.ReadString(Data.Models.Metadata.Machine.CloneOfIdKey)); + Assert.Equal("comment", machine.ReadString(Data.Models.Metadata.Machine.CommentKey)); + Assert.Equal("company", machine.ReadString(Data.Models.Metadata.Machine.CompanyKey)); + Assert.Equal("control", machine.ReadString(Data.Models.Metadata.Machine.ControlKey)); + Assert.Equal("country", machine.ReadString(Data.Models.Metadata.Machine.CountryKey)); + Assert.Equal("description", machine.ReadString(Data.Models.Metadata.Machine.DescriptionKey)); + Assert.Equal("dirname", machine.ReadString(Data.Models.Metadata.Machine.DirNameKey)); + Assert.Equal("displaycount", machine.ReadString(Data.Models.Metadata.Machine.DisplayCountKey)); + Assert.Equal("displaytype", machine.ReadString(Data.Models.Metadata.Machine.DisplayTypeKey)); + Assert.Equal("duplicateid", machine.ReadString(Data.Models.Metadata.Machine.DuplicateIDKey)); + Assert.Equal("emulator", machine.ReadString(Data.Models.Metadata.Machine.EmulatorKey)); + Assert.Equal("extra", machine.ReadString(Data.Models.Metadata.Machine.ExtraKey)); + Assert.Equal("favorite", machine.ReadString(Data.Models.Metadata.Machine.FavoriteKey)); + Assert.Equal("genmsxid", machine.ReadString(Data.Models.Metadata.Machine.GenMSXIDKey)); + Assert.Equal("history", machine.ReadString(Data.Models.Metadata.Machine.HistoryKey)); + Assert.Equal("id", machine.ReadString(Data.Models.Metadata.Machine.IdKey)); + Assert.Equal(HashType.CRC32.ZeroString, machine.ReadString(Data.Models.Metadata.Machine.Im1CRCKey)); + Assert.Equal(HashType.CRC32.ZeroString, machine.ReadString(Data.Models.Metadata.Machine.Im2CRCKey)); + Assert.Equal("imagenumber", machine.ReadString(Data.Models.Metadata.Machine.ImageNumberKey)); + Assert.Equal("yes", machine.ReadString(Data.Models.Metadata.Machine.IsBiosKey)); + Assert.Equal("yes", machine.ReadString(Data.Models.Metadata.Machine.IsDeviceKey)); + Assert.Equal("yes", machine.ReadString(Data.Models.Metadata.Machine.IsMechanicalKey)); + Assert.Equal("language", machine.ReadString(Data.Models.Metadata.Machine.LanguageKey)); + Assert.Equal("location", machine.ReadString(Data.Models.Metadata.Machine.LocationKey)); + Assert.Equal("manufacturer", machine.ReadString(Data.Models.Metadata.Machine.ManufacturerKey)); + Assert.Equal("name", machine.ReadString(Data.Models.Metadata.Machine.NameKey)); + Assert.Equal("notes", machine.ReadString(Data.Models.Metadata.Machine.NotesKey)); + Assert.Equal("playedcount", machine.ReadString(Data.Models.Metadata.Machine.PlayedCountKey)); + Assert.Equal("playedtime", machine.ReadString(Data.Models.Metadata.Machine.PlayedTimeKey)); + Assert.Equal("players", machine.ReadString(Data.Models.Metadata.Machine.PlayersKey)); + Assert.Equal("publisher", machine.ReadString(Data.Models.Metadata.Machine.PublisherKey)); + Assert.Equal("rebuildto", machine.ReadString(Data.Models.Metadata.Machine.RebuildToKey)); + Assert.Equal("releasenumber", machine.ReadString(Data.Models.Metadata.Machine.ReleaseNumberKey)); + Assert.Equal("romof", machine.ReadString(Data.Models.Metadata.Machine.RomOfKey)); + Assert.Equal("rotation", machine.ReadString(Data.Models.Metadata.Machine.RotationKey)); + Assert.Equal("yes", machine.ReadString(Data.Models.Metadata.Machine.RunnableKey)); + Assert.Equal("sampleof", machine.ReadString(Data.Models.Metadata.Machine.SampleOfKey)); + Assert.Equal("savetype", machine.ReadString(Data.Models.Metadata.Machine.SaveTypeKey)); + Assert.Equal("sourcefile", machine.ReadString(Data.Models.Metadata.Machine.SourceFileKey)); + Assert.Equal("sourcerom", machine.ReadString(Data.Models.Metadata.Machine.SourceRomKey)); + Assert.Equal("status", machine.ReadString(Data.Models.Metadata.Machine.StatusKey)); + Assert.Equal("yes", machine.ReadString(Data.Models.Metadata.Machine.SupportedKey)); + Assert.Equal("system", machine.ReadString(Data.Models.Metadata.Machine.SystemKey)); + Assert.Equal("tags", machine.ReadString(Data.Models.Metadata.Machine.TagsKey)); + Assert.Equal("year", machine.ReadString(Data.Models.Metadata.Machine.YearKey)); + + Data.Models.Metadata.Adjuster[]? adjusters = machine.ReadItemArray(Data.Models.Metadata.Machine.AdjusterKey); + Assert.NotNull(adjusters); + Data.Models.Metadata.Adjuster adjuster = Assert.Single(adjusters); + ValidateMetadataAdjuster(adjuster); + + Data.Models.Metadata.Archive[]? archives = machine.ReadItemArray(Data.Models.Metadata.Machine.ArchiveKey); + Assert.NotNull(archives); + Data.Models.Metadata.Archive archive = Assert.Single(archives); + ValidateMetadataArchive(archive); + + Data.Models.Metadata.BiosSet[]? biosSets = machine.ReadItemArray(Data.Models.Metadata.Machine.BiosSetKey); + Assert.NotNull(biosSets); + Data.Models.Metadata.BiosSet biosSet = Assert.Single(biosSets); + ValidateMetadataBiosSet(biosSet); + + Data.Models.Metadata.Chip[]? chips = machine.ReadItemArray(Data.Models.Metadata.Machine.ChipKey); + Assert.NotNull(chips); + Data.Models.Metadata.Chip chip = Assert.Single(chips); + ValidateMetadataChip(chip); + + Data.Models.Metadata.Configuration[]? configurations = machine.ReadItemArray(Data.Models.Metadata.Machine.ConfigurationKey); + Assert.NotNull(configurations); + Data.Models.Metadata.Configuration configuration = Assert.Single(configurations); + ValidateMetadataConfiguration(configuration); + + Data.Models.Metadata.Device[]? devices = machine.ReadItemArray(Data.Models.Metadata.Machine.DeviceKey); + Assert.NotNull(devices); + Data.Models.Metadata.Device device = Assert.Single(devices); + ValidateMetadataDevice(device); + + Data.Models.Metadata.DeviceRef[]? deviceRefs = machine.ReadItemArray(Data.Models.Metadata.Machine.DeviceRefKey); + Assert.NotNull(deviceRefs); + Data.Models.Metadata.DeviceRef deviceRef = Assert.Single(deviceRefs); + ValidateMetadataDeviceRef(deviceRef); + + Data.Models.Metadata.DipSwitch[]? dipSwitches = machine.ReadItemArray(Data.Models.Metadata.Machine.DipSwitchKey); + Assert.NotNull(dipSwitches); + Assert.Equal(2, dipSwitches.Length); + Data.Models.Metadata.DipSwitch dipSwitch = dipSwitches[0]; + ValidateMetadataDipSwitch(dipSwitch); + + Data.Models.Metadata.Disk[]? disks = machine.ReadItemArray(Data.Models.Metadata.Machine.DiskKey); + Assert.NotNull(disks); + Assert.Equal(2, disks.Length); + Data.Models.Metadata.Disk disk = disks[0]; + ValidateMetadataDisk(disk); + + Data.Models.Metadata.Display[]? displays = machine.ReadItemArray(Data.Models.Metadata.Machine.DisplayKey); + Assert.NotNull(displays); + Assert.Equal(2, displays.Length); + Data.Models.Metadata.Display? display = Array.Find(displays, d => !d.ContainsKey(Data.Models.Metadata.Video.AspectXKey)); + ValidateMetadataDisplay(display); + + Data.Models.Metadata.Driver[]? drivers = machine.ReadItemArray(Data.Models.Metadata.Machine.DriverKey); + Assert.NotNull(drivers); + Data.Models.Metadata.Driver driver = Assert.Single(drivers); + ValidateMetadataDriver(driver); + + // TODO: Implement this validation + // Data.Models.Metadata.Dump[]? dumps = machine.ReadItemArray(Data.Models.Metadata.Machine.DumpKey); + // Assert.NotNull(dumps); + // Data.Models.Metadata.Dump dump = Assert.Single(dumps); + // ValidateMetadataDump(dump); + + Data.Models.Metadata.Feature[]? features = machine.ReadItemArray(Data.Models.Metadata.Machine.FeatureKey); + Assert.NotNull(features); + Assert.Equal(2, features.Length); + Data.Models.Metadata.Feature feature = features[0]; + ValidateMetadataFeature(feature); + + Data.Models.Metadata.Info[]? infos = machine.ReadItemArray(Data.Models.Metadata.Machine.InfoKey); + Assert.NotNull(infos); + Data.Models.Metadata.Info info = Assert.Single(infos); + ValidateMetadataInfo(info); + + Data.Models.Metadata.Input[]? inputs = machine.ReadItemArray(Data.Models.Metadata.Machine.InputKey); + Assert.NotNull(inputs); + Data.Models.Metadata.Input input = Assert.Single(inputs); + ValidateMetadataInput(input); + + Data.Models.Metadata.Media[]? media = machine.ReadItemArray(Data.Models.Metadata.Machine.MediaKey); + Assert.NotNull(media); + Data.Models.Metadata.Media medium = Assert.Single(media); + ValidateMetadataMedia(medium); + + Data.Models.Metadata.Part[]? parts = machine.ReadItemArray(Data.Models.Metadata.Machine.PartKey); + Assert.NotNull(parts); + Data.Models.Metadata.Part part = Assert.Single(parts); + ValidateMetadataPart(part); + + Data.Models.Metadata.Port[]? ports = machine.ReadItemArray(Data.Models.Metadata.Machine.PortKey); + Assert.NotNull(ports); + Data.Models.Metadata.Port port = Assert.Single(ports); + ValidateMetadataPort(port); + + Data.Models.Metadata.RamOption[]? ramOptions = machine.ReadItemArray(Data.Models.Metadata.Machine.RamOptionKey); + Assert.NotNull(ramOptions); + Data.Models.Metadata.RamOption ramOption = Assert.Single(ramOptions); + ValidateMetadataRamOption(ramOption); + + Data.Models.Metadata.Release[]? releases = machine.ReadItemArray(Data.Models.Metadata.Machine.ReleaseKey); + Assert.NotNull(releases); + Data.Models.Metadata.Release release = Assert.Single(releases); + ValidateMetadataRelease(release); + + Data.Models.Metadata.Rom[]? roms = machine.ReadItemArray(Data.Models.Metadata.Machine.RomKey); + Assert.NotNull(roms); + Assert.Equal(2, roms.Length); + Data.Models.Metadata.Rom rom = roms[0]; + ValidateMetadataRom(rom); + + Data.Models.Metadata.Sample[]? samples = machine.ReadItemArray(Data.Models.Metadata.Machine.SampleKey); + Assert.NotNull(samples); + Data.Models.Metadata.Sample sample = Assert.Single(samples); + ValidateMetadataSample(sample); + + Data.Models.Metadata.SharedFeat[]? sharedFeats = machine.ReadItemArray(Data.Models.Metadata.Machine.SharedFeatKey); + Assert.NotNull(sharedFeats); + Data.Models.Metadata.SharedFeat sharedFeat = Assert.Single(sharedFeats); + ValidateMetadataSharedFeat(sharedFeat); + + Data.Models.Metadata.Slot[]? slots = machine.ReadItemArray(Data.Models.Metadata.Machine.SlotKey); + Assert.NotNull(slots); + Data.Models.Metadata.Slot slot = Assert.Single(slots); + ValidateMetadataSlot(slot); + + Data.Models.Metadata.SoftwareList[]? softwareLists = machine.ReadItemArray(Data.Models.Metadata.Machine.SoftwareListKey); + Assert.NotNull(softwareLists); + Data.Models.Metadata.SoftwareList softwareList = Assert.Single(softwareLists); + ValidateMetadataSoftwareList(softwareList); + + Data.Models.Metadata.Sound[]? sounds = machine.ReadItemArray(Data.Models.Metadata.Machine.SoundKey); + Assert.NotNull(sounds); + Data.Models.Metadata.Sound sound = Assert.Single(sounds); + ValidateMetadataSound(sound); + + Data.Models.Logiqx.Trurip? trurip = machine.Read(Data.Models.Metadata.Machine.TruripKey); + ValidateMetadataTrurip(trurip); + + Data.Models.Metadata.Video[]? videos = machine.ReadItemArray(Data.Models.Metadata.Machine.VideoKey); + Assert.NotNull(videos); + Data.Models.Metadata.Video video = Assert.Single(videos); + ValidateMetadataVideo(video); + } + + private static void ValidateMetadataAdjuster(Data.Models.Metadata.Adjuster? adjuster) + { + Assert.NotNull(adjuster); + Assert.True(adjuster.ReadBool(Data.Models.Metadata.Adjuster.DefaultKey)); + Assert.Equal("name", adjuster.ReadString(Data.Models.Metadata.Adjuster.NameKey)); + + Data.Models.Metadata.Condition? condition = adjuster.Read(Data.Models.Metadata.Adjuster.ConditionKey); + ValidateMetadataCondition(condition); + } + + private static void ValidateMetadataAnalog(Data.Models.Metadata.Analog? analog) + { + Assert.NotNull(analog); + Assert.Equal("mask", analog.ReadString(Data.Models.Metadata.Analog.MaskKey)); + } + + private static void ValidateMetadataArchive(Data.Models.Metadata.Archive? archive) + { + Assert.NotNull(archive); + Assert.Equal("name", archive.ReadString(Data.Models.Metadata.Archive.NameKey)); + } + + private static void ValidateMetadataBiosSet(Data.Models.Metadata.BiosSet? biosSet) + { + Assert.NotNull(biosSet); + Assert.True(biosSet.ReadBool(Data.Models.Metadata.BiosSet.DefaultKey)); + Assert.Equal("description", biosSet.ReadString(Data.Models.Metadata.BiosSet.DescriptionKey)); + Assert.Equal("name", biosSet.ReadString(Data.Models.Metadata.BiosSet.NameKey)); + } + + private static void ValidateMetadataChip(Data.Models.Metadata.Chip? chip) + { + Assert.NotNull(chip); + Assert.Equal(12345, chip.ReadLong(Data.Models.Metadata.Chip.ClockKey)); + Assert.Equal("flags", chip.ReadString(Data.Models.Metadata.Chip.FlagsKey)); + Assert.Equal("name", chip.ReadString(Data.Models.Metadata.Chip.NameKey)); + Assert.True(chip.ReadBool(Data.Models.Metadata.Chip.SoundOnlyKey)); + Assert.Equal("tag", chip.ReadString(Data.Models.Metadata.Chip.TagKey)); + Assert.Equal("cpu", chip.ReadString(Data.Models.Metadata.Chip.ChipTypeKey)); + } + + private static void ValidateMetadataCondition(Data.Models.Metadata.Condition? condition) + { + Assert.NotNull(condition); + Assert.Equal("value", condition.ReadString(Data.Models.Metadata.Condition.ValueKey)); + Assert.Equal("mask", condition.ReadString(Data.Models.Metadata.Condition.MaskKey)); + Assert.Equal("eq", condition.ReadString(Data.Models.Metadata.Condition.RelationKey)); + Assert.Equal("tag", condition.ReadString(Data.Models.Metadata.Condition.TagKey)); + } + + private static void ValidateMetadataConfiguration(Data.Models.Metadata.Configuration? configuration) + { + Assert.NotNull(configuration); + Assert.Equal("mask", configuration.ReadString(Data.Models.Metadata.Configuration.MaskKey)); + Assert.Equal("name", configuration.ReadString(Data.Models.Metadata.Configuration.NameKey)); + Assert.Equal("tag", configuration.ReadString(Data.Models.Metadata.Configuration.TagKey)); + + Data.Models.Metadata.Condition? condition = configuration.Read(Data.Models.Metadata.Configuration.ConditionKey); + ValidateMetadataCondition(condition); + + Data.Models.Metadata.ConfLocation[]? confLocations = configuration.ReadItemArray(Data.Models.Metadata.Configuration.ConfLocationKey); + Assert.NotNull(confLocations); + Data.Models.Metadata.ConfLocation? confLocation = Assert.Single(confLocations); + ValidateMetadataConfLocation(confLocation); + + Data.Models.Metadata.ConfSetting[]? confSettings = configuration.ReadItemArray(Data.Models.Metadata.Configuration.ConfSettingKey); + Assert.NotNull(confSettings); + Data.Models.Metadata.ConfSetting? confSetting = Assert.Single(confSettings); + ValidateMetadataConfSetting(confSetting); + } + + private static void ValidateMetadataConfLocation(Data.Models.Metadata.ConfLocation? confLocation) + { + Assert.NotNull(confLocation); + Assert.True(confLocation.ReadBool(Data.Models.Metadata.ConfLocation.InvertedKey)); + Assert.Equal("name", confLocation.ReadString(Data.Models.Metadata.ConfLocation.NameKey)); + Assert.Equal("number", confLocation.ReadString(Data.Models.Metadata.ConfLocation.NumberKey)); + } + + private static void ValidateMetadataConfSetting(Data.Models.Metadata.ConfSetting? confSetting) + { + Assert.NotNull(confSetting); + Assert.True(confSetting.ReadBool(Data.Models.Metadata.ConfSetting.DefaultKey)); + Assert.Equal("name", confSetting.ReadString(Data.Models.Metadata.ConfSetting.NameKey)); + Assert.Equal("value", confSetting.ReadString(Data.Models.Metadata.ConfSetting.ValueKey)); + + Data.Models.Metadata.Condition? condition = confSetting.Read(Data.Models.Metadata.ConfSetting.ConditionKey); + ValidateMetadataCondition(condition); + } + + private static void ValidateMetadataControl(Data.Models.Metadata.Control? control) + { + Assert.NotNull(control); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.ButtonsKey)); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.KeyDeltaKey)); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.MaximumKey)); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.MinimumKey)); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.PlayerKey)); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.ReqButtonsKey)); + Assert.True(control.ReadBool(Data.Models.Metadata.Control.ReverseKey)); + Assert.Equal(12345, control.ReadLong(Data.Models.Metadata.Control.SensitivityKey)); + Assert.Equal("lightgun", control.ReadString(Data.Models.Metadata.Control.ControlTypeKey)); + Assert.Equal("ways", control.ReadString(Data.Models.Metadata.Control.WaysKey)); + Assert.Equal("ways2", control.ReadString(Data.Models.Metadata.Control.Ways2Key)); + Assert.Equal("ways3", control.ReadString(Data.Models.Metadata.Control.Ways3Key)); + } + + private static void ValidateMetadataDataArea(Data.Models.Metadata.DataArea? dataArea) + { + Assert.NotNull(dataArea); + Assert.Equal("big", dataArea.ReadString(Data.Models.Metadata.DataArea.EndiannessKey)); + Assert.Equal("name", dataArea.ReadString(Data.Models.Metadata.DataArea.NameKey)); + Assert.Equal(12345, dataArea.ReadLong(Data.Models.Metadata.DataArea.SizeKey)); + Assert.Equal(64, dataArea.ReadLong(Data.Models.Metadata.DataArea.WidthKey)); + + Data.Models.Metadata.Rom[]? roms = dataArea.ReadItemArray(Data.Models.Metadata.DataArea.RomKey); + Assert.NotNull(roms); + Data.Models.Metadata.Rom? rom = Assert.Single(roms); + ValidateMetadataRom(rom); + } + + private static void ValidateMetadataDevice(Data.Models.Metadata.Device? device) + { + Assert.NotNull(device); + Assert.Equal("fixedimage", device.ReadString(Data.Models.Metadata.Device.FixedImageKey)); + Assert.Equal("interface", device.ReadString(Data.Models.Metadata.Device.InterfaceKey)); + Assert.Equal(1, device.ReadLong(Data.Models.Metadata.Device.MandatoryKey)); + Assert.Equal("tag", device.ReadString(Data.Models.Metadata.Device.TagKey)); + Assert.Equal("punchtape", device.ReadString(Data.Models.Metadata.Device.DeviceTypeKey)); + + Data.Models.Metadata.Extension[]? extensions = device.ReadItemArray(Data.Models.Metadata.Device.ExtensionKey); + Assert.NotNull(extensions); + Data.Models.Metadata.Extension? extension = Assert.Single(extensions); + ValidateMetadataExtension(extension); + + Data.Models.Metadata.Instance? instance = device.Read(Data.Models.Metadata.Device.InstanceKey); + ValidateMetadataInstance(instance); + } + + private static void ValidateMetadataDeviceRef(Data.Models.Metadata.DeviceRef? deviceRef) + { + Assert.NotNull(deviceRef); + Assert.Equal("name", deviceRef.ReadString(Data.Models.Metadata.DeviceRef.NameKey)); + } + + private static void ValidateMetadataDipLocation(Data.Models.Metadata.DipLocation? dipLocation) + { + Assert.NotNull(dipLocation); + Assert.True(dipLocation.ReadBool(Data.Models.Metadata.DipLocation.InvertedKey)); + Assert.Equal("name", dipLocation.ReadString(Data.Models.Metadata.DipLocation.NameKey)); + Assert.Equal("number", dipLocation.ReadString(Data.Models.Metadata.DipLocation.NumberKey)); + } + + private static void ValidateMetadataDipSwitch(Data.Models.Metadata.DipSwitch? dipSwitch) + { + Assert.NotNull(dipSwitch); + Assert.True(dipSwitch.ReadBool(Data.Models.Metadata.DipSwitch.DefaultKey)); + Assert.Equal("mask", dipSwitch.ReadString(Data.Models.Metadata.DipSwitch.MaskKey)); + Assert.Equal("name", dipSwitch.ReadString(Data.Models.Metadata.DipSwitch.NameKey)); + Assert.Equal("tag", dipSwitch.ReadString(Data.Models.Metadata.DipSwitch.TagKey)); + + Data.Models.Metadata.Condition? condition = dipSwitch.Read(Data.Models.Metadata.DipSwitch.ConditionKey); + ValidateMetadataCondition(condition); + + Data.Models.Metadata.DipLocation[]? dipLocations = dipSwitch.ReadItemArray(Data.Models.Metadata.DipSwitch.DipLocationKey); + Assert.NotNull(dipLocations); + Data.Models.Metadata.DipLocation? dipLocation = Assert.Single(dipLocations); + ValidateMetadataDipLocation(dipLocation); + + Data.Models.Metadata.DipValue[]? dipValues = dipSwitch.ReadItemArray(Data.Models.Metadata.DipSwitch.DipValueKey); + Assert.NotNull(dipValues); + Data.Models.Metadata.DipValue? dipValue = Assert.Single(dipValues); + ValidateMetadataDipValue(dipValue); + + string[]? entries = dipSwitch.ReadStringArray(Data.Models.Metadata.DipSwitch.EntryKey); + Assert.NotNull(entries); + string entry = Assert.Single(entries); + Assert.Equal("entry", entry); + } + + private static void ValidateMetadataDipValue(Data.Models.Metadata.DipValue? dipValue) + { + Assert.NotNull(dipValue); + Assert.True(dipValue.ReadBool(Data.Models.Metadata.DipValue.DefaultKey)); + Assert.Equal("name", dipValue.ReadString(Data.Models.Metadata.DipValue.NameKey)); + Assert.Equal("value", dipValue.ReadString(Data.Models.Metadata.DipValue.ValueKey)); + + Data.Models.Metadata.Condition? condition = dipValue.Read(Data.Models.Metadata.DipValue.ConditionKey); + ValidateMetadataCondition(condition); + } + + private static void ValidateMetadataDisk(Data.Models.Metadata.Disk? disk) + { + Assert.NotNull(disk); + Assert.Equal("flags", disk.ReadString(Data.Models.Metadata.Disk.FlagsKey)); + Assert.Equal("index", disk.ReadString(Data.Models.Metadata.Disk.IndexKey)); + Assert.Equal(HashType.MD5.ZeroString, disk.ReadString(Data.Models.Metadata.Disk.MD5Key)); + Assert.Equal("merge", disk.ReadString(Data.Models.Metadata.Disk.MergeKey)); + Assert.Equal("name", disk.ReadString(Data.Models.Metadata.Disk.NameKey)); + Assert.True(disk.ReadBool(Data.Models.Metadata.Disk.OptionalKey)); + Assert.Equal("region", disk.ReadString(Data.Models.Metadata.Disk.RegionKey)); + Assert.Equal(HashType.SHA1.ZeroString, disk.ReadString(Data.Models.Metadata.Disk.SHA1Key)); + Assert.True(disk.ReadBool(Data.Models.Metadata.Disk.WritableKey)); + } + + private static void ValidateMetadataDiskArea(Data.Models.Metadata.DiskArea? diskArea) + { + Assert.NotNull(diskArea); + Assert.Equal("name", diskArea.ReadString(Data.Models.Metadata.DiskArea.NameKey)); + + Data.Models.Metadata.Disk[]? disks = diskArea.ReadItemArray(Data.Models.Metadata.DiskArea.DiskKey); + Assert.NotNull(disks); + Data.Models.Metadata.Disk? disk = Assert.Single(disks); + ValidateMetadataDisk(disk); + } + + private static void ValidateMetadataDisplay(Data.Models.Metadata.Display? display) + { + Assert.NotNull(display); + Assert.True(display.ReadBool(Data.Models.Metadata.Display.FlipXKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.HBEndKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.HBStartKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.HeightKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.HTotalKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.PixClockKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.RefreshKey)); + Assert.Equal(90, display.ReadLong(Data.Models.Metadata.Display.RotateKey)); + Assert.Equal("tag", display.ReadString(Data.Models.Metadata.Display.TagKey)); + Assert.Equal("vector", display.ReadString(Data.Models.Metadata.Display.DisplayTypeKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.VBEndKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.VBStartKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.VTotalKey)); + Assert.Equal(12345, display.ReadLong(Data.Models.Metadata.Display.WidthKey)); + } + + private static void ValidateMetadataDriver(Data.Models.Metadata.Driver? driver) + { + Assert.NotNull(driver); + Assert.Equal("plain", driver.ReadString(Data.Models.Metadata.Driver.BlitKey)); + Assert.Equal("good", driver.ReadString(Data.Models.Metadata.Driver.CocktailKey)); + Assert.Equal("good", driver.ReadString(Data.Models.Metadata.Driver.ColorKey)); + Assert.Equal("good", driver.ReadString(Data.Models.Metadata.Driver.EmulationKey)); + Assert.True(driver.ReadBool(Data.Models.Metadata.Driver.IncompleteKey)); + Assert.True(driver.ReadBool(Data.Models.Metadata.Driver.NoSoundHardwareKey)); + Assert.Equal("pallettesize", driver.ReadString(Data.Models.Metadata.Driver.PaletteSizeKey)); + Assert.True(driver.ReadBool(Data.Models.Metadata.Driver.RequiresArtworkKey)); + Assert.Equal("supported", driver.ReadString(Data.Models.Metadata.Driver.SaveStateKey)); + Assert.Equal("good", driver.ReadString(Data.Models.Metadata.Driver.SoundKey)); + Assert.Equal("good", driver.ReadString(Data.Models.Metadata.Driver.StatusKey)); + Assert.True(driver.ReadBool(Data.Models.Metadata.Driver.UnofficialKey)); + } + + private static void ValidateMetadataExtension(Data.Models.Metadata.Extension? extension) + { + Assert.NotNull(extension); + Assert.Equal("name", extension.ReadString(Data.Models.Metadata.Extension.NameKey)); + } + + private static void ValidateMetadataFeature(Data.Models.Metadata.Feature? feature) + { + Assert.NotNull(feature); + Assert.Equal("name", feature.ReadString(Data.Models.Metadata.Feature.NameKey)); + Assert.Equal("imperfect", feature.ReadString(Data.Models.Metadata.Feature.OverallKey)); + Assert.Equal("imperfect", feature.ReadString(Data.Models.Metadata.Feature.StatusKey)); + Assert.Equal("protection", feature.ReadString(Data.Models.Metadata.Feature.FeatureTypeKey)); + Assert.Equal("value", feature.ReadString(Data.Models.Metadata.Feature.ValueKey)); + } + + private static void ValidateMetadataInfo(Data.Models.Metadata.Info? info) + { + Assert.NotNull(info); + Assert.Equal("name", info.ReadString(Data.Models.Metadata.Info.NameKey)); + Assert.Equal("value", info.ReadString(Data.Models.Metadata.Info.ValueKey)); + } + + private static void ValidateMetadataInput(Data.Models.Metadata.Input? input) + { + Assert.NotNull(input); + Assert.Equal(12345, input.ReadLong(Data.Models.Metadata.Input.ButtonsKey)); + Assert.Equal(12345, input.ReadLong(Data.Models.Metadata.Input.CoinsKey)); + Assert.Equal(12345, input.ReadLong(Data.Models.Metadata.Input.PlayersKey)); + Assert.True(input.ReadBool(Data.Models.Metadata.Input.ServiceKey)); + Assert.True(input.ReadBool(Data.Models.Metadata.Input.TiltKey)); + + Data.Models.Metadata.Control[]? controls = input.ReadItemArray(Data.Models.Metadata.Input.ControlKey); + Assert.NotNull(controls); + Data.Models.Metadata.Control? control = Assert.Single(controls); + ValidateMetadataControl(control); + } + + private static void ValidateMetadataInstance(Data.Models.Metadata.Instance? instance) + { + Assert.NotNull(instance); + Assert.Equal("briefname", instance.ReadString(Data.Models.Metadata.Instance.BriefNameKey)); + Assert.Equal("name", instance.ReadString(Data.Models.Metadata.Instance.NameKey)); + } + + private static void ValidateMetadataMedia(Data.Models.Metadata.Media? media) + { + Assert.NotNull(media); + Assert.Equal(HashType.MD5.ZeroString, media.ReadString(Data.Models.Metadata.Media.MD5Key)); + Assert.Equal("name", media.ReadString(Data.Models.Metadata.Media.NameKey)); + Assert.Equal(HashType.SHA1.ZeroString, media.ReadString(Data.Models.Metadata.Media.SHA1Key)); + Assert.Equal(HashType.SHA256.ZeroString, media.ReadString(Data.Models.Metadata.Media.SHA256Key)); + Assert.Equal(HashType.SpamSum.ZeroString, media.ReadString(Data.Models.Metadata.Media.SpamSumKey)); + } + + private static void ValidateMetadataPart(Data.Models.Metadata.Part? part) + { + Assert.NotNull(part); + Assert.Equal("interface", part.ReadString(Data.Models.Metadata.Part.InterfaceKey)); + Assert.Equal("name", part.ReadString(Data.Models.Metadata.Part.NameKey)); + + Data.Models.Metadata.DataArea[]? dataAreas = part.ReadItemArray(Data.Models.Metadata.Part.DataAreaKey); + Assert.NotNull(dataAreas); + Data.Models.Metadata.DataArea? dataArea = Assert.Single(dataAreas); + ValidateMetadataDataArea(dataArea); + + Data.Models.Metadata.DiskArea[]? diskAreas = part.ReadItemArray(Data.Models.Metadata.Part.DiskAreaKey); + Assert.NotNull(diskAreas); + Data.Models.Metadata.DiskArea? diskArea = Assert.Single(diskAreas); + ValidateMetadataDiskArea(diskArea); + + Data.Models.Metadata.DipSwitch[]? dipSwitches = part.ReadItemArray(Data.Models.Metadata.Part.DipSwitchKey); + Assert.NotNull(dipSwitches); + Data.Models.Metadata.DipSwitch? dipSwitch = Assert.Single(dipSwitches); + ValidateMetadataDipSwitch(dipSwitch); + + Data.Models.Metadata.Feature[]? features = part.ReadItemArray(Data.Models.Metadata.Part.FeatureKey); + Assert.NotNull(features); + Data.Models.Metadata.Feature? feature = Assert.Single(features); + ValidateMetadataFeature(feature); + } + + private static void ValidateMetadataPort(Data.Models.Metadata.Port? port) + { + Assert.NotNull(port); + Assert.Equal("tag", port.ReadString(Data.Models.Metadata.Port.TagKey)); + + Data.Models.Metadata.Analog[]? dipValues = port.ReadItemArray(Data.Models.Metadata.Port.AnalogKey); + Assert.NotNull(dipValues); + Data.Models.Metadata.Analog? dipValue = Assert.Single(dipValues); + ValidateMetadataAnalog(dipValue); + } + + private static void ValidateMetadataRamOption(Data.Models.Metadata.RamOption? ramOption) + { + Assert.NotNull(ramOption); + Assert.Equal("content", ramOption.ReadString(Data.Models.Metadata.RamOption.ContentKey)); + Assert.True(ramOption.ReadBool(Data.Models.Metadata.RamOption.DefaultKey)); + Assert.Equal("name", ramOption.ReadString(Data.Models.Metadata.RamOption.NameKey)); + } + + private static void ValidateMetadataRelease(Data.Models.Metadata.Release? release) + { + Assert.NotNull(release); + Assert.Equal("date", release.ReadString(Data.Models.Metadata.Release.DateKey)); + Assert.True(release.ReadBool(Data.Models.Metadata.Release.DefaultKey)); + Assert.Equal("language", release.ReadString(Data.Models.Metadata.Release.LanguageKey)); + Assert.Equal("name", release.ReadString(Data.Models.Metadata.Release.NameKey)); + Assert.Equal("region", release.ReadString(Data.Models.Metadata.Release.RegionKey)); + } + + private static void ValidateMetadataRom(Data.Models.Metadata.Rom? rom) + { + Assert.NotNull(rom); + Assert.Equal("album", rom.ReadString(Data.Models.Metadata.Rom.AlbumKey)); + Assert.Equal("alt_romname", rom.ReadString(Data.Models.Metadata.Rom.AltRomnameKey)); + Assert.Equal("alt_title", rom.ReadString(Data.Models.Metadata.Rom.AltTitleKey)); + Assert.Equal("artist", rom.ReadString(Data.Models.Metadata.Rom.ArtistKey)); + Assert.Equal("asr_detected_lang", rom.ReadString(Data.Models.Metadata.Rom.ASRDetectedLangKey)); + Assert.Equal("asr_detected_lang_conf", rom.ReadString(Data.Models.Metadata.Rom.ASRDetectedLangConfKey)); + Assert.Equal("asr_transcribed_lang", rom.ReadString(Data.Models.Metadata.Rom.ASRTranscribedLangKey)); + Assert.Equal("bios", rom.ReadString(Data.Models.Metadata.Rom.BiosKey)); + Assert.Equal("bitrate", rom.ReadString(Data.Models.Metadata.Rom.BitrateKey)); + Assert.Equal("btih", rom.ReadString(Data.Models.Metadata.Rom.BitTorrentMagnetHashKey)); + Assert.Equal("cloth_cover_detection_module_version", rom.ReadString(Data.Models.Metadata.Rom.ClothCoverDetectionModuleVersionKey)); + Assert.Equal("collection-catalog-number", rom.ReadString(Data.Models.Metadata.Rom.CollectionCatalogNumberKey)); + Assert.Equal("comment", rom.ReadString(Data.Models.Metadata.Rom.CommentKey)); + Assert.Equal(HashType.CRC32.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.CRCKey)); + Assert.Equal("creator", rom.ReadString(Data.Models.Metadata.Rom.CreatorKey)); + Assert.Equal("date", rom.ReadString(Data.Models.Metadata.Rom.DateKey)); + Assert.True(rom.ReadBool(Data.Models.Metadata.Rom.DisposeKey)); + Assert.Equal("extension", rom.ReadString(Data.Models.Metadata.Rom.ExtensionKey)); + Assert.Equal(12345, rom.ReadLong(Data.Models.Metadata.Rom.FileCountKey)); + Assert.True(rom.ReadBool(Data.Models.Metadata.Rom.FileIsAvailableKey)); + Assert.Equal("flags", rom.ReadString(Data.Models.Metadata.Rom.FlagsKey)); + Assert.Equal("format", rom.ReadString(Data.Models.Metadata.Rom.FormatKey)); + Assert.Equal("header", rom.ReadString(Data.Models.Metadata.Rom.HeaderKey)); + Assert.Equal("height", rom.ReadString(Data.Models.Metadata.Rom.HeightKey)); + Assert.Equal("hocr_char_to_word_hocr_version", rom.ReadString(Data.Models.Metadata.Rom.hOCRCharToWordhOCRVersionKey)); + Assert.Equal("hocr_char_to_word_module_version", rom.ReadString(Data.Models.Metadata.Rom.hOCRCharToWordModuleVersionKey)); + Assert.Equal("hocr_fts_text_hocr_version", rom.ReadString(Data.Models.Metadata.Rom.hOCRFtsTexthOCRVersionKey)); + Assert.Equal("hocr_fts_text_module_version", rom.ReadString(Data.Models.Metadata.Rom.hOCRFtsTextModuleVersionKey)); + Assert.Equal("hocr_pageindex_hocr_version", rom.ReadString(Data.Models.Metadata.Rom.hOCRPageIndexhOCRVersionKey)); + Assert.Equal("hocr_pageindex_module_version", rom.ReadString(Data.Models.Metadata.Rom.hOCRPageIndexModuleVersionKey)); + Assert.True(rom.ReadBool(Data.Models.Metadata.Rom.InvertedKey)); + Assert.Equal("mtime", rom.ReadString(Data.Models.Metadata.Rom.LastModifiedTimeKey)); + Assert.Equal("length", rom.ReadString(Data.Models.Metadata.Rom.LengthKey)); + Assert.Equal("load16_byte", rom.ReadString(Data.Models.Metadata.Rom.LoadFlagKey)); + Assert.Equal("matrix_number", rom.ReadString(Data.Models.Metadata.Rom.MatrixNumberKey)); + Assert.Equal(HashType.MD2.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.MD2Key)); + Assert.Equal(HashType.MD4.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.MD4Key)); + Assert.Equal(HashType.MD5.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.MD5Key)); + Assert.Null(rom.ReadString(Data.Models.Metadata.Rom.OpenMSXMediaType)); // Omit due to other test + Assert.Equal("merge", rom.ReadString(Data.Models.Metadata.Rom.MergeKey)); + Assert.True(rom.ReadBool(Data.Models.Metadata.Rom.MIAKey)); + Assert.Equal("name", rom.ReadString(Data.Models.Metadata.Rom.NameKey)); + Assert.Equal("ocr", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRKey)); + Assert.Equal("ocr_converted", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRConvertedKey)); + Assert.Equal("ocr_detected_lang", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRDetectedLangKey)); + Assert.Equal("ocr_detected_lang_conf", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRDetectedLangConfKey)); + Assert.Equal("ocr_detected_script", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRDetectedScriptKey)); + Assert.Equal("ocr_detected_script_conf", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRDetectedScriptConfKey)); + Assert.Equal("ocr_module_version", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRModuleVersionKey)); + Assert.Equal("ocr_parameters", rom.ReadString(Data.Models.Metadata.Rom.TesseractOCRParametersKey)); + Assert.Equal("offset", rom.ReadString(Data.Models.Metadata.Rom.OffsetKey)); + Assert.True(rom.ReadBool(Data.Models.Metadata.Rom.OptionalKey)); + Assert.Equal("original", rom.ReadString(Data.Models.Metadata.Rom.OriginalKey)); + Assert.Equal("pdf_module_version", rom.ReadString(Data.Models.Metadata.Rom.PDFModuleVersionKey)); + Assert.Equal("preview-image", rom.ReadString(Data.Models.Metadata.Rom.PreviewImageKey)); + Assert.Equal("publisher", rom.ReadString(Data.Models.Metadata.Rom.PublisherKey)); + Assert.Equal("region", rom.ReadString(Data.Models.Metadata.Rom.RegionKey)); + Assert.Equal("remark", rom.ReadString(Data.Models.Metadata.Rom.RemarkKey)); + Assert.Equal(HashType.RIPEMD128.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Equal(HashType.RIPEMD160.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Equal("rotation", rom.ReadString(Data.Models.Metadata.Rom.RotationKey)); + Assert.Equal("serial", rom.ReadString(Data.Models.Metadata.Rom.SerialKey)); + Assert.Equal(HashType.SHA1.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal(HashType.SHA256.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Equal(HashType.SHA384.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Equal(HashType.SHA512.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Equal(12345, rom.ReadLong(Data.Models.Metadata.Rom.SizeKey)); + Assert.True(rom.ReadBool(Data.Models.Metadata.Rom.SoundOnlyKey)); + Assert.Equal("source", rom.ReadString(Data.Models.Metadata.Rom.SourceKey)); + Assert.Equal(HashType.SpamSum.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.SpamSumKey)); + Assert.Equal("start", rom.ReadString(Data.Models.Metadata.Rom.StartKey)); + Assert.Equal("good", rom.ReadString(Data.Models.Metadata.Rom.StatusKey)); + Assert.Equal("summation", rom.ReadString(Data.Models.Metadata.Rom.SummationKey)); + Assert.Equal("title", rom.ReadString(Data.Models.Metadata.Rom.TitleKey)); + Assert.Equal("track", rom.ReadString(Data.Models.Metadata.Rom.TrackKey)); + Assert.Equal("type", rom.ReadString(Data.Models.Metadata.Rom.OpenMSXType)); + Assert.Equal("value", rom.ReadString(Data.Models.Metadata.Rom.ValueKey)); + Assert.Equal("whisper_asr_module_version", rom.ReadString(Data.Models.Metadata.Rom.WhisperASRModuleVersionKey)); + Assert.Equal("whisper_model_hash", rom.ReadString(Data.Models.Metadata.Rom.WhisperModelHashKey)); + Assert.Equal("whisper_model_name", rom.ReadString(Data.Models.Metadata.Rom.WhisperModelNameKey)); + Assert.Equal("whisper_version", rom.ReadString(Data.Models.Metadata.Rom.WhisperVersionKey)); + Assert.Equal("width", rom.ReadString(Data.Models.Metadata.Rom.WidthKey)); + Assert.Equal("word_conf_0_10", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval0To10Key)); + Assert.Equal("word_conf_11_20", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval11To20Key)); + Assert.Equal("word_conf_21_30", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval21To30Key)); + Assert.Equal("word_conf_31_40", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval31To40Key)); + Assert.Equal("word_conf_41_50", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval41To50Key)); + Assert.Equal("word_conf_51_60", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval51To60Key)); + Assert.Equal("word_conf_61_70", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval61To70Key)); + Assert.Equal("word_conf_71_80", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval71To80Key)); + Assert.Equal("word_conf_81_90", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval81To90Key)); + Assert.Equal("word_conf_91_100", rom.ReadString(Data.Models.Metadata.Rom.WordConfidenceInterval91To100Key)); + Assert.Equal(HashType.XxHash3.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.xxHash364Key)); + Assert.Equal(HashType.XxHash128.ZeroString, rom.ReadString(Data.Models.Metadata.Rom.xxHash3128Key)); + } + + private static void ValidateMetadataSample(Data.Models.Metadata.Sample? sample) + { + Assert.NotNull(sample); + Assert.Equal("name", sample.ReadString(Data.Models.Metadata.Sample.NameKey)); + } + + private static void ValidateMetadataSharedFeat(Data.Models.Metadata.SharedFeat? sharedFeat) + { + Assert.NotNull(sharedFeat); + Assert.Equal("name", sharedFeat.ReadString(Data.Models.Metadata.SharedFeat.NameKey)); + Assert.Equal("value", sharedFeat.ReadString(Data.Models.Metadata.SharedFeat.ValueKey)); + } + + private static void ValidateMetadataSlot(Data.Models.Metadata.Slot? slot) + { + Assert.NotNull(slot); + Assert.Equal("name", slot.ReadString(Data.Models.Metadata.Slot.NameKey)); + + Data.Models.Metadata.SlotOption[]? slotOptions = slot.ReadItemArray(Data.Models.Metadata.Slot.SlotOptionKey); + Assert.NotNull(slotOptions); + Data.Models.Metadata.SlotOption? slotOption = Assert.Single(slotOptions); + ValidateMetadataSlotOption(slotOption); + } + + private static void ValidateMetadataSlotOption(Data.Models.Metadata.SlotOption? slotOption) + { + Assert.NotNull(slotOption); + Assert.True(slotOption.ReadBool(Data.Models.Metadata.SlotOption.DefaultKey)); + Assert.Equal("devname", slotOption.ReadString(Data.Models.Metadata.SlotOption.DevNameKey)); + Assert.Equal("name", slotOption.ReadString(Data.Models.Metadata.SlotOption.NameKey)); + } + + private static void ValidateMetadataSoftwareList(Data.Models.Metadata.SoftwareList? softwareList) + { + Assert.NotNull(softwareList); + Assert.Equal("description", softwareList.ReadString(Data.Models.Metadata.SoftwareList.DescriptionKey)); + Assert.Equal("filter", softwareList.ReadString(Data.Models.Metadata.SoftwareList.FilterKey)); + Assert.Equal("name", softwareList.ReadString(Data.Models.Metadata.SoftwareList.NameKey)); + Assert.Equal("notes", softwareList.ReadString(Data.Models.Metadata.SoftwareList.NotesKey)); + Assert.Equal("original", softwareList.ReadString(Data.Models.Metadata.SoftwareList.StatusKey)); + Assert.Equal("tag", softwareList.ReadString(Data.Models.Metadata.SoftwareList.TagKey)); + + // TODO: Figure out why Data.Models.Metadata.SoftwareList.SoftwareKey doesn't get processed + } + + private static void ValidateMetadataSound(Data.Models.Metadata.Sound? sound) + { + Assert.NotNull(sound); + Assert.Equal(12345, sound.ReadLong(Data.Models.Metadata.Sound.ChannelsKey)); + } + + private static void ValidateMetadataTrurip(Data.Models.Logiqx.Trurip? trurip) + { + Assert.NotNull(trurip); + Assert.Equal("titleid", trurip.TitleID); + Assert.Equal("publisher", trurip.Publisher); + Assert.Equal("developer", trurip.Developer); + Assert.Equal("year", trurip.Year); + Assert.Equal("genre", trurip.Genre); + Assert.Equal("subgenre", trurip.Subgenre); + Assert.Equal("ratings", trurip.Ratings); + Assert.Equal("score", trurip.Score); + Assert.Equal("players", trurip.Players); + Assert.Equal("enabled", trurip.Enabled); + Assert.Equal("yes", trurip.CRC); + Assert.Equal("sourcefile", trurip.Source); + Assert.Equal("cloneof", trurip.CloneOf); + Assert.Equal("relatedto", trurip.RelatedTo); + } + + private static void ValidateMetadataVideo(Data.Models.Metadata.Video? video) + { + Assert.NotNull(video); + Assert.Equal(12345, video.ReadLong(Data.Models.Metadata.Video.AspectXKey)); + Assert.Equal(12345, video.ReadLong(Data.Models.Metadata.Video.AspectYKey)); + Assert.Equal(12345, video.ReadLong(Data.Models.Metadata.Video.HeightKey)); + Assert.Equal("vertical", video.ReadString(Data.Models.Metadata.Video.OrientationKey)); + Assert.Equal(12345, video.ReadLong(Data.Models.Metadata.Video.RefreshKey)); + Assert.Equal("vector", video.ReadString(Data.Models.Metadata.Video.ScreenKey)); + Assert.Equal(12345, video.ReadLong(Data.Models.Metadata.Video.WidthKey)); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatFileTests.cs b/SabreTools.Metadata.DatFiles.Test/DatFileTests.cs new file mode 100644 index 00000000..7f0abe4a --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatFileTests.cs @@ -0,0 +1,2343 @@ +using System.Collections.Generic; +using System.IO; +using SabreTools.Hashing; +using SabreTools.Metadata.DatFiles.Formats; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public partial class DatFileTests + { + #region Constructor + + [Fact] + public void Constructor_Null() + { + DatFile? datFile = null; + DatFile created = new Logiqx(datFile, useGame: false); + + Assert.NotNull(created.Header); + Assert.NotNull(created.Items); + Assert.Equal(0, created.Items.DatStatistics.TotalCount); + Assert.NotNull(created.ItemsDB); + Assert.Equal(0, created.ItemsDB.DatStatistics.TotalCount); + } + + [Fact] + public void Constructor_NonNull() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("key"); + + DatItem rom = new Rom(); + rom.SetName("rom"); + rom.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + rom.SetFieldValue(DatItem.SourceKey, source); + rom.SetFieldValue(DatItem.MachineKey, machine); + + DatFile? datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + datFile.AddItem(rom, statsOnly: false); + + long sourceIndex = datFile.AddSourceDB(source); + long machineIndex = datFile.AddMachineDB(machine); + datFile.AddItemDB(rom, machineIndex, sourceIndex, statsOnly: false); + + DatFile created = new Logiqx(datFile, useGame: false); + created.BucketBy(ItemKey.Machine); + + Assert.NotNull(created.Header); + Assert.Equal("name", created.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + + Assert.NotNull(created.Items); + DatItem datItem = Assert.Single(created.GetItemsForBucket("key")); + Assert.True(datItem is Rom); + + Assert.NotNull(created.ItemsDB); + KeyValuePair dbKvp = Assert.Single(created.GetItemsForBucketDB("key")); + Assert.Equal(0, dbKvp.Key); + Assert.True(dbKvp.Value is Rom); + } + + #endregion + + #region ClearEmpty + + [Fact] + public void ClearEmpty_Items() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem datItem = new Rom(); + datItem.SetFieldValue(DatItem.SourceKey, source); + datItem.SetFieldValue(DatItem.MachineKey, machine); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.AddItem(datItem, statsOnly: false); + + datFile.ClearEmpty(); + Assert.Single(datFile.Items.SortedKeys); + } + + [Fact] + public void ClearEmpty_ItemsDB() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem datItem = new Rom(); + + DatFile datFile = new Logiqx(datFile: null, useGame: false); + long sourceIndex = datFile.AddSourceDB(source); + long machineIndex = datFile.AddMachineDB(machine); + _ = datFile.AddItemDB(datItem, machineIndex, sourceIndex, statsOnly: false); + + datFile.ClearEmpty(); + Assert.Single(datFile.ItemsDB.SortedKeys); + } + + #endregion + + #region FillHeaderFromPath + + [Fact] + public void FillHeaderFromPath_NoNameNoDesc_NotBare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, false); + + Assert.Equal("Filename (1980-01-01)", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Filename (1980-01-01)", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NoNameNoDesc_Bare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, true); + + Assert.Equal("Filename", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Filename", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NoNameDesc_NotBare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, "Description"); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, false); + + Assert.Equal("Description (1980-01-01)", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Description", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NoNameDesc_Bare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, "Description"); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, true); + + Assert.Equal("Description", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Description", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NameNoDesc_NotBare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "Name"); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, false); + + Assert.Equal("Name", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Name (1980-01-01)", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NameNoDesc_Bare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "Name"); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, string.Empty); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, true); + + Assert.Equal("Name", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Name", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NameDesc_NotBare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "Name"); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, "Description"); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, false); + + Assert.Equal("Name", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Description", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + [Fact] + public void FillHeaderFromPath_NameDesc_Bare() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "Name "); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, "Description "); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, "1980-01-01"); + + string path = Path.Combine("Fake", "Path", "Filename"); + datFile.FillHeaderFromPath(path, true); + + Assert.Equal("Name", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + Assert.Equal("Description", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + } + + #endregion + + #region SetHeader + + [Fact] + public void SetHeaderTest() + { + DatHeader datHeader = new DatHeader(); + datHeader.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + + DatFile? datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "notname"); + + datFile.SetHeader(datHeader); + Assert.NotNull(datFile.Header); + Assert.Equal("name", datFile.Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + } + + #endregion + + #region SetModifiers + + [Fact] + public void SetModifiersTest() + { + DatModifiers datModifiers = new DatModifiers(); + datModifiers.AddExtension = ".new"; + + DatFile? datFile = new Logiqx(datFile: null, useGame: false); + datFile.Modifiers.AddExtension = ".old"; + + datFile.SetModifiers(datModifiers); + Assert.NotNull(datFile.Modifiers); + Assert.Equal(".new", datFile.Modifiers.AddExtension); + } + + #endregion + + #region ResetDictionary + + [Fact] + public void ResetDictionaryTest() + { + DatFile datFile = new Logiqx(datFile: null, useGame: false); + datFile.Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + datFile.AddItem(new Rom(), statsOnly: false); + datFile.AddItemDB(new Rom(), 0, 0, false); + + datFile.ResetDictionary(); + + Assert.NotNull(datFile.Header); + Assert.NotNull(datFile.Items); + Assert.Equal(0, datFile.Items.DatStatistics.TotalCount); + Assert.NotNull(datFile.ItemsDB); + Assert.Equal(0, datFile.ItemsDB.DatStatistics.TotalCount); + } + + #endregion + + #region ProcessItemName + + [Theory] + [InlineData(false, false, false, false, null, false, null, false, null, false, "name")] + [InlineData(false, false, false, false, null, false, null, false, null, true, "name")] + [InlineData(false, false, false, false, null, false, null, false, "add", false, "name")] + [InlineData(false, false, false, false, null, false, null, false, "add", true, "name")] + [InlineData(false, false, false, false, null, false, null, true, null, false, "name")] + [InlineData(false, false, false, false, null, false, null, true, null, true, "name")] + [InlineData(false, false, false, false, null, false, null, true, "add", false, "name")] + [InlineData(false, false, false, false, null, false, null, true, "add", true, "name")] + [InlineData(false, false, false, false, null, false, "rep", false, null, false, "name")] + [InlineData(false, false, false, false, null, false, "rep", false, null, true, "name")] + [InlineData(false, false, false, false, null, false, "rep", false, "add", false, "name")] + [InlineData(false, false, false, false, null, false, "rep", false, "add", true, "name")] + [InlineData(false, false, false, false, null, false, "rep", true, null, false, "name")] + [InlineData(false, false, false, false, null, false, "rep", true, null, true, "name")] + [InlineData(false, false, false, false, null, false, "rep", true, "add", false, "name")] + [InlineData(false, false, false, false, null, false, "rep", true, "add", true, "name")] + [InlineData(false, false, false, false, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, false, null, false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, false, null, true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, false, "add", false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, false, "add", true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, true, null, false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, true, null, true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, true, "add", false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, null, true, "add", true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", false, null, false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", false, null, true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", false, "add", false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", false, "add", true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", true, null, false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", true, null, true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", true, "add", false, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", false, "rep", true, "add", true, "name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, null, false, null, false, null, false, "name")] + [InlineData(false, false, false, true, null, false, null, false, null, true, "machine/name")] + [InlineData(false, false, false, true, null, false, null, false, "add", false, "nameadd")] + [InlineData(false, false, false, true, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(false, false, false, true, null, false, null, true, null, false, "name")] + [InlineData(false, false, false, true, null, false, null, true, null, true, "machine/name")] + [InlineData(false, false, false, true, null, false, null, true, "add", false, "nameadd")] + [InlineData(false, false, false, true, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(false, false, false, true, null, false, "rep", false, null, false, "namerep")] + [InlineData(false, false, false, true, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(false, false, false, true, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(false, false, false, true, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(false, false, false, true, null, false, "rep", true, null, false, "name")] + [InlineData(false, false, false, true, null, false, "rep", true, null, true, "machine/name")] + [InlineData(false, false, false, true, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(false, false, false, true, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(false, false, false, true, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, false, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, null, false, null, false, null, false, "name")] + [InlineData(false, false, true, false, null, false, null, false, null, true, "name")] + [InlineData(false, false, true, false, null, false, null, false, "add", false, "name")] + [InlineData(false, false, true, false, null, false, null, false, "add", true, "name")] + [InlineData(false, false, true, false, null, false, null, true, null, false, "name")] + [InlineData(false, false, true, false, null, false, null, true, null, true, "name")] + [InlineData(false, false, true, false, null, false, null, true, "add", false, "name")] + [InlineData(false, false, true, false, null, false, null, true, "add", true, "name")] + [InlineData(false, false, true, false, null, false, "rep", false, null, false, "name")] + [InlineData(false, false, true, false, null, false, "rep", false, null, true, "name")] + [InlineData(false, false, true, false, null, false, "rep", false, "add", false, "name")] + [InlineData(false, false, true, false, null, false, "rep", false, "add", true, "name")] + [InlineData(false, false, true, false, null, false, "rep", true, null, false, "name")] + [InlineData(false, false, true, false, null, false, "rep", true, null, true, "name")] + [InlineData(false, false, true, false, null, false, "rep", true, "add", false, "name")] + [InlineData(false, false, true, false, null, false, "rep", true, "add", true, "name")] + [InlineData(false, false, true, false, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, false, null, false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, false, null, true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, false, "add", false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, false, "add", true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, true, null, false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, true, null, true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, true, "add", false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, null, true, "add", true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", false, null, false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", false, null, true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", false, "add", false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", false, "add", true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", true, null, false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", true, null, true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", true, "add", false, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", false, "rep", true, "add", true, "name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, null, false, null, false, null, false, "name")] + [InlineData(false, false, true, true, null, false, null, false, null, true, "machine/name")] + [InlineData(false, false, true, true, null, false, null, false, "add", false, "nameadd")] + [InlineData(false, false, true, true, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(false, false, true, true, null, false, null, true, null, false, "name")] + [InlineData(false, false, true, true, null, false, null, true, null, true, "machine/name")] + [InlineData(false, false, true, true, null, false, null, true, "add", false, "nameadd")] + [InlineData(false, false, true, true, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(false, false, true, true, null, false, "rep", false, null, false, "namerep")] + [InlineData(false, false, true, true, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(false, false, true, true, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(false, false, true, true, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(false, false, true, true, null, false, "rep", true, null, false, "name")] + [InlineData(false, false, true, true, null, false, "rep", true, null, true, "machine/name")] + [InlineData(false, false, true, true, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(false, false, true, true, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(false, false, true, true, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, false, true, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, null, false, null, false, null, false, "name")] + [InlineData(false, true, false, false, null, false, null, false, null, true, "machine/name")] + [InlineData(false, true, false, false, null, false, null, false, "add", false, "nameadd")] + [InlineData(false, true, false, false, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(false, true, false, false, null, false, null, true, null, false, "name")] + [InlineData(false, true, false, false, null, false, null, true, null, true, "machine/name")] + [InlineData(false, true, false, false, null, false, null, true, "add", false, "nameadd")] + [InlineData(false, true, false, false, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(false, true, false, false, null, false, "rep", false, null, false, "namerep")] + [InlineData(false, true, false, false, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(false, true, false, false, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(false, true, false, false, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(false, true, false, false, null, false, "rep", true, null, false, "name")] + [InlineData(false, true, false, false, null, false, "rep", true, null, true, "machine/name")] + [InlineData(false, true, false, false, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(false, true, false, false, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(false, true, false, false, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, null, false, null, false, null, false, "name")] + [InlineData(false, true, false, true, null, false, null, false, null, true, "machine/name")] + [InlineData(false, true, false, true, null, false, null, false, "add", false, "nameadd")] + [InlineData(false, true, false, true, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(false, true, false, true, null, false, null, true, null, false, "name")] + [InlineData(false, true, false, true, null, false, null, true, null, true, "machine/name")] + [InlineData(false, true, false, true, null, false, null, true, "add", false, "nameadd")] + [InlineData(false, true, false, true, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(false, true, false, true, null, false, "rep", false, null, false, "namerep")] + [InlineData(false, true, false, true, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(false, true, false, true, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(false, true, false, true, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(false, true, false, true, null, false, "rep", true, null, false, "name")] + [InlineData(false, true, false, true, null, false, "rep", true, null, true, "machine/name")] + [InlineData(false, true, false, true, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(false, true, false, true, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(false, true, false, true, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, false, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, null, false, null, false, null, false, "name")] + [InlineData(false, true, true, false, null, false, null, false, null, true, "machine/name")] + [InlineData(false, true, true, false, null, false, null, false, "add", false, "nameadd")] + [InlineData(false, true, true, false, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(false, true, true, false, null, false, null, true, null, false, "name")] + [InlineData(false, true, true, false, null, false, null, true, null, true, "machine/name")] + [InlineData(false, true, true, false, null, false, null, true, "add", false, "nameadd")] + [InlineData(false, true, true, false, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(false, true, true, false, null, false, "rep", false, null, false, "namerep")] + [InlineData(false, true, true, false, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(false, true, true, false, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(false, true, true, false, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(false, true, true, false, null, false, "rep", true, null, false, "name")] + [InlineData(false, true, true, false, null, false, "rep", true, null, true, "machine/name")] + [InlineData(false, true, true, false, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(false, true, true, false, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(false, true, true, false, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, null, false, null, false, null, false, "name")] + [InlineData(false, true, true, true, null, false, null, false, null, true, "machine/name")] + [InlineData(false, true, true, true, null, false, null, false, "add", false, "nameadd")] + [InlineData(false, true, true, true, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(false, true, true, true, null, false, null, true, null, false, "name")] + [InlineData(false, true, true, true, null, false, null, true, null, true, "machine/name")] + [InlineData(false, true, true, true, null, false, null, true, "add", false, "nameadd")] + [InlineData(false, true, true, true, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(false, true, true, true, null, false, "rep", false, null, false, "namerep")] + [InlineData(false, true, true, true, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(false, true, true, true, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(false, true, true, true, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(false, true, true, true, null, false, "rep", true, null, false, "name")] + [InlineData(false, true, true, true, null, false, "rep", true, null, true, "machine/name")] + [InlineData(false, true, true, true, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(false, true, true, true, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(false, true, true, true, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(false, true, true, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, false, false, null, false, null, false, null, false, "name")] + [InlineData(true, false, false, false, null, false, null, false, null, true, "name")] + [InlineData(true, false, false, false, null, false, null, false, "add", false, "name")] + [InlineData(true, false, false, false, null, false, null, false, "add", true, "name")] + [InlineData(true, false, false, false, null, false, null, true, null, false, "name")] + [InlineData(true, false, false, false, null, false, null, true, null, true, "name")] + [InlineData(true, false, false, false, null, false, null, true, "add", false, "name")] + [InlineData(true, false, false, false, null, false, null, true, "add", true, "name")] + [InlineData(true, false, false, false, null, false, "rep", false, null, false, "name")] + [InlineData(true, false, false, false, null, false, "rep", false, null, true, "name")] + [InlineData(true, false, false, false, null, false, "rep", false, "add", false, "name")] + [InlineData(true, false, false, false, null, false, "rep", false, "add", true, "name")] + [InlineData(true, false, false, false, null, false, "rep", true, null, false, "name")] + [InlineData(true, false, false, false, null, false, "rep", true, null, true, "name")] + [InlineData(true, false, false, false, null, false, "rep", true, "add", false, "name")] + [InlineData(true, false, false, false, null, false, "rep", true, "add", true, "name")] + [InlineData(true, false, false, false, null, true, null, false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, null, true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, null, true, "rep", true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, false, null, false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, false, null, true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, false, "add", false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, false, "add", true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, true, null, false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, true, null, true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, true, "add", false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, null, true, "add", true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", false, null, false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", false, null, true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", false, "add", false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", false, "add", true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", true, null, false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", true, null, true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", true, "add", false, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", false, "rep", true, "add", true, "name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, null, true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, null, false, null, false, null, false, "\"name\"")] + [InlineData(true, false, false, true, null, false, null, false, null, true, "\"machine/name\"")] + [InlineData(true, false, false, true, null, false, null, false, "add", false, "\"nameadd\"")] + [InlineData(true, false, false, true, null, false, null, false, "add", true, "\"machine/nameadd\"")] + [InlineData(true, false, false, true, null, false, null, true, null, false, "\"name\"")] + [InlineData(true, false, false, true, null, false, null, true, null, true, "\"machine/name\"")] + [InlineData(true, false, false, true, null, false, null, true, "add", false, "\"nameadd\"")] + [InlineData(true, false, false, true, null, false, null, true, "add", true, "\"machine/nameadd\"")] + [InlineData(true, false, false, true, null, false, "rep", false, null, false, "\"namerep\"")] + [InlineData(true, false, false, true, null, false, "rep", false, null, true, "\"machine/namerep\"")] + [InlineData(true, false, false, true, null, false, "rep", false, "add", false, "\"namerepadd\"")] + [InlineData(true, false, false, true, null, false, "rep", false, "add", true, "\"machine/namerepadd\"")] + [InlineData(true, false, false, true, null, false, "rep", true, null, false, "\"name\"")] + [InlineData(true, false, false, true, null, false, "rep", true, null, true, "\"machine/name\"")] + [InlineData(true, false, false, true, null, false, "rep", true, "add", false, "\"nameadd\"")] + [InlineData(true, false, false, true, null, false, "rep", true, "add", true, "\"machine/nameadd\"")] + [InlineData(true, false, false, true, null, true, null, false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, null, true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, null, true, "rep", true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, false, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, false, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, false, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, false, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, true, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, true, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, true, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, null, true, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", false, null, false, "machine_name\"namerep\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", false, null, true, "machine_name\"machine/namerep\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_name\"namerepadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_name\"machine/namerepadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", true, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", true, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, null, true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, false, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, false, true, false, null, false, null, false, null, false, "name")] + [InlineData(true, false, true, false, null, false, null, false, null, true, "name")] + [InlineData(true, false, true, false, null, false, null, false, "add", false, "name")] + [InlineData(true, false, true, false, null, false, null, false, "add", true, "name")] + [InlineData(true, false, true, false, null, false, null, true, null, false, "name")] + [InlineData(true, false, true, false, null, false, null, true, null, true, "name")] + [InlineData(true, false, true, false, null, false, null, true, "add", false, "name")] + [InlineData(true, false, true, false, null, false, null, true, "add", true, "name")] + [InlineData(true, false, true, false, null, false, "rep", false, null, false, "name")] + [InlineData(true, false, true, false, null, false, "rep", false, null, true, "name")] + [InlineData(true, false, true, false, null, false, "rep", false, "add", false, "name")] + [InlineData(true, false, true, false, null, false, "rep", false, "add", true, "name")] + [InlineData(true, false, true, false, null, false, "rep", true, null, false, "name")] + [InlineData(true, false, true, false, null, false, "rep", true, null, true, "name")] + [InlineData(true, false, true, false, null, false, "rep", true, "add", false, "name")] + [InlineData(true, false, true, false, null, false, "rep", true, "add", true, "name")] + [InlineData(true, false, true, false, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, false, null, false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, false, null, true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, false, "add", false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, false, "add", true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, true, null, false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, true, null, true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, true, "add", false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, null, true, "add", true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", false, null, false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", false, null, true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", false, "add", false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", false, "add", true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", true, null, false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", true, null, true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", true, "add", false, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", false, "rep", true, "add", true, "name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, null, false, null, false, null, false, "name")] + [InlineData(true, false, true, true, null, false, null, false, null, true, "machine/name")] + [InlineData(true, false, true, true, null, false, null, false, "add", false, "nameadd")] + [InlineData(true, false, true, true, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(true, false, true, true, null, false, null, true, null, false, "name")] + [InlineData(true, false, true, true, null, false, null, true, null, true, "machine/name")] + [InlineData(true, false, true, true, null, false, null, true, "add", false, "nameadd")] + [InlineData(true, false, true, true, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(true, false, true, true, null, false, "rep", false, null, false, "namerep")] + [InlineData(true, false, true, true, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(true, false, true, true, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(true, false, true, true, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(true, false, true, true, null, false, "rep", true, null, false, "name")] + [InlineData(true, false, true, true, null, false, "rep", true, null, true, "machine/name")] + [InlineData(true, false, true, true, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(true, false, true, true, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(true, false, true, true, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, false, true, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, false, false, null, false, null, false, null, false, "\"name\"")] + [InlineData(true, true, false, false, null, false, null, false, null, true, "\"machine/name\"")] + [InlineData(true, true, false, false, null, false, null, false, "add", false, "\"nameadd\"")] + [InlineData(true, true, false, false, null, false, null, false, "add", true, "\"machine/nameadd\"")] + [InlineData(true, true, false, false, null, false, null, true, null, false, "\"name\"")] + [InlineData(true, true, false, false, null, false, null, true, null, true, "\"machine/name\"")] + [InlineData(true, true, false, false, null, false, null, true, "add", false, "\"nameadd\"")] + [InlineData(true, true, false, false, null, false, null, true, "add", true, "\"machine/nameadd\"")] + [InlineData(true, true, false, false, null, false, "rep", false, null, false, "\"namerep\"")] + [InlineData(true, true, false, false, null, false, "rep", false, null, true, "\"machine/namerep\"")] + [InlineData(true, true, false, false, null, false, "rep", false, "add", false, "\"namerepadd\"")] + [InlineData(true, true, false, false, null, false, "rep", false, "add", true, "\"machine/namerepadd\"")] + [InlineData(true, true, false, false, null, false, "rep", true, null, false, "\"name\"")] + [InlineData(true, true, false, false, null, false, "rep", true, null, true, "\"machine/name\"")] + [InlineData(true, true, false, false, null, false, "rep", true, "add", false, "\"nameadd\"")] + [InlineData(true, true, false, false, null, false, "rep", true, "add", true, "\"machine/nameadd\"")] + [InlineData(true, true, false, false, null, true, null, false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, null, true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, null, true, "rep", true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, false, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, false, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, false, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, false, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, true, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, true, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, true, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, null, true, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", false, null, false, "machine_name\"namerep\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", false, null, true, "machine_name\"machine/namerep\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", false, "add", false, "machine_name\"namerepadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", false, "add", true, "machine_name\"machine/namerepadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", true, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", true, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", true, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", false, "rep", true, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, null, true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, null, false, null, false, null, false, "\"name\"")] + [InlineData(true, true, false, true, null, false, null, false, null, true, "\"machine/name\"")] + [InlineData(true, true, false, true, null, false, null, false, "add", false, "\"nameadd\"")] + [InlineData(true, true, false, true, null, false, null, false, "add", true, "\"machine/nameadd\"")] + [InlineData(true, true, false, true, null, false, null, true, null, false, "\"name\"")] + [InlineData(true, true, false, true, null, false, null, true, null, true, "\"machine/name\"")] + [InlineData(true, true, false, true, null, false, null, true, "add", false, "\"nameadd\"")] + [InlineData(true, true, false, true, null, false, null, true, "add", true, "\"machine/nameadd\"")] + [InlineData(true, true, false, true, null, false, "rep", false, null, false, "\"namerep\"")] + [InlineData(true, true, false, true, null, false, "rep", false, null, true, "\"machine/namerep\"")] + [InlineData(true, true, false, true, null, false, "rep", false, "add", false, "\"namerepadd\"")] + [InlineData(true, true, false, true, null, false, "rep", false, "add", true, "\"machine/namerepadd\"")] + [InlineData(true, true, false, true, null, false, "rep", true, null, false, "\"name\"")] + [InlineData(true, true, false, true, null, false, "rep", true, null, true, "\"machine/name\"")] + [InlineData(true, true, false, true, null, false, "rep", true, "add", false, "\"nameadd\"")] + [InlineData(true, true, false, true, null, false, "rep", true, "add", true, "\"machine/nameadd\"")] + [InlineData(true, true, false, true, null, true, null, false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, null, true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", false, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", false, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", false, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", false, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", true, null, false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", true, null, true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", true, "add", false, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, null, true, "rep", true, "add", true, "\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, false, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, false, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, false, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, false, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, true, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, true, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, true, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, null, true, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", false, null, false, "machine_name\"namerep\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", false, null, true, "machine_name\"machine/namerep\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_name\"namerepadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_name\"machine/namerepadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", true, null, false, "machine_name\"name\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", true, null, true, "machine_name\"machine/name\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_name\"nameadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_name\"machine/nameadd\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, null, true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", false, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", false, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", true, null, false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", true, null, true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, false, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_name\"da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz\"machine_name")] + [InlineData(true, true, true, false, null, false, null, false, null, false, "name")] + [InlineData(true, true, true, false, null, false, null, false, null, true, "machine/name")] + [InlineData(true, true, true, false, null, false, null, false, "add", false, "nameadd")] + [InlineData(true, true, true, false, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(true, true, true, false, null, false, null, true, null, false, "name")] + [InlineData(true, true, true, false, null, false, null, true, null, true, "machine/name")] + [InlineData(true, true, true, false, null, false, null, true, "add", false, "nameadd")] + [InlineData(true, true, true, false, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(true, true, true, false, null, false, "rep", false, null, false, "namerep")] + [InlineData(true, true, true, false, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(true, true, true, false, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(true, true, true, false, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(true, true, true, false, null, false, "rep", true, null, false, "name")] + [InlineData(true, true, true, false, null, false, "rep", true, null, true, "machine/name")] + [InlineData(true, true, true, false, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(true, true, true, false, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(true, true, true, false, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, false, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, null, false, null, false, null, false, "name")] + [InlineData(true, true, true, true, null, false, null, false, null, true, "machine/name")] + [InlineData(true, true, true, true, null, false, null, false, "add", false, "nameadd")] + [InlineData(true, true, true, true, null, false, null, false, "add", true, "machine/nameadd")] + [InlineData(true, true, true, true, null, false, null, true, null, false, "name")] + [InlineData(true, true, true, true, null, false, null, true, null, true, "machine/name")] + [InlineData(true, true, true, true, null, false, null, true, "add", false, "nameadd")] + [InlineData(true, true, true, true, null, false, null, true, "add", true, "machine/nameadd")] + [InlineData(true, true, true, true, null, false, "rep", false, null, false, "namerep")] + [InlineData(true, true, true, true, null, false, "rep", false, null, true, "machine/namerep")] + [InlineData(true, true, true, true, null, false, "rep", false, "add", false, "namerepadd")] + [InlineData(true, true, true, true, null, false, "rep", false, "add", true, "machine/namerepadd")] + [InlineData(true, true, true, true, null, false, "rep", true, null, false, "name")] + [InlineData(true, true, true, true, null, false, "rep", true, null, true, "machine/name")] + [InlineData(true, true, true, true, null, false, "rep", true, "add", false, "nameadd")] + [InlineData(true, true, true, true, null, false, "rep", true, "add", true, "machine/nameadd")] + [InlineData(true, true, true, true, null, true, null, false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, null, true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", false, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", false, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", false, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", false, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", true, null, false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", true, null, true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", true, "add", false, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, null, true, "rep", true, "add", true, "da/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, false, null, false, "machine_namenamemachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, false, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, false, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, false, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, true, null, false, "machine_namenamemachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, null, true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", false, null, false, "machine_namenamerepmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", false, null, true, "machine_namemachine/namerepmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", false, "add", false, "machine_namenamerepaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", false, "add", true, "machine_namemachine/namerepaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", true, null, false, "machine_namenamemachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", true, null, true, "machine_namemachine/namemachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", true, "add", false, "machine_namenameaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", false, "rep", true, "add", true, "machine_namemachine/nameaddmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, null, true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", false, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", false, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", false, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", false, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", true, null, false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", true, null, true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", true, "add", false, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + [InlineData(true, true, true, true, "%machine%_%name%", true, "rep", true, "add", true, "machine_nameda/39/a3/ee/da39a3ee5e6b4b0d3255bfef95601890afd80709.gzmachine_name")] + public void ProcessItemNameTest( + bool quotes, + bool useRomName, + bool forceRemoveQuotes, + bool forceRomName, + string? fix, + bool depot, + string? replaceExtension, + bool removeExtension, + string? addExtension, + bool gameName, + string expected) + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + DatItem item = new Rom(); + item.SetFieldValue(Data.Models.Metadata.Rom.NameKey, "name"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString); + + DatFile? datFile = new Logiqx(datFile: null, useGame: false); + datFile.Modifiers.Prefix = fix; + datFile.Modifiers.Postfix = fix; + datFile.Modifiers.AddExtension = addExtension; + datFile.Modifiers.RemoveExtension = removeExtension; + datFile.Modifiers.ReplaceExtension = replaceExtension; + datFile.Modifiers.GameName = gameName; + datFile.Modifiers.Quotes = quotes; + datFile.Modifiers.UseRomName = useRomName; + if (depot) + datFile.Modifiers.OutputDepot = new DepotInformation(isActive: true, depth: 4); + + datFile.ProcessItemName(item, machine, forceRemoveQuotes, forceRomName); + string? actual = item.GetName()?.Replace('\\', '/'); + Assert.Equal(expected, actual); + } + + #endregion + + #region FormatPrefixPostfix + + [Fact] + public void FormatPrefixPostfix_EmptyFix() + { + string fix = string.Empty; + string expected = string.Empty; + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.ManufacturerKey, "manufacturer"); + machine.SetFieldValue(Data.Models.Metadata.Machine.PublisherKey, "publisher"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CategoryKey, "category"); + + DatItem item = new Rom(); + item.SetFieldValue(Data.Models.Metadata.Rom.NameKey, "name"); + item.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + item.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "md2"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "md4"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "md5"); + item.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "ripemd128"); + item.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "ripemd160"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "sha1"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "sha256"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "sha384"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "sha512"); + item.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "spamsum"); + + string actual = DatFile.FormatPrefixPostfix(item, machine, fix); + Assert.Equal(expected, actual); + } + + [Fact] + public void FormatPrefixPostfix_Disk() + { + string fix = "%game%_%machine%_%name%_%manufacturer%_%publisher%_%category%_%crc%_%md2%_%md4%_%md5%_%sha1%_%sha256%_%sha384%_%sha512%_%size%_%spamsum%"; + string expected = "machine_machine_name_manufacturer_publisher_category____md5_sha1_____"; + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.ManufacturerKey, "manufacturer"); + machine.SetFieldValue(Data.Models.Metadata.Machine.PublisherKey, "publisher"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CategoryKey, "category"); + + DatItem item = new Disk(); + item.SetFieldValue(Data.Models.Metadata.Disk.NameKey, "name"); + item.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, "md5"); + item.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "sha1"); + + string actual = DatFile.FormatPrefixPostfix(item, machine, fix); + Assert.Equal(expected, actual); + } + + [Fact] + public void FormatPrefixPostfix_File() + { + string fix = "%game%_%machine%_%name%_%manufacturer%_%publisher%_%category%_%crc%_%md2%_%md4%_%md5%_%sha1%_%sha256%_%sha384%_%sha512%_%size%_%spamsum%"; + string expected = "machine_machine_name.bin_manufacturer_publisher_category_00000000___d41d8cd98f00b204e9800998ecf8427e_da39a3ee5e6b4b0d3255bfef95601890afd80709_e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855___12345_"; + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.ManufacturerKey, "manufacturer"); + machine.SetFieldValue(Data.Models.Metadata.Machine.PublisherKey, "publisher"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CategoryKey, "category"); + + DatItem item = new DatItems.Formats.File + { + Id = "name", + Extension = "bin", + Size = 12345, + CRC = HashType.CRC32.ZeroString, + MD5 = HashType.MD5.ZeroString, + SHA1 = HashType.SHA1.ZeroString, + SHA256 = HashType.SHA256.ZeroString, + }; + + string actual = DatFile.FormatPrefixPostfix(item, machine, fix); + Assert.Equal(expected, actual); + } + + [Fact] + public void FormatPrefixPostfix_Media() + { + string fix = "%game%_%machine%_%name%_%manufacturer%_%publisher%_%category%_%crc%_%md2%_%md4%_%md5%_%sha1%_%sha256%_%sha384%_%sha512%_%size%_%spamsum%"; + string expected = "machine_machine_name_manufacturer_publisher_category____md5_sha1_sha256____spamsum"; + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.ManufacturerKey, "manufacturer"); + machine.SetFieldValue(Data.Models.Metadata.Machine.PublisherKey, "publisher"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CategoryKey, "category"); + + DatItem item = new Media(); + item.SetFieldValue(Data.Models.Metadata.Media.NameKey, "name"); + item.SetFieldValue(Data.Models.Metadata.Media.MD5Key, "md5"); + item.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "sha1"); + item.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, "sha256"); + item.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, "spamsum"); + + string actual = DatFile.FormatPrefixPostfix(item, machine, fix); + Assert.Equal(expected, actual); + } + + [Fact] + public void FormatPrefixPostfix_Rom() + { + string fix = "%game%_%machine%_%name%_%manufacturer%_%publisher%_%category%_%crc%_%md2%_%md4%_%md5%_%ripemd128%_%ripemd160%_%sha1%_%sha256%_%sha384%_%sha512%_%size%_%spamsum%"; + string expected = "machine_machine_name_manufacturer_publisher_category_crc_md2_md4_md5_ripemd128_ripemd160_sha1_sha256_sha384_sha512_12345_spamsum"; + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + machine.SetFieldValue(Data.Models.Metadata.Machine.ManufacturerKey, "manufacturer"); + machine.SetFieldValue(Data.Models.Metadata.Machine.PublisherKey, "publisher"); + machine.SetFieldValue(Data.Models.Metadata.Machine.CategoryKey, "category"); + + DatItem item = new Rom(); + item.SetFieldValue(Data.Models.Metadata.Rom.NameKey, "name"); + item.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + item.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "md2"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "md4"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "md5"); + item.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "ripemd128"); + item.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "ripemd160"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "sha1"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "sha256"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "sha384"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "sha512"); + item.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "spamsum"); + + string actual = DatFile.FormatPrefixPostfix(item, machine, fix); + Assert.Equal(expected, actual); + } + + #endregion + + #region ProcessNullifiedItem + + [Fact] + public void ProcessNullifiedItem_NonRom() + { + DatItem item = new Sample(); + + DatItem actual = DatFile.ProcessNullifiedItem(item); + Sample? sample = actual as Sample; + Assert.NotNull(sample); + Assert.Null(sample.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Null(sample.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + } + + [Fact] + public void ProcessNullifiedItem_SizeNonNull() + { + DatItem item = new Rom(); + item.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + + DatItem actual = DatFile.ProcessNullifiedItem(item); + Rom? rom = actual as Rom; + Assert.NotNull(rom); + Assert.Equal(12345, rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + } + + [Fact] + public void ProcessNullifiedItem_CrcNonNull() + { + DatItem item = new Rom(); + item.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString); + + DatItem actual = DatFile.ProcessNullifiedItem(item); + Rom? rom = actual as Rom; + Assert.NotNull(rom); + Assert.Null(rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal(HashType.CRC32.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Null(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + } + + [Fact] + public void ProcessNullifiedItem_AllNull() + { + DatItem item = new Rom(); + item.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "null"); + item.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "null"); + + DatItem actual = DatFile.ProcessNullifiedItem(item); + Rom? rom = actual as Rom; + Assert.NotNull(rom); + Assert.Equal(0, rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal(HashType.CRC32.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Equal(HashType.MD2.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Equal(HashType.MD4.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Equal(HashType.MD5.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Equal(HashType.RIPEMD128.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Equal(HashType.RIPEMD160.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Equal(HashType.SHA1.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal(HashType.SHA256.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Equal(HashType.SHA384.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Equal(HashType.SHA512.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Equal(HashType.SpamSum.ZeroString, rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + } + + #endregion + + #region ContainsWritable + + [Fact] + public void ContainsWritable_Empty_True() + { + List datItems = []; + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ContainsWritable(datItems); + Assert.True(actual); + } + + [Fact] + public void ContainsWritable_NoWritable_False() + { + List datItems = [new Blank()]; + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ContainsWritable(datItems); + Assert.False(actual); + } + + [Fact] + public void ContainsWritable_Writable_True() + { + List datItems = [new Rom()]; + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ContainsWritable(datItems); + Assert.True(actual); + } + + #endregion + + #region GetDuplicateSuffix + + [Fact] + public void GetDuplicateSuffix_Disk_NoHash_Generic() + { + DatItem datItem = new Disk(); + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal("_1", actual); + } + + [Fact] + public void GetDuplicateSuffix_Disk_MD5() + { + string hash = "XXXXXX"; + DatItem datItem = new Disk(); + datItem.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Disk_SHA1() + { + string hash = "XXXXXX"; + DatItem datItem = new Disk(); + datItem.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_File_NoHash_Generic() + { + DatItem datItem = new DatItems.Formats.File(); + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal("_1", actual); + } + + [Fact] + public void GetDuplicateSuffix_File_CRC() + { + string hash = "deadbeef"; + DatItems.Formats.File datItem = new DatItems.Formats.File { CRC = hash }; + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_File_MD5() + { + string hash = "000000000000000000000000deadbeef"; + DatItem datItem = new DatItems.Formats.File { MD5 = hash }; + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_File_SHA1() + { + string hash = "00000000000000000000000000000000deadbeef"; + DatItem datItem = new DatItems.Formats.File { SHA1 = hash }; + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_File_SHA256() + { + string hash = "00000000000000000000000000000000000000000000000000000000deadbeef"; + DatItem datItem = new DatItems.Formats.File { SHA256 = hash }; + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Media_NoHash_Generic() + { + DatItem datItem = new Media(); + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal("_1", actual); + } + + [Fact] + public void GetDuplicateSuffix_Media_MD5() + { + string hash = "XXXXXX"; + DatItem datItem = new Media(); + datItem.SetFieldValue(Data.Models.Metadata.Media.MD5Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Media_SHA1() + { + string hash = "XXXXXX"; + DatItem datItem = new Media(); + datItem.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Media_SHA256() + { + string hash = "XXXXXX"; + DatItem datItem = new Media(); + datItem.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Media_SpamSum() + { + string hash = "XXXXXX"; + DatItem datItem = new Media(); + datItem.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_NoHash_Generic() + { + DatItem datItem = new Rom(); + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal("_1", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_CRC() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_MD2() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_MD4() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_MD5() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_RIPEMD128() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_RIPEMD160() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_SHA1() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_SHA256() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_SHA384() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_SHA512() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Rom_SpamSum() + { + string hash = "XXXXXX"; + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, hash); + + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal($"_{hash}", actual); + } + + [Fact] + public void GetDuplicateSuffix_Other_Generic() + { + DatItem datItem = new Sample(); + string actual = DatFile.GetDuplicateSuffix(datItem); + Assert.Equal("_1", actual); + } + + #endregion + + #region ResolveNames + + [Fact] + public void ResolveNames_EmptyList_Empty() + { + List datItems = []; + + DatFile datFile = new Logiqx(null, useGame: false); + + List actual = datFile.ResolveNames(datItems); + Assert.Empty(actual); + } + + [Fact] + public void ResolveNames_SingleItem_Single() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List datItems = [romA]; + + DatFile datFile = new Logiqx(null, useGame: false); + + List actual = datFile.ResolveNames(datItems); + DatItem actualItemA = Assert.Single(actual); + Rom? actualRomA = actualItemA as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("name", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + [Fact] + public void ResolveNames_NonDuplicate_AllUntouched() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("romA"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + Rom romB = new Rom(); + romB.SetName("romB"); + romB.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 23456); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc2"); + romB.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romB.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List datItems = [romA, romB]; + + DatFile datFile = new Logiqx(null, useGame: false); + + List actual = datFile.ResolveNames(datItems); + Assert.Equal(2, actual.Count); + + Rom? actualRomA = actual[0] as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("romA", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + + Rom? actualRomB = actual[1] as Rom; + Assert.NotNull(actualRomB); + Assert.Equal("romB", actualRomB.GetName()); + Assert.Equal(23456, actualRomB.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc2", actualRomB.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + [Fact] + public void ResolveNames_AllDuplicate_Single() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("rom"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + Rom romB = new Rom(); + romB.SetName("rom"); + romB.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romB.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romB.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List datItems = [romA, romB]; + + DatFile datFile = new Logiqx(null, useGame: false); + + List actual = datFile.ResolveNames(datItems); + DatItem actualItemA = Assert.Single(actual); + Rom? actualRomA = actualItemA as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("rom", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + [Fact] + public void ResolveNames_NameMatch_SingleRenamed() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("rom"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + Rom romB = new Rom(); + romB.SetName("rom"); + romB.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 23456); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc2"); + romB.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romB.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List datItems = [romA, romB]; + + DatFile datFile = new Logiqx(null, useGame: false); + + List actual = datFile.ResolveNames(datItems); + Assert.Equal(2, actual.Count); + + Rom? actualRomA = actual[0] as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("rom", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + + Rom? actualRomB = actual[1] as Rom; + Assert.NotNull(actualRomB); + Assert.Equal("rom_crc2", actualRomB.GetName()); + Assert.Equal(23456, actualRomB.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc2", actualRomB.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + #endregion + + #region ResolveNamesDB + + [Fact] + public void ResolveNamesDB_EmptyList_Empty() + { + List> mappings = []; + + DatFile datFile = new Logiqx(null, useGame: false); + + List> actual = datFile.ResolveNamesDB(mappings); + Assert.Empty(actual); + } + + [Fact] + public void ResolveNamesDB_SingleItem_Single() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List> mappings = + [ + new KeyValuePair(0, romA), + ]; + DatFile datFile = new Logiqx(null, useGame: false); + + List> actual = datFile.ResolveNamesDB(mappings); + KeyValuePair actualItemA = Assert.Single(actual); + Rom? actualRomA = actualItemA.Value as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("name", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + [Fact] + public void ResolveNamesDB_NonDuplicate_AllUntouched() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("romA"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + Rom romB = new Rom(); + romB.SetName("romB"); + romB.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 23456); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc2"); + romB.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romB.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List> mappings = + [ + new KeyValuePair(0, romA), + new KeyValuePair(1, romB), + ]; + DatFile datFile = new Logiqx(null, useGame: false); + + List> actual = datFile.ResolveNamesDB(mappings); + Assert.Equal(2, actual.Count); + + Rom? actualRomA = actual[0].Value as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("romA", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + + Rom? actualRomB = actual[1].Value as Rom; + Assert.NotNull(actualRomB); + Assert.Equal("romB", actualRomB.GetName()); + Assert.Equal(23456, actualRomB.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc2", actualRomB.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + [Fact] + public void ResolveNamesDB_AllDuplicate_Single() + { + DatFile datFile = new Logiqx(null, useGame: false); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + long machineIndex = datFile.AddMachineDB(machine); + + Source source = new Source(0); + long sourceIndex = datFile.AddSourceDB(source); + + Rom romA = new Rom(); + romA.SetName("rom"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + long romAIndex = datFile.AddItemDB(romA, machineIndex, sourceIndex, statsOnly: false); + + Rom romB = new Rom(); + romB.SetName("rom"); + romB.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + long romBIndex = datFile.AddItemDB(romB, machineIndex, sourceIndex, statsOnly: false); + + List> mappings = + [ + new KeyValuePair(romAIndex, romA), + new KeyValuePair(romBIndex, romB), + ]; + + List> actual = datFile.ResolveNamesDB(mappings); + KeyValuePair actualItemA = Assert.Single(actual); + Rom? actualRomA = actualItemA.Value as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("rom", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + [Fact] + public void ResolveNamesDB_NameMatch_SingleRenamed() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "machine"); + + Source source = new Source(0); + + Rom romA = new Rom(); + romA.SetName("rom"); + romA.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + romA.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romA.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + Rom romB = new Rom(); + romB.SetName("rom"); + romB.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 23456); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc2"); + romB.SetFieldValue(DatItem.MachineKey, (Machine)machine.Clone()); + romB.SetFieldValue(DatItem.SourceKey, (Source)source.Clone()); + + List> mappings = + [ + new KeyValuePair(0, romA), + new KeyValuePair(1, romB), + ]; + DatFile datFile = new Logiqx(null, useGame: false); + + List> actual = datFile.ResolveNamesDB(mappings); + Assert.Equal(2, actual.Count); + + Rom? actualRomA = actual[0].Value as Rom; + Assert.NotNull(actualRomA); + Assert.Equal("rom", actualRomA.GetName()); + Assert.Equal(12345, actualRomA.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc", actualRomA.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + + Rom? actualRomB = actual[1].Value as Rom; + Assert.NotNull(actualRomB); + Assert.Equal("rom_crc2", actualRomB.GetName()); + Assert.Equal(23456, actualRomB.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("crc2", actualRomB.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + } + + #endregion + + #region ShouldIgnore + + [Fact] + public void ShouldIgnore_NullItem_True() + { + DatItem? datItem = null; + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.True(actual); + } + + [Fact] + public void ShouldIgnore_RemoveSet_True() + { + DatItem? datItem = new Rom(); + datItem.SetFieldValue(DatItem.RemoveKey, true); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.True(actual); + } + + [Fact] + public void ShouldIgnore_Blank_True() + { + DatItem? datItem = new Blank(); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.True(actual); + } + + [Fact] + public void ShouldIgnore_IgnoreBlanksZeroRom_True() + { + DatItem? datItem = new Rom(); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.True(actual); + } + + [Fact] + public void ShouldIgnore_NoIgnoreBlanksZeroRom_False() + { + DatItem? datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.NameKey, "name"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: false); + Assert.False(actual); + } + + [Fact] + public void ShouldIgnore_UnsupportedType_True() + { + DatItem? datItem = new DatItems.Formats.SoftwareList(); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.True(actual); + } + + [Fact] + public void ShouldIgnore_MissingRequired_True() + { + DatItem? datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.NameKey, "name"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.True(actual); + } + + [Fact] + public void ShouldIgnore_AllVerified_False() + { + DatItem? datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.NameKey, "name"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "crc"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "crc"); + DatFile datFile = new Logiqx(null, useGame: false); + + bool actual = datFile.ShouldIgnore(datItem, ignoreBlanks: true); + Assert.False(actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatHeaderTests.cs b/SabreTools.Metadata.DatFiles.Test/DatHeaderTests.cs new file mode 100644 index 00000000..50c82e52 --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatHeaderTests.cs @@ -0,0 +1,211 @@ +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public class DatHeaderTests + { + #region CanOpenSpecified + + [Fact] + public void CanOpenSpecified_Missing() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.CanOpenKey, null); + Assert.False(header.CanOpenSpecified); + } + + [Fact] + public void CanOpenSpecified_Empty() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.CanOpenKey, []); + Assert.False(header.CanOpenSpecified); + } + + [Fact] + public void CanOpenSpecified_Exists() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.CanOpenKey, ["value"]); + Assert.True(header.CanOpenSpecified); + } + + #endregion + + #region ImagesSpecified + + [Fact] + public void ImagesSpecified_Missing() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.ImagesKey, null); + Assert.False(header.ImagesSpecified); + } + + [Fact] + public void ImagesSpecified_Exists() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.ImagesKey, new()); + Assert.True(header.ImagesSpecified); + } + + #endregion + + #region InfosSpecified + + [Fact] + public void InfosSpecified_Missing() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.InfosKey, null); + Assert.False(header.InfosSpecified); + } + + [Fact] + public void InfosSpecified_Exists() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.InfosKey, new()); + Assert.True(header.InfosSpecified); + } + + #endregion + + #region NewDatSpecified + + [Fact] + public void NewDatSpecified_Missing() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.NewDatKey, null); + Assert.False(header.NewDatSpecified); + } + + [Fact] + public void NewDatSpecified_Exists() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.NewDatKey, new()); + Assert.True(header.NewDatSpecified); + } + + #endregion + + #region SearchSpecified + + [Fact] + public void SearchSpecified_Missing() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.SearchKey, null); + Assert.False(header.SearchSpecified); + } + + [Fact] + public void SearchSpecified_Exists() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.SearchKey, new()); + Assert.True(header.SearchSpecified); + } + + #endregion + + #region Clone + + [Fact] + public void CloneTest() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + + object clone = header.Clone(); + DatHeader? actual = clone as DatHeader; + Assert.NotNull(actual); + Assert.Equal("name", actual.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + } + + #endregion + + #region CloneFormat + + [Fact] + public void CloneFormatTest() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Logiqx); + + object clone = header.Clone(); + DatHeader? actual = clone as DatHeader; + Assert.NotNull(actual); + Assert.Equal(DatFormat.Logiqx, actual.GetFieldValue(DatHeader.DatFormatKey)); + } + + #endregion + + #region GetInternalClone + + [Fact] + public void GetInternalCloneTest() + { + DatHeader header = new DatHeader(); + header.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + + Data.Models.Metadata.Header actual = header.GetInternalClone(); + Assert.Equal("name", actual[Data.Models.Metadata.Header.NameKey]); + } + + #endregion + + #region Equals + + [Fact] + public void Equals_Null_False() + { + DatHeader self = new DatHeader(); + DatHeader? other = null; + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_DefaultInternal_True() + { + DatHeader self = new DatHeader(); + DatHeader? other = new DatHeader(); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + [Fact] + public void Equals_MismatchedInternal_False() + { + DatHeader self = new DatHeader(); + self.SetFieldValue(Data.Models.Metadata.Header.NameKey, "self"); + + DatHeader? other = new DatHeader(); + other.SetFieldValue(Data.Models.Metadata.Header.NameKey, "other"); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_EqualInternal_True() + { + DatHeader self = new DatHeader(); + self.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + + DatHeader? other = new DatHeader(); + other.SetFieldValue(Data.Models.Metadata.Header.NameKey, "name"); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/DatStatisticsTests.cs b/SabreTools.Metadata.DatFiles.Test/DatStatisticsTests.cs new file mode 100644 index 00000000..80f174b8 --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/DatStatisticsTests.cs @@ -0,0 +1,326 @@ +using SabreTools.Hashing; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public class DatStatisticsTests + { + #region Constructor + + [Fact] + public void DefaultConstructorTest() + { + var stats = new DatStatistics(); + + Assert.Null(stats.DisplayName); + Assert.Equal(0, stats.MachineCount); + Assert.False(stats.IsDirectory); + } + + [Fact] + public void NamedConstructorTest() + { + var stats = new DatStatistics("name", isDirectory: true); + + Assert.Equal("name", stats.DisplayName); + Assert.Equal(0, stats.MachineCount); + Assert.True(stats.IsDirectory); + } + + #endregion + + #region End to End + + [Fact] + public void AddRemoveStatisticsTest() + { + // Get items for testing + var disk = CreateDisk(); + var file = CreateFile(); + var media = CreateMedia(); + var rom = CreateRom(); + var sample = CreateSample(); + + // Create an empty stats object + var stats = new DatStatistics(); + + // Validate pre-add values + Assert.Equal(0, stats.TotalCount); + Assert.Equal(0, stats.TotalSize); + Assert.Equal(0, stats.GetHashCount(HashType.CRC32)); + Assert.Equal(0, stats.GetHashCount(HashType.MD2)); + Assert.Equal(0, stats.GetHashCount(HashType.MD4)); + Assert.Equal(0, stats.GetHashCount(HashType.MD5)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA1)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA256)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA384)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA512)); + Assert.Equal(0, stats.GetHashCount(HashType.SpamSum)); + Assert.Equal(0, stats.GetItemCount(ItemType.Disk)); + Assert.Equal(0, stats.GetItemCount(ItemType.File)); + Assert.Equal(0, stats.GetItemCount(ItemType.Media)); + Assert.Equal(0, stats.GetItemCount(ItemType.Rom)); + Assert.Equal(0, stats.GetItemCount(ItemType.Sample)); + Assert.Equal(0, stats.GetStatusCount(ItemStatus.Good)); + + // AddItemStatistics + stats.AddItemStatistics(disk); + stats.AddItemStatistics(file); + stats.AddItemStatistics(media); + stats.AddItemStatistics(rom); + stats.AddItemStatistics(sample); + + // Validate post-add values + Assert.Equal(5, stats.TotalCount); + Assert.Equal(2, stats.TotalSize); + Assert.Equal(2, stats.GetHashCount(HashType.CRC32)); + Assert.Equal(1, stats.GetHashCount(HashType.MD2)); + Assert.Equal(1, stats.GetHashCount(HashType.MD4)); + Assert.Equal(4, stats.GetHashCount(HashType.MD5)); + Assert.Equal(1, stats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(1, stats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(4, stats.GetHashCount(HashType.SHA1)); + Assert.Equal(3, stats.GetHashCount(HashType.SHA256)); + Assert.Equal(1, stats.GetHashCount(HashType.SHA384)); + Assert.Equal(1, stats.GetHashCount(HashType.SHA512)); + Assert.Equal(2, stats.GetHashCount(HashType.SpamSum)); + Assert.Equal(1, stats.GetItemCount(ItemType.Disk)); + Assert.Equal(1, stats.GetItemCount(ItemType.File)); + Assert.Equal(1, stats.GetItemCount(ItemType.Media)); + Assert.Equal(1, stats.GetItemCount(ItemType.Rom)); + Assert.Equal(1, stats.GetItemCount(ItemType.Sample)); + Assert.Equal(2, stats.GetStatusCount(ItemStatus.Good)); + + // RemoveItemStatistics + stats.RemoveItemStatistics(disk); + stats.RemoveItemStatistics(file); + stats.RemoveItemStatistics(media); + stats.RemoveItemStatistics(rom); + stats.RemoveItemStatistics(sample); + + // Validate post-remove values + Assert.Equal(0, stats.TotalCount); + Assert.Equal(0, stats.TotalSize); + Assert.Equal(0, stats.GetHashCount(HashType.CRC32)); + Assert.Equal(0, stats.GetHashCount(HashType.MD2)); + Assert.Equal(0, stats.GetHashCount(HashType.MD4)); + Assert.Equal(0, stats.GetHashCount(HashType.MD5)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA1)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA256)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA384)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA512)); + Assert.Equal(0, stats.GetHashCount(HashType.SpamSum)); + Assert.Equal(0, stats.GetItemCount(ItemType.Disk)); + Assert.Equal(0, stats.GetItemCount(ItemType.File)); + Assert.Equal(0, stats.GetItemCount(ItemType.Media)); + Assert.Equal(0, stats.GetItemCount(ItemType.Rom)); + Assert.Equal(0, stats.GetItemCount(ItemType.Sample)); + Assert.Equal(0, stats.GetStatusCount(ItemStatus.Good)); + } + + [Fact] + public void ResetStatisticsTest() + { + // Get items for testing + var disk = CreateDisk(); + var file = CreateFile(); + var media = CreateMedia(); + var rom = CreateRom(); + var sample = CreateSample(); + + // Create an empty stats object + var stats = new DatStatistics(); + + // Validate pre-add values + Assert.Equal(0, stats.TotalCount); + Assert.Equal(0, stats.TotalSize); + Assert.Equal(0, stats.GetHashCount(HashType.CRC32)); + Assert.Equal(0, stats.GetHashCount(HashType.MD2)); + Assert.Equal(0, stats.GetHashCount(HashType.MD4)); + Assert.Equal(0, stats.GetHashCount(HashType.MD5)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA1)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA256)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA384)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA512)); + Assert.Equal(0, stats.GetHashCount(HashType.SpamSum)); + Assert.Equal(0, stats.GetItemCount(ItemType.Disk)); + Assert.Equal(0, stats.GetItemCount(ItemType.File)); + Assert.Equal(0, stats.GetItemCount(ItemType.Media)); + Assert.Equal(0, stats.GetItemCount(ItemType.Rom)); + Assert.Equal(0, stats.GetItemCount(ItemType.Sample)); + Assert.Equal(0, stats.GetStatusCount(ItemStatus.Good)); + + // AddItemStatistics + stats.AddItemStatistics(disk); + stats.AddItemStatistics(file); + stats.AddItemStatistics(media); + stats.AddItemStatistics(rom); + stats.AddItemStatistics(sample); + + // Validate post-add values + Assert.Equal(5, stats.TotalCount); + Assert.Equal(2, stats.TotalSize); + Assert.Equal(2, stats.GetHashCount(HashType.CRC32)); + Assert.Equal(1, stats.GetHashCount(HashType.MD2)); + Assert.Equal(1, stats.GetHashCount(HashType.MD4)); + Assert.Equal(4, stats.GetHashCount(HashType.MD5)); + Assert.Equal(1, stats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(1, stats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(4, stats.GetHashCount(HashType.SHA1)); + Assert.Equal(3, stats.GetHashCount(HashType.SHA256)); + Assert.Equal(1, stats.GetHashCount(HashType.SHA384)); + Assert.Equal(1, stats.GetHashCount(HashType.SHA512)); + Assert.Equal(2, stats.GetHashCount(HashType.SpamSum)); + Assert.Equal(1, stats.GetItemCount(ItemType.Disk)); + Assert.Equal(1, stats.GetItemCount(ItemType.File)); + Assert.Equal(1, stats.GetItemCount(ItemType.Media)); + Assert.Equal(1, stats.GetItemCount(ItemType.Rom)); + Assert.Equal(1, stats.GetItemCount(ItemType.Sample)); + Assert.Equal(2, stats.GetStatusCount(ItemStatus.Good)); + + // ResetStatistics + stats.ResetStatistics(); + + // Validate post-reset values + Assert.Equal(0, stats.TotalCount); + Assert.Equal(0, stats.TotalSize); + Assert.Equal(0, stats.GetHashCount(HashType.CRC32)); + Assert.Equal(0, stats.GetHashCount(HashType.MD2)); + Assert.Equal(0, stats.GetHashCount(HashType.MD4)); + Assert.Equal(0, stats.GetHashCount(HashType.MD5)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(0, stats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA1)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA256)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA384)); + Assert.Equal(0, stats.GetHashCount(HashType.SHA512)); + Assert.Equal(0, stats.GetHashCount(HashType.SpamSum)); + Assert.Equal(0, stats.GetItemCount(ItemType.Disk)); + Assert.Equal(0, stats.GetItemCount(ItemType.File)); + Assert.Equal(0, stats.GetItemCount(ItemType.Media)); + Assert.Equal(0, stats.GetItemCount(ItemType.Rom)); + Assert.Equal(0, stats.GetItemCount(ItemType.Sample)); + Assert.Equal(0, stats.GetStatusCount(ItemStatus.Good)); + } + + #endregion + + #region AddStatistics + + [Fact] + public void AddStatisticsTest() + { + var rom = CreateRom(); + var origStats = new DatStatistics(); + origStats.AddItemStatistics(rom); + + var newStats = new DatStatistics(); + newStats.AddStatistics(origStats); + + Assert.Equal(1, newStats.TotalCount); + Assert.Equal(1, newStats.TotalSize); + Assert.Equal(1, newStats.GetHashCount(HashType.CRC32)); + Assert.Equal(1, newStats.GetHashCount(HashType.MD2)); + Assert.Equal(1, newStats.GetHashCount(HashType.MD4)); + Assert.Equal(1, newStats.GetHashCount(HashType.MD5)); + Assert.Equal(1, newStats.GetHashCount(HashType.RIPEMD128)); + Assert.Equal(1, newStats.GetHashCount(HashType.RIPEMD160)); + Assert.Equal(1, newStats.GetHashCount(HashType.SHA1)); + Assert.Equal(1, newStats.GetHashCount(HashType.SHA256)); + Assert.Equal(1, newStats.GetHashCount(HashType.SHA384)); + Assert.Equal(1, newStats.GetHashCount(HashType.SHA512)); + Assert.Equal(1, newStats.GetHashCount(HashType.SpamSum)); + Assert.Equal(1, newStats.GetItemCount(ItemType.Rom)); + Assert.Equal(1, newStats.GetStatusCount(ItemStatus.Good)); + } + + #endregion + + #region Helpers + + /// + /// Create a Disk for testing + /// + private static Disk CreateDisk() + { + var disk = new Disk(); + + disk.SetFieldValue(Data.Models.Metadata.Disk.StatusKey, ItemStatus.Good.AsStringValue()); + disk.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, HashType.MD5.ZeroString); + disk.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, HashType.SHA1.ZeroString); + + return disk; + } + + /// + /// Create a File for testing + /// + private static File CreateFile() + { + var file = new File + { + Size = 1, + CRC = HashType.CRC32.ZeroString, + MD5 = HashType.MD5.ZeroString, + SHA1 = HashType.SHA1.ZeroString, + SHA256 = HashType.SHA256.ZeroString + }; + + return file; + } + + /// + /// Create a Media for testing + /// + private static Media CreateMedia() + { + var media = new Media(); + + media.SetFieldValue(Data.Models.Metadata.Media.MD5Key, HashType.MD5.ZeroString); + media.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, HashType.SHA1.ZeroString); + media.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, HashType.SHA256.ZeroString); + media.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, HashType.SpamSum.ZeroString); + + return media; + } + + /// + /// Create a Rom for testing + /// + private static Rom CreateRom() + { + var rom = new Rom(); + + rom.SetFieldValue(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Good.AsStringValue()); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 1); + rom.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, HashType.MD2.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, HashType.MD4.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, HashType.MD5.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, HashType.RIPEMD128.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, HashType.RIPEMD160.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, HashType.SHA256.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, HashType.SHA384.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, HashType.SHA512.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, HashType.SpamSum.ZeroString); + + return rom; + } + + /// + /// Create a Sample for testing + /// + private static Sample CreateSample() => new(); + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/ExtensionsTests.cs b/SabreTools.Metadata.DatFiles.Test/ExtensionsTests.cs new file mode 100644 index 00000000..899777d4 --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/ExtensionsTests.cs @@ -0,0 +1,114 @@ +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public class ExtensionsTests + { + #region String to Enum + + [Theory] + [InlineData(null, MergingFlag.None)] + [InlineData("none", MergingFlag.None)] + [InlineData("split", MergingFlag.Split)] + [InlineData("merged", MergingFlag.Merged)] + [InlineData("nonmerged", MergingFlag.NonMerged)] + [InlineData("unmerged", MergingFlag.NonMerged)] + [InlineData("fullmerged", MergingFlag.FullMerged)] + [InlineData("device", MergingFlag.DeviceNonMerged)] + [InlineData("devicenonmerged", MergingFlag.DeviceNonMerged)] + [InlineData("deviceunmerged", MergingFlag.DeviceNonMerged)] + [InlineData("full", MergingFlag.FullNonMerged)] + [InlineData("fullnonmerged", MergingFlag.FullNonMerged)] + [InlineData("fullunmerged", MergingFlag.FullNonMerged)] + public void AsMergingFlagTest(string? field, MergingFlag expected) + { + MergingFlag actual = field.AsMergingFlag(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, NodumpFlag.None)] + [InlineData("none", NodumpFlag.None)] + [InlineData("obsolete", NodumpFlag.Obsolete)] + [InlineData("required", NodumpFlag.Required)] + [InlineData("ignore", NodumpFlag.Ignore)] + public void AsNodumpFlagTest(string? field, NodumpFlag expected) + { + NodumpFlag actual = field.AsNodumpFlag(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, PackingFlag.None)] + [InlineData("none", PackingFlag.None)] + [InlineData("yes", PackingFlag.Zip)] + [InlineData("zip", PackingFlag.Zip)] + [InlineData("no", PackingFlag.Unzip)] + [InlineData("unzip", PackingFlag.Unzip)] + [InlineData("partial", PackingFlag.Partial)] + [InlineData("flat", PackingFlag.Flat)] + [InlineData("fileonly", PackingFlag.FileOnly)] + public void AsPackingFlagTest(string? field, PackingFlag expected) + { + PackingFlag actual = field.AsPackingFlag(); + Assert.Equal(expected, actual); + } + + #endregion + + #region Enum to String + + [Theory] + [InlineData(MergingFlag.None, true, "none")] + [InlineData(MergingFlag.None, false, "none")] + [InlineData(MergingFlag.Split, true, "split")] + [InlineData(MergingFlag.Split, false, "split")] + [InlineData(MergingFlag.Merged, true, "merged")] + [InlineData(MergingFlag.Merged, false, "merged")] + [InlineData(MergingFlag.NonMerged, true, "unmerged")] + [InlineData(MergingFlag.NonMerged, false, "nonmerged")] + [InlineData(MergingFlag.FullMerged, true, "fullmerged")] + [InlineData(MergingFlag.FullMerged, false, "fullmerged")] + [InlineData(MergingFlag.DeviceNonMerged, true, "deviceunmerged")] + [InlineData(MergingFlag.DeviceNonMerged, false, "device")] + [InlineData(MergingFlag.FullNonMerged, true, "fullunmerged")] + [InlineData(MergingFlag.FullNonMerged, false, "full")] + public void FromMergingFlagTest(MergingFlag field, bool useSecond, string? expected) + { + string? actual = field.AsStringValue(useSecond); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(NodumpFlag.None, "none")] + [InlineData(NodumpFlag.Obsolete, "obsolete")] + [InlineData(NodumpFlag.Required, "required")] + [InlineData(NodumpFlag.Ignore, "ignore")] + public void FromNodumpFlagTest(NodumpFlag field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(PackingFlag.None, true, "none")] + [InlineData(PackingFlag.None, false, "none")] + [InlineData(PackingFlag.Zip, true, "yes")] + [InlineData(PackingFlag.Zip, false, "zip")] + [InlineData(PackingFlag.Unzip, true, "no")] + [InlineData(PackingFlag.Unzip, false, "unzip")] + [InlineData(PackingFlag.Partial, true, "partial")] + [InlineData(PackingFlag.Partial, false, "partial")] + [InlineData(PackingFlag.Flat, true, "flat")] + [InlineData(PackingFlag.Flat, false, "flat")] + [InlineData(PackingFlag.FileOnly, true, "fileonly")] + [InlineData(PackingFlag.FileOnly, false, "fileonly")] + public void FromPackingFlagTest(PackingFlag field, bool useSecond, string? expected) + { + string? actual = field.AsStringValue(useSecond); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/FormatsTests.cs b/SabreTools.Metadata.DatFiles.Test/FormatsTests.cs new file mode 100644 index 00000000..96b898de --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/FormatsTests.cs @@ -0,0 +1,1647 @@ +using System; +using System.Linq; +using SabreTools.Metadata.DatFiles.Formats; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + /// + /// Contains tests for all specific DatFile formats + /// + public class FormatsTests + { + #region Testing Constants + + /// + /// All defined item types + /// + private static readonly ItemType[] AllTypes = Enum.GetValues(); + + #endregion + + #region ArchiveDotOrg + + [Fact] + public void ArchiveDotOrg_SupportedTypes() + { + var datFile = new ArchiveDotOrg(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + #endregion + + #region AttractMode + + [Fact] + public void AttractMode_SupportedTypes() + { + var datFile = new AttractMode(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void AttractMode_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new AttractMode(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + ])); + } + + #endregion + + #region ClrMamePro + + [Fact] + public void ClrMamePro_SupportedTypes() + { + var datFile = new ClrMamePro(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Archive, + ItemType.BiosSet, + ItemType.Chip, + ItemType.DipSwitch, + ItemType.Disk, + ItemType.Display, + ItemType.Driver, + ItemType.Input, + ItemType.Media, + ItemType.Release, + ItemType.Rom, + ItemType.Sample, + ItemType.Sound, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Release() + { + var datItem = new Release(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Release.NameKey, + Data.Models.Metadata.Release.RegionKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_BiosSet() + { + var datItem = new BiosSet(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.BiosSet.NameKey, + Data.Models.Metadata.BiosSet.DescriptionKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Sample() + { + var datItem = new Sample(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Sample.NameKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Archive() + { + var datItem = new Archive(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Archive.NameKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Chip() + { + var datItem = new Chip(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Chip.ChipTypeKey, + Data.Models.Metadata.Chip.NameKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Display() + { + var datItem = new Display(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Display.DisplayTypeKey, + Data.Models.Metadata.Display.RotateKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Sound() + { + var datItem = new Sound(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Sound.ChannelsKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Input() + { + var datItem = new Input(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Input.PlayersKey, + Data.Models.Metadata.Input.ControlKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_DipSwitch() + { + var datItem = new DipSwitch(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.DipSwitch.NameKey, + ])); + } + + [Fact] + public void ClrMamePro_GetMissingRequiredFields_Driver() + { + var datItem = new Driver(); + var datFile = new ClrMamePro(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Driver.StatusKey, + Data.Models.Metadata.Driver.EmulationKey, + ])); + } + + #endregion + + #region DosCenter + + [Fact] + public void DosCenter_SupportedTypes() + { + var datFile = new DosCenter(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void DosCenter_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new DosCenter(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.CRCKey, + ])); + } + + #endregion + + #region EverdriveSMDB + + [Fact] + public void EverdriveSMDB_SupportedTypes() + { + var datFile = new EverdriveSMDB(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void EverdriveSMDB_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new EverdriveSMDB(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SHA256Key, + Data.Models.Metadata.Rom.SHA1Key, + Data.Models.Metadata.Rom.MD5Key, + Data.Models.Metadata.Rom.CRCKey, + ])); + } + + #endregion + + #region Hashfile + + [Fact] + public void SfvFile_SupportedTypes() + { + var datFile = new SfvFile(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void SfvFile_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new SfvFile(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.CRCKey, + ])); + } + + [Fact] + public void Md2File_SupportedTypes() + { + var datFile = new Md2File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void Md2File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Md2File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.MD2Key, + ])); + } + + [Fact] + public void Md4File_SupportedTypes() + { + var datFile = new Md4File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void Md4File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Md4File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.MD4Key, + ])); + } + + [Fact] + public void Md5File_SupportedTypes() + { + var datFile = new Md5File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void Md5File_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new Md5File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.MD5Key, + ])); + } + + [Fact] + public void Md5File_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new Md5File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.MD5Key, + ])); + } + + [Fact] + public void Md5File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Md5File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.MD5Key, + ])); + } + + [Fact] + public void RipeMD128File_SupportedTypes() + { + var datFile = new RipeMD128File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void RipeMD128File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new RipeMD128File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.RIPEMD128Key, + ])); + } + + [Fact] + public void RipeMD160File_SupportedTypes() + { + var datFile = new RipeMD160File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void RipeMD160File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new RipeMD160File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.RIPEMD160Key, + ])); + } + + [Fact] + public void Sha1File_SupportedTypes() + { + var datFile = new Sha1File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void Sha1File_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new Sha1File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void Sha1File_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new Sha1File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SHA1Key, + ])); + } + + [Fact] + public void Sha1File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Sha1File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + [Fact] + public void Sha256File_SupportedTypes() + { + var datFile = new Sha256File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void Sha256File_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new Sha256File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SHA256Key, + ])); + } + + [Fact] + public void Sha256File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Sha256File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SHA256Key, + ])); + } + + [Fact] + public void Sha384File_SupportedTypes() + { + var datFile = new Sha384File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void Sha384File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Sha384File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SHA384Key, + ])); + } + + [Fact] + public void Sha512File_SupportedTypes() + { + var datFile = new Sha512File(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void Sha512File_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Sha512File(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SHA512Key, + ])); + } + + [Fact] + public void SpamSumFile_SupportedTypes() + { + var datFile = new SpamSumFile(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void SpamSumFile_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new SpamSumFile(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SpamSumKey, + ])); + } + + [Fact] + public void SpamSumFile_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new SpamSumFile(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SpamSumKey, + ])); + } + + #endregion + + #region Listrom + + [Fact] + public void Listrom_SupportedTypes() + { + var datFile = new Listrom(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Disk, + ItemType.Rom, + ])); + } + + [Fact] + public void Listrom_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new Listrom(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void Listrom_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Listrom(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.CRCKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + #endregion + + #region Listxml + + [Fact] + public void Listxml_SupportedTypes() + { + var datFile = new Listxml(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Adjuster, + ItemType.BiosSet, + ItemType.Chip, + ItemType.Condition, + ItemType.Configuration, + ItemType.Device, + ItemType.DeviceRef, + ItemType.DipSwitch, + ItemType.Disk, + ItemType.Display, + ItemType.Driver, + ItemType.Feature, + ItemType.Input, + ItemType.Port, + ItemType.RamOption, + ItemType.Rom, + ItemType.Sample, + ItemType.Slot, + ItemType.SoftwareList, + ItemType.Sound, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_BiosSet() + { + var datItem = new BiosSet(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.BiosSet.NameKey, + Data.Models.Metadata.BiosSet.DescriptionKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_DeviceRef() + { + var datItem = new DeviceRef(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.DeviceRef.NameKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Sample() + { + var datItem = new Sample(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Sample.NameKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Chip() + { + var datItem = new Chip(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Chip.NameKey, + Data.Models.Metadata.Chip.ChipTypeKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Display() + { + var datItem = new Display(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Display.DisplayTypeKey, + Data.Models.Metadata.Display.RefreshKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Sound() + { + var datItem = new Sound(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Sound.ChannelsKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Input() + { + var datItem = new Input(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Input.PlayersKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_DipSwitch() + { + var datItem = new DipSwitch(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.DipSwitch.NameKey, + Data.Models.Metadata.DipSwitch.TagKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Configuration() + { + var datItem = new Configuration(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Configuration.NameKey, + Data.Models.Metadata.Configuration.TagKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Port() + { + var datItem = new Port(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Port.TagKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Adjuster() + { + var datItem = new Adjuster(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Adjuster.NameKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Driver() + { + var datItem = new Driver(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Driver.StatusKey, + Data.Models.Metadata.Driver.EmulationKey, + Data.Models.Metadata.Driver.CocktailKey, + Data.Models.Metadata.Driver.SaveStateKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Feature() + { + var datItem = new Feature(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Feature.FeatureTypeKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Device() + { + var datItem = new Device(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Device.DeviceTypeKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_Slot() + { + var datItem = new Slot(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Slot.NameKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_SoftwareList() + { + var datItem = new DatItems.Formats.SoftwareList(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.SoftwareList.TagKey, + Data.Models.Metadata.SoftwareList.NameKey, + Data.Models.Metadata.SoftwareList.StatusKey, + ])); + } + + [Fact] + public void Listxml_GetMissingRequiredFields_RamOption() + { + var datItem = new RamOption(); + var datFile = new Listxml(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.RamOption.NameKey, + ])); + } + + #endregion + + #region Logiqx + + [Fact] + public void Logiqx_SupportedTypes() + { + var datFile = new Logiqx(null, false); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Archive, + ItemType.BiosSet, + ItemType.DeviceRef, + ItemType.Disk, + ItemType.Driver, + ItemType.Media, + ItemType.Release, + ItemType.Rom, + ItemType.Sample, + ItemType.SoftwareList, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Release() + { + var datItem = new Release(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Release.NameKey, + Data.Models.Metadata.Release.RegionKey, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_BiosSet() + { + var datItem = new BiosSet(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.BiosSet.NameKey, + Data.Models.Metadata.BiosSet.DescriptionKey, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SHA1Key, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_DeviceRef() + { + var datItem = new DeviceRef(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.DeviceRef.NameKey, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Sample() + { + var datItem = new Sample(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Sample.NameKey, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Archive() + { + var datItem = new Archive(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Archive.NameKey, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_Driver() + { + var datItem = new Driver(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Driver.StatusKey, + Data.Models.Metadata.Driver.EmulationKey, + Data.Models.Metadata.Driver.CocktailKey, + Data.Models.Metadata.Driver.SaveStateKey, + ])); + } + + [Fact] + public void Logiqx_GetMissingRequiredFields_SoftwareList() + { + var datItem = new DatItems.Formats.SoftwareList(); + var datFile = new Logiqx(null, false); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.SoftwareList.TagKey, + Data.Models.Metadata.SoftwareList.NameKey, + Data.Models.Metadata.SoftwareList.StatusKey, + ])); + } + + #endregion + + #region Missfile + + [Fact] + public void Missfile_SupportedTypes() + { + var datFile = new Missfile(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual(AllTypes)); + } + + [Fact] + public void Missfile_ParseFile_Throws() + { + var datFile = new Missfile(null); + Assert.Throws(() => datFile.ParseFile("path", 0, true)); + } + + #endregion + + #region OfflineList + + [Fact] + public void OfflineList_SupportedTypes() + { + var datFile = new OfflineList(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void OfflineList_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new OfflineList(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.CRCKey, + ])); + } + + #endregion + + #region OpenMSX + + [Fact] + public void OpenMSX_SupportedTypes() + { + var datFile = new OpenMSX(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void OpenMSX_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new OpenMSX(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + #endregion + + #region RomCenter + + [Fact] + public void RomCenter_SupportedTypes() + { + var datFile = new RomCenter(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Rom, + ])); + } + + [Fact] + public void RomCenter_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new RomCenter(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.CRCKey, + Data.Models.Metadata.Rom.SizeKey, + ])); + } + + #endregion + + #region SabreJSON + + [Fact] + public void SabreJSON_SupportedTypes() + { + var datFile = new SabreJSON(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual(AllTypes)); + } + + #endregion + + #region SabreXML + + [Fact] + public void SabreXML_SupportedTypes() + { + var datFile = new SabreXML(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual(AllTypes)); + } + + #endregion + + #region SeparatedValue + + [Fact] + public void CommaSeparatedValue_SupportedTypes() + { + var datFile = new CommaSeparatedValue(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void CommaSeparatedValue_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new CommaSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void CommaSeparatedValue_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new CommaSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SHA1Key, + ])); + } + + [Fact] + public void CommaSeparatedValue_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new CommaSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + [Fact] + public void SemicolonSeparatedValue_SupportedTypes() + { + var datFile = new SemicolonSeparatedValue(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void SemicolonSeparatedValue_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new SemicolonSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void SemicolonSeparatedValue_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new SemicolonSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SHA1Key, + ])); + } + + [Fact] + public void SemicolonSeparatedValue_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new SemicolonSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + [Fact] + public void TabSeparatedValue_SupportedTypes() + { + var datFile = new TabSeparatedValue(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ])); + } + + [Fact] + public void TabSeparatedValue_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new TabSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Disk.NameKey, + Data.Models.Metadata.Disk.SHA1Key, + ])); + } + + [Fact] + public void TabSeparatedValue_GetMissingRequiredFields_Media() + { + var datItem = new Media(); + var datFile = new TabSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Media.NameKey, + Data.Models.Metadata.Media.SHA1Key, + ])); + } + + [Fact] + public void TabSeparatedValue_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new TabSeparatedValue(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Rom.NameKey, + Data.Models.Metadata.Rom.SizeKey, + Data.Models.Metadata.Rom.SHA1Key, + ])); + } + + #endregion + + #region SoftwareList + + [Fact] + public void SoftwareList_SupportedTypes() + { + var datFile = new Formats.SoftwareList(null); + var actual = datFile.SupportedTypes; + Assert.True(actual.SequenceEqual([ + ItemType.DipSwitch, + ItemType.Disk, + ItemType.Info, + ItemType.PartFeature, + ItemType.Rom, + ItemType.SharedFeat, + ])); + } + + [Fact] + public void SoftwareList_GetMissingRequiredFields_DipSwitch() + { + var datItem = new DipSwitch(); + var datFile = new Formats.SoftwareList(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Part.NameKey, + Data.Models.Metadata.Part.InterfaceKey, + Data.Models.Metadata.DipSwitch.NameKey, + Data.Models.Metadata.DipSwitch.TagKey, + Data.Models.Metadata.DipSwitch.MaskKey, + ])); + } + + [Fact] + public void SoftwareList_GetMissingRequiredFields_Disk() + { + var datItem = new Disk(); + var datFile = new Formats.SoftwareList(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Part.NameKey, + Data.Models.Metadata.Part.InterfaceKey, + Data.Models.Metadata.DiskArea.NameKey, + Data.Models.Metadata.Disk.NameKey, + ])); + } + + [Fact] + public void SoftwareList_GetMissingRequiredFields_Info() + { + var datItem = new Info(); + var datFile = new Formats.SoftwareList(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Info.NameKey, + ])); + } + + [Fact] + public void SoftwareList_GetMissingRequiredFields_Rom() + { + var datItem = new Rom(); + var datFile = new Formats.SoftwareList(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.Part.NameKey, + Data.Models.Metadata.Part.InterfaceKey, + Data.Models.Metadata.DataArea.NameKey, + Data.Models.Metadata.DataArea.SizeKey, + ])); + } + + [Fact] + public void SoftwareList_GetMissingRequiredFields_SharedFeat() + { + var datItem = new SharedFeat(); + var datFile = new Formats.SoftwareList(null); + + var actual = datFile.GetMissingRequiredFields(datItem); + + Assert.NotNull(actual); + Assert.True(actual.SequenceEqual([ + Data.Models.Metadata.SharedFeat.NameKey, + ])); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/ItemDictionaryDBTests.cs b/SabreTools.Metadata.DatFiles.Test/ItemDictionaryDBTests.cs new file mode 100644 index 00000000..610182ae --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/ItemDictionaryDBTests.cs @@ -0,0 +1,1025 @@ +using System.Collections.Generic; +using SabreTools.Hashing; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public class ItemDictionaryDBTests + { + #region AddItem + + [Fact] + public void AddItem_Disk_WithHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem disk = new Disk(); + disk.SetName("item"); + disk.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "deadbeef"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(disk, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Disk); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey)); + } + + [Fact] + public void AddItem_Disk_WithoutHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem disk = new Disk(); + disk.SetName("item"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(disk, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Disk); + Assert.Equal("nodump", actual.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey)); + } + + [Fact] + public void AddItem_File_WithHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + var file = new File(); + file.SetName("item"); + file.SHA1 = "deadbeef"; + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(file, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is File); + //Assert.Equal("none", actual.GetStringFieldValue(File.StatusKey)); + } + + [Fact] + public void AddItem_File_WithoutHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem file = new File(); + file.SetName("item"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(file, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is File); + //Assert.Equal("nodump", actual.GetStringFieldValue(File.StatusKey)); + } + + [Fact] + public void AddItem_Media_WithHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem media = new Media(); + media.SetName("item"); + media.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "deadbeef"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(media, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Media); + //Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Media.StatusKey)); + } + + [Fact] + public void AddItem_Media_WithoutHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem media = new Media(); + media.SetName("item"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(media, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Media); + //Assert.Equal("nodump", actual.GetStringFieldValue(Data.Models.Metadata.Media.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithHashesWithSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "deadbeef"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(rom, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Rom); + Assert.Equal(12345, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithoutHashesWithSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(rom, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Rom); + Assert.Equal(12345, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("nodump", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithHashesWithoutSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "deadbeef"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(rom, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Rom); + Assert.Null(actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithoutHashesWithoutSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(rom, machineIndex, sourceIndex, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")).Value; + Assert.True(actual is Rom); + Assert.Equal(0, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal(HashType.SHA1.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_StatsOnly() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem item = new Rom(); + item.SetName("item"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: true); + + Assert.Empty(dict.GetItemsForBucket("default")); + } + + [Fact] + public void AddItem_NormalAdd() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem item = new Rom(); + item.SetName("item"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + Assert.Single(dict.GetItemsForBucket("default")); + } + + #endregion + + #region AddMachine + + [Fact] + public void AddMachineTest() + { + Machine machine = new Machine(); + var dict = new ItemDictionaryDB(); + long machineIndex = dict.AddMachine(machine); + + Assert.Equal(0, machineIndex); + Assert.Single(dict.GetMachines()); + } + + #endregion + + #region AddSource + + [Fact] + public void AddSourceTest() + { + Source source = new Source(0, source: null); + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + + Assert.Equal(0, sourceIndex); + Assert.Single(dict.GetSources()); + } + + #endregion + + #region ClearMarked + + [Fact] + public void ClearMarkedTest() + { + // Setup the items + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + rom2.SetFieldValue(DatItem.RemoveKey, true); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + // Setup the dictionary + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + dict.AddItem(rom1, machineIndex, sourceIndex, statsOnly: false); + dict.AddItem(rom2, machineIndex, sourceIndex, statsOnly: false); + + dict.ClearMarked(); + string key = Assert.Single(dict.SortedKeys); + Assert.Equal("game-1", key); + Dictionary items = dict.GetItemsForBucket(key); + Assert.Single(items); + } + + #endregion + + #region GetItemsForBucket + + [Fact] + public void GetItemsForBucket_NullBucketName() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetItemsForBucket(null, filter: false); + + Assert.Empty(actual); + } + + [Fact] + public void GetItemsForBucket_InvalidBucketName() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetItemsForBucket("INVALID", filter: false); + + Assert.Empty(actual); + } + + [Fact] + public void GetItemsForBucket_RemovedFilter() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.RemoveKey, true); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetItemsForBucket("machine", filter: true); + + Assert.Empty(actual); + } + + [Fact] + public void GetItemsForBucket_RemovedNoFilter() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.RemoveKey, true); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetItemsForBucket("machine", filter: false); + + Assert.Single(actual); + } + + [Fact] + public void GetItemsForBucket_Standard() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetItemsForBucket("machine", filter: false); + + Assert.Single(actual); + } + + #endregion + + #region GetMachine + + [Fact] + public void GetMachineTest() + { + Machine machine = new Machine(); + var dict = new ItemDictionaryDB(); + long machineIndex = dict.AddMachine(machine); + + Assert.Equal(0, machineIndex); + var actual = dict.GetMachine(machineIndex); + Assert.NotNull(actual); + } + + #endregion + + #region GetMachineForItem + + [Fact] + public void GetMachineForItemTest() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + DatItem item = new Rom(); + + var dict = new ItemDictionaryDB(); + long machineIndex = dict.AddMachine(machine); + long sourceIndex = dict.AddSource(source); + long itemIndex = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetMachineForItem(itemIndex); + Assert.Equal(0, actual.Key); + Assert.NotNull(actual.Value); + } + + #endregion + + #region GetSource + + [Fact] + public void GetSourceTest() + { + Source source = new Source(0, source: null); + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + + Assert.Equal(0, sourceIndex); + var actual = dict.GetSource(sourceIndex); + Assert.NotNull(actual); + } + + #endregion + + #region GetSourceForItem + + [Fact] + public void GetSourceForItemTest() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + DatItem item = new Rom(); + + var dict = new ItemDictionaryDB(); + long machineIndex = dict.AddMachine(machine); + long sourceIndex = dict.AddSource(source); + long itemIndex = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + var actual = dict.GetSourceForItem(itemIndex); + Assert.Equal(0, actual.Key); + Assert.NotNull(actual.Value); + } + + #endregion + + #region RemapDatItemToMachine + + [Fact] + public void RemapDatItemToMachineTest() + { + Source source = new Source(0, source: null); + + Machine origMachine = new Machine(); + origMachine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "original"); + + Machine newMachine = new Machine(); + newMachine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "new"); + + DatItem datItem = new Rom(); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long origMachineIndex = dict.AddMachine(origMachine); + long newMachineIndex = dict.AddMachine(newMachine); + long itemIndex = dict.AddItem(datItem, origMachineIndex, sourceIndex, statsOnly: false); + + dict.RemapDatItemToMachine(itemIndex, newMachineIndex); + + var actual = dict.GetMachineForItem(itemIndex); + Assert.Equal(1, actual.Key); + Assert.NotNull(actual.Value); + Assert.Equal("new", actual.Value.GetName()); + } + + #endregion + + #region RemoveBucket + + [Fact] + public void RemoveBucketTest() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem datItem = new Rom(); + datItem.SetName("rom-1"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + dict.AddItem(datItem, machineIndex, sourceIndex, statsOnly: false); + + dict.RemoveBucket("game-1"); + + Assert.Empty(dict.GetItemsForBucket("game-1")); + } + + #endregion + + #region RemoveItem + + [Fact] + public void RemoveItemTest() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem datItem = new Rom(); + datItem.SetName("rom-1"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + long itemIndex = dict.AddItem(datItem, machineIndex, sourceIndex, statsOnly: false); + + dict.RemoveItem(itemIndex); + + Assert.Empty(dict.GetItemsForBucket("game-1")); + } + + #endregion + + #region RemoveMachine + + [Fact] + public void RemoveMachineTest() + { + Machine machine = new Machine(); + var dict = new ItemDictionaryDB(); + long machineIndex = dict.AddMachine(machine); + + bool actual = dict.RemoveMachine(machineIndex); + Assert.True(actual); + Assert.Empty(dict.GetMachines()); + } + + #endregion + + #region BucketBy + + [Theory] + [InlineData(ItemKey.NULL, 2)] + [InlineData(ItemKey.Machine, 2)] + [InlineData(ItemKey.CRC, 1)] + [InlineData(ItemKey.SHA1, 4)] + public void BucketByTest(ItemKey itemKey, int expected) + { + // Setup the items + Source source = new Source(0, source: null); + + Machine machine1 = new Machine(); + machine1.SetName("game-1"); + + Machine machine2 = new Machine(); + machine2.SetName("game-2"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom3 = new Rom(); + rom3.SetName("rom-3"); + rom3.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + rom3.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "00000ea4014ce66679e7e17d56ac510f67e39e26"); + rom3.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom4 = new Rom(); + rom4.SetName("rom-4"); + rom4.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + rom4.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "00000151d437442e74e5134023fab8bf694a2487"); + rom4.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + // Setup the dictionary + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machine1Index = dict.AddMachine(machine1); + long machine2Index = dict.AddMachine(machine2); + dict.AddItem(rom1, machine1Index, sourceIndex, statsOnly: false); + dict.AddItem(rom2, machine1Index, sourceIndex, statsOnly: false); + dict.AddItem(rom3, machine2Index, sourceIndex, statsOnly: false); + dict.AddItem(rom4, machine2Index, sourceIndex, statsOnly: false); + + dict.BucketBy(itemKey); + Assert.Equal(expected, dict.SortedKeys.Length); + } + + #endregion + + #region Deduplicate + + [Fact] + public void DeduplicateTest() + { + // Setup the items + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + // Setup the dictionary + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + dict.AddItem(rom1, machineIndex, sourceIndex, statsOnly: false); + dict.AddItem(rom2, machineIndex, sourceIndex, statsOnly: false); + + dict.Deduplicate(); + Assert.Equal(1, dict.DatStatistics.TotalCount); + } + + #endregion + + #region GetDuplicateStatus + + [Fact] + public void GetDuplicateStatus_NullOther_NoDupe() + { + var dict = new ItemDictionaryDB(); + + Source? selfSource = null; + Source? lastSource = null; + + KeyValuePair? item = new KeyValuePair(0, new Rom()); + KeyValuePair? lastItem = null; + + var actual = dict.GetDuplicateStatus(item, selfSource, lastItem, lastSource); + Assert.Equal((DupeType)0x00, actual); + } + + [Fact] + public void GetDuplicateStatus_DifferentTypes_NoDupe() + { + var dict = new ItemDictionaryDB(); + + Source? selfSource = null; + Source? lastSource = null; + + KeyValuePair? rom = new KeyValuePair(0, new Rom()); + KeyValuePair? lastItem = new KeyValuePair(1, new Disk()); + var actual = dict.GetDuplicateStatus(rom, selfSource, lastItem, lastSource); + Assert.Equal((DupeType)0x00, actual); + } + + [Fact] + public void GetDuplicateStatus_MismatchedHashes_NoDupe() + { + var dict = new ItemDictionaryDB(); + + Source? sourceA = new Source(0); + long sourceAIndex = dict.AddSource(sourceA); + Source? sourceB = new Source(1); + long sourceBIndex = dict.AddSource(sourceB); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + long machineAIndex = dict.AddMachine(machineA); + + Machine? machineB = new Machine(); + machineB.SetName("name-same"); + long machineBIndex = dict.AddMachine(machineB); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "BEEFDEAD"); + long romAIndex = dict.AddItem(romA, machineAIndex, sourceAIndex); + KeyValuePair? romAPair = new KeyValuePair(romAIndex, romA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romBIndex = dict.AddItem(romB, machineBIndex, sourceBIndex); + KeyValuePair? romBPair = new KeyValuePair(romBIndex, romB); + + var actual = dict.GetDuplicateStatus(romAPair, sourceA, romBPair, sourceB); + Assert.Equal((DupeType)0x00, actual); + } + + [Fact] + public void GetDuplicateStatus_DifferentSource_NameMatch_ExternalAll() + { + var dict = new ItemDictionaryDB(); + + Source? sourceA = new Source(0); + long sourceAIndex = dict.AddSource(sourceA); + Source? sourceB = new Source(1); + long sourceBIndex = dict.AddSource(sourceB); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + long machineAIndex = dict.AddMachine(machineA); + + Machine? machineB = new Machine(); + machineB.SetName("name-same"); + long machineBIndex = dict.AddMachine(machineB); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romAIndex = dict.AddItem(romA, machineAIndex, sourceAIndex); + KeyValuePair? romAPair = new KeyValuePair(romAIndex, romA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romBIndex = dict.AddItem(romB, machineBIndex, sourceBIndex); + KeyValuePair? romBPair = new KeyValuePair(romBIndex, romB); + + var actual = dict.GetDuplicateStatus(romAPair, sourceA, romBPair, sourceB); + Assert.Equal(DupeType.External | DupeType.All, actual); + } + + [Fact] + public void GetDuplicateStatus_DifferentSource_NoNameMatch_ExternalHash() + { + var dict = new ItemDictionaryDB(); + + Source? sourceA = new Source(0); + long sourceAIndex = dict.AddSource(sourceA); + Source? sourceB = new Source(1); + long sourceBIndex = dict.AddSource(sourceB); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + long machineAIndex = dict.AddMachine(machineA); + + Machine? machineB = new Machine(); + machineB.SetName("not-name-same"); + long machineBIndex = dict.AddMachine(machineB); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romAIndex = dict.AddItem(romA, machineAIndex, sourceAIndex); + KeyValuePair? romAPair = new KeyValuePair(romAIndex, romA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romBIndex = dict.AddItem(romB, machineBIndex, sourceBIndex); + KeyValuePair? romBPair = new KeyValuePair(romBIndex, romB); + + var actual = dict.GetDuplicateStatus(romAPair, sourceA, romBPair, sourceB); + Assert.Equal(DupeType.External | DupeType.Hash, actual); + } + + [Fact] + public void GetDuplicateStatus_SameSource_NameMatch_InternalAll() + { + var dict = new ItemDictionaryDB(); + + Source? sourceA = new Source(0); + long sourceAIndex = dict.AddSource(sourceA); + Source? sourceB = new Source(0); + long sourceBIndex = dict.AddSource(sourceB); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + long machineAIndex = dict.AddMachine(machineA); + + Machine? machineB = new Machine(); + machineB.SetName("name-same"); + long machineBIndex = dict.AddMachine(machineB); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romAIndex = dict.AddItem(romA, machineAIndex, sourceAIndex); + KeyValuePair? romAPair = new KeyValuePair(romAIndex, romA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romBIndex = dict.AddItem(romB, machineBIndex, sourceBIndex); + KeyValuePair? romBPair = new KeyValuePair(romBIndex, romB); + + var actual = dict.GetDuplicateStatus(romAPair, sourceA, romBPair, sourceB); + Assert.Equal(DupeType.Internal | DupeType.All, actual); + } + + [Fact] + public void GetDuplicateStatus_SameSource_NoNameMatch_InternalHash() + { + var dict = new ItemDictionaryDB(); + + Source? sourceA = new Source(0); + long sourceAIndex = dict.AddSource(sourceA); + Source? sourceB = new Source(0); + long sourceBIndex = dict.AddSource(sourceB); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + long machineAIndex = dict.AddMachine(machineA); + + Machine? machineB = new Machine(); + machineB.SetName("not-name-same"); + long machineBIndex = dict.AddMachine(machineB); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romAIndex = dict.AddItem(romA, machineAIndex, sourceAIndex); + KeyValuePair? romAPair = new KeyValuePair(romAIndex, romA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + long romBIndex = dict.AddItem(romB, machineBIndex, sourceBIndex); + KeyValuePair? romBPair = new KeyValuePair(romBIndex, romB); + + var actual = dict.GetDuplicateStatus(romAPair, sourceA, romBPair, sourceB); + Assert.Equal(DupeType.Internal | DupeType.Hash, actual); + } + + #endregion + + #region GetDuplicates + + [Theory] + [InlineData(true, 1)] + [InlineData(false, 0)] + public void GetDuplicatesTest(bool hasDuplicate, int expected) + { + // Setup the items + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + // Setup the dictionary + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + dict.AddItem(rom1, machineIndex, sourceIndex, statsOnly: false); + dict.AddItem(rom2, machineIndex, sourceIndex, statsOnly: false); + + // Setup the test item + DatItem rom = new Rom(); + rom.SetName("rom-1"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, hasDuplicate ? "1024" : "2048"); + + var actual = dict.GetDuplicates(new KeyValuePair(-1, rom)); + Assert.Equal(expected, actual.Count); + } + + #endregion + + #region HasDuplicates + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void HasDuplicatesTest(bool expected) + { + // Setup the items + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + + // Setup the dictionary + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + dict.AddItem(rom1, machineIndex, sourceIndex, statsOnly: false); + dict.AddItem(rom2, machineIndex, sourceIndex, statsOnly: false); + + // Setup the test item + DatItem rom = new Rom(); + rom.SetName("rom-1"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, expected ? "1024" : "2048"); + + bool actual = dict.HasDuplicates(new KeyValuePair(-1, rom)); + Assert.Equal(expected, actual); + } + + #endregion + + #region RecalculateStats + + [Fact] + public void RecalculateStatsTest() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetName("rom"); + item.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + item.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + + var dict = new ItemDictionaryDB(); + long sourceIndex = dict.AddSource(source); + long machineIndex = dict.AddMachine(machine); + _ = dict.AddItem(item, machineIndex, sourceIndex, statsOnly: false); + + Assert.Equal(1, dict.DatStatistics.TotalCount); + Assert.Equal(1, dict.DatStatistics.GetItemCount(ItemType.Rom)); + Assert.Equal(12345, dict.DatStatistics.TotalSize); + Assert.Equal(1, dict.DatStatistics.GetHashCount(HashType.CRC32)); + Assert.Equal(0, dict.DatStatistics.GetHashCount(HashType.MD5)); + + item.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "deadbeef"); + + dict.RecalculateStats(); + + Assert.Equal(1, dict.DatStatistics.TotalCount); + Assert.Equal(1, dict.DatStatistics.GetItemCount(ItemType.Rom)); + Assert.Equal(12345, dict.DatStatistics.TotalSize); + Assert.Equal(1, dict.DatStatistics.GetHashCount(HashType.CRC32)); + Assert.Equal(1, dict.DatStatistics.GetHashCount(HashType.MD5)); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/ItemDictionaryTests.cs b/SabreTools.Metadata.DatFiles.Test/ItemDictionaryTests.cs new file mode 100644 index 00000000..76f8161c --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/ItemDictionaryTests.cs @@ -0,0 +1,818 @@ +using System.Collections.Generic; +using SabreTools.Hashing; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatFiles.Test +{ + public class ItemDictionaryTests + { + #region AddItem + + [Fact] + public void AddItem_Disk_WithHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem disk = new Disk(); + disk.SetName("item"); + disk.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "deadbeef"); + disk.SetFieldValue(DatItem.SourceKey, source); + disk.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(disk, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Disk); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey)); + } + + [Fact] + public void AddItem_Disk_WithoutHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem disk = new Disk(); + disk.SetName("item"); + disk.SetFieldValue(DatItem.SourceKey, source); + disk.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(disk, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Disk); + Assert.Equal("nodump", actual.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey)); + } + + [Fact] + public void AddItem_File_WithHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + var file = new File(); + file.SetName("item"); + file.SHA1 = "deadbeef"; + file.SetFieldValue(DatItem.SourceKey, source); + file.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(file, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is File); + //Assert.Equal("none", actual.GetStringFieldValue(File.StatusKey)); + } + + [Fact] + public void AddItem_File_WithoutHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem file = new File(); + file.SetName("item"); + file.SetFieldValue(DatItem.SourceKey, source); + file.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(file, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is File); + //Assert.Equal("nodump", actual.GetStringFieldValue(File.StatusKey)); + } + + [Fact] + public void AddItem_Media_WithHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem media = new Media(); + media.SetName("item"); + media.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "deadbeef"); + media.SetFieldValue(DatItem.SourceKey, source); + media.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(media, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Media); + //Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Media.StatusKey)); + } + + [Fact] + public void AddItem_Media_WithoutHashes() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem media = new Media(); + media.SetName("item"); + media.SetFieldValue(DatItem.SourceKey, source); + media.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(media, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Media); + //Assert.Equal("nodump", actual.GetStringFieldValue(Data.Models.Metadata.Media.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithHashesWithSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "deadbeef"); + rom.SetFieldValue(DatItem.SourceKey, source); + rom.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(rom, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Rom); + Assert.Equal(12345, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithoutHashesWithSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + rom.SetFieldValue(DatItem.SourceKey, source); + rom.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(rom, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Rom); + Assert.Equal(12345, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Null(actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("nodump", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithHashesWithoutSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "deadbeef"); + rom.SetFieldValue(DatItem.SourceKey, source); + rom.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(rom, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Rom); + Assert.Null(actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_Rom_WithoutHashesWithoutSize() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem rom = new Rom(); + rom.SetName("item"); + rom.SetFieldValue(DatItem.SourceKey, source); + rom.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(rom, statsOnly: false); + + DatItem actual = Assert.Single(dict.GetItemsForBucket("default")); + Assert.True(actual is Rom); + Assert.Equal(0, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal(HashType.SHA1.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("none", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + } + + [Fact] + public void AddItem_StatsOnly() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem item = new Rom(); + item.SetName("item"); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: true); + + Assert.Empty(dict.GetItemsForBucket("default")); + } + + [Fact] + public void AddItem_NormalAdd() + { + Source source = new Source(0, source: null); + Machine machine = new Machine(); + + DatItem item = new Rom(); + item.SetName("item"); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + Assert.Single(dict.GetItemsForBucket("default")); + } + + #endregion + + #region ClearMarked + + [Fact] + public void ClearMarkedTest() + { + // Setup the items + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + rom2.SetFieldValue(DatItem.RemoveKey, true); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom2.CopyMachineInformation(machine); + + // Setup the dictionary + var dict = new ItemDictionary(); + dict.AddItem(rom1, statsOnly: false); + dict.AddItem(rom2, statsOnly: false); + + dict.ClearMarked(); + Assert.Empty(dict.GetItemsForBucket("default")); + List items = dict.GetItemsForBucket("game-1"); + Assert.Single(items); + } + + #endregion + + #region GetItemsForBucket + + [Fact] + public void GetItemsForBucket_NullBucketName() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + var actual = dict.GetItemsForBucket(null, filter: false); + + Assert.Empty(actual); + } + + [Fact] + public void GetItemsForBucket_InvalidBucketName() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + var actual = dict.GetItemsForBucket("INVALID", filter: false); + + Assert.Empty(actual); + } + + [Fact] + public void GetItemsForBucket_RemovedFilter() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.RemoveKey, true); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + var actual = dict.GetItemsForBucket("machine", filter: true); + + Assert.Empty(actual); + } + + [Fact] + public void GetItemsForBucket_RemovedNoFilter() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.RemoveKey, true); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + var actual = dict.GetItemsForBucket("machine", filter: false); + + Assert.Single(actual); + } + + [Fact] + public void GetItemsForBucket_Standard() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + var actual = dict.GetItemsForBucket("machine", filter: false); + + Assert.Single(actual); + } + + #endregion + + #region RemoveBucket + + [Fact] + public void RemoveBucketTest() + { + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem datItem = new Rom(); + datItem.SetName("rom-1"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + datItem.CopyMachineInformation(machine); + + var dict = new ItemDictionary(); + dict.AddItem(datItem, statsOnly: false); + + dict.RemoveBucket("game-1"); + + Assert.Empty(dict.GetItemsForBucket("default")); + Assert.Empty(dict.GetItemsForBucket("game-1")); + } + + #endregion + + #region RemoveItem + + [Fact] + public void RemoveItemTest() + { + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem datItem = new Rom(); + datItem.SetName("rom-1"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + datItem.CopyMachineInformation(machine); + + var dict = new ItemDictionary(); + dict.AddItem(datItem, statsOnly: false); + + dict.RemoveItem("game-1", (Rom)datItem.Clone(), 0); + + Assert.Empty(dict.GetItemsForBucket("default")); + Assert.Empty(dict.GetItemsForBucket("game-1")); + } + + #endregion + + #region BucketBy + + [Theory] + [InlineData(ItemKey.NULL, 2)] + [InlineData(ItemKey.Machine, 2)] + [InlineData(ItemKey.CRC, 1)] + [InlineData(ItemKey.SHA1, 4)] + public void BucketByTest(ItemKey itemKey, int expected) + { + // Setup the items + Machine machine1 = new Machine(); + machine1.SetName("game-1"); + + Machine machine2 = new Machine(); + machine2.SetName("game-2"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine1); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine1); + + DatItem rom3 = new Rom(); + rom3.SetName("rom-3"); + rom3.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + rom3.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "00000ea4014ce66679e7e17d56ac510f67e39e26"); + rom3.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine2); + + DatItem rom4 = new Rom(); + rom4.SetName("rom-4"); + rom4.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEAEEF"); + rom4.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "00000151d437442e74e5134023fab8bf694a2487"); + rom4.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine2); + + // Setup the dictionary + var dict = new ItemDictionary(); + dict.AddItem(rom1, statsOnly: false); + dict.AddItem(rom2, statsOnly: false); + dict.AddItem(rom3, statsOnly: false); + dict.AddItem(rom4, statsOnly: false); + + dict.BucketBy(itemKey); + Assert.Equal(expected, dict.SortedKeys.Length); + } + + #endregion + + #region Deduplicate + + [Fact] + public void DeduplicateTest() + { + // Setup the items + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom2.CopyMachineInformation(machine); + + // Setup the dictionary + var dict = new ItemDictionary(); + dict.AddItem(rom1, statsOnly: false); + dict.AddItem(rom2, statsOnly: false); + + dict.Deduplicate(); + Assert.Equal(1, dict.DatStatistics.TotalCount); + } + + #endregion + + #region GetDuplicateStatus + + [Fact] + public void GetDuplicateStatus_NullOther_NoDupe() + { + var dict = new ItemDictionary(); + DatItem item = new Rom(); + DatItem? lastItem = null; + var actual = dict.GetDuplicateStatus(item, lastItem); + Assert.Equal((DupeType)0x00, actual); + } + + [Fact] + public void GetDuplicateStatus_DifferentTypes_NoDupe() + { + var dict = new ItemDictionary(); + var rom = new Rom(); + DatItem? lastItem = new Disk(); + var actual = dict.GetDuplicateStatus(rom, lastItem); + Assert.Equal((DupeType)0x00, actual); + } + + [Fact] + public void GetDuplicateStatus_MismatchedHashes_NoDupe() + { + var dict = new ItemDictionary(); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + + Machine? machineB = new Machine(); + machineB.SetName("name-same"); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "BEEFDEAD"); + romA.SetFieldValue(DatItem.SourceKey, new Source(0)); + romA.CopyMachineInformation(machineA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romB.SetFieldValue(DatItem.SourceKey, new Source(1)); + romB.CopyMachineInformation(machineB); + + var actual = dict.GetDuplicateStatus(romA, romB); + Assert.Equal((DupeType)0x00, actual); + } + + [Fact] + public void GetDuplicateStatus_DifferentSource_NameMatch_ExternalAll() + { + var dict = new ItemDictionary(); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + + Machine? machineB = new Machine(); + machineB.SetName("name-same"); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romA.SetFieldValue(DatItem.SourceKey, new Source(0)); + romA.CopyMachineInformation(machineA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romB.SetFieldValue(DatItem.SourceKey, new Source(1)); + romB.CopyMachineInformation(machineB); + + var actual = dict.GetDuplicateStatus(romA, romB); + Assert.Equal(DupeType.External | DupeType.All, actual); + } + + [Fact] + public void GetDuplicateStatus_DifferentSource_NoNameMatch_ExternalHash() + { + var dict = new ItemDictionary(); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + + Machine? machineB = new Machine(); + machineB.SetName("not-name-same"); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romA.SetFieldValue(DatItem.SourceKey, new Source(0)); + romA.CopyMachineInformation(machineA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romB.SetFieldValue(DatItem.SourceKey, new Source(1)); + romB.CopyMachineInformation(machineB); + + var actual = dict.GetDuplicateStatus(romA, romB); + Assert.Equal(DupeType.External | DupeType.Hash, actual); + } + + [Fact] + public void GetDuplicateStatus_SameSource_NameMatch_InternalAll() + { + var dict = new ItemDictionary(); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + + Machine? machineB = new Machine(); + machineB.SetName("name-same"); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romA.SetFieldValue(DatItem.SourceKey, new Source(0)); + romA.CopyMachineInformation(machineA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romB.SetFieldValue(DatItem.SourceKey, new Source(0)); + romB.CopyMachineInformation(machineB); + + var actual = dict.GetDuplicateStatus(romA, romB); + Assert.Equal(DupeType.Internal | DupeType.All, actual); + } + + [Fact] + public void GetDuplicateStatus_SameSource_NoNameMatch_InternalHash() + { + var dict = new ItemDictionary(); + + Machine? machineA = new Machine(); + machineA.SetName("name-same"); + + Machine? machineB = new Machine(); + machineB.SetName("not-name-same"); + + var romA = new Rom(); + romA.SetName("same-name"); + romA.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romA.SetFieldValue(DatItem.SourceKey, new Source(0)); + romA.CopyMachineInformation(machineA); + + var romB = new Rom(); + romB.SetName("same-name"); + romB.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + romB.SetFieldValue(DatItem.SourceKey, new Source(0)); + romB.CopyMachineInformation(machineB); + + var actual = dict.GetDuplicateStatus(romA, romB); + Assert.Equal(DupeType.Internal | DupeType.Hash, actual); + } + + #endregion + + #region GetDuplicates + + [Theory] + [InlineData(true, 1)] + [InlineData(false, 0)] + public void GetDuplicatesTest(bool hasDuplicate, int expected) + { + // Setup the items + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom2.CopyMachineInformation(machine); + + // Setup the dictionary + var dict = new ItemDictionary(); + dict.AddItem(rom1, statsOnly: false); + dict.AddItem(rom2, statsOnly: false); + + // Setup the test item + DatItem rom = new Rom(); + rom.SetName("rom-1"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, hasDuplicate ? "1024" : "2048"); + rom.CopyMachineInformation(machine); + + var actual = dict.GetDuplicates(rom); + Assert.Equal(expected, actual.Count); + } + + #endregion + + #region HasDuplicates + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void HasDuplicatesTest(bool expected) + { + // Setup the items + Machine machine = new Machine(); + machine.SetName("game-1"); + + DatItem rom1 = new Rom(); + rom1.SetName("rom-1"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom1.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine); + + DatItem rom2 = new Rom(); + rom2.SetName("rom-2"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "000000e948edcb4f7704b8af85a77a3339ecce44"); + rom2.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "1024"); + rom1.CopyMachineInformation(machine); + + // Setup the dictionary + var dict = new ItemDictionary(); + dict.AddItem("game-1", rom1); + dict.AddItem("game-1", rom2); + + // Setup the test item + DatItem rom = new Rom(); + rom.SetName("rom-1"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "0000000fbbb37f8488100b1b4697012de631a5e6"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, expected ? "1024" : "2048"); + rom1.CopyMachineInformation(machine); + + bool actual = dict.HasDuplicates(rom); + Assert.Equal(expected, actual); + } + + #endregion + + #region RecalculateStats + + [Fact] + public void RecalculateStatsTest() + { + Source source = new Source(0, source: null); + + Machine machine = new Machine(); + machine.SetName("machine"); + + DatItem item = new Rom(); + item.SetName("rom"); + item.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, 12345); + item.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "deadbeef"); + item.SetFieldValue(DatItem.SourceKey, source); + item.SetFieldValue(DatItem.MachineKey, machine); + + var dict = new ItemDictionary(); + _ = dict.AddItem(item, statsOnly: false); + + Assert.Equal(1, dict.DatStatistics.TotalCount); + Assert.Equal(1, dict.DatStatistics.GetItemCount(ItemType.Rom)); + Assert.Equal(12345, dict.DatStatistics.TotalSize); + Assert.Equal(1, dict.DatStatistics.GetHashCount(HashType.CRC32)); + Assert.Equal(0, dict.DatStatistics.GetHashCount(HashType.MD5)); + + item.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "deadbeef"); + + dict.RecalculateStats(); + + Assert.Equal(1, dict.DatStatistics.TotalCount); + Assert.Equal(1, dict.DatStatistics.GetItemCount(ItemType.Rom)); + Assert.Equal(12345, dict.DatStatistics.TotalSize); + Assert.Equal(1, dict.DatStatistics.GetHashCount(HashType.CRC32)); + Assert.Equal(1, dict.DatStatistics.GetHashCount(HashType.MD5)); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles.Test/SabreTools.Metadata.DatFiles.Test.csproj b/SabreTools.Metadata.DatFiles.Test/SabreTools.Metadata.DatFiles.Test.csproj new file mode 100644 index 00000000..959644aa --- /dev/null +++ b/SabreTools.Metadata.DatFiles.Test/SabreTools.Metadata.DatFiles.Test.csproj @@ -0,0 +1,24 @@ + + + + net8.0;net9.0;net10.0 + false + latest + enable + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SabreTools.Metadata.DatFiles/DatFile.Filtering.cs b/SabreTools.Metadata.DatFiles/DatFile.Filtering.cs new file mode 100644 index 00000000..ac991cc0 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatFile.Filtering.cs @@ -0,0 +1,801 @@ +using System; +using System.Collections.Generic; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Collections.Concurrent; +#endif +using System.IO; +using System.Text.RegularExpressions; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Threading.Tasks; +#endif +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +#pragma warning disable IDE0057 // Use range operator +#pragma warning disable IDE0059 // Unnecessary assignment of a value +namespace SabreTools.Metadata.DatFiles +{ + public partial class DatFile + { + #region Constants + + /// + /// Scene name Regex pattern + /// + private const string SceneNamePattern = @"([0-9]{2}\.[0-9]{2}\.[0-9]{2}-)(.*?-.*?)"; + + #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) + { + ExecuteFiltersImpl(filterRunner); + ExecuteFiltersImplDB(filterRunner); + } + + /// + /// Use game descriptions as names, updating cloneof/romof/sampleof + /// + /// True if the error that is thrown should be thrown back to the caller, false otherwise + public void MachineDescriptionToName(bool throwOnError = false) + { + MachineDescriptionToNameImpl(throwOnError); + MachineDescriptionToNameImplDB(throwOnError); + } + + /// + /// Ensure that all roms are in their own game (or at least try to ensure) + /// + public void SetOneRomPerGame() + { + SetOneRomPerGameImpl(); + SetOneRomPerGameImplDB(); + } + + /// + /// Filter a DAT using 1G1R logic given an ordered set of regions + /// + /// List of regions in order of priority + /// + /// In the most technical sense, the way that the region list is being used does not + /// confine its values to be just regions. Since it's essentially acting like a + /// specialized version of the machine name filter, anything that is usually encapsulated + /// in parenthesis would be matched on, including disc numbers, languages, editions, + /// and anything else commonly used. Please note that, unlike other existing 1G1R + /// solutions, this does not have the ability to contain custom mappings of parent + /// to clone sets based on name, nor does it have the ability to match on the + /// Release DatItem type. + /// + public void SetOneGamePerRegion(List regionList) + { + SetOneGamePerRegionImpl(regionList); + SetOneGamePerRegionImplDB(regionList); + } + + /// + /// Strip the dates from the beginning of scene-style set names + /// + public void StripSceneDatesFromItems() + { + StripSceneDatesFromItemsImpl(); + StripSceneDatesFromItemsImplDB(); + } + + #endregion + + #region Filtering Implementations + + /// + /// Create machine to description mapping dictionary + /// + /// Applies to + private IDictionary CreateMachineToDescriptionMapping() + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + ConcurrentDictionary mapping = new(); +#else + Dictionary mapping = []; +#endif +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(Items.SortedKeys, key => +#else + foreach (var key in Items.SortedKeys) +#endif + { + var items = GetItemsForBucket(key); + if (items is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + foreach (DatItem item in items) + { + // Get the current machine + var machine = item.GetMachine(); + if (machine is null) + continue; + + // Get the values to check against + string? machineName = machine.GetName(); + string? machineDesc = machine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey); + if (machineName is null || machineDesc is null) + continue; + + // Adjust the description + machineDesc = machineDesc.Replace('/', '_').Replace("\"", "''").Replace(":", " -"); + if (machineName == machineDesc) + continue; + + // If the key mapping doesn't exist, add it +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + mapping.TryAdd(machineName, machineDesc); +#else + if (!mapping.ContainsKey(machineName)) + mapping[machineName] = machineDesc; +#endif + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + + return mapping; + } + + /// + /// Create machine to description mapping dictionary + /// + /// Applies to + private Dictionary CreateMachineToDescriptionMappingDB() + { + Dictionary mapping = []; + foreach (var machine in GetMachinesDB()) + { + // Get the current machine + if (machine.Value is null) + continue; + + // Get the values to check against + string? machineName = machine.Value.GetName(); + string? machineDesc = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey); + if (machineName is null || machineDesc is null) + continue; + + // Adjust the description + machineDesc = machineDesc.Replace('/', '_').Replace("\"", "''").Replace(":", " -"); + if (machineName == machineDesc) + continue; + + // If the key mapping doesn't exist, add it + if (!mapping.ContainsKey(machineName)) + mapping[machineName] = machineDesc; + } + + return mapping; + } + + /// + /// Execute all filters in a filter runner on the items in the dictionary + /// + /// Preconfigured filter runner to use + /// Applies to + private void ExecuteFiltersImpl(FilterRunner filterRunner) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(Items.SortedKeys, key => +#else + foreach (var key in Items.SortedKeys) +#endif + { + ExecuteFilterOnBucket(filterRunner, key); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Execute all filters in a filter runner on the items in the dictionary + /// + /// Preconfigured filter runner to use + /// Applies to + private void ExecuteFiltersImplDB(FilterRunner filterRunner) + { + List keys = [.. ItemsDB.SortedKeys]; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(keys, key => +#else + foreach (var key in keys) +#endif + { + ExecuteFilterOnBucketDB(filterRunner, key); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Execute all filters in a filter runner on a single bucket + /// + /// Preconfigured filter runner to use + /// Name of the bucket to filter on + /// Applies to + private void ExecuteFilterOnBucket(FilterRunner filterRunner, string bucketName) + { + List? items = GetItemsForBucket(bucketName); + if (items is null) + return; + + // Filter all items in the current key + foreach (var item in items) + { + if (!item.PassesFilter(filterRunner)) + item.SetFieldValue(DatItem.RemoveKey, true); + } + } + + /// + /// Execute all filters in a filter runner on a single bucket + /// + /// Preconfigured filter runner to use + /// Name of the bucket to filter on + /// Applies to + private void ExecuteFilterOnBucketDB(FilterRunner filterRunner, string bucketName) + { + var items = GetItemsForBucketDB(bucketName); + if (items is null) + return; + + // Filter all items in the current key + List newItems = []; + foreach (var item in items) + { + if (!item.Value.PassesFilterDB(filterRunner)) + item.Value.SetFieldValue(DatItem.RemoveKey, true); + } + } + + /// + /// Use game descriptions as names, updating cloneof/romof/sampleof + /// + /// True if the error that is thrown should be thrown back to the caller, false otherwise + /// Applies to + private void MachineDescriptionToNameImpl(bool throwOnError = false) + { + try + { + // First we want to get a mapping for all games to description + var mapping = CreateMachineToDescriptionMapping(); + + // Now we loop through every item and update accordingly + UpdateMachineNamesFromDescriptions(mapping); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Warning(ex.ToString()); + } + } + + /// + /// Use game descriptions as names, updating cloneof/romof/sampleof + /// + /// True if the error that is thrown should be thrown back to the caller, false otherwise + /// Applies to + private void MachineDescriptionToNameImplDB(bool throwOnError = false) + { + try + { + // First we want to get a mapping for all games to description + var mapping = CreateMachineToDescriptionMappingDB(); + + // Now we loop through every item and update accordingly + UpdateMachineNamesFromDescriptionsDB(mapping); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Warning(ex.ToString()); + } + } + + /// + /// Filter a DAT using 1G1R logic given an ordered set of regions + /// + /// List of regions in order of priority + /// Applies to + private void SetOneGamePerRegionImpl(List regionList) + { + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Then we want to get a mapping of all machines to parents + Dictionary> parents = []; + foreach (string key in Items.SortedKeys) + { + DatItem item = GetItemsForBucket(key)[0]; + + // Get machine information + Machine? machine = item.GetMachine(); + string? machineName = machine?.GetName()?.ToLowerInvariant(); + if (machine is null || machineName is null) + continue; + + // Get the string values + string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)?.ToLowerInvariant(); + string? romOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)?.ToLowerInvariant(); + + // Match on CloneOf first + if (!string.IsNullOrEmpty(cloneOf)) + { + if (!parents.ContainsKey(cloneOf!)) + parents.Add(cloneOf!, []); + + parents[cloneOf!].Add(machineName); + } + + // Then by RomOf + else if (!string.IsNullOrEmpty(romOf)) + { + if (!parents.ContainsKey(romOf!)) + parents.Add(romOf!, []); + + parents[romOf!].Add(machineName); + } + + // Otherwise, treat it as a parent + else + { + if (!parents.ContainsKey(machineName)) + parents.Add(machineName, []); + + parents[machineName].Add(machineName); + } + } + + // Once we have the full list of mappings, filter out games to keep + foreach (string key in parents.Keys) + { + // Find the first machine that matches the regions in order, if possible + string? machine = default; + foreach (string region in regionList) + { + machine = parents[key].Find(m => Regex.IsMatch(m, @"\(.*" + region + @".*\)", RegexOptions.IgnoreCase)); + if (machine != default) + break; + } + + // If we didn't get a match, use the parent + if (machine == default) + machine = key; + + // Remove the key from the list + parents[key].Remove(machine); + + // Remove the rest of the items from this key + parents[key].ForEach(k => RemoveBucket(k)); + } + + // Finally, strip out the parent tags + RemoveMachineRelationshipTagsImpl(); + } + + /// + /// Filter a DAT using 1G1R logic given an ordered set of regions + /// + /// List of regions in order of priority + /// Applies to + private void SetOneGamePerRegionImplDB(List regionList) + { + // Then we want to get a mapping of all machines to parents + Dictionary> parents = []; + foreach (var machine in GetMachinesDB()) + { + if (machine.Value is null) + continue; + + // Get machine information + Machine? machineObj = machine.Value; + string? machineName = machineObj?.GetName()?.ToLowerInvariant(); + if (machineObj is null || machineName is null) + continue; + + // Get the string values + string? cloneOf = machineObj.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey)?.ToLowerInvariant(); + string? romOf = machineObj.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey)?.ToLowerInvariant(); + + // Match on CloneOf first + if (!string.IsNullOrEmpty(cloneOf)) + { + if (!parents.ContainsKey(cloneOf!)) + parents.Add(cloneOf!, []); + + parents[cloneOf!].Add(machineName); + } + + // Then by RomOf + else if (!string.IsNullOrEmpty(romOf)) + { + if (!parents.ContainsKey(romOf!)) + parents.Add(romOf!, []); + + parents[romOf!].Add(machineName); + } + + // Otherwise, treat it as a parent + else + { + if (!parents.ContainsKey(machineName)) + parents.Add(machineName, []); + + parents[machineName].Add(machineName); + } + } + + // Once we have the full list of mappings, filter out games to keep + foreach (string key in parents.Keys) + { + // Find the first machine that matches the regions in order, if possible + string? machine = default; + foreach (string region in regionList) + { + machine = parents[key].Find(m => Regex.IsMatch(m, @"\(.*" + region + @".*\)", RegexOptions.IgnoreCase)); + if (machine != default) + break; + } + + // If we didn't get a match, use the parent + if (machine == default) + machine = key; + + // Remove the key from the list + parents[key].Remove(machine); + + // Remove the rest of the items from this key + parents[key].ForEach(k => RemoveMachineDB(k)); + } + + // Finally, strip out the parent tags + RemoveMachineRelationshipTagsImplDB(); + } + + /// + /// Ensure that all roms are in their own game (or at least try to ensure) + /// + /// Applies to + private void SetOneRomPerGameImpl() + { + // For each rom, we want to update the game to be "/" +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(Items.SortedKeys, key => +#else + foreach (var key in Items.SortedKeys) +#endif + { + var items = GetItemsForBucket(key); + if (items is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + for (int i = 0; i < items.Count; i++) + { + SetOneRomPerGameImpl(items[i]); + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Set internal names to match One Rom Per Game (ORPG) logic + /// + /// DatItem to run logic on + /// Applies to + private static void SetOneRomPerGameImpl(DatItem datItem) + { + // If the item name is null + string? itemName = datItem.GetName(); + if (itemName is null) + return; + + // Get the current machine + var machine = datItem.GetMachine(); + if (machine is null) + return; + + // Clone current machine to avoid conflict + machine = (Machine)machine.Clone(); + + // Reassign the item to the new machine + datItem.SetFieldValue(DatItem.MachineKey, machine); + + // Remove extensions from File and Rom items + if (datItem is DatItems.Formats.File || datItem is Rom) + { + string[] splitname = itemName.Split('.'); + itemName = machine.GetName() + + $"/{string.Join(".", splitname, 0, splitname.Length > 1 ? splitname.Length - 1 : 1)}"; + } + else + { + itemName = machine.GetName() + $"/{itemName}"; + } + + // Strip off "Default" prefix only for ORPG + if (itemName.StartsWith("Default")) + itemName = itemName.Substring("Default".Length + 1); + + machine.SetName(itemName); + datItem.SetName(Path.GetFileName(datItem.GetName())); + } + + /// + /// Ensure that all roms are in their own game (or at least try to ensure) + /// + /// Applies to + private void SetOneRomPerGameImplDB() + { + // For each rom, we want to update the game to be "/" +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(ItemsDB.SortedKeys, key => +#else + foreach (var key in ItemsDB.SortedKeys) +#endif + { + var items = GetItemsForBucketDB(key); + if (items is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + foreach (var item in items) + { + SetOneRomPerGameImplDB(item); + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Set internal names to match One Rom Per Game (ORPG) logic + /// + /// DatItem to run logic on + /// Applies to + private void SetOneRomPerGameImplDB(KeyValuePair datItem) + { + // If the item name is null + string? itemName = datItem.Value.GetName(); + if (datItem.Key < 0 || itemName is null) + return; + + // Get the current machine + var machine = GetMachineForItemDB(datItem.Key); + if (machine.Value is null) + return; + + // Clone current machine to avoid conflict + long newMachineIndex = AddMachineDB((Machine)machine.Value.Clone()); + machine = new KeyValuePair(newMachineIndex, ItemsDB.GetMachine(newMachineIndex)); + if (machine.Value is null) + return; + + // Reassign the item to the new machine + ItemsDB.RemapDatItemToMachine(datItem.Key, newMachineIndex); + + // Remove extensions from File and Rom items + if (datItem.Value is DatItems.Formats.File || datItem.Value is Rom) + { + string[] splitname = itemName.Split('.'); + itemName = machine.Value.GetName() + + $"/{string.Join(".", splitname, 0, splitname.Length > 1 ? splitname.Length - 1 : 1)}"; + } + else + { + itemName = machine.Value.GetName() + $"/{itemName}"; + } + + // Strip off "Default" prefix only for ORPG + if (itemName.StartsWith("Default")) + itemName = itemName.Substring("Default".Length + 1); + + machine.Value.SetName(itemName); + datItem.Value.SetName(Path.GetFileName(datItem.Value.GetName())); + } + + /// + /// Strip the dates from the beginning of scene-style set names + /// + /// Applies to + private void StripSceneDatesFromItemsImpl() + { + // Now process all of the roms +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(Items.SortedKeys, key => +#else + foreach (var key in Items.SortedKeys) +#endif + { + var items = GetItemsForBucket(key); + if (items is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + foreach (DatItem item in items) + { + // Get the current machine + var machine = item.GetMachine(); + if (machine is null) + continue; + + // Get the values to check against + string? machineName = machine.GetName(); + string? machineDesc = machine.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey); + + if (machineName is not null && Regex.IsMatch(machineName, SceneNamePattern)) + item.GetMachine()!.SetName(Regex.Replace(machineName, SceneNamePattern, "$2")); + + if (machineDesc is not null && Regex.IsMatch(machineDesc, SceneNamePattern)) + item.GetMachine()!.SetFieldValue(Data.Models.Metadata.Machine.DescriptionKey, Regex.Replace(machineDesc, SceneNamePattern, "$2")); + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Strip the dates from the beginning of scene-style set names + /// + /// Applies to + private void StripSceneDatesFromItemsImplDB() + { + // Now process all of the machines +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(GetMachinesDB(), machine => +#else + foreach (var machine in GetMachinesDB()) +#endif + { + // Get the current machine + if (machine.Value is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + // Get the values to check against + string? machineName = machine.Value.GetName(); + string? machineDesc = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.DescriptionKey); + + if (machineName is not null && Regex.IsMatch(machineName, SceneNamePattern)) + machine.Value.SetName(Regex.Replace(machineName, SceneNamePattern, "$2")); + + if (machineDesc is not null && Regex.IsMatch(machineDesc, SceneNamePattern)) + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.DescriptionKey, Regex.Replace(machineDesc, SceneNamePattern, "$2")); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Update machine names from descriptions according to mappings + /// + /// Applies to + private void UpdateMachineNamesFromDescriptions(IDictionary mapping) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(Items.SortedKeys, key => +#else + foreach (var key in Items.SortedKeys) +#endif + { + var items = GetItemsForBucket(key); + if (items is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + foreach (DatItem item in items) + { + // Get the current machine + var machine = item.GetMachine(); + if (machine is null) + continue; + + // Get the values to check against + string? machineName = machine.GetName(); + string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + string? romOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + string? sampleOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey); + + // Update machine name + if (machineName is not null && mapping.ContainsKey(machineName)) + machine.SetName(mapping[machineName]); + + // Update cloneof + if (cloneOf is not null && mapping.ContainsKey(cloneOf)) + machine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, mapping[cloneOf]); + + // Update romof + if (romOf is not null && mapping.ContainsKey(romOf)) + machine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, mapping[romOf]); + + // Update sampleof + if (sampleOf is not null && mapping.ContainsKey(sampleOf)) + machine.SetFieldValue(Data.Models.Metadata.Machine.SampleOfKey, mapping[sampleOf]); + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Update machine names from descriptions according to mappings + /// + /// Applies to + private void UpdateMachineNamesFromDescriptionsDB(Dictionary mapping) + { + foreach (var machine in GetMachinesDB()) + { + // Get the current machine + if (machine.Value is null) + continue; + + // Get the values to check against + string? machineName = machine.Value.GetName(); + string? cloneOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + string? romOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + string? sampleOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.SampleOfKey); + + // Update machine name + if (machineName is not null && mapping.ContainsKey(machineName)) + machine.Value.SetName(mapping[machineName]); + + // Update cloneof + if (cloneOf is not null && mapping.ContainsKey(cloneOf)) + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, mapping[cloneOf]); + + // Update romof + if (romOf is not null && mapping.ContainsKey(romOf)) + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, mapping[romOf]); + + // Update sampleof + if (sampleOf is not null && mapping.ContainsKey(sampleOf)) + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.SampleOfKey, mapping[sampleOf]); + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatFile.FromMetadata.cs b/SabreTools.Metadata.DatFiles/DatFile.FromMetadata.cs new file mode 100644 index 00000000..8e498fe7 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatFile.FromMetadata.cs @@ -0,0 +1,715 @@ +using System; +using System.Collections.Generic; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Threading.Tasks; +#endif +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +#pragma warning disable IDE0056 // Use index operator +#pragma warning disable IDE0060 // Remove unused parameter +namespace SabreTools.Metadata.DatFiles +{ + public partial class DatFile + { + #region From Metadata + + /// + /// Convert metadata information + /// + /// Metadata file to convert + /// Name of the file to be parsed + /// Index ID for the DAT + /// True if full pathnames are to be kept, false otherwise + /// True to only add item statistics while parsing, false otherwise + /// Optional FilterRunner to filter items on parse + internal void ConvertFromMetadata(Data.Models.Metadata.MetadataFile? item, + string filename, + int indexId, + bool keep, + bool statsOnly, + FilterRunner? filterRunner) + { + // If the metadata file is invalid, we can't do anything + if (item is null || item.Count == 0) + return; + + // Create an internal source and add to the dictionary + var source = new Source(indexId, filename); + // long sourceIndex = AddSourceDB(source); + + // Get the header from the metadata + var header = item.Read(Data.Models.Metadata.MetadataFile.HeaderKey); + if (header is not null) + ConvertHeader(header, keep); + + // Get the machines from the metadata + var machines = item.ReadItemArray(Data.Models.Metadata.MetadataFile.MachineKey); + if (machines is not null) + ConvertMachines(machines, source, sourceIndex: 0, statsOnly, filterRunner); + } + + /// + /// Convert header information + /// + /// Header to convert + /// True if full pathnames are to be kept, false otherwise + private void ConvertHeader(Data.Models.Metadata.Header? item, bool keep) + { + // If the header is invalid, we can't do anything + if (item is null || item.Count == 0) + return; + + // Create an internal header + var header = new DatHeader(item); + + // Convert subheader values + if (item.ContainsKey(Data.Models.Metadata.Header.CanOpenKey)) + { + var canOpen = item.Read(Data.Models.Metadata.Header.CanOpenKey); + if (canOpen?.Extension is not null) + Header.SetFieldValue(Data.Models.Metadata.Header.CanOpenKey, canOpen.Extension); + } + + if (item.ContainsKey(Data.Models.Metadata.Header.ImagesKey)) + { + var images = item.Read(Data.Models.Metadata.Header.ImagesKey); + Header.SetFieldValue(Data.Models.Metadata.Header.ImagesKey, images); + } + + if (item.ContainsKey(Data.Models.Metadata.Header.InfosKey)) + { + var infos = item.Read(Data.Models.Metadata.Header.InfosKey); + Header.SetFieldValue(Data.Models.Metadata.Header.InfosKey, infos); + } + + if (item.ContainsKey(Data.Models.Metadata.Header.NewDatKey)) + { + var newDat = item.Read(Data.Models.Metadata.Header.NewDatKey); + Header.SetFieldValue(Data.Models.Metadata.Header.NewDatKey, newDat); + } + + if (item.ContainsKey(Data.Models.Metadata.Header.SearchKey)) + { + var search = item.Read(Data.Models.Metadata.Header.SearchKey); + Header.SetFieldValue(Data.Models.Metadata.Header.SearchKey, search); + } + + // Selectively set all possible fields -- TODO: Figure out how to make this less manual + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.AuthorKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.AuthorKey, header.GetStringFieldValue(Data.Models.Metadata.Header.AuthorKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.BiosModeKey).AsMergingFlag() == MergingFlag.None) + Header.SetFieldValue(Data.Models.Metadata.Header.BiosModeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.BiosModeKey).AsMergingFlag().AsStringValue()); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.BuildKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.BuildKey, header.GetStringFieldValue(Data.Models.Metadata.Header.BuildKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.CategoryKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.CategoryKey, header.GetStringFieldValue(Data.Models.Metadata.Header.CategoryKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.CommentKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.CommentKey, header.GetStringFieldValue(Data.Models.Metadata.Header.CommentKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.DateKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.DateKey, header.GetStringFieldValue(Data.Models.Metadata.Header.DateKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.DatVersionKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.DatVersionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.DatVersionKey)); + if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.DebugKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.DebugKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.DebugKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.EmailKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.EmailKey, header.GetStringFieldValue(Data.Models.Metadata.Header.EmailKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.EmulatorVersionKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.EmulatorVersionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.EmulatorVersionKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ForceMergingKey).AsMergingFlag() == MergingFlag.None) + Header.SetFieldValue(Data.Models.Metadata.Header.ForceMergingKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ForceMergingKey).AsMergingFlag().AsStringValue()); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ForceNodumpKey).AsNodumpFlag() == NodumpFlag.None) + Header.SetFieldValue(Data.Models.Metadata.Header.ForceNodumpKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ForceNodumpKey).AsNodumpFlag().AsStringValue()); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ForcePackingKey).AsPackingFlag() == PackingFlag.None) + Header.SetFieldValue(Data.Models.Metadata.Header.ForcePackingKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ForcePackingKey).AsPackingFlag().AsStringValue()); + if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.ForceZippingKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.ForceZippingKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.ForceZippingKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.HeaderKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.HeaderKey, header.GetStringFieldValue(Data.Models.Metadata.Header.HeaderKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.HomepageKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.HomepageKey, header.GetStringFieldValue(Data.Models.Metadata.Header.HomepageKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.IdKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.IdKey, header.GetStringFieldValue(Data.Models.Metadata.Header.IdKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ImFolderKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.ImFolderKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ImFolderKey)); + if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.LockBiosModeKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.LockBiosModeKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.LockBiosModeKey)); + if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.LockRomModeKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.LockRomModeKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.LockRomModeKey)); + if (Header.GetBoolFieldValue(Data.Models.Metadata.Header.LockSampleModeKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.LockSampleModeKey, header.GetBoolFieldValue(Data.Models.Metadata.Header.LockSampleModeKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.MameConfigKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.MameConfigKey, header.GetStringFieldValue(Data.Models.Metadata.Header.MameConfigKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.NotesKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.NotesKey, header.GetStringFieldValue(Data.Models.Metadata.Header.NotesKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.PluginKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.PluginKey, header.GetStringFieldValue(Data.Models.Metadata.Header.PluginKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RefNameKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.RefNameKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RefNameKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RomModeKey).AsMergingFlag() == MergingFlag.None) + Header.SetFieldValue(Data.Models.Metadata.Header.RomModeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RomModeKey).AsMergingFlag().AsStringValue()); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RomTitleKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.RomTitleKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RomTitleKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.RootDirKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.RootDirKey, header.GetStringFieldValue(Data.Models.Metadata.Header.RootDirKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.SampleModeKey).AsMergingFlag() == MergingFlag.None) + Header.SetFieldValue(Data.Models.Metadata.Header.SampleModeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.SampleModeKey).AsMergingFlag().AsStringValue()); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.SchemaLocationKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.SchemaLocationKey, header.GetStringFieldValue(Data.Models.Metadata.Header.SchemaLocationKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsHeightKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.ScreenshotsHeightKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsHeightKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsWidthKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.ScreenshotsWidthKey, header.GetStringFieldValue(Data.Models.Metadata.Header.ScreenshotsWidthKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.SystemKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.SystemKey, header.GetStringFieldValue(Data.Models.Metadata.Header.SystemKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.TimestampKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.TimestampKey, header.GetStringFieldValue(Data.Models.Metadata.Header.TimestampKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.TypeKey, header.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.UrlKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.UrlKey, header.GetStringFieldValue(Data.Models.Metadata.Header.UrlKey)); + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.VersionKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.VersionKey, header.GetStringFieldValue(Data.Models.Metadata.Header.VersionKey)); + + // Handle implied SuperDAT + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.NameKey)?.Contains(" - SuperDAT") == true && keep) + { + if (Header.GetStringFieldValue(Data.Models.Metadata.Header.TypeKey) is null) + Header.SetFieldValue(Data.Models.Metadata.Header.TypeKey, "SuperDAT"); + } + } + + /// + /// Convert machines information + /// + /// Machine array to convert + /// Source to use with the converted items + /// Index of the Source to use with the converted items + /// True to only add item statistics while parsing, false otherwise + /// Optional FilterRunner to filter items on parse + private void ConvertMachines(Data.Models.Metadata.Machine[]? items, + Source source, + long sourceIndex, + bool statsOnly, + FilterRunner? filterRunner) + { + // If the array is invalid, we can't do anything + if (items is null || items.Length == 0) + return; + + // Loop through the machines and add +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(items, machine => +#else + foreach (var machine in items) +#endif + { + ConvertMachine(machine, source, sourceIndex, statsOnly, filterRunner); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Convert machine information + /// + /// Machine to convert + /// Source to use with the converted items + /// Index of the Source to use with the converted items + /// True to only add item statistics while parsing, false otherwise + /// Optional FilterRunner to filter items on parse + private void ConvertMachine(Data.Models.Metadata.Machine? item, + Source source, + long sourceIndex, + bool statsOnly, + FilterRunner? filterRunner) + { + // If the machine is invalid, we can't do anything + if (item is null || item.Count == 0) + return; + + // If the machine doesn't pass the filter + if (filterRunner is not null && !filterRunner.Run(item)) + return; + + // Create an internal machine and add to the dictionary + var machine = new Machine(item); + // long machineIndex = AddMachineDB(machine); + + // Convert items in the machine + if (item.ContainsKey(Data.Models.Metadata.Machine.AdjusterKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.AdjusterKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Adjuster(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.ArchiveKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.ArchiveKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Archive(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.BiosSetKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.BiosSetKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new BiosSet(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.ChipKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.ChipKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Chip(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.ConfigurationKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.ConfigurationKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Configuration(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DeviceKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DeviceKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Device(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DeviceRefKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DeviceRefKey) ?? []; + // Do not filter these due to later use + Array.ForEach(items, item => + { + var datItem = new DeviceRef(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DipSwitchKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DipSwitchKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new DipSwitch(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DiskKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DiskKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Disk(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DisplayKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DisplayKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Display(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DriverKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DriverKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Driver(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.DumpKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.DumpKey) ?? []; + for (int i = 0; i < items.Length; i++) + { + var datItem = new Rom(items[i], machine, source, i); + if (datItem.GetName() is not null) + { + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + } + } + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.FeatureKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.FeatureKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Feature(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.InfoKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.InfoKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Info(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.InputKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.InputKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Input(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.MediaKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.MediaKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Media(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.PartKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.PartKey) ?? []; + ProcessItems(items, machine, machineIndex: 0, source, sourceIndex, statsOnly, filterRunner); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.PortKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.PortKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Port(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.RamOptionKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.RamOptionKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new RamOption(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.ReleaseKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.ReleaseKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Release(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.RomKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.RomKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Rom(item, machine, source); + datItem.SetFieldValue(DatItem.SourceKey, source); + datItem.CopyMachineInformation(machine); + + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.SampleKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.SampleKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Sample(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.SharedFeatKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.SharedFeatKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new SharedFeat(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.SlotKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.SlotKey) ?? []; + // Do not filter these due to later use + Array.ForEach(items, item => + { + var datItem = new Slot(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.SoftwareListKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.SoftwareListKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new SoftwareList(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.SoundKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.SoundKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Sound(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + + if (item.ContainsKey(Data.Models.Metadata.Machine.VideoKey)) + { + var items = item.ReadItemArray(Data.Models.Metadata.Machine.VideoKey) ?? []; + var filtered = filterRunner is null ? items : Array.FindAll(items, i => filterRunner.Run(item)); + Array.ForEach(filtered, item => + { + var datItem = new Display(item, machine, source); + AddItem(datItem, statsOnly); + // AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + }); + } + } + + /// + /// Convert Part information + /// + /// Array of internal items to convert + /// Machine to use with the converted items + /// Index of the Machine to use with the converted items + /// Source to use with the converted items + /// Index of the Source to use with the converted items + /// True to only add item statistics while parsing, false otherwise + /// Optional FilterRunner to filter items on parse + private void ProcessItems(Data.Models.Metadata.Part[] items, + Machine machine, + long machineIndex, + Source source, + long sourceIndex, + bool statsOnly, + FilterRunner? filterRunner) + { + // If the array is null or empty, return without processing + if (items.Length == 0) + return; + + // Loop through the items and add + foreach (var item in items) + { + var partItem = new Part(item, machine, source); + + // Handle subitems + var dataAreas = item.ReadItemArray(Data.Models.Metadata.Part.DataAreaKey); + if (dataAreas is not null) + { + foreach (var dataArea in dataAreas) + { + var dataAreaItem = new DataArea(dataArea, machine, source); + var roms = dataArea.ReadItemArray(Data.Models.Metadata.DataArea.RomKey); + if (roms is null) + continue; + + // Handle "offset" roms + List addRoms = []; + foreach (var rom in roms) + { + // If the item doesn't pass the filter + if (filterRunner is not null && !filterRunner.Run(rom)) + continue; + + // Convert the item + var romItem = new Rom(rom, machine, source); + long? size = romItem.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey); + + // If the rom is a continue or ignore + string? loadFlag = rom.ReadString(Data.Models.Metadata.Rom.LoadFlagKey); + if (loadFlag is not null + && (loadFlag.Equals("continue", StringComparison.OrdinalIgnoreCase) + || loadFlag.Equals("ignore", StringComparison.OrdinalIgnoreCase))) + { + var lastRom = addRoms[addRoms.Count - 1]; + long? lastSize = lastRom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey); + lastRom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, lastSize + size); + continue; + } + + romItem.SetFieldValue(Rom.DataAreaKey, dataAreaItem); + romItem.SetFieldValue(Rom.PartKey, partItem); + + addRoms.Add(romItem); + } + + // Add all of the adjusted roms + foreach (var romItem in addRoms) + { + AddItem(romItem, statsOnly); + // AddItemDB(romItem, machineIndex, sourceIndex, statsOnly); + } + } + } + + var diskAreas = item.ReadItemArray(Data.Models.Metadata.Part.DiskAreaKey); + if (diskAreas is not null) + { + foreach (var diskArea in diskAreas) + { + var diskAreaitem = new DiskArea(diskArea, machine, source); + var disks = diskArea.ReadItemArray(Data.Models.Metadata.DiskArea.DiskKey); + if (disks is null) + continue; + + foreach (var disk in disks) + { + // If the item doesn't pass the filter + if (filterRunner is not null && !filterRunner.Run(disk)) + continue; + + var diskItem = new Disk(disk, machine, source); + diskItem.SetFieldValue(Disk.DiskAreaKey, diskAreaitem); + diskItem.SetFieldValue(Disk.PartKey, partItem); + + AddItem(diskItem, statsOnly); + // AddItemDB(diskItem, machineIndex, sourceIndex, statsOnly); + } + } + } + + var dipSwitches = item.ReadItemArray(Data.Models.Metadata.Part.DipSwitchKey); + if (dipSwitches is not null) + { + foreach (var dipSwitch in dipSwitches) + { + // If the item doesn't pass the filter + if (filterRunner is not null && !filterRunner.Run(dipSwitch)) + continue; + + var dipSwitchItem = new DipSwitch(dipSwitch, machine, source); + dipSwitchItem.SetFieldValue(DipSwitch.PartKey, partItem); + + AddItem(dipSwitchItem, statsOnly); + // AddItemDB(dipSwitchItem, machineIndex, sourceIndex, statsOnly); + } + } + + var partFeatures = item.ReadItemArray(Data.Models.Metadata.Part.FeatureKey); + if (partFeatures is not null) + { + foreach (var partFeature in partFeatures) + { + // If the item doesn't pass the filter + if (filterRunner is not null && !filterRunner.Run(partFeature)) + continue; + + var partFeatureItem = new PartFeature(partFeature); + partFeatureItem.SetFieldValue(DipSwitch.PartKey, partItem); + partFeatureItem.SetFieldValue(DatItem.SourceKey, source); + partFeatureItem.CopyMachineInformation(machine); + + AddItem(partFeatureItem, statsOnly); + // AddItemDB(partFeatureItem, machineIndex, sourceIndex, statsOnly); + } + } + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatFile.Splitting.cs b/SabreTools.Metadata.DatFiles/DatFile.Splitting.cs new file mode 100644 index 00000000..f352de5f --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatFile.Splitting.cs @@ -0,0 +1,1506 @@ +using System; +using System.Collections.Generic; +using System.Linq; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Threading.Tasks; +#endif +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles +{ + public partial class DatFile + { + #region Splitting + + /// + /// Use cdevice_ref tags to get full non-merged sets and remove parenting tags + /// + /// This is a destructive process and items will be removed + public void ApplyDeviceNonMerged() + { + _logger.User("Creating device non-merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Now we want to loop through all of the games and set the correct information + while (AddItemsFromDevices(false, false)) ; + while (AddItemsFromDevices(true, false)) ; + + // Then, remove the romof and cloneof tags so it's not picked up by the manager + RemoveMachineRelationshipTags(); + } + + /// + /// Use cloneof tags to create merged sets and remove the tags plus deduplicating if tags don't catch everything + /// + /// This is a destructive process and items will be removed + public void ApplyFullyMerged() + { + _logger.User("Creating fully merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Now we want to loop through all of the games and set the correct information + AddItemsFromChildren(true, false); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveItemsFromRomOfChild(); + + // Remove any name duplicates left over + RemoveNameDuplicates(); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveMachineRelationshipTags(); + } + + /// + /// Use cloneof tags to create non-merged sets and remove the tags plus using the device_ref tags to get full sets + /// + /// This is a destructive process and items will be removed + public void ApplyFullyNonMerged() + { + _logger.User("Creating fully non-merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Now we want to loop through all of the games and set the correct information + while (AddItemsFromDevices(true, true)) ; + AddItemsFromDevices(false, true); + AddItemsFromCloneOfParent(); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + AddItemsFromRomOfParent(); + + // Then, remove the romof and cloneof tags so it's not picked up by the manager + RemoveMachineRelationshipTags(); + } + + /// + /// Use cloneof tags to create merged sets and remove the tags + /// + /// This is a destructive process and items will be removed + public void ApplyMerged() + { + _logger.User("Creating merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Now we want to loop through all of the games and set the correct information + AddItemsFromChildren(true, true); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveItemsFromRomOfChild(); + + // Remove any name duplicates left over + RemoveNameDuplicates(); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveMachineRelationshipTags(); + } + + /// + /// Use cloneof tags to create non-merged sets and remove the tags + /// + /// This is a destructive process and items will be removed + public void ApplyNonMerged() + { + _logger.User("Creating non-merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Now we want to loop through all of the games and set the correct information + AddItemsFromCloneOfParent(); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveItemsFromRomOfChild(); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveMachineRelationshipTags(); + } + + /// + /// Use cloneof and romof tags to create split sets and remove the tags + /// + /// This is a destructive process and items will be removed + public void ApplySplit() + { + _logger.User("Creating split sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + BucketBy(ItemKey.Machine, norename: true); + + // Now we want to loop through all of the games and set the correct information + RemoveItemsFromCloneOfChild(); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveItemsFromRomOfChild(); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveMachineRelationshipTags(); + } + + #endregion + + #region Splitting Steps + + /// + /// Use cloneof tags to add items to the parents, removing the child sets in the process + /// + /// True to add DatItems to subfolder of parent (not including Disk), false otherwise + /// True to skip checking for duplicate ROMs in parent, false otherwise + /// Assumes items are bucketed by + internal void AddItemsFromChildren(bool subfolder, bool skipDedup) + { + AddItemsFromChildrenImpl(subfolder, skipDedup); + AddItemsFromChildrenImplDB(subfolder, skipDedup); + } + + /// + /// Use cloneof tags to add items to the children, setting the new romof tag in the process + /// + /// Assumes items are bucketed by + internal void AddItemsFromCloneOfParent() + { + AddItemsFromCloneOfParentImpl(); + AddItemsFromCloneOfParentImplDB(); + } + + /// + /// Use device_ref and optionally slotoption tags to add items to the children + /// + /// True if only child device sets are touched, false for non-device sets + /// True if slotoptions tags are used as well, false otherwise + /// Assumes items are bucketed by + internal bool AddItemsFromDevices(bool deviceOnly, bool useSlotOptions) + { + bool foundnew = AddItemsFromDevicesImpl(deviceOnly, useSlotOptions); + foundnew |= AddItemsFromDevicesImplDB(deviceOnly, useSlotOptions); + return foundnew; + } + + /// + /// Use romof tags to add items to the children + /// + /// Assumes items are bucketed by + internal void AddItemsFromRomOfParent() + { + AddItemsFromRomOfParentImpl(); + AddItemsFromRomOfParentImplDB(); + } + + /// + /// Remove all BIOS and device sets + /// + /// Assumes items are bucketed by + internal void RemoveBiosAndDeviceSets() + { + RemoveBiosAndDeviceSetsImpl(); + RemoveBiosAndDeviceSetsImplDB(); + } + + /// + /// Use cloneof tags to remove items from the children + /// + /// Assumes items are bucketed by + internal void RemoveItemsFromCloneOfChild() + { + RemoveItemsFromCloneOfChildImpl(); + RemoveItemsFromCloneOfChildImplDB(); + } + + /// + /// Use romof tags to remove bios items from children + /// + /// Assumes items are bucketed by + internal void RemoveItemsFromRomOfChild() + { + RemoveItemsFromRomOfChildImpl(); + RemoveItemsFromRomOfChildImplDB(); + } + + /// + /// Remove all romof and cloneof tags from all machines + /// + /// Assumes items are bucketed by + internal void RemoveMachineRelationshipTags() + { + RemoveMachineRelationshipTagsImpl(); + RemoveMachineRelationshipTagsImplDB(); + } + + /// + /// Remove duplicates within a bucket that share the same name + /// + /// Assumes items are bucketed by + internal void RemoveNameDuplicates() + { + RemoveNameDuplicatesImpl(); + RemoveNameDuplicatesImplDB(); + } + + #endregion + + #region Splitting Implementations + + /// + /// Use cloneof tags to add items to the parents, removing the child sets in the process + /// + /// True to add DatItems to subfolder of parent (not including Disk), false otherwise + /// True to skip checking for duplicate ROMs in parent, false otherwise + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void AddItemsFromChildrenImpl(bool subfolder, bool skipDedup) + { + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) + continue; + + // Get the machine + var machine = items[0].GetMachine(); + if (machine is null) + continue; + + // Get the cloneof parent items + string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + List parentItems = GetItemsForBucket(cloneOf); + if (cloneOf is null) + continue; + + // Otherwise, move the items from the current game to a subfolder of the parent game + DatItem copyFrom; + if (parentItems.Count == 0) + { + copyFrom = new Rom(); + copyFrom.GetMachine()!.SetName(cloneOf); + copyFrom.GetMachine()!.SetFieldValue(Data.Models.Metadata.Machine.DescriptionKey, cloneOf); + } + else + { + copyFrom = parentItems[0]; + } + + items = GetItemsForBucket(bucket); + foreach (DatItem item in items) + { + // Special disk handling + if (item is Disk disk) + { + string? mergeTag = disk.GetStringFieldValue(Data.Models.Metadata.Disk.MergeKey); + + // If the merge tag exists and the parent already contains it, skip + if (mergeTag is not null && GetItemsForBucket(cloneOf) + .FindAll(i => i is Disk) + .ConvertAll(i => (i as Disk)!.GetName()) + .Contains(mergeTag)) + { + continue; + } + + // If the merge tag exists but the parent doesn't contain it, add to parent + else if (mergeTag is not null && !GetItemsForBucket(cloneOf) + .FindAll(i => i is Disk) + .ConvertAll(i => (i as Disk)!.GetName()) + .Contains(mergeTag)) + { + disk.CopyMachineInformation(copyFrom); + AddItem(disk, statsOnly: false); + } + + // If there is no merge tag, add to parent + else if (mergeTag is null && !GetItemsForBucket(cloneOf) + .FindAll(i => i is Disk) + .ConvertAll(i => (i as Disk)!.GetName()) + .Contains(disk.GetName())) + { + disk.CopyMachineInformation(copyFrom); + AddItem(disk, statsOnly: false); + } + } + + // Special rom handling + else if (item is Rom rom) + { + string? mergeTag = rom.GetStringFieldValue(Data.Models.Metadata.Rom.MergeKey); + + // If the merge tag exists and the parent already contains it, skip + if (mergeTag is not null && GetItemsForBucket(cloneOf) + .FindAll(i => i is Rom) + .ConvertAll(i => (i as Rom)!.GetName()) + .Contains(mergeTag)) + { + continue; + } + + // If the merge tag exists but the parent doesn't contain it, add to subfolder of parent + else if (mergeTag is not null && !GetItemsForBucket(cloneOf) + .FindAll(i => i is Rom) + .ConvertAll(i => (i as Rom)!.GetName()) + .Contains(mergeTag)) + { + if (subfolder) + rom.SetName($"{rom.GetMachine()!.GetName()}\\{rom.GetName()}"); + + rom.CopyMachineInformation(copyFrom); + AddItem(rom, statsOnly: false); + } + + // If the parent doesn't already contain this item, add to subfolder of parent + else if (!GetItemsForBucket(cloneOf).Exists(i => i.Equals(item)) || skipDedup) + { + if (subfolder) + rom.SetName($"{item.GetMachine()!.GetName()}\\{rom.GetName()}"); + + rom.CopyMachineInformation(copyFrom); + AddItem(rom, statsOnly: false); + } + } + + // All other that would be missing to subfolder of parent + else if (!GetItemsForBucket(cloneOf).Exists(i => i.Equals(item))) + { + if (subfolder) + item.SetName($"{item.GetMachine()!.GetName()}\\{item.GetName()}"); + + item.CopyMachineInformation(copyFrom); + AddItem(item, statsOnly: false); + } + } + + // Then, remove the old game so it's not picked up by the writer + RemoveBucket(bucket); + } + } + + /// + /// Use cloneof tags to add items to the parents, removing the child sets in the process + /// + /// True to add DatItems to subfolder of parent (not including Disk), false otherwise + /// True to skip checking for duplicate ROMs in parent, false otherwise + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void AddItemsFromChildrenImplDB(bool subfolder, bool skipDedup) + { + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Get the machine for the first item + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) + continue; + + // Get the clone parent + string? cloneOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + if (string.IsNullOrEmpty(cloneOf)) + continue; + + // Get the clone parent machine + var cloneOfMachine = ItemsDB.GetMachine(cloneOf); + if (cloneOfMachine.Value is null) + continue; + + items = GetItemsForBucketDB(bucket); + foreach (var item in items) + { + // Get the source for the current item + var source = GetSourceForItemDB(item.Key); + + // Get the parent items and current machine name + Dictionary parentItems = GetItemsForBucketDB(cloneOf); + if (parentItems.Count == 0) + continue; + + string? machineName = GetMachineForItemDB(item.Key).Value? + .GetName(); + + // Special disk handling + if (item.Value is Disk disk) + { + string? mergeTag = disk.GetStringFieldValue(Data.Models.Metadata.Disk.MergeKey); + + // If the merge tag exists and the parent already contains it, skip + if (mergeTag is not null && GetItemsForBucketDB(cloneOf).Values + .Where(i => i is Disk) + .Select(i => (i as Disk)!.GetName()) + .Contains(mergeTag)) + { + continue; + } + + // If the merge tag exists but the parent doesn't contain it, add to parent + else if (mergeTag is not null && !GetItemsForBucketDB(cloneOf).Values + .Where(i => i is Disk) + .Select(i => (i as Disk)!.GetName()) + .Contains(mergeTag)) + { + ItemsDB.RemapDatItemToMachine(item.Key, cloneOfMachine.Key); + ItemsDB.AddItem(item.Value, cloneOfMachine.Key, source.Key); + } + + // If there is no merge tag, add to parent + else if (mergeTag is null && !GetItemsForBucketDB(cloneOf).Values + .Where(i => i is Disk) + .Select(i => (i as Disk)!.GetName()) + .Contains(disk.GetName())) + { + ItemsDB.RemapDatItemToMachine(item.Key, cloneOfMachine.Key); + ItemsDB.AddItem(item.Value, cloneOfMachine.Key, source.Key); + } + } + + // Special rom handling + else if (item.Value is Rom rom) + { + string? mergeTag = rom.GetStringFieldValue(Data.Models.Metadata.Rom.MergeKey); + + // If the merge tag exists and the parent already contains it, skip + if (mergeTag is not null && GetItemsForBucketDB(cloneOf).Values + .Where(i => i is Rom) + .Select(i => (i as Rom)!.GetName()) + .Contains(mergeTag)) + { + continue; + } + + // If the merge tag exists but the parent doesn't contain it, add to subfolder of parent + else if (mergeTag is not null && !GetItemsForBucketDB(cloneOf).Values + .Where(i => i is Rom) + .Select(i => (i as Rom)!.GetName()) + .Contains(mergeTag)) + { + if (subfolder) + rom.SetName($"{machineName}\\{rom.GetName()}"); + + ItemsDB.RemapDatItemToMachine(item.Key, machineIndex: cloneOfMachine.Key); + ItemsDB.AddItem(item.Value, cloneOfMachine.Key, source.Key); + } + + // If the parent doesn't already contain this item, add to subfolder of parent + else if (!GetItemsForBucketDB(cloneOf).Values.Contains(item.Value) || skipDedup) + { + if (subfolder) + rom.SetName($"{machineName}\\{rom.GetName()}"); + + ItemsDB.RemapDatItemToMachine(item.Key, cloneOfMachine.Key); + ItemsDB.AddItem(item.Value, cloneOfMachine.Key, source.Key); + } + } + + // All other that would be missing to subfolder of parent + else if (!GetItemsForBucketDB(cloneOf).Values.Contains(item.Value)) + { + if (subfolder) + item.Value.SetName($"{machineName}\\{item.Value.GetName()}"); + + ItemsDB.RemapDatItemToMachine(item.Key, cloneOfMachine.Key); + ItemsDB.AddItem(item.Value, cloneOfMachine.Key, source.Key); + } + + // Remove the current item + RemoveItemDB(item.Key); + } + } + } + + /// + /// Use cloneof tags to add items to the children, setting the new romof tag in the process + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void AddItemsFromCloneOfParentImpl() + { + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) + continue; + + // Get the machine + var machine = items[0].GetMachine(); + if (machine is null) + continue; + + // Get the cloneof parent items + string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + List parentItems = GetItemsForBucket(cloneOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we copy the items from the parent to the current game + DatItem copyFrom = items[0]; + foreach (DatItem item in parentItems) + { + DatItem datItem = (DatItem)item.Clone(); + datItem.CopyMachineInformation(copyFrom); + if (!items.Exists(i => string.Equals(i.GetName(), datItem.GetName(), StringComparison.OrdinalIgnoreCase)) + && !items.Exists(i => i.Equals(datItem))) + { + AddItem(datItem, statsOnly: false); + } + } + + // Now we want to get the parent romof tag and put it in each of the items + items = GetItemsForBucket(bucket); + string? romof = GetItemsForBucket(cloneOf)[0].GetMachine()!.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + foreach (DatItem item in items) + { + item.GetMachine()!.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, romof); + } + } + } + + /// + /// Use cloneof tags to add items to the children, setting the new romof tag in the process + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void AddItemsFromCloneOfParentImplDB() + { + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Get the source for the first item + var source = GetSourceForItemDB(items.First().Key); + + // Get the machine for the first item in the list + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) + continue; + + // Get the clone parent + string? cloneOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + if (string.IsNullOrEmpty(cloneOf)) + continue; + + // If the parent doesn't have any items, we want to continue + Dictionary parentItems = GetItemsForBucketDB(cloneOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we copy the items from the parent to the current game + foreach (var item in parentItems) + { + DatItem datItem = (DatItem)item.Value.Clone(); + if (items.Values.Any(i => i.GetName()?.ToLowerInvariant() == datItem.GetName()?.ToLowerInvariant()) + && items.Values.Any(i => i == datItem)) + { + ItemsDB.AddItem(datItem, machine.Key, source.Key); + } + } + + // Get the parent machine + var parentMachine = GetMachineForItemDB(GetItemsForBucketDB(cloneOf).First().Key); + if (parentMachine.Value is null) + continue; + + // Now we want to get the parent romof tag and put it in each of the items + items = GetItemsForBucketDB(bucket); + string? romof = parentMachine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + foreach (var key in items.Keys) + { + var itemMachine = GetMachineForItemDB(key); + if (itemMachine.Value is null) + continue; + + // TODO: Remove merge tags here + itemMachine.Value.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, romof); + } + } + } + + /// + /// Use device_ref and optionally slotoption tags to add items to the children + /// + /// True if only child device sets are touched, false for non-device sets + /// True if slotoptions tags are used as well, false otherwise + /// True if any items were processed, false otherwise + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private bool AddItemsFromDevicesImpl(bool deviceOnly, bool useSlotOptions) + { + bool foundnew = false; + + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket doesn't have items + List datItems = GetItemsForBucket(bucket); + if (datItems.Count == 0) + continue; + + // If the machine (is/is not) a device, we want to continue + if (deviceOnly ^ (datItems[0].GetMachine()!.GetBoolFieldValue(Data.Models.Metadata.Machine.IsDeviceKey) == true)) + continue; + + // Get the first item from the bucket + DatItem copyFrom = datItems[0]; + + // Get all device reference names from the current machine + HashSet deviceReferences = []; + deviceReferences.UnionWith(datItems + .FindAll(i => i is DeviceRef) + .ConvertAll(i => i as DeviceRef) + .ConvertAll(dr => dr!.GetName())); + + // Get all slot option names from the current machine + HashSet slotOptions = []; + slotOptions.UnionWith(datItems + .FindAll(i => i is Slot) + .ConvertAll(i => i as Slot) + .FindAll(s => s!.SlotOptionsSpecified) + .SelectMany(s => s!.GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey)!) + .Select(so => so.GetStringFieldValue(Data.Models.Metadata.SlotOption.DevNameKey))); + + // If we're checking device references + if (deviceReferences.Count > 0) + { + // Loop through all names and check the corresponding machines + var newDeviceReferences = new HashSet(); + foreach (string? deviceReference in deviceReferences) + { + // Add to the list of new device reference names + List devItems = GetItemsForBucket(deviceReference); + if (devItems.Count == 0) + continue; + + newDeviceReferences.UnionWith(devItems + .FindAll(i => i is DeviceRef) + .ConvertAll(i => (i as DeviceRef)!.GetName()!)); + + // Set new machine information and add to the current machine + foreach (DatItem item in devItems) + { + // If the parent machine doesn't already contain this item, add it + if (!datItems.Exists(i => i.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) == item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) && i.GetName() == item.GetName())) + { + // Set that we found new items + foundnew = true; + + // Clone the item and then add it + DatItem datItem = (DatItem)item.Clone(); + datItem.CopyMachineInformation(copyFrom); + datItems.Add(datItem); + AddItem(datItem, statsOnly: false); + } + } + } + + // Now that every device reference is accounted for, add the new list of device references, if they don't already exist + foreach (string deviceReference in newDeviceReferences) + { + if (!deviceReferences.Contains(deviceReference)) + { + deviceReferences.Add(deviceReference); + var deviceRef = new DeviceRef(); + deviceRef.SetName(deviceReference); + deviceRef.CopyMachineInformation(copyFrom); + Items.AddItem(deviceRef, statsOnly: false); + } + } + } + + // If we're checking slotoptions + if (useSlotOptions && slotOptions.Count > 0) + { + // Loop through all names and check the corresponding machines + var newSlotOptions = new HashSet(); + foreach (string? slotOption in slotOptions) + { + // Add to the list of new slot option names + List slotItems = GetItemsForBucket(slotOption); + if (slotItems.Count == 0) + continue; + + newSlotOptions.UnionWith(slotItems + .FindAll(i => i is Slot) + .FindAll(s => (s as Slot)!.SlotOptionsSpecified) + .SelectMany(s => (s as Slot)!.GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey)!) + .Select(o => o.GetStringFieldValue(Data.Models.Metadata.SlotOption.DevNameKey)!)); + + // Set new machine information and add to the current machine + foreach (DatItem item in slotItems) + { + // If the parent machine doesn't already contain this item, add it + if (!datItems.Exists(i => i.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) == item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) && i.GetName() == item.GetName())) + { + // Set that we found new items + foundnew = true; + + // Clone the item and then add it + DatItem datItem = (DatItem)item.Clone(); + datItem.CopyMachineInformation(copyFrom); + datItems.Add(datItem); + AddItem(datItem, statsOnly: false); + } + } + } + + // Now that every device is accounted for, add the new list of slot options, if they don't already exist + foreach (string slotOption in newSlotOptions) + { + if (!slotOptions.Contains(slotOption)) + { + slotOptions.Add(slotOption); + var slotOptionItem = new SlotOption(); + slotOptionItem.SetFieldValue(Data.Models.Metadata.SlotOption.DevNameKey, slotOption); + slotOptionItem.CopyMachineInformation(copyFrom); + + var slotItem = new Slot(); + slotItem.SetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey, [slotOptionItem]); + slotItem.CopyMachineInformation(copyFrom); + + Items.AddItem(slotItem, statsOnly: false); + } + } + } + } + + return foundnew; + } + + /// + /// Use device_ref and optionally slotoption tags to add items to the children + /// + /// True if only child device sets are touched, false for non-device sets + /// True if slotoptions tags are used as well, false otherwise + /// True if any items were processed, false otherwise + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private bool AddItemsFromDevicesImplDB(bool deviceOnly, bool useSlotOptions) + { + bool foundnew = false; + + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Get the source for the first item + var source = GetSourceForItemDB(items.First().Key); + + // Get the machine for the first item + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) + continue; + + // If the machine (is/is not) a device, we want to continue + if (deviceOnly ^ (machine.Value.GetBoolFieldValue(Data.Models.Metadata.Machine.IsDeviceKey) == true)) + continue; + + // Get all device reference names from the current machine + List deviceReferences = items.Values + .Where(i => i is DeviceRef) + .Select(i => i as DeviceRef) + .Select(dr => dr!.GetName()) + .Distinct() + .ToList(); + + // Get all slot option names from the current machine + List slotOptions = items.Values + .Where(i => i is Slot) + .Select(i => i as Slot) + .Where(s => s!.SlotOptionsSpecified) + .SelectMany(s => s!.GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey)!) + .Select(so => so.GetStringFieldValue(Data.Models.Metadata.SlotOption.DevNameKey)) + .Distinct() + .ToList(); + + // If we're checking device references + if (deviceReferences.Count > 0) + { + // Loop through all names and check the corresponding machines + var newDeviceReferences = new HashSet(); + foreach (string? deviceReference in deviceReferences) + { + // If the device reference is invalid + if (deviceReference is null) + continue; + + // If the machine doesn't exist then we continue + Dictionary devItems = GetItemsForBucketDB(deviceReference); + if (devItems.Count == 0) + continue; + + // Add to the list of new device reference names + newDeviceReferences.UnionWith(devItems.Values + .Where(i => i is DeviceRef) + .Select(i => (i as DeviceRef)!.GetName()!)); + + // Set new machine information and add to the current machine + var copyFrom = GetMachineForItemDB(items.First().Key); + if (copyFrom.Value is null) + continue; + + foreach (var item in devItems.Values) + { + // If the parent machine doesn't already contain this item, add it + if (!items.Values.Any(i => i.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) == item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) + && i.GetName() == item.GetName())) + { + // Set that we found new items + foundnew = true; + + // Clone the item and then add it + DatItem datItem = (DatItem)item.Clone(); + ItemsDB.AddItem(datItem, machine.Key, source.Key); + } + } + } + + // Now that every device reference is accounted for, add the new list of device references, if they don't already exist + foreach (string deviceReference in newDeviceReferences) + { + if (!deviceReferences.Contains(deviceReference)) + { + var deviceRef = new DeviceRef(); + deviceRef.SetName(deviceReference); + ItemsDB.AddItem(deviceRef, machine.Key, source.Key); + } + } + } + + // If we're checking slotoptions + if (useSlotOptions && slotOptions.Count > 0) + { + // Loop through all names and check the corresponding machines + var newSlotOptions = new HashSet(); + foreach (string? slotOption in slotOptions) + { + // If the slot option is invalid + if (slotOption is null) + continue; + + // If the machine doesn't exist then we continue + Dictionary slotItems = GetItemsForBucketDB(slotOption); + if (slotItems.Count == 0) + continue; + + // Add to the list of new slot option names + newSlotOptions.UnionWith(slotItems.Values + .Where(i => i is Slot) + .Where(s => (s as Slot)!.SlotOptionsSpecified) + .SelectMany(s => (s as Slot)!.GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey)!) + .Select(o => o.GetStringFieldValue(Data.Models.Metadata.SlotOption.DevNameKey)!)); + + // Set new machine information and add to the current machine + var copyFrom = GetMachineForItemDB(GetItemsForBucketDB(bucket).First().Key); + if (copyFrom.Value is null) + continue; + + foreach (var item in slotItems.Values) + { + // If the parent machine doesn't already contain this item, add it + if (!items.Values.Any(i => i.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) == item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) + && i.GetName() == item.GetName())) + { + // Set that we found new items + foundnew = true; + + // Clone the item and then add it + DatItem datItem = (DatItem)item.Clone(); + ItemsDB.AddItem(datItem, machine.Key, source.Key); + } + } + } + + // Now that every device is accounted for, add the new list of slot options, if they don't already exist + foreach (string slotOption in newSlotOptions) + { + if (!slotOptions.Contains(slotOption)) + { + var slotOptionItem = new SlotOption(); + slotOptionItem.SetFieldValue(Data.Models.Metadata.SlotOption.DevNameKey, slotOption); + + var slotItem = new Slot(); + slotItem.SetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey, [slotOptionItem]); + + ItemsDB.AddItem(slotItem, machine.Key, source.Key); + } + } + } + } + + return foundnew; + } + + /// + /// Use romof tags to add items to the children + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void AddItemsFromRomOfParentImpl() + { + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) + continue; + + // Get the machine + var machine = items[0].GetMachine(); + if (machine is null) + continue; + + // Get the romof parent items + string? romOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + List parentItems = GetItemsForBucket(romOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we copy the items from the parent to the current game + DatItem copyFrom = items[0]; + foreach (DatItem item in parentItems) + { + DatItem datItem = (DatItem)item.Clone(); + datItem.CopyMachineInformation(copyFrom); + if (!items.Exists(i => i.GetName() == datItem.GetName()) && !items.Exists(i => i.Equals(datItem))) + AddItem(datItem, statsOnly: false); + } + } + } + + /// + /// Use romof tags to add items to the children + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void AddItemsFromRomOfParentImplDB() + { + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Get the source for the first item + var source = GetSourceForItemDB(items.First().Key); + + // Get the machine for the first item + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) + continue; + + // Get the romof parent items + string? romOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + Dictionary parentItems = GetItemsForBucketDB(romOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we copy the items from the parent to the current game + foreach (var item in parentItems) + { + DatItem datItem = (DatItem)item.Value.Clone(); + if (items.Any(i => i.Value.GetName() == datItem.GetName()) + && items.Any(i => i.Value == datItem)) + { + ItemsDB.AddItem(datItem, machine.Key, source.Key); + } + } + } + } + + /// + /// Remove all BIOS and device sets + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void RemoveBiosAndDeviceSetsImpl() + { + string[] buckets = [.. Items.SortedKeys]; + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(buckets, bucket => +#else + foreach (string bucket in buckets) +#endif + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + // Get the machine + var machine = items[0].GetMachine(); + if (machine is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + // Remove flagged items + if ((machine.GetBoolFieldValue(Data.Models.Metadata.Machine.IsBiosKey) == true) + || (machine.GetBoolFieldValue(Data.Models.Metadata.Machine.IsDeviceKey) == true)) + { + RemoveBucket(bucket); + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Remove all BIOS and device sets + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void RemoveBiosAndDeviceSetsImplDB() + { + string[] buckets = [.. ItemsDB.SortedKeys]; + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(buckets, bucket => +#else + foreach (string bucket in buckets) +#endif + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + // Get the machine + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + // Remove flagged items + if ((machine.Value.GetBoolFieldValue(Data.Models.Metadata.Machine.IsBiosKey) == true) + || (machine.Value.GetBoolFieldValue(Data.Models.Metadata.Machine.IsDeviceKey) == true)) + { + foreach (var key in items.Keys) + { + RemoveItemDB(key); + } + } + + // Remove the machine + RemoveMachineDB(machine.Key); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Use cloneof tags to remove items from the children + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void RemoveItemsFromCloneOfChildImpl() + { + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) + continue; + + // Get the machine + var machine = items[0].GetMachine(); + if (machine is null) + continue; + + // Get the cloneof parent items + string? cloneOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + List parentItems = GetItemsForBucket(cloneOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we remove the parent items from the current game + foreach (DatItem item in parentItems) + { + while (true) + { + // Find the next index that matches the item + int index = items.FindIndex(i => i.Equals(item)); + if (index < 0) + break; + + // Remove the item from the local and internal lists + RemoveItem(bucket, items[index], index); + items.RemoveAt(index); + } + } + + // Now we want to get the parent romof tag and put it in each of the remaining items + items = GetItemsForBucket(bucket); + string? romof = GetItemsForBucket(cloneOf)[0].GetMachine()!.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + foreach (DatItem item in items) + { + item.GetMachine()!.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, romof); + } + } + } + + /// + /// Use cloneof tags to remove items from the children + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void RemoveItemsFromCloneOfChildImplDB() + { + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Get the machine for the first item + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) + continue; + + // Get the cloneof parent items + string? cloneOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey); + Dictionary parentItems = GetItemsForBucketDB(cloneOf); + if (parentItems is null || parentItems.Count == 0) + continue; + + // If the parent exists and has items, we remove the parent items from the current game + foreach (var item in parentItems) + { + var matchedItems = items.Where(i => i.Value.Equals(item.Value)); + foreach (var match in matchedItems) + { + RemoveItemDB(match.Key); + } + } + + // Now we want to get the parent romof tag and put it in each of the remaining items + items = GetItemsForBucketDB(bucket); + machine = GetMachineForItemDB(GetItemsForBucketDB(cloneOf).First().Key); + if (machine.Value is null) + continue; + + string? romof = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + foreach (var item in items) + { + machine = GetMachineForItemDB(item.Key); + if (machine.Value is null) + continue; + + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, romof); + } + } + } + + /// + /// Use romof tags to remove items from children + /// + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void RemoveItemsFromRomOfChildImpl() + { + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) + continue; + + // Get the machine + var machine = items[0].GetMachine(); + if (machine is null) + continue; + + // Get the romof parent items + string? romOf = machine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + List parentItems = GetItemsForBucket(romOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we remove the parent items from the current game + foreach (DatItem item in parentItems) + { + while (true) + { + // Find the next index that matches the item + int index = items.FindIndex(i => i.Equals(item)); + if (index < 0) + break; + + // Remove the item from the local and internal lists + RemoveItem(bucket, items[index], index); + items.RemoveAt(index); + } + } + } + } + + /// + /// Use romof tags to remove bios items from children + /// + /// True if only child Bios sets are touched, false for non-bios sets + /// + /// Applies to . + /// Assumes items are bucketed by . + /// + private void RemoveItemsFromRomOfChildImplDB() + { + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Get the machine for the item + var machine = GetMachineForItemDB(items.First().Key); + if (machine.Value is null) + continue; + + // Get the romof parent items + string? romOf = machine.Value.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey); + Dictionary parentItems = GetItemsForBucketDB(romOf); + if (parentItems.Count == 0) + continue; + + // If the parent exists and has items, we remove the items that are in the parent from the current game + foreach (var item in parentItems) + { + var matchedItems = items.Where(i => i.Value.Equals(item.Value)); + foreach (var match in matchedItems) + { + RemoveItemDB(match.Key); + } + } + } + } + + /// + /// Remove all romof and cloneof tags from all machines + /// + /// Applies to + private void RemoveMachineRelationshipTagsImpl() + { + string[] buckets = [.. Items.SortedKeys]; + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(buckets, bucket => +#else + foreach (string bucket in buckets) +#endif + { + // If the bucket has no items in it + var items = GetItemsForBucket(bucket); + if (items is null || items.Count == 0) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + foreach (DatItem item in items) + { + // Remove the merge tag + item.RemoveField(Data.Models.Metadata.Rom.MergeKey); + + // Get the machine + var machine = item.GetMachine(); + if (machine is null) + continue; + + machine.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, null); + machine.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, null); + machine.SetFieldValue(Data.Models.Metadata.Machine.SampleOfKey, null); + } +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Remove all romof and cloneof tags from all machines + /// + /// Applies to + private void RemoveMachineRelationshipTagsImplDB() + { + var machines = GetMachinesDB(); + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(machines, machine => +#else + foreach (var machine in machines) +#endif + { + // TODO: Remove merge tags here + // Get the machine + if (machine.Value is null) +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return; +#else + continue; +#endif + + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.CloneOfKey, null); + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.RomOfKey, null); + machine.Value.SetFieldValue(Data.Models.Metadata.Machine.SampleOfKey, null); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Remove duplicates within a bucket that share the same name + /// + /// Applies to + private void RemoveNameDuplicatesImpl() + { + string[] buckets = [.. Items.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + List items = GetItemsForBucket(bucket); + if (items.Count == 0) + continue; + + // Loop through the items and ignore existing names + List names = []; + for (int i = 0; i < items.Count; i++) + { + // Skip non-Disk and non-Rom items + if (items[i] is not Disk && items[i] is not Rom) + continue; + + // Get the item name + string? name = items[i].GetName(); + if (string.IsNullOrEmpty(name)) + continue; + + // If the item already exists + if (names.Contains(name!)) + { + Items.RemoveItem(bucket, items[i], i); + items.RemoveAt(i); + i--; + continue; + } + + // Add the name to the list for checking + names.Add(name!); + } + } + } + + /// + /// Remove duplicates within a bucket that share the same name + /// + /// Applies to + private void RemoveNameDuplicatesImplDB() + { + string[] buckets = [.. ItemsDB.SortedKeys]; + foreach (string bucket in buckets) + { + // If the bucket has no items in it + Dictionary items = GetItemsForBucketDB(bucket); + if (items.Count == 0) + continue; + + // Loop through the items and ignore existing names + List names = []; + foreach (var item in items) + { + // Skip non-Disk and non-Rom items + if (item.Value is not Disk && item.Value is not Rom) + continue; + + // Get the item name + string? name = item.Value.GetName(); + if (string.IsNullOrEmpty(name)) + continue; + + // If the item already exists + if (names.Contains(name!)) + { + ItemsDB.RemoveItem(item.Key); + continue; + } + + // Add the name to the list for checking + names.Add(name!); + } + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatFile.ToMetadata.cs b/SabreTools.Metadata.DatFiles/DatFile.ToMetadata.cs new file mode 100644 index 00000000..8239142b --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatFile.ToMetadata.cs @@ -0,0 +1,1107 @@ +using System.Collections.Generic; +using System.Linq; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatFiles +{ + public partial class DatFile + { + #region To Metadata + + /// + /// Convert metadata information + /// + internal Data.Models.Metadata.MetadataFile? ConvertToMetadata(bool ignoreblanks = false) + { + // If we don't have items, we can't do anything + if (DatStatistics.TotalCount == 0) + return null; + + // Create an object to hold the data + var metadataFile = new Data.Models.Metadata.MetadataFile(); + + // Convert and assign the header + var header = Header.GetInternalClone(); + if (header is not null) + metadataFile[Data.Models.Metadata.MetadataFile.HeaderKey] = header; + + // Convert and assign the machines + var machines = ConvertMachines(ignoreblanks); + if (machines is not null) + metadataFile[Data.Models.Metadata.MetadataFile.MachineKey] = machines; + + return metadataFile; + } + + /// + /// Convert metadata information + /// + internal Data.Models.Metadata.MetadataFile? ConvertToMetadataDB(bool ignoreblanks = false) + { + // If we don't have items, we can't do anything + if (ItemsDB.DatStatistics.TotalCount == 0) + return null; + + // Create an object to hold the data + var metadataFile = new Data.Models.Metadata.MetadataFile(); + + // Convert and assign the header + var header = Header.GetInternalClone(); + if (header is not null) + metadataFile[Data.Models.Metadata.MetadataFile.HeaderKey] = header; + + // Convert and assign the machines + var machines = ConvertMachinesDB(ignoreblanks); + if (machines is not null) + metadataFile[Data.Models.Metadata.MetadataFile.MachineKey] = machines; + + return metadataFile; + } + + /// + /// Convert machines information + /// + private Data.Models.Metadata.Machine[]? ConvertMachines(bool ignoreblanks = false) + { + // Create a machine list to hold all outputs + List machines = []; + + // Loop through the sorted items and create games for them + foreach (string key in Items.SortedKeys) + { + var items = Items.GetItemsForBucket(key, filter: true); + if (items is null || items.Count == 0) + continue; + + // Create a machine to hold everything + var machine = items[0].GetMachine()!.GetInternalClone(); + + // Handle Trurip object, if it exists + if (machine.ContainsKey(Data.Models.Metadata.Machine.TruripKey)) + { + var trurip = machine.Read(Data.Models.Metadata.Machine.TruripKey); + if (trurip is not null) + { + var truripItem = trurip.ConvertToLogiqx(); + truripItem.Publisher = machine.ReadString(Data.Models.Metadata.Machine.PublisherKey); + truripItem.Year = machine.ReadString(Data.Models.Metadata.Machine.YearKey); + truripItem.Players = machine.ReadString(Data.Models.Metadata.Machine.PlayersKey); + truripItem.Source = machine.ReadString(Data.Models.Metadata.Machine.SourceFileKey); + truripItem.CloneOf = machine.ReadString(Data.Models.Metadata.Machine.CloneOfKey); + machine[Data.Models.Metadata.Machine.TruripKey] = truripItem; + } + } + + // Create mapping dictionaries for the Parts, DataAreas, and DiskAreas associated with this machine + Dictionary partMappings = []; + Dictionary dataAreaMappings = []; + Dictionary diskAreaMappings = []; + + // Loop through and convert the items to respective lists + for (int index = 0; index < items.Count; index++) + { + // Get the item + var item = items[index]; + + // Check for a "null" item + item = ProcessNullifiedItem(item); + + // Skip if we're ignoring the item + if (ShouldIgnore(item, ignoreblanks)) + continue; + + switch (item) + { + case DatItems.Formats.Adjuster adjuster: + var adjusterItem = adjuster.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.AdjusterKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.AdjusterKey, adjusterItem); + break; + case DatItems.Formats.Archive archive: + var archiveItem = archive.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ArchiveKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ArchiveKey, archiveItem); + break; + case DatItems.Formats.BiosSet biosSet: + var biosSetItem = biosSet.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.BiosSetKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.BiosSetKey, biosSetItem); + break; + case DatItems.Formats.Chip chip: + var chipItem = chip.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ChipKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ChipKey, chipItem); + break; + case DatItems.Formats.Configuration configuration: + var configurationItem = configuration.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ConfigurationKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ConfigurationKey, configurationItem); + break; + case DatItems.Formats.Device device: + var deviceItem = device.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DeviceKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DeviceKey, deviceItem); + break; + case DatItems.Formats.DeviceRef deviceRef: + var deviceRefItem = deviceRef.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DeviceRefKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DeviceRefKey, deviceRefItem); + break; + case DatItems.Formats.DipSwitch dipSwitch: + var dipSwitchItem = dipSwitch.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DipSwitchKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DipSwitchKey, dipSwitchItem); + + // Add Part mapping + bool dipSwitchContainsPart = dipSwitchItem.ContainsKey(DatItems.Formats.DipSwitch.PartKey); + if (dipSwitchContainsPart) + { + var partItem = dipSwitchItem.Read(DatItems.Formats.DipSwitch.PartKey); + if (partItem is not null) + partMappings[partItem.GetInternalClone()] = dipSwitchItem; + } + + break; + case DatItems.Formats.Disk disk: + var diskItem = disk.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DiskKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DiskKey, diskItem); + + // Add Part and DiskArea mappings + bool diskContainsPart = diskItem.ContainsKey(DatItems.Formats.Disk.PartKey); + bool diskContainsDiskArea = diskItem.ContainsKey(DatItems.Formats.Disk.DiskAreaKey); + if (diskContainsPart && diskContainsDiskArea) + { + var partItem = diskItem.Read(DatItems.Formats.Disk.PartKey); + if (partItem is not null) + { + var partItemInternal = partItem.GetInternalClone(); + partMappings[partItemInternal] = diskItem; + + var diskAreaItem = diskItem.Read(DatItems.Formats.Disk.DiskAreaKey); + if (diskAreaItem is not null) + diskAreaMappings[partItemInternal] = (diskAreaItem.GetInternalClone(), diskItem); + } + } + + break; + case DatItems.Formats.Display display: + var displayItem = ProcessItem(display, machine); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DisplayKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DisplayKey, displayItem); + break; + case DatItems.Formats.Driver driver: + var driverItem = driver.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DriverKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DriverKey, driverItem); + break; + case DatItems.Formats.Feature feature: + var featureItem = feature.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey, featureItem); + break; + case DatItems.Formats.Info info: + var infoItem = info.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.InfoKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.InfoKey, infoItem); + break; + case DatItems.Formats.Input input: + var inputItem = input.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.InputKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.InputKey, inputItem); + break; + case DatItems.Formats.Media media: + var mediaItem = media.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.MediaKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.MediaKey, mediaItem); + break; + case DatItems.Formats.PartFeature partFeature: + var partFeatureItem = partFeature.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey, partFeatureItem); + + // Add Part mapping + bool partFeatureContainsPart = partFeatureItem.ContainsKey(DatItems.Formats.PartFeature.PartKey); + if (partFeatureContainsPart) + { + var partItem = partFeatureItem.Read(DatItems.Formats.PartFeature.PartKey); + if (partItem is not null) + partMappings[partItem.GetInternalClone()] = partFeatureItem; + } + + break; + case DatItems.Formats.Port port: + var portItem = port.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.PortKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.PortKey, portItem); + break; + case DatItems.Formats.RamOption ramOption: + var ramOptionItem = ramOption.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.RamOptionKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.RamOptionKey, ramOptionItem); + break; + case DatItems.Formats.Release release: + var releaseItem = release.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ReleaseKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ReleaseKey, releaseItem); + break; + case DatItems.Formats.Rom rom: + var romItem = ProcessItem(rom, machine); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.RomKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.RomKey, romItem); + + // Add Part and DataArea mappings + bool romContainsPart = romItem.ContainsKey(DatItems.Formats.Rom.PartKey); + bool romContainsDataArea = romItem.ContainsKey(DatItems.Formats.Rom.DataAreaKey); + if (romContainsPart && romContainsDataArea) + { + var partItem = romItem.Read(DatItems.Formats.Rom.PartKey); + if (partItem is not null) + { + var partItemInternal = partItem.GetInternalClone(); + partMappings[partItemInternal] = romItem; + + var dataAreaItem = romItem.Read(DatItems.Formats.Rom.DataAreaKey); + if (dataAreaItem is not null) + dataAreaMappings[partItemInternal] = (dataAreaItem.GetInternalClone(), romItem); + } + } + + break; + case DatItems.Formats.Sample sample: + var sampleItem = sample.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SampleKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SampleKey, sampleItem); + break; + case DatItems.Formats.SharedFeat sharedFeat: + var sharedFeatItem = sharedFeat.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SharedFeatKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SharedFeatKey, sharedFeatItem); + break; + case DatItems.Formats.Slot slot: + var slotItem = slot.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SlotKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SlotKey, slotItem); + break; + case DatItems.Formats.SoftwareList softwareList: + var softwareListItem = softwareList.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SoftwareListKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SoftwareListKey, softwareListItem); + break; + case DatItems.Formats.Sound sound: + var soundItem = sound.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SoundKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SoundKey, soundItem); + break; + } + } + + // Handle Part, DiskItem, and DatItem mappings, if they exist + if (partMappings.Count != 0) + { + // Create a collection to hold the inverted Parts + Dictionary partItems = []; + + // Loop through the Part mappings + foreach (var partMapping in partMappings) + { + // Get the mapping pieces + var partItem = partMapping.Key; + var datItem = partMapping.Value; + + // Get the part name and skip if there's none + string? partName = partItem.ReadString(Data.Models.Metadata.Part.NameKey); + if (partName is null) + continue; + + // Create the part in the dictionary, if needed + if (!partItems.ContainsKey(partName)) + partItems[partName] = []; + + // Copy over string values + partItems[partName][Data.Models.Metadata.Part.NameKey] = partName; + if (!partItems[partName].ContainsKey(Data.Models.Metadata.Part.InterfaceKey)) + partItems[partName][Data.Models.Metadata.Part.InterfaceKey] = partItem.ReadString(Data.Models.Metadata.Part.InterfaceKey); + + // Clear any empty fields + ClearEmptyKeys(partItems[partName]); + + // If the item has a DataArea mapping + if (dataAreaMappings.ContainsKey(partItem)) + { + // Get the mapped items + var (dataArea, romItem) = dataAreaMappings[partItem]; + + // Clear any empty fields + ClearEmptyKeys(romItem); + + // Get the data area name and skip if there's none + string? dataAreaName = dataArea.ReadString(Data.Models.Metadata.DataArea.NameKey); + if (dataAreaName is not null) + { + // Get existing data areas as a list + var dataAreasArr = partItems[partName].Read(Data.Models.Metadata.Part.DataAreaKey) ?? []; + List dataAreas = [.. dataAreasArr]; + + // Find the existing disk area to append to, otherwise create a new disk area + int dataAreaIndex = dataAreas.FindIndex(da => da.ReadString(Data.Models.Metadata.DataArea.NameKey) == dataAreaName); + Data.Models.Metadata.DataArea aggregateDataArea; + if (dataAreaIndex > -1) + { + aggregateDataArea = dataAreas[dataAreaIndex]; + } + else + { + aggregateDataArea = []; + aggregateDataArea[Data.Models.Metadata.DataArea.EndiannessKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.EndiannessKey); + aggregateDataArea[Data.Models.Metadata.DataArea.NameKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.NameKey); + aggregateDataArea[Data.Models.Metadata.DataArea.SizeKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.SizeKey); + aggregateDataArea[Data.Models.Metadata.DataArea.WidthKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.WidthKey); + } + + // Clear any empty fields + ClearEmptyKeys(aggregateDataArea); + + // Get existing roms as a list + var romsArr = aggregateDataArea.Read(Data.Models.Metadata.DataArea.RomKey) ?? []; + List roms = [.. romsArr]; + + // Add the rom to the data area + roms.Add(romItem); + + // Assign back the roms + aggregateDataArea[Data.Models.Metadata.DataArea.RomKey] = roms.ToArray(); + + // Assign back the data area + if (dataAreaIndex > -1) + dataAreas[dataAreaIndex] = aggregateDataArea; + else + dataAreas.Add(aggregateDataArea); + + // Assign back the data areas array + partItems[partName][Data.Models.Metadata.Part.DataAreaKey] = dataAreas.ToArray(); + } + } + + // If the item has a DiskArea mapping + if (diskAreaMappings.ContainsKey(partItem)) + { + // Get the mapped items + var (diskArea, diskItem) = diskAreaMappings[partItem]; + + // Clear any empty fields + ClearEmptyKeys(diskItem); + + // Get the disk area name and skip if there's none + string? diskAreaName = diskArea.ReadString(Data.Models.Metadata.DiskArea.NameKey); + if (diskAreaName is not null) + { + // Get existing disk areas as a list + var diskAreasArr = partItems[partName].Read(Data.Models.Metadata.Part.DiskAreaKey) ?? []; + List diskAreas = [.. diskAreasArr]; + + // Find the existing disk area to append to, otherwise create a new disk area + int diskAreaIndex = diskAreas.FindIndex(da => da.ReadString(Data.Models.Metadata.DiskArea.NameKey) == diskAreaName); + Data.Models.Metadata.DiskArea aggregateDiskArea; + if (diskAreaIndex > -1) + { + aggregateDiskArea = diskAreas[diskAreaIndex]; + } + else + { + aggregateDiskArea = []; + aggregateDiskArea[Data.Models.Metadata.DiskArea.NameKey] = diskArea.ReadString(Data.Models.Metadata.DiskArea.NameKey); + } + + // Clear any empty fields + ClearEmptyKeys(aggregateDiskArea); + + // Get existing disks as a list + var disksArr = aggregateDiskArea.Read(Data.Models.Metadata.DiskArea.DiskKey) ?? []; + List disks = [.. disksArr]; + + // Add the disk to the data area + disks.Add(diskItem); + + // Assign back the disks + aggregateDiskArea[Data.Models.Metadata.DiskArea.DiskKey] = disks.ToArray(); + + // Assign back the disk area + if (diskAreaIndex > -1) + diskAreas[diskAreaIndex] = aggregateDiskArea; + else + diskAreas.Add(aggregateDiskArea); + + // Assign back the disk areas array + partItems[partName][Data.Models.Metadata.Part.DiskAreaKey] = diskAreas.ToArray(); + } + } + + // If the item is a DipSwitch + if (datItem is Data.Models.Metadata.DipSwitch dipSwitchItem) + { + // Get existing dipswitches as a list + var dipSwitchesArr = partItems[partName].Read(Data.Models.Metadata.Part.DipSwitchKey) ?? []; + List dipSwitches = [.. dipSwitchesArr]; + + // Clear any empty fields + ClearEmptyKeys(dipSwitchItem); + + // Add the dipswitch + dipSwitches.Add(dipSwitchItem); + + // Assign back the dipswitches + partItems[partName][Data.Models.Metadata.Part.DipSwitchKey] = dipSwitches.ToArray(); + } + + // If the item is a Feature + else if (datItem is Data.Models.Metadata.Feature featureItem) + { + // Get existing features as a list + var featuresArr = partItems[partName].Read(Data.Models.Metadata.Part.FeatureKey) ?? []; + List features = [.. featuresArr]; + + // Clear any empty fields + ClearEmptyKeys(featureItem); + + // Add the feature + features.Add(featureItem); + + // Assign back the features + partItems[partName][Data.Models.Metadata.Part.FeatureKey] = features.ToArray(); + } + } + + // Assign the part array to the machine + machine[Data.Models.Metadata.Machine.PartKey] = (Data.Models.Metadata.Part[])[.. partItems.Values]; + } + + // Add the machine to the list + machines.Add(machine); + } + + // Return the list of machines + return [.. machines]; + } + + /// + /// Convert machines information + /// + private Data.Models.Metadata.Machine[]? ConvertMachinesDB(bool ignoreblanks = false) + { + // Create a machine list to hold all outputs + List machines = []; + + // Loop through the sorted items and create games for them + foreach (string key in ItemsDB.SortedKeys) + { + var items = GetItemsForBucketDB(key, filter: true); + if (items is null || items.Count == 0) + continue; + + // Create a machine to hold everything + var machine = GetMachineForItemDB(items.First().Key).Value!.GetInternalClone(); + + // Handle Trurip object, if it exists + if (machine.ContainsKey(Data.Models.Metadata.Machine.TruripKey)) + { + var trurip = machine.Read(Data.Models.Metadata.Machine.TruripKey); + if (trurip is not null) + { + var truripItem = trurip.ConvertToLogiqx(); + truripItem.Publisher = machine.ReadString(Data.Models.Metadata.Machine.PublisherKey); + truripItem.Year = machine.ReadString(Data.Models.Metadata.Machine.YearKey); + truripItem.Players = machine.ReadString(Data.Models.Metadata.Machine.PlayersKey); + truripItem.Source = machine.ReadString(Data.Models.Metadata.Machine.SourceFileKey); + truripItem.CloneOf = machine.ReadString(Data.Models.Metadata.Machine.CloneOfKey); + machine[Data.Models.Metadata.Machine.TruripKey] = truripItem; + } + } + + // Create mapping dictionaries for the Parts, DataAreas, and DiskAreas associated with this machine + Dictionary partMappings = []; + Dictionary dataAreaMappings = []; + Dictionary diskAreaMappings = []; + + // Loop through and convert the items to respective lists + foreach (var kvp in items) + { + // Check for a "null" item + var item = new KeyValuePair(kvp.Key, ProcessNullifiedItem(kvp.Value)); + + // Skip if we're ignoring the item + if (ShouldIgnore(item.Value, ignoreblanks)) + continue; + + switch (item.Value) + { + case DatItems.Formats.Adjuster adjuster: + var adjusterItem = adjuster.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.AdjusterKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.AdjusterKey, adjusterItem); + break; + case DatItems.Formats.Archive archive: + var archiveItem = archive.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ArchiveKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ArchiveKey, archiveItem); + break; + case DatItems.Formats.BiosSet biosSet: + var biosSetItem = biosSet.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.BiosSetKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.BiosSetKey, biosSetItem); + break; + case DatItems.Formats.Chip chip: + var chipItem = chip.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ChipKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ChipKey, chipItem); + break; + case DatItems.Formats.Configuration configuration: + var configurationItem = configuration.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ConfigurationKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ConfigurationKey, configurationItem); + break; + case DatItems.Formats.Device device: + var deviceItem = device.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DeviceKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DeviceKey, deviceItem); + break; + case DatItems.Formats.DeviceRef deviceRef: + var deviceRefItem = deviceRef.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DeviceRefKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DeviceRefKey, deviceRefItem); + break; + case DatItems.Formats.DipSwitch dipSwitch: + var dipSwitchItem = dipSwitch.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DipSwitchKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DipSwitchKey, dipSwitchItem); + + // Add Part mapping + bool dipSwitchContainsPart = dipSwitchItem.ContainsKey(DatItems.Formats.DipSwitch.PartKey); + if (dipSwitchContainsPart) + { + var partItem = dipSwitchItem.Read(DatItems.Formats.DipSwitch.PartKey); + if (partItem is not null) + partMappings[partItem.GetInternalClone()] = dipSwitchItem; + } + + break; + case DatItems.Formats.Disk disk: + var diskItem = disk.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DiskKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DiskKey, diskItem); + + // Add Part and DiskArea mappings + bool diskContainsPart = diskItem.ContainsKey(DatItems.Formats.Disk.PartKey); + bool diskContainsDiskArea = diskItem.ContainsKey(DatItems.Formats.Disk.DiskAreaKey); + if (diskContainsPart && diskContainsDiskArea) + { + var partItem = diskItem.Read(DatItems.Formats.Disk.PartKey); + if (partItem is not null) + { + var partItemInternal = partItem.GetInternalClone(); + partMappings[partItemInternal] = diskItem; + + var diskAreaItem = diskItem.Read(DatItems.Formats.Disk.DiskAreaKey); + if (diskAreaItem is not null) + diskAreaMappings[partItemInternal] = (diskAreaItem.GetInternalClone(), diskItem); + } + } + + break; + case DatItems.Formats.Display display: + var displayItem = ProcessItem(display, machine); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DisplayKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DisplayKey, displayItem); + break; + case DatItems.Formats.Driver driver: + var driverItem = driver.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DriverKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DriverKey, driverItem); + break; + case DatItems.Formats.Feature feature: + var featureItem = feature.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey, featureItem); + break; + case DatItems.Formats.Info info: + var infoItem = info.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.InfoKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.InfoKey, infoItem); + break; + case DatItems.Formats.Input input: + var inputItem = input.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.InputKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.InputKey, inputItem); + break; + case DatItems.Formats.Media media: + var mediaItem = media.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.MediaKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.MediaKey, mediaItem); + break; + case DatItems.Formats.PartFeature partFeature: + var partFeatureItem = partFeature.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.FeatureKey, partFeatureItem); + + // Add Part mapping + bool partFeatureContainsPart = partFeatureItem.ContainsKey(DatItems.Formats.PartFeature.PartKey); + if (partFeatureContainsPart) + { + var partItem = partFeatureItem.Read(DatItems.Formats.PartFeature.PartKey); + if (partItem is not null) + partMappings[partItem.GetInternalClone()] = partFeatureItem; + } + + break; + case DatItems.Formats.Port port: + var portItem = port.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.PortKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.PortKey, portItem); + break; + case DatItems.Formats.RamOption ramOption: + var ramOptionItem = ramOption.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.RamOptionKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.RamOptionKey, ramOptionItem); + break; + case DatItems.Formats.Release release: + var releaseItem = release.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.ReleaseKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.ReleaseKey, releaseItem); + break; + case DatItems.Formats.Rom rom: + var romItem = ProcessItem(rom, machine); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.RomKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.RomKey, romItem); + + // Add Part and DataArea mappings + bool romContainsPart = romItem.ContainsKey(DatItems.Formats.Rom.PartKey); + bool romContainsDataArea = romItem.ContainsKey(DatItems.Formats.Rom.DataAreaKey); + if (romContainsPart && romContainsDataArea) + { + var partItem = romItem.Read(DatItems.Formats.Rom.PartKey); + if (partItem is not null) + { + var partItemInternal = partItem.GetInternalClone(); + partMappings[partItemInternal] = romItem; + + var dataAreaItem = romItem.Read(DatItems.Formats.Rom.DataAreaKey); + if (dataAreaItem is not null) + dataAreaMappings[partItemInternal] = (dataAreaItem.GetInternalClone(), romItem); + } + } + + break; + case DatItems.Formats.Sample sample: + var sampleItem = sample.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SampleKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SampleKey, sampleItem); + break; + case DatItems.Formats.SharedFeat sharedFeat: + var sharedFeatItem = sharedFeat.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SharedFeatKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SharedFeatKey, sharedFeatItem); + break; + case DatItems.Formats.Slot slot: + var slotItem = slot.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SlotKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SlotKey, slotItem); + break; + case DatItems.Formats.SoftwareList softwareList: + var softwareListItem = softwareList.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SoftwareListKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SoftwareListKey, softwareListItem); + break; + case DatItems.Formats.Sound sound: + var soundItem = sound.GetInternalClone(); + EnsureMachineKey(machine, Data.Models.Metadata.Machine.SoundKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.SoundKey, soundItem); + break; + } + } + + // Handle Part, DiskItem, and DatItem mappings, if they exist + if (partMappings.Count != 0) + { + // Create a collection to hold the inverted Parts + Dictionary partItems = []; + + // Loop through the Part mappings + foreach (var partMapping in partMappings) + { + // Get the mapping pieces + var partItem = partMapping.Key; + var datItem = partMapping.Value; + + // Get the part name and skip if there's none + string? partName = partItem.ReadString(Data.Models.Metadata.Part.NameKey); + if (partName is null) + continue; + + // Create the part in the dictionary, if needed + if (!partItems.ContainsKey(partName)) + partItems[partName] = []; + + // Copy over string values + partItems[partName][Data.Models.Metadata.Part.NameKey] = partName; + if (!partItems[partName].ContainsKey(Data.Models.Metadata.Part.InterfaceKey)) + partItems[partName][Data.Models.Metadata.Part.InterfaceKey] = partItem.ReadString(Data.Models.Metadata.Part.InterfaceKey); + + // Clear any empty fields + ClearEmptyKeys(partItems[partName]); + + // If the item has a DataArea mapping + if (dataAreaMappings.ContainsKey(partItem)) + { + // Get the mapped items + var (dataArea, romItem) = dataAreaMappings[partItem]; + + // Clear any empty fields + ClearEmptyKeys(romItem); + + // Get the data area name and skip if there's none + string? dataAreaName = dataArea.ReadString(Data.Models.Metadata.DataArea.NameKey); + if (dataAreaName is not null) + { + // Get existing data areas as a list + var dataAreasArr = partItems[partName].Read(Data.Models.Metadata.Part.DataAreaKey) ?? []; + List dataAreas = [.. dataAreasArr]; + + // Find the existing disk area to append to, otherwise create a new disk area + int dataAreaIndex = dataAreas.FindIndex(da => da.ReadString(Data.Models.Metadata.DataArea.NameKey) == dataAreaName); + Data.Models.Metadata.DataArea aggregateDataArea; + if (dataAreaIndex > -1) + { + aggregateDataArea = dataAreas[dataAreaIndex]; + } + else + { + aggregateDataArea = []; + aggregateDataArea[Data.Models.Metadata.DataArea.EndiannessKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.EndiannessKey); + aggregateDataArea[Data.Models.Metadata.DataArea.NameKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.NameKey); + aggregateDataArea[Data.Models.Metadata.DataArea.SizeKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.SizeKey); + aggregateDataArea[Data.Models.Metadata.DataArea.WidthKey] = dataArea.ReadString(Data.Models.Metadata.DataArea.WidthKey); + } + + // Clear any empty fields + ClearEmptyKeys(aggregateDataArea); + + // Get existing roms as a list + var romsArr = aggregateDataArea.Read(Data.Models.Metadata.DataArea.RomKey) ?? []; + List roms = [.. romsArr]; + + // Add the rom to the data area + roms.Add(romItem); + + // Assign back the roms + aggregateDataArea[Data.Models.Metadata.DataArea.RomKey] = roms.ToArray(); + + // Assign back the data area + if (dataAreaIndex > -1) + dataAreas[dataAreaIndex] = aggregateDataArea; + else + dataAreas.Add(aggregateDataArea); + + // Assign back the data areas array + partItems[partName][Data.Models.Metadata.Part.DataAreaKey] = dataAreas.ToArray(); + } + } + + // If the item has a DiskArea mapping + if (diskAreaMappings.ContainsKey(partItem)) + { + // Get the mapped items + var (diskArea, diskItem) = diskAreaMappings[partItem]; + + // Clear any empty fields + ClearEmptyKeys(diskItem); + + // Get the disk area name and skip if there's none + string? diskAreaName = diskArea.ReadString(Data.Models.Metadata.DiskArea.NameKey); + if (diskAreaName is not null) + { + // Get existing disk areas as a list + var diskAreasArr = partItems[partName].Read(Data.Models.Metadata.Part.DiskAreaKey) ?? []; + List diskAreas = [.. diskAreasArr]; + + // Find the existing disk area to append to, otherwise create a new disk area + int diskAreaIndex = diskAreas.FindIndex(da => da.ReadString(Data.Models.Metadata.DiskArea.NameKey) == diskAreaName); + Data.Models.Metadata.DiskArea aggregateDiskArea; + if (diskAreaIndex > -1) + { + aggregateDiskArea = diskAreas[diskAreaIndex]; + } + else + { + aggregateDiskArea = []; + aggregateDiskArea[Data.Models.Metadata.DiskArea.NameKey] = diskArea.ReadString(Data.Models.Metadata.DiskArea.NameKey); + } + + // Clear any empty fields + ClearEmptyKeys(aggregateDiskArea); + + // Get existing disks as a list + var disksArr = aggregateDiskArea.Read(Data.Models.Metadata.DiskArea.DiskKey) ?? []; + List disks = [.. disksArr]; + + // Add the disk to the data area + disks.Add(diskItem); + + // Assign back the disks + aggregateDiskArea[Data.Models.Metadata.DiskArea.DiskKey] = disks.ToArray(); + + // Assign back the disk area + if (diskAreaIndex > -1) + diskAreas[diskAreaIndex] = aggregateDiskArea; + else + diskAreas.Add(aggregateDiskArea); + + // Assign back the disk areas array + partItems[partName][Data.Models.Metadata.Part.DiskAreaKey] = diskAreas.ToArray(); + } + } + + // If the item is a DipSwitch + if (datItem is Data.Models.Metadata.DipSwitch dipSwitchItem) + { + // Get existing dipswitches as a list + var dipSwitchesArr = partItems[partName].Read(Data.Models.Metadata.Part.DipSwitchKey) ?? []; + List dipSwitches = [.. dipSwitchesArr]; + + // Clear any empty fields + ClearEmptyKeys(dipSwitchItem); + + // Add the dipswitch + dipSwitches.Add(dipSwitchItem); + + // Assign back the dipswitches + partItems[partName][Data.Models.Metadata.Part.DipSwitchKey] = dipSwitches.ToArray(); + } + + // If the item is a Feature + else if (datItem is Data.Models.Metadata.Feature featureItem) + { + // Get existing features as a list + var featuresArr = partItems[partName].Read(Data.Models.Metadata.Part.FeatureKey) ?? []; + List features = [.. featuresArr]; + + // Clear any empty fields + ClearEmptyKeys(featureItem); + + // Add the feature + features.Add(featureItem); + + // Assign back the features + partItems[partName][Data.Models.Metadata.Part.FeatureKey] = features.ToArray(); + } + } + + // Assign the part array to the machine + machine[Data.Models.Metadata.Machine.PartKey] = (Data.Models.Metadata.Part[])[.. partItems.Values]; + } + + // Add the machine to the list + machines.Add(machine); + } + + // Return the list of machines + return [.. machines]; + } + + /// + /// Convert Display information + /// + /// Item to convert + /// Machine to use for Video + /// + /// This method is required because both a Display and a Video + /// item might be created and added for a given Display input. + /// + private static Data.Models.Metadata.Display ProcessItem(DatItems.Formats.Display item, Data.Models.Metadata.Machine machine) + { + var displayItem = item.GetInternalClone(); + + // Create a Video for any item that has specific fields + if (displayItem.ContainsKey(Data.Models.Metadata.Video.AspectXKey)) + { + var videoItem = new Data.Models.Metadata.Video + { + [Data.Models.Metadata.Video.AspectXKey] = displayItem.ReadLong(Data.Models.Metadata.Video.AspectXKey).ToString(), + [Data.Models.Metadata.Video.AspectYKey] = displayItem.ReadLong(Data.Models.Metadata.Video.AspectYKey).ToString(), + [Data.Models.Metadata.Video.HeightKey] = displayItem.ReadLong(Data.Models.Metadata.Display.HeightKey).ToString(), + [Data.Models.Metadata.Video.RefreshKey] = displayItem.ReadDouble(Data.Models.Metadata.Display.RefreshKey).ToString(), + [Data.Models.Metadata.Video.ScreenKey] = displayItem.ReadString(Data.Models.Metadata.Display.DisplayTypeKey).AsDisplayType().AsStringValue(), + [Data.Models.Metadata.Video.WidthKey] = displayItem.ReadLong(Data.Models.Metadata.Display.WidthKey).ToString() + }; + + switch (displayItem.ReadLong(Data.Models.Metadata.Display.RotateKey)) + { + case 0: + case 180: + videoItem[Data.Models.Metadata.Video.OrientationKey] = "horizontal"; + break; + case 90: + case 270: + videoItem[Data.Models.Metadata.Video.OrientationKey] = "vertical"; + break; + } + + EnsureMachineKey(machine, Data.Models.Metadata.Machine.VideoKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.VideoKey, videoItem); + } + + return displayItem; + } + + /// + /// Convert Rom information + /// + /// Item to convert + /// Machine to use for Part and DataArea + /// + /// This method is required because both a Rom and a Dump + /// item might be created and added for a given Rom input. + /// + private static Data.Models.Metadata.Rom ProcessItem(DatItems.Formats.Rom item, Data.Models.Metadata.Machine machine) + { + var romItem = item.GetInternalClone(); + + // Create a Dump for every Rom that has a subtype + switch (romItem.ReadString(Data.Models.Metadata.Rom.OpenMSXMediaType).AsOpenMSXSubType()) + { + case OpenMSXSubType.Rom: + var dumpRom = new Data.Models.Metadata.Dump(); + var rom = new Data.Models.Metadata.Rom(); + + rom[Data.Models.Metadata.Rom.NameKey] = romItem.ReadString(Data.Models.Metadata.Rom.NameKey); + rom[Data.Models.Metadata.Rom.OffsetKey] = romItem.ReadString(Data.Models.Metadata.Rom.StartKey) ?? romItem.ReadString(Data.Models.Metadata.Rom.OffsetKey); + rom[Data.Models.Metadata.Rom.OpenMSXType] = romItem.ReadString(Data.Models.Metadata.Rom.OpenMSXType); + rom[Data.Models.Metadata.Rom.RemarkKey] = romItem.ReadString(Data.Models.Metadata.Rom.RemarkKey); + rom[Data.Models.Metadata.Rom.SHA1Key] = romItem.ReadString(Data.Models.Metadata.Rom.SHA1Key); + rom[Data.Models.Metadata.Rom.StartKey] = romItem.ReadString(Data.Models.Metadata.Rom.StartKey) ?? romItem.ReadString(Data.Models.Metadata.Rom.OffsetKey); + + dumpRom[Data.Models.Metadata.Dump.RomKey] = rom; + + var romOriginal = romItem.Read("ORIGINAL"); + if (romOriginal is not null) + { + var newOriginal = new Data.Models.Metadata.Original + { + [Data.Models.Metadata.Original.ValueKey] = romOriginal.Value.FromYesNo(), + [Data.Models.Metadata.Original.ContentKey] = romOriginal.Content, + }; + dumpRom[Data.Models.Metadata.Dump.OriginalKey] = newOriginal; + } + + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DumpKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DumpKey, dumpRom); + break; + + case OpenMSXSubType.MegaRom: + var dumpMegaRom = new Data.Models.Metadata.Dump(); + var megaRom = new Data.Models.Metadata.Rom(); + + megaRom[Data.Models.Metadata.Rom.NameKey] = romItem.ReadString(Data.Models.Metadata.Rom.NameKey); + megaRom[Data.Models.Metadata.Rom.OffsetKey] = romItem.ReadString(Data.Models.Metadata.Rom.StartKey) ?? romItem.ReadString(Data.Models.Metadata.Rom.OffsetKey); + megaRom[Data.Models.Metadata.Rom.OpenMSXType] = romItem.ReadString(Data.Models.Metadata.Rom.OpenMSXType); + megaRom[Data.Models.Metadata.Rom.RemarkKey] = romItem.ReadString(Data.Models.Metadata.Rom.RemarkKey); + megaRom[Data.Models.Metadata.Rom.SHA1Key] = romItem.ReadString(Data.Models.Metadata.Rom.SHA1Key); + megaRom[Data.Models.Metadata.Rom.StartKey] = romItem.ReadString(Data.Models.Metadata.Rom.StartKey) ?? romItem.ReadString(Data.Models.Metadata.Rom.OffsetKey); + + dumpMegaRom[Data.Models.Metadata.Dump.MegaRomKey] = megaRom; + + var megaRomOriginal = romItem.Read("ORIGINAL"); + if (megaRomOriginal is not null) + { + var newOriginal = new Data.Models.Metadata.Original + { + [Data.Models.Metadata.Original.ValueKey] = megaRomOriginal.Value.FromYesNo(), + [Data.Models.Metadata.Original.ContentKey] = megaRomOriginal.Content, + }; + dumpMegaRom[Data.Models.Metadata.Dump.OriginalKey] = newOriginal; + } + + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DumpKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DumpKey, dumpMegaRom); + break; + + case OpenMSXSubType.SCCPlusCart: + var dumpSccPlusCart = new Data.Models.Metadata.Dump(); + var sccPlusCart = new Data.Models.Metadata.Rom(); + + sccPlusCart[Data.Models.Metadata.Rom.NameKey] = romItem.ReadString(Data.Models.Metadata.Rom.NameKey); + sccPlusCart[Data.Models.Metadata.Rom.OffsetKey] = romItem.ReadString(Data.Models.Metadata.Rom.StartKey) ?? romItem.ReadString(Data.Models.Metadata.Rom.OffsetKey); + sccPlusCart[Data.Models.Metadata.Rom.OpenMSXType] = romItem.ReadString(Data.Models.Metadata.Rom.OpenMSXType); + sccPlusCart[Data.Models.Metadata.Rom.RemarkKey] = romItem.ReadString(Data.Models.Metadata.Rom.RemarkKey); + sccPlusCart[Data.Models.Metadata.Rom.SHA1Key] = romItem.ReadString(Data.Models.Metadata.Rom.SHA1Key); + sccPlusCart[Data.Models.Metadata.Rom.StartKey] = romItem.ReadString(Data.Models.Metadata.Rom.StartKey) ?? romItem.ReadString(Data.Models.Metadata.Rom.OffsetKey); + + dumpSccPlusCart[Data.Models.Metadata.Dump.RomKey] = sccPlusCart; + + var sccPlusCartOriginal = romItem.Read("ORIGINAL"); + if (sccPlusCartOriginal is not null) + { + var newOriginal = new Data.Models.Metadata.Original + { + [Data.Models.Metadata.Original.ValueKey] = sccPlusCartOriginal.Value.FromYesNo(), + [Data.Models.Metadata.Original.ContentKey] = sccPlusCartOriginal.Content, + }; + dumpSccPlusCart[Data.Models.Metadata.Dump.OriginalKey] = newOriginal; + } + + EnsureMachineKey(machine, Data.Models.Metadata.Machine.DumpKey); + AppendToMachineKey(machine, Data.Models.Metadata.Machine.DumpKey, dumpSccPlusCart); + break; + } + + return romItem; + } + + /// + /// Ensure a key in a machine + /// + private static void EnsureMachineKey(Data.Models.Metadata.Machine machine, string key) + { + if (machine.Read(key) is null) +#if NET20 || NET35 || NET40 || NET452 + machine[key] = new T[0]; +#else + machine[key] = System.Array.Empty(); +#endif + } + + /// + /// Append to a machine key as if its an array + /// + private static void AppendToMachineKey(Data.Models.Metadata.Machine machine, string key, T value) where T : Data.Models.Metadata.DatItem + { + // Get the existing array + var arr = machine.Read(key); + if (arr is null) + return; + + // Trim all null fields + ClearEmptyKeys(value); + + // Add to the array + List list = [.. arr]; + list.Add(value); + machine[key] = list.ToArray(); + } + + /// + /// Clear empty keys from a DictionaryBase object + /// + private static void ClearEmptyKeys(Data.Models.Metadata.DictionaryBase obj) + { + string[] fieldNames = [.. obj.Keys]; + foreach (string fieldName in fieldNames) + { + if (obj[fieldName] is null) + obj.Remove(fieldName); + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatFile.cs b/SabreTools.Metadata.DatFiles/DatFile.cs new file mode 100644 index 00000000..6d863b8f --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatFile.cs @@ -0,0 +1,1252 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.Tools; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using SabreTools.Hashing; +using SabreTools.IO.Logging; +using SabreTools.Text.Compare; + +namespace SabreTools.Metadata.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(); + + /// + /// Modifier values + /// + [JsonProperty("modifiers"), XmlElement("modifiers")] + public DatModifiers Modifiers { get; private set; } = new DatModifiers(); + + /// + /// DatItems and related statistics + /// + [JsonProperty("items"), XmlElement("items")] + public ItemDictionary Items { get; private set; } = new ItemDictionary(); + + /// + /// DatItems and related statistics + /// + [JsonProperty("items"), XmlElement("items")] + public ItemDictionaryDB ItemsDB { get; private set; } = new ItemDictionaryDB(); + + /// + /// DAT statistics + /// + [JsonIgnore, XmlIgnore] + public DatStatistics DatStatistics => Items.DatStatistics; + //public DatStatistics DatStatistics => ItemsDB.DatStatistics; + + /// + /// List of supported types for writing + /// + public abstract ItemType[] SupportedTypes { get; } + + #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 is not null) + { + Header = (DatHeader)datFile.Header.Clone(); + Modifiers = (DatModifiers)datFile.Modifiers.Clone(); + Items = datFile.Items; + ItemsDB = datFile.ItemsDB; + } + } + + /// + /// 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(Data.Models.Metadata.Header.NameKey); + string? description = Header.GetStringFieldValue(Data.Models.Metadata.Header.DescriptionKey); + string? date = Header.GetStringFieldValue(Data.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)) + { + name = description + (bare ? string.Empty : $" ({date})"); + } + + // If the name is defined but not the description, set the description from the name + else if (!string.IsNullOrEmpty(name) && string.IsNullOrEmpty(description)) + { + description = 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 || NETSTANDARD + name = splitpath[splitpath.Length - 1]; + description = splitpath[splitpath.Length - 1] + (bare ? string.Empty : $" ({date})"); +#else + name = splitpath[^1] + (bare ? string.Empty : $" ({date})"); + description = splitpath[^1] + (bare ? string.Empty : $" ({date})"); +#endif + } + + // Trim both fields + name = name?.Trim(); + description = description?.Trim(); + + // Set the fields back + Header.SetFieldValue(Data.Models.Metadata.Header.NameKey, name); + Header.SetFieldValue(Data.Models.Metadata.Header.DescriptionKey, description); + } + + #endregion + + #region Accessors + + /// + /// Remove any keys that have null or empty values + /// + public void ClearEmpty() + { + ClearEmptyImpl(); + ClearEmptyImplDB(); + } + + /// + /// Set the internal header + /// + /// Replacement header to be used + public void SetHeader(DatHeader? datHeader) + { + if (datHeader is not null) + Header = (DatHeader)datHeader.Clone(); + } + + /// + /// Set the internal header + /// + /// Replacement header to be used + public void SetModifiers(DatModifiers datModifers) + { + Modifiers = (DatModifiers)datModifers.Clone(); + } + + /// + /// Remove any keys that have null or empty values + /// + private void ClearEmptyImpl() + { + foreach (string key in Items.SortedKeys) + { + // If the value is empty, remove + List value = GetItemsForBucket(key); + if (value.Count == 0) + RemoveBucket(key); + + // If there are no non-blank items, remove + else if (value.FindIndex(i => i is not null && i is not Blank) == -1) + RemoveBucket(key); + } + } + + /// + /// Remove any keys that have null or empty values + /// + private void ClearEmptyImplDB() + { + foreach (string key in ItemsDB.SortedKeys) + { + // If the value is empty, remove + List value = [.. GetItemsForBucketDB(key).Values]; + if (value.Count == 0) + RemoveBucketDB(key); + + // If there are no non-blank items, remove + else if (value.FindIndex(i => i is not null && i is not Blank) == -1) + RemoveBucketDB(key); + } + } + + #endregion + + #region Item Dictionary Passthrough - Accessors + + /// + /// 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 + public string AddItem(DatItem item, bool statsOnly) + { + return Items.AddItem(item, statsOnly); + } + + /// + /// Add a DatItem to the dictionary after validation + /// + /// Item data to validate + /// Index of the machine related to the item + /// Index of the source related to the item + /// True to only add item statistics while parsing, false otherwise + /// The index for the added item, -1 on error + public long AddItemDB(DatItem item, long machineIndex, long sourceIndex, bool statsOnly) + { + return ItemsDB.AddItem(item, machineIndex, sourceIndex, statsOnly); + } + + /// + /// Add a machine, returning the insert index + /// + public long AddMachineDB(Machine machine) + { + return ItemsDB.AddMachine(machine); + } + + /// + /// Add a source, returning the insert index + /// + public long AddSourceDB(Source source) + { + return ItemsDB.AddSource(source); + } + + /// + /// Remove all items marked for removal + /// + public void ClearMarked() + { + Items.ClearMarked(); + ItemsDB.ClearMarked(); + } + + /// + /// Get the items associated with a bucket name + /// + public List GetItemsForBucket(string? bucketName, bool filter = false) + => Items.GetItemsForBucket(bucketName, filter); + + /// + /// Get the indices and items associated with a bucket name + /// + public Dictionary GetItemsForBucketDB(string? bucketName, bool filter = false) + => ItemsDB.GetItemsForBucket(bucketName, filter); + + /// + /// Get all machines and their indicies + /// + public IDictionary GetMachinesDB() + => ItemsDB.GetMachines(); + + /// + /// Get the index and machine associated with an item index + /// + public KeyValuePair GetMachineForItemDB(long itemIndex) + => ItemsDB.GetMachineForItem(itemIndex); + + /// + /// Get the index and source associated with an item index + /// + public KeyValuePair GetSourceForItemDB(long itemIndex) + => ItemsDB.GetSourceForItem(itemIndex); + + /// + /// Remove a key from the file dictionary if it exists + /// + /// Key in the dictionary to remove + public bool RemoveBucket(string key) + { + return Items.RemoveBucket(key); + } + + /// + /// Remove a key from the file dictionary if it exists + /// + /// Key in the dictionary to remove + public bool RemoveBucketDB(string key) + { + return ItemsDB.RemoveBucket(key); + } + + /// + /// Remove the indexed instance of a value from the file dictionary if it exists + /// + /// Key in the dictionary to remove from + /// Value to remove from the dictionary + /// Index of the item to be removed + public bool RemoveItem(string key, DatItem value, int index) + { + return Items.RemoveItem(key, value, index); + } + + /// + /// Remove an item, returning if it could be removed + /// + public bool RemoveItemDB(long itemIndex) + { + return ItemsDB.RemoveItem(itemIndex); + } + + /// + /// Remove a machine, returning if it could be removed + /// + public bool RemoveMachineDB(long machineIndex) + { + return ItemsDB.RemoveMachine(machineIndex); + } + + /// + /// Remove a machine, returning if it could be removed + /// + public bool RemoveMachineDB(string machineName) + { + return ItemsDB.RemoveMachine(machineName); + } + + /// + /// Reset the internal item dictionary + /// + public void ResetDictionary() + { + Items = new ItemDictionary(); + ItemsDB = new ItemDictionaryDB(); + } + + #endregion + + #region Item Dictionary Passthrough - Bucketing + + /// + /// Take the arbitrarily bucketed Files Dictionary and convert to one bucketed by a user-defined method + /// + /// ItemKey enum representing how to bucket the individual items + /// True if the key should be lowercased (default), false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + public void BucketBy(ItemKey bucketBy, bool lower = true, bool norename = true) + { + Items.BucketBy(bucketBy, lower, norename); + //ItemsDB.BucketBy(bucketBy, lower, norename); + } + + /// + /// Perform deduplication based on the deduplication type provided + /// + public void Deduplicate() + { + Items.Deduplicate(); + ItemsDB.Deduplicate(); + } + + /// + /// List all duplicates found in a DAT based on a DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// List of matched DatItem objects + public List GetDuplicates(DatItem datItem, bool sorted = false) + => Items.GetDuplicates(datItem, sorted); + + /// + /// List all duplicates found in a DAT based on a DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// List of matched DatItem objects + public Dictionary GetDuplicatesDB(KeyValuePair datItem, bool sorted = false) + => ItemsDB.GetDuplicates(datItem, sorted); + + /// + /// Check if a DAT contains the given DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// True if it contains the rom, false otherwise + public bool HasDuplicates(DatItem datItem, bool sorted = false) + => Items.HasDuplicates(datItem, sorted); + + /// + /// Check if a DAT contains the given DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// True if it contains the rom, false otherwise + public bool HasDuplicates(KeyValuePair datItem, bool sorted = false) + => ItemsDB.HasDuplicates(datItem, sorted); + + #endregion + + #region Item Dictionary Passthrough - Statistics + + /// + /// Recalculate the statistics for the Dat + /// + public void RecalculateStats() + { + Items.RecalculateStats(); + ItemsDB.RecalculateStats(); + } + + #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 + /// True to only add item statistics while parsing, false otherwise + /// Optional FilterRunner to filter items on parse + /// 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, + FilterRunner? filterRunner = null, + bool throwOnError = false); + + #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); + + /// + /// 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, false otherwise + /// + /// There are some unique interactions that can occur because of the large number of effective + /// inputs into this method. + /// - If both a replacement extension is set and the remove extension flag is enabled, + /// the replacement extension will be overridden by the remove extension flag. + /// - Extension addition, removal, and replacement are not done at all if the output + /// depot is specified. Only prefix and postfix logic is applied. + /// - Both methods of using the item name are overridden if the output depot is specified. + /// Instead, the name is always set based on the SHA-1 hash. + /// + protected internal void ProcessItemName(DatItem item, Machine? machine, bool forceRemoveQuotes, bool forceRomName) + { + // Get the relevant processing values + bool quotes = !forceRemoveQuotes && Modifiers.Quotes; + bool useRomName = forceRomName || Modifiers.UseRomName; + + // Create the full Prefix + string pre = Modifiers.Prefix + (quotes ? "\"" : string.Empty); + pre = FormatPrefixPostfix(item, machine, pre); + + // Create the full Postfix + string post = (quotes ? "\"" : string.Empty) + Modifiers.Postfix; + post = FormatPrefixPostfix(item, machine, post); + + // Get the name to update + string? name = (useRomName + ? item.GetName() + : machine?.GetName()) ?? string.Empty; + + // If we're in Depot mode, take care of that instead + if (Modifiers.OutputDepot?.IsActive == true) + { + if (item is Disk disk) + { + // We can only write out if there's a SHA-1 + string? sha1 = disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key); + if (!string.IsNullOrEmpty(sha1)) + { + name = Utilities.GetDepotPath(sha1, Modifiers.OutputDepot.Depth)?.Replace('\\', '/'); + item.SetName($"{pre}{name}{post}"); + } + } + else if (item is DatItems.Formats.File file) + { + // We can only write out if there's a SHA-1 + string? sha1 = file.SHA1; + if (!string.IsNullOrEmpty(sha1)) + { + name = Utilities.GetDepotPath(sha1, Modifiers.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 + string? sha1 = media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key); + if (!string.IsNullOrEmpty(sha1)) + { + name = Utilities.GetDepotPath(sha1, Modifiers.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 + string? sha1 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key); + if (!string.IsNullOrEmpty(sha1)) + { + name = Utilities.GetDepotPath(sha1, Modifiers.OutputDepot.Depth)?.Replace('\\', '/'); + item.SetName($"{pre}{name}{post}"); + } + } + + return; + } + + if (!string.IsNullOrEmpty(Modifiers.ReplaceExtension) || Modifiers.RemoveExtension) + { + if (Modifiers.RemoveExtension) + Modifiers.ReplaceExtension = string.Empty; + + string? dir = Path.GetDirectoryName(name); + if (dir is not null) + { + dir = dir.TrimStart(Path.DirectorySeparatorChar); + name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + Modifiers.ReplaceExtension); + } + } + + if (!string.IsNullOrEmpty(Modifiers.AddExtension)) + name += Modifiers.AddExtension; + + if (useRomName && Modifiers.GameName) + name = Path.Combine(machine?.GetName() ?? string.Empty, name); + + // Now assign back the formatted name + name = $"{pre}{name}{post}"; + if (useRomName) + item.SetName(name); + else + machine?.SetName(name); + } + + /// + /// Format a prefix or postfix string + /// + /// DatItem to create a prefix/postfix for + /// Machine to get information from + /// Prefix or postfix pattern to populate + /// Sanitized string representing the postfix or prefix + protected internal static string FormatPrefixPostfix(DatItem item, Machine? machine, string fix) + { + // Initialize strings + string? type = item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + string + game = machine?.GetName() ?? string.Empty, + manufacturer = machine?.GetStringFieldValue(Data.Models.Metadata.Machine.ManufacturerKey) ?? string.Empty, + publisher = machine?.GetStringFieldValue(Data.Models.Metadata.Machine.PublisherKey) ?? string.Empty, + category = machine?.GetStringFieldValue(Data.Models.Metadata.Machine.CategoryKey) ?? string.Empty, + name = item.GetName() ?? type.AsItemType().AsStringValue() ?? string.Empty, + crc = string.Empty, + md2 = string.Empty, + md4 = string.Empty, + md5 = string.Empty, + ripemd128 = string.Empty, + ripemd160 = string.Empty, + sha1 = string.Empty, + sha256 = string.Empty, + sha384 = string.Empty, + sha512 = string.Empty, + size = string.Empty, + spamsum = string.Empty; + + // Ensure we have the proper values for replacement + if (item is Disk disk) + { + md5 = disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key) ?? string.Empty; + sha1 = disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key) ?? string.Empty; + } + else if (item is DatItems.Formats.File file) + { + name = $"{file.Id}.{file.Extension}"; + size = file.Size.ToString() ?? string.Empty; + crc = file.CRC ?? string.Empty; + md5 = file.MD5 ?? string.Empty; + sha1 = file.SHA1 ?? string.Empty; + sha256 = file.SHA256 ?? string.Empty; + } + else if (item is Media media) + { + md5 = media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key) ?? string.Empty; + sha1 = media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key) ?? string.Empty; + sha256 = media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key) ?? string.Empty; + spamsum = media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey) ?? string.Empty; + } + else if (item is Rom rom) + { + crc = rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey) ?? string.Empty; + md2 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key) ?? string.Empty; + md4 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key) ?? string.Empty; + md5 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key) ?? string.Empty; + ripemd128 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key) ?? string.Empty; + ripemd160 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key) ?? string.Empty; + sha1 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key) ?? string.Empty; + sha256 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key) ?? string.Empty; + sha384 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key) ?? string.Empty; + sha512 = rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key) ?? string.Empty; + size = rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey).ToString() ?? string.Empty; + spamsum = rom.GetStringFieldValue(Data.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("%md2%", md2) + .Replace("%md4%", md4) + .Replace("%md5%", md5) + .Replace("%ripemd128%", ripemd128) + .Replace("%ripemd160%", ripemd160) + .Replace("%sha1%", sha1) + .Replace("%sha256%", sha256) + .Replace("%sha384%", sha384) + .Replace("%sha512%", sha512) + .Replace("%size%", size) + .Replace("%spamsum%", spamsum); + + return fix; + } + + /// + /// Process any DatItems that are "null", usually created from directory population + /// + /// DatItem to check for "null" status + /// Cleaned DatItem, if possible + protected internal static DatItem ProcessNullifiedItem(DatItem item) + { + // If we don't have a Rom, we can ignore it + if (item is not Rom rom) + return item; + + // If the item has a size + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is not null) + return rom; + + // If the item CRC isn't "null" + if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey) != "null") + return rom; + + // If the Rom has "null" characteristics, ensure all fields + rom.SetName(rom.GetName() == "null" ? "-" : rom.GetName()); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "0"); + rom.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey) == "null" ? HashType.CRC32.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key) == "null" ? HashType.MD2.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key) == "null" ? HashType.MD4.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key) == "null" ? HashType.MD5.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key) == "null" ? HashType.RIPEMD128.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key) == "null" ? HashType.RIPEMD160.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key) == "null" ? HashType.SHA1.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key) == "null" ? HashType.SHA256.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key) == "null" ? HashType.SHA384.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key) == "null" ? HashType.SHA512.ZeroString : null); + rom.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, + rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey) == "null" ? HashType.SpamSum.ZeroString : null); + + return rom; + } + + /// + /// Return list of required fields missing from a DatItem + /// + /// List of missing required fields, null or empty if none were found + protected internal virtual List? GetMissingRequiredFields(DatItem datItem) => null; + + /// + /// Get if a list contains any writable items + /// + /// DatItems to check + /// True if the list contains at least one writable item, false otherwise + /// Empty list are kept with this + protected internal bool ContainsWritable(List datItems) + { + // Empty list are considered writable + if (datItems.Count == 0) + return true; + + foreach (DatItem datItem in datItems) + { + ItemType itemType = datItem.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType(); + if (Array.Exists(SupportedTypes, t => t == itemType)) + return true; + } + + return false; + } + + /// + /// Get unique duplicate suffix on name collision + /// + /// String representing the suffix + protected internal static string GetDuplicateSuffix(DatItem datItem) + { + return datItem switch + { + Disk diskItem => GetDuplicateSuffix(diskItem), + DatItems.Formats.File fileItem => GetDuplicateSuffix(fileItem), + Media mediaItem => GetDuplicateSuffix(mediaItem), + Rom romItem => GetDuplicateSuffix(romItem), + _ => "_1", + }; + } + + /// + /// Resolve name duplicates in an arbitrary set of DatItems based on the supplied information + /// + /// List of DatItem objects representing the items to be merged + /// A List of DatItem objects representing the renamed items + protected internal List ResolveNames(List datItems) + { + // Ignore empty lists + if (datItems.Count == 0) + return []; + + // Create the output list + List output = []; + + // First we want to make sure the list is in alphabetical order + Sort(ref datItems, true); + + // Now we want to loop through and check names + DatItem? lastItem = null; + string? lastrenamed = null; + int lastid = 0; + for (int i = 0; i < datItems.Count; i++) + { + DatItem datItem = datItems[i]; + + // If we have the first item, we automatically add it + if (lastItem is null) + { + output.Add(datItem); + lastItem = datItem; + continue; + } + + // Get the last item name, if applicable + string lastItemName = lastItem.GetName() + ?? lastItem.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue() + ?? string.Empty; + + // Get the current item name, if applicable + string datItemName = datItem.GetName() + ?? datItem.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue() + ?? string.Empty; + + // If the current item exactly matches the last item, then we don't add it +#if NET20 || NET35 + if ((Items.GetDuplicateStatus(datItem, lastItem) & DupeType.All) != 0) +#else + if (Items.GetDuplicateStatus(datItem, lastItem).HasFlag(DupeType.All)) +#endif + { + _logger.Verbose($"Exact duplicate found for '{datItemName}'"); + continue; + } + + // If the current name matches the previous name, rename the current item + else if (datItemName == lastItemName) + { + _logger.Verbose($"Name duplicate found for '{datItemName}'"); + + // Get the duplicate suffix + datItemName += GetDuplicateSuffix(datItem); + lastrenamed ??= datItemName; + + // If we have a conflict with the last renamed item, do the right thing + if (datItemName == lastrenamed) + { + lastrenamed = datItemName; + datItemName += lastid == 0 ? string.Empty : "_" + lastid; + lastid++; + } + // If we have no conflict, then we want to reset the lastrenamed and id + else + { + lastrenamed = null; + lastid = 0; + } + + // Set the item name back to the datItem + datItem.SetName(datItemName); + + output.Add(datItem); + } + + // Otherwise, we say that we have a valid named file + else + { + output.Add(datItem); + lastItem = datItem; + lastrenamed = null; + lastid = 0; + } + } + + // One last sort to make sure this is ordered + Sort(ref output, true); + + return output; + } + + /// + /// Resolve name duplicates in an arbitrary set of DatItems based on the supplied information + /// + /// List of item ID to DatItem mappings representing the items to be merged + /// A List of DatItem objects representing the renamed items + protected internal List> ResolveNamesDB(List> mappings) + { + // Ignore empty lists + if (mappings.Count == 0) + return []; + + // Create the output dict + List> output = []; + + // First we want to make sure the list is in alphabetical order + SortDB(ref mappings, true); + + // Now we want to loop through and check names + KeyValuePair? lastItem = null; + string? lastrenamed = null; + int lastid = 0; + foreach (var datItem in mappings) + { + // If we have the first item, we automatically add it + if (lastItem is null) + { + output.Add(datItem); + lastItem = datItem; + continue; + } + + // Get the last item name, if applicable + string lastItemName = lastItem.Value.Value.GetName() + ?? lastItem.Value.Value.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue() + ?? string.Empty; + + // Get the current item name, if applicable + string datItemName = datItem.Value.GetName() + ?? datItem.Value.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue() + ?? string.Empty; + + // Get sources for both items + var datItemSource = ItemsDB.GetSourceForItem(datItem.Key); + var lastItemSource = ItemsDB.GetSourceForItem(lastItem.Value.Key); + + // If the current item exactly matches the last item, then we don't add it +#if NET20 || NET35 + if ((ItemsDB.GetDuplicateStatus(datItem, datItemSource.Value, lastItem, lastItemSource.Value) & DupeType.All) != 0) +#else + if (ItemsDB.GetDuplicateStatus(datItem, datItemSource.Value, lastItem, lastItemSource.Value).HasFlag(DupeType.All)) +#endif + { + _logger.Verbose($"Exact duplicate found for '{datItemName}'"); + continue; + } + + // If the current name matches the previous name, rename the current item + else if (datItemName == lastItemName) + { + _logger.Verbose($"Name duplicate found for '{datItemName}'"); + + // Get the duplicate suffix + datItemName += GetDuplicateSuffix(datItem.Value); + lastrenamed ??= datItemName; + + // If we have a conflict with the last renamed item, do the right thing + if (datItemName == lastrenamed) + { + lastrenamed = datItemName; + datItemName += lastid == 0 ? string.Empty : "_" + lastid; + lastid++; + } + // If we have no conflict, then we want to reset the lastrenamed and id + else + { + lastrenamed = null; + lastid = 0; + } + + // Set the item name back to the datItem + datItem.Value.SetName(datItemName); + output.Add(datItem); + } + + // Otherwise, we say that we have a valid named file + else + { + output.Add(datItem); + lastItem = datItem; + lastrenamed = null; + lastid = 0; + } + } + + // One last sort to make sure this is ordered + SortDB(ref output, true); + + return output; + } + + /// + /// 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 internal bool ShouldIgnore(DatItem? datItem, bool ignoreBlanks) + { + // If this is invoked with a null DatItem, we ignore + if (datItem is 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(Data.Models.Metadata.Rom.SizeKey); + if (size == 0 || size is 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 + ItemType itemType = datItem.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType(); + if (!Array.Exists(SupportedTypes, t => t == itemType)) + { + string itemString = JsonConvert.SerializeObject(datItem, Formatting.None); + _logger.Verbose($"Item '{itemString}' was skipped because it was not supported for output"); + return true; + } + + // If we have an item with missing required fields + List? missingFields = GetMissingRequiredFields(datItem); + if (missingFields is not 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: {string.Join(", ", [.. missingFields])}"); +#else + _logger.Verbose($"Item '{itemString}' was skipped because it was missing required fields: {string.Join(", ", missingFields)}"); +#endif + return true; + } + + return false; + } + + /// + /// Get unique duplicate suffix on name collision + /// + private static string GetDuplicateSuffix(Disk datItem) + { + string? md5 = datItem.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key); + if (!string.IsNullOrEmpty(md5)) + return $"_{md5}"; + + string? sha1 = datItem.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key); + if (!string.IsNullOrEmpty(sha1)) + return $"_{sha1}"; + + return "_1"; + } + + /// + /// Get unique duplicate suffix on name collision + /// + /// String representing the suffix + private static string GetDuplicateSuffix(DatItems.Formats.File datItem) + { + if (!string.IsNullOrEmpty(datItem.CRC)) + return $"_{datItem.CRC}"; + else if (!string.IsNullOrEmpty(datItem.MD5)) + return $"_{datItem.MD5}"; + else if (!string.IsNullOrEmpty(datItem.SHA1)) + return $"_{datItem.SHA1}"; + else if (!string.IsNullOrEmpty(datItem.SHA256)) + return $"_{datItem.SHA256}"; + else + return "_1"; + } + + /// + /// Get unique duplicate suffix on name collision + /// + private static string GetDuplicateSuffix(Media datItem) + { + string? md5 = datItem.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key); + if (!string.IsNullOrEmpty(md5)) + return $"_{md5}"; + + string? sha1 = datItem.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key); + if (!string.IsNullOrEmpty(sha1)) + return $"_{sha1}"; + + string? sha256 = datItem.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key); + if (!string.IsNullOrEmpty(sha256)) + return $"_{sha256}"; + + string? spamSum = datItem.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey); + if (!string.IsNullOrEmpty(spamSum)) + return $"_{spamSum}"; + + return "_1"; + } + + /// + /// Get unique duplicate suffix on name collision + /// + private static string GetDuplicateSuffix(Rom datItem) + { + string? crc = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey); + if (!string.IsNullOrEmpty(crc)) + return $"_{crc}"; + + string? md2 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key); + if (!string.IsNullOrEmpty(md2)) + return $"_{md2}"; + + string? md4 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key); + if (!string.IsNullOrEmpty(md4)) + return $"_{md4}"; + + string? md5 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key); + if (!string.IsNullOrEmpty(md5)) + return $"_{md5}"; + + string? ripemd128 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key); + if (!string.IsNullOrEmpty(ripemd128)) + return $"_{ripemd128}"; + + string? ripemd160 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key); + if (!string.IsNullOrEmpty(ripemd160)) + return $"_{ripemd160}"; + + string? sha1 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key); + if (!string.IsNullOrEmpty(sha1)) + return $"_{sha1}"; + + string? sha256 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key); + if (!string.IsNullOrEmpty(sha256)) + return $"_{sha256}"; + + string? sha384 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key); + if (!string.IsNullOrEmpty(sha384)) + return $"_{sha384}"; + + string? sha512 = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key); + if (!string.IsNullOrEmpty(sha512)) + return $"_{sha512}"; + + string? spamSum = datItem.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey); + if (!string.IsNullOrEmpty(spamSum)) + return $"_{spamSum}"; + + return "_1"; + } + + /// + /// Sort a list of DatItem objects by SourceID, Game, and Name (in order) + /// + /// List of DatItem objects representing the items to be sorted + /// True if files are not renamed, false otherwise + /// True if it sorted correctly, false otherwise + private static bool Sort(ref List items, bool norename) + { + // Create the comparer extenal to the delegate + var nc = new NaturalComparer(); + + items.Sort(delegate (DatItem x, DatItem y) + { + try + { + // Compare on source if renaming + if (!norename) + { + int xSourceIndex = x.GetFieldValue(DatItem.SourceKey)?.Index ?? 0; + int ySourceIndex = y.GetFieldValue(DatItem.SourceKey)?.Index ?? 0; + if (xSourceIndex != ySourceIndex) + return xSourceIndex - ySourceIndex; + } + + // If machine names don't match + string? xMachineName = x.GetMachine()?.GetName(); + string? yMachineName = y.GetMachine()?.GetName(); + if (xMachineName != yMachineName) + return nc.Compare(xMachineName, yMachineName); + + // If types don't match + string? xType = x.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + string? yType = y.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + if (xType != yType) + return xType.AsItemType() - yType.AsItemType(); + + // If directory names don't match + string? xDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(x.GetName() ?? string.Empty)); + string? yDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(y.GetName() ?? string.Empty)); + if (xDirectoryName != yDirectoryName) + return nc.Compare(xDirectoryName, yDirectoryName); + + // If item names don't match + string? xName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(x.GetName() ?? string.Empty)); + string? yName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(y.GetName() ?? string.Empty)); + return nc.Compare(xName, yName); + } + catch + { + // Absorb the error + return 0; + } + }); + + return true; + } + + /// + /// Sort a list of DatItem objects by SourceID, Game, and Name (in order) + /// + /// List of item ID to DatItem mappings representing the items to be sorted + /// True if files are not renamed, false otherwise + /// True if it sorted correctly, false otherwise + private bool SortDB(ref List> mappings, bool norename) + { + // Create the comparer extenal to the delegate + var nc = new NaturalComparer(); + + mappings.Sort(delegate (KeyValuePair x, KeyValuePair y) + { + try + { + // Compare on source if renaming + if (!norename) + { + int xSourceIndex = ItemsDB.GetSourceForItem(x.Key).Value?.Index ?? 0; + int ySourceIndex = ItemsDB.GetSourceForItem(y.Key).Value?.Index ?? 0; + if (xSourceIndex != ySourceIndex) + return xSourceIndex - ySourceIndex; + } + + // If machine names don't match + string? xMachineName = ItemsDB.GetMachineForItem(x.Key).Value?.GetName(); + string? yMachineName = ItemsDB.GetMachineForItem(y.Key).Value?.GetName(); + if (xMachineName != yMachineName) + return nc.Compare(xMachineName, yMachineName); + + // If types don't match + string? xType = x.Value.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + string? yType = y.Value.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + if (xType != yType) + return xType.AsItemType() - yType.AsItemType(); + + // If directory names don't match + string? xDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(x.Value.GetName() ?? string.Empty)); + string? yDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(y.Value.GetName() ?? string.Empty)); + if (xDirectoryName != yDirectoryName) + return nc.Compare(xDirectoryName, yDirectoryName); + + // If item names don't match + string? xName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(x.Value.GetName() ?? string.Empty)); + string? yName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(y.Value.GetName() ?? string.Empty)); + return nc.Compare(xName, yName); + } + catch + { + // Absorb the error + return 0; + } + }); + + return true; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatHeader.cs b/SabreTools.Metadata.DatFiles/DatHeader.cs new file mode 100644 index 00000000..ef7921bd --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatHeader.cs @@ -0,0 +1,229 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Represents all possible DAT header information + /// + [JsonObject("header"), XmlRoot("header")] + public sealed class DatHeader : ModelBackedItem, ICloneable + { + #region Constants + + /// + /// Read or write format + /// + public const string DatFormatKey = "DATFORMAT"; + + /// + /// External name of the DAT + /// + public const string FileNameKey = "FILENAME"; + + #endregion + + #region Fields + + [JsonIgnore] + public bool CanOpenSpecified + { + get + { + var canOpen = GetStringArrayFieldValue(Data.Models.Metadata.Header.CanOpenKey); + return canOpen is not null && canOpen.Length > 0; + } + } + + [JsonIgnore] + public bool ImagesSpecified + { + get + { + return GetFieldValue(Data.Models.Metadata.Header.ImagesKey) is not null; + } + } + + [JsonIgnore] + public bool InfosSpecified + { + get + { + return GetFieldValue(Data.Models.Metadata.Header.InfosKey) is not null; + } + } + + [JsonIgnore] + public bool NewDatSpecified + { + get + { + return GetFieldValue(Data.Models.Metadata.Header.NewDatKey) is not null; + } + } + + [JsonIgnore] + public bool SearchSpecified + { + get + { + return GetFieldValue(Data.Models.Metadata.Header.SearchKey) is not null; + } + } + + #endregion + + #region Constructors + + public DatHeader() { } + + public DatHeader(Data.Models.Metadata.Header header) + { + // Create a new internal model + _internal = []; + + // Get all fields to automatically copy without processing + var nonItemFields = TypeHelper.GetConstants(typeof(Data.Models.Metadata.Header)); + if (nonItemFields is not null) + { + // Populate the internal machine from non-filter fields + foreach (string fieldName in nonItemFields) + { + if (header.ContainsKey(fieldName)) + _internal[fieldName] = header[fieldName]; + } + } + + // Get all fields specific to the DatFiles implementation + var nonStandardFields = TypeHelper.GetConstants(typeof(DatHeader)); + if (nonStandardFields is not null) + { + // Populate the internal machine from filter fields + foreach (string fieldName in nonStandardFields) + { + if (header.ContainsKey(fieldName)) + _internal[fieldName] = header[fieldName]; + } + } + + // Get all no-filter fields + if (header.ContainsKey(Data.Models.Metadata.Header.CanOpenKey)) + _internal[Data.Models.Metadata.Header.CanOpenKey] = header[Data.Models.Metadata.Header.CanOpenKey]; + if (header.ContainsKey(Data.Models.Metadata.Header.ImagesKey)) + _internal[Data.Models.Metadata.Header.ImagesKey] = header[Data.Models.Metadata.Header.ImagesKey]; + if (header.ContainsKey(Data.Models.Metadata.Header.InfosKey)) + _internal[Data.Models.Metadata.Header.InfosKey] = header[Data.Models.Metadata.Header.InfosKey]; + if (header.ContainsKey(Data.Models.Metadata.Header.NewDatKey)) + _internal[Data.Models.Metadata.Header.NewDatKey] = header[Data.Models.Metadata.Header.NewDatKey]; + if (header.ContainsKey(Data.Models.Metadata.Header.SearchKey)) + _internal[Data.Models.Metadata.Header.SearchKey] = header[Data.Models.Metadata.Header.SearchKey]; + } + + #endregion + + #region Cloning Methods + + /// + /// Clone the current header + /// + public object Clone() => new DatHeader(GetInternalClone()); + + /// + /// Clone just the format from the current header + /// + public DatHeader CloneFormat() + { + var header = new DatHeader(); + + header.SetFieldValue(DatFormatKey, GetFieldValue(DatFormatKey)); + + return header; + } + + /// + /// Get a clone of the current internal model + /// + public Data.Models.Metadata.Header GetInternalClone() + { + var header = (_internal.Clone() as Data.Models.Metadata.Header)!; + + // Remove fields with default values + if (header.ReadString(Data.Models.Metadata.Header.ForceMergingKey).AsMergingFlag() == MergingFlag.None) + header.Remove(Data.Models.Metadata.Header.ForceMergingKey); + if (header.ReadString(Data.Models.Metadata.Header.ForceNodumpKey).AsNodumpFlag() == NodumpFlag.None) + header.Remove(Data.Models.Metadata.Header.ForceNodumpKey); + if (header.ReadString(Data.Models.Metadata.Header.ForcePackingKey).AsPackingFlag() == PackingFlag.None) + header.Remove(Data.Models.Metadata.Header.ForcePackingKey); + if (header.ReadString(Data.Models.Metadata.Header.BiosModeKey).AsMergingFlag() == MergingFlag.None) + header.Remove(Data.Models.Metadata.Header.BiosModeKey); + if (header.ReadString(Data.Models.Metadata.Header.RomModeKey).AsMergingFlag() == MergingFlag.None) + header.Remove(Data.Models.Metadata.Header.RomModeKey); + if (header.ReadString(Data.Models.Metadata.Header.SampleModeKey).AsMergingFlag() == MergingFlag.None) + header.Remove(Data.Models.Metadata.Header.SampleModeKey); + + // Convert subheader values + if (CanOpenSpecified) + header[Data.Models.Metadata.Header.CanOpenKey] = new Data.Models.OfflineList.CanOpen { Extension = GetStringArrayFieldValue(Data.Models.Metadata.Header.CanOpenKey) }; + if (ImagesSpecified) + header[Data.Models.Metadata.Header.ImagesKey] = GetFieldValue(Data.Models.Metadata.Header.ImagesKey); + if (InfosSpecified) + header[Data.Models.Metadata.Header.InfosKey] = GetFieldValue(Data.Models.Metadata.Header.InfosKey); + if (NewDatSpecified) + header[Data.Models.Metadata.Header.NewDatKey] = GetFieldValue(Data.Models.Metadata.Header.NewDatKey); + if (SearchSpecified) + header[Data.Models.Metadata.Header.SearchKey] = GetFieldValue(Data.Models.Metadata.Header.SearchKey); + + return header; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not DatHeader otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not DatHeader otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + #endregion + + #region Manipulation + + /// + /// Runs a filter and determines if it passes or not + /// + /// Filter runner to use for checking + /// True if the Machine passes the filter, false otherwise + public bool PassesFilter(FilterRunner filterRunner) => filterRunner.Run(_internal); + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatModifiers.cs b/SabreTools.Metadata.DatFiles/DatModifiers.cs new file mode 100644 index 00000000..f6ec9253 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatModifiers.cs @@ -0,0 +1,88 @@ +using System; + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Represents various modifiers that can be applied to a DAT + /// + public sealed class DatModifiers : ICloneable + { + #region Fields + + /// + /// Text to prepend to all outputted lines + /// + public string? Prefix { get; set; } = null; + + /// + /// Text to append to all outputted lines + /// + public string? Postfix { get; set; } = null; + + /// + /// Add a new extension to all items + /// + public string? AddExtension { get; set; } = null; + + /// + /// Remove all item extensions + /// + public bool RemoveExtension { get; set; } = false; + + /// + /// Replace all item extensions + /// + public string? ReplaceExtension { get; set; } = null; + + /// + /// Output the machine name before the item name + /// + public bool GameName { get; set; } = false; + + /// + /// Wrap quotes around the entire line, sans prefix and postfix + /// + public bool Quotes { get; set; } = false; + + /// + /// Use the item name instead of machine name on output + /// + public bool UseRomName { get; set; } = false; + + /// + /// Input depot information + /// + public DepotInformation? InputDepot { get; set; } = null; + + /// + /// Output depot information + /// + public DepotInformation? OutputDepot { get; set; } = null; + + #endregion + + #region Cloning Methods + + /// + /// Clone the current modifiers + /// + public object Clone() + { + return new DatModifiers + { + Prefix = this.Prefix, + Postfix = this.Postfix, + AddExtension = this.AddExtension, + RemoveExtension = this.RemoveExtension, + ReplaceExtension = this.ReplaceExtension, + GameName = this.GameName, + Quotes = this.Quotes, + UseRomName = this.UseRomName, + InputDepot = (DepotInformation?)this.InputDepot?.Clone(), + OutputDepot = (DepotInformation?)this.OutputDepot?.Clone(), + }; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DatStatistics.cs b/SabreTools.Metadata.DatFiles/DatStatistics.cs new file mode 100644 index 00000000..5324c710 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DatStatistics.cs @@ -0,0 +1,725 @@ +using System.Collections.Generic; +using SabreTools.Hashing; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Statistics wrapper for outputting + /// + public class DatStatistics + { + #region Private instance variables + + /// + /// Number of items for each hash type + /// + private readonly Dictionary _hashCounts = []; + + /// + /// Number of items for each item type + /// + private readonly Dictionary _itemCounts = []; + + /// + /// Number of items for each item status + /// + private readonly Dictionary _statusCounts = []; + + /// + /// Lock for statistics calculation + /// + private readonly object statsLock = new(); + + #endregion + + #region Fields + + /// + /// Overall item count + /// + public long TotalCount { get; private set; } = 0; + + /// + /// Number of machines + /// + /// Special count only used by statistics output + public long GameCount { get; set; } = 0; + + /// + /// Total uncompressed size + /// + public long TotalSize { get; private set; } = 0; + + /// + /// Number of items with the remove flag + /// + public long RemovedCount { get; private set; } = 0; + + /// + /// Name to display on output + /// + public string? DisplayName { get; set; } + + /// + /// Total machine count to use on output + /// + public long MachineCount { get; set; } + + /// + /// Determines if statistics are for a directory or not + /// + public readonly bool IsDirectory; + + #endregion + + #region Constructors + + /// + /// Default constructor + /// + public DatStatistics() + { + DisplayName = null; + MachineCount = 0; + IsDirectory = false; + } + + /// + /// Constructor for aggregate data + /// + public DatStatistics(string? displayName, bool isDirectory) + { + DisplayName = displayName; + MachineCount = 0; + IsDirectory = isDirectory; + } + + #endregion + + #region Accessors + + /// + /// Add to the statistics for a given DatItem + /// + /// Item to add info from + public void AddItemStatistics(DatItem item) + { + lock (statsLock) + { + // No matter what the item is, we increment the count + TotalCount++; + + // Increment removal count + if (item.GetBoolFieldValue(DatItem.RemoveKey) == true) + RemovedCount++; + + // Increment the item count for the type + AddItemCount(item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType()); + + // Some item types require special processing + switch (item) + { + case Disk disk: + AddItemStatistics(disk); + break; + case File file: + AddItemStatistics(file); + break; + case Media media: + AddItemStatistics(media); + break; + case Rom rom: + AddItemStatistics(rom); + break; + default: + break; + } + } + } + + /// + /// Add to the statistics for a given DatItem + /// + /// Item to add info from + public void AddItemStatistics(Data.Models.Metadata.DatItem item) + { + lock (statsLock) + { + // No matter what the item is, we increment the count + TotalCount++; + + // Increment removal count + if (item.ReadBool(DatItem.RemoveKey) == true) + RemovedCount++; + + // Increment the item count for the type + AddItemCount(item.ReadString(Data.Models.Metadata.DatItem.TypeKey).AsItemType()); + +#pragma warning disable IDE0010 + // Some item types require special processing + switch (item) + { + case Data.Models.Metadata.Disk disk: + AddItemStatistics(disk); + break; + case Data.Models.Metadata.Media media: + AddItemStatistics(media); + break; + case Data.Models.Metadata.Rom rom: + AddItemStatistics(rom); + break; + } +#pragma warning restore IDE0010 + } + } + + /// + /// Add statistics from another DatStatistics object + /// + /// DatStatistics object to add from + public void AddStatistics(DatStatistics stats) + { + TotalCount += stats.TotalCount; + + // Loop through and add stats for all items + foreach (var itemCountKvp in stats._itemCounts) + { + AddItemCount(itemCountKvp.Key, itemCountKvp.Value); + } + + GameCount += stats.GameCount; + + TotalSize += stats.TotalSize; + + // Individual hash counts + foreach (var hashCountKvp in stats._hashCounts) + { + AddHashCount(hashCountKvp.Key, hashCountKvp.Value); + } + + // Individual status counts + foreach (var statusCountKvp in stats._statusCounts) + { + AddStatusCount(statusCountKvp.Key, statusCountKvp.Value); + } + + RemovedCount += stats.RemovedCount; + } + + /// + /// Get the item count for a given hash type, defaulting to 0 if it does not exist + /// + /// Hash type to retrieve + /// The number of items with that hash, if it exists + public long GetHashCount(HashType hashType) + { + lock (_hashCounts) + { + if (!_hashCounts.TryGetValue(hashType, out long value)) + return 0; + + return value; + } + } + + /// + /// Get the item count for a given item type, defaulting to 0 if it does not exist + /// + /// Item type to retrieve + /// The number of items of that type, if it exists + public long GetItemCount(ItemType itemType) + { + lock (_itemCounts) + { + if (!_itemCounts.TryGetValue(itemType, out long value)) + return 0; + + return value; + } + } + + /// + /// Get the item count for a given item status, defaulting to 0 if it does not exist + /// + /// Item status to retrieve + /// The number of items of that type, if it exists + public long GetStatusCount(ItemStatus itemStatus) + { + lock (_statusCounts) + { + if (!_statusCounts.TryGetValue(itemStatus, out long value)) + return 0; + + return value; + } + } + + /// + /// Remove from the statistics given a DatItem + /// + /// Item to remove info for + public void RemoveItemStatistics(DatItem item) + { + // If we have a null item, we can't do anything + if (item is null) + return; + + lock (statsLock) + { + // No matter what the item is, we decrease the count + TotalCount--; + + // Decrement removal count + if (item.GetBoolFieldValue(DatItem.RemoveKey) == true) + RemovedCount--; + + // Decrement the item count for the type + RemoveItemCount(item.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType()); + +#pragma warning disable IDE0010 + // Some item types require special processing + switch (item) + { + case Disk disk: + RemoveItemStatistics(disk); + break; + case File file: + RemoveItemStatistics(file); + break; + case Media media: + RemoveItemStatistics(media); + break; + case Rom rom: + RemoveItemStatistics(rom); + break; + } +#pragma warning restore IDE0010 + } + } + + /// + /// Remove from the statistics given a DatItem + /// + /// Item to remove info for + public void RemoveItemStatistics(Data.Models.Metadata.DatItem item) + { + // If we have a null item, we can't do anything + if (item is null) + return; + + lock (statsLock) + { + // No matter what the item is, we decrease the count + TotalCount--; + + // Decrement removal count + if (item.ReadBool(DatItem.RemoveKey) == true) + RemovedCount--; + + // Decrement the item count for the type + RemoveItemCount(item.ReadString(Data.Models.Metadata.DatItem.TypeKey).AsItemType()); + +#pragma warning disable IDE0010 + // Some item types require special processing + switch (item) + { + case Data.Models.Metadata.Disk disk: + RemoveItemStatistics(disk); + break; + case Data.Models.Metadata.Media media: + RemoveItemStatistics(media); + break; + case Data.Models.Metadata.Rom rom: + RemoveItemStatistics(rom); + break; + } +#pragma warning restore IDE0010 + } + } + + /// + /// Reset all statistics + /// + public void ResetStatistics() + { + _hashCounts.Clear(); + _itemCounts.Clear(); + _statusCounts.Clear(); + + TotalCount = 0; + GameCount = 0; + TotalSize = 0; + RemovedCount = 0; + } + + /// + /// Increment the hash count for a given hash type + /// + /// Hash type to increment + /// Amount to increment by, defaults to 1 + private void AddHashCount(HashType hashType, long interval = 1) + { + lock (_hashCounts) + { + if (!_hashCounts.ContainsKey(hashType)) + _hashCounts[hashType] = 0; + + _hashCounts[hashType] += interval; + if (_hashCounts[hashType] < 0) + _hashCounts[hashType] = 0; + } + } + + /// + /// Increment the item count for a given item type + /// + /// Item type to increment + /// Amount to increment by, defaults to 1 + private void AddItemCount(ItemType itemType, long interval = 1) + { + lock (_itemCounts) + { + if (!_itemCounts.ContainsKey(itemType)) + _itemCounts[itemType] = 0; + + _itemCounts[itemType] += interval; + if (_itemCounts[itemType] < 0) + _itemCounts[itemType] = 0; + } + } + + /// + /// Add to the statistics for a given Disk + /// + /// Item to add info from + private void AddItemStatistics(Disk disk) + { + if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + AddHashCount(HashType.MD5, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1); + } + + AddStatusCount(ItemStatus.BadDump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + AddStatusCount(ItemStatus.Good, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + AddStatusCount(ItemStatus.Nodump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + AddStatusCount(ItemStatus.Verified, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Add to the statistics for a given Disk + /// + /// Item to add info from + private void AddItemStatistics(Data.Models.Metadata.Disk disk) + { + if (disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + AddHashCount(HashType.MD5, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1); + } + + AddStatusCount(ItemStatus.BadDump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + AddStatusCount(ItemStatus.Good, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + AddStatusCount(ItemStatus.Nodump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + AddStatusCount(ItemStatus.Verified, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Add to the statistics for a given File + /// + /// Item to add info from + private void AddItemStatistics(File file) + { + TotalSize += file.Size ?? 0; + AddHashCount(HashType.CRC32, string.IsNullOrEmpty(file.CRC) ? 0 : 1); + AddHashCount(HashType.MD5, string.IsNullOrEmpty(file.MD5) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(file.SHA1) ? 0 : 1); + AddHashCount(HashType.SHA256, string.IsNullOrEmpty(file.SHA256) ? 0 : 1); + } + + /// + /// Add to the statistics for a given Media + /// + /// Item to add info from + private void AddItemStatistics(Media media) + { + AddHashCount(HashType.MD5, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1); + AddHashCount(HashType.SHA256, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1); + AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1); + } + + /// + /// Add to the statistics for a given Media + /// + /// Item to add info from + private void AddItemStatistics(Data.Models.Metadata.Media media) + { + AddHashCount(HashType.MD5, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1); + AddHashCount(HashType.SHA256, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1); + AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1); + } + + /// + /// Add to the statistics for a given Rom + /// + /// Item to add info from + private void AddItemStatistics(Rom rom) + { + if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + TotalSize += rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) ?? 0; + AddHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1); + AddHashCount(HashType.MD2, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1); + AddHashCount(HashType.MD4, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1); + AddHashCount(HashType.MD5, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1); + AddHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1); + AddHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1); + AddHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1); + AddHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1); + AddHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1); + AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1); + } + + AddStatusCount(ItemStatus.BadDump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + AddStatusCount(ItemStatus.Good, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + AddStatusCount(ItemStatus.Nodump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + AddStatusCount(ItemStatus.Verified, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Add to the statistics for a given Rom + /// + /// Item to add info from + private void AddItemStatistics(Data.Models.Metadata.Rom rom) + { + if (rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + TotalSize += rom.ReadLong(Data.Models.Metadata.Rom.SizeKey) ?? 0; + AddHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1); + AddHashCount(HashType.MD2, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1); + AddHashCount(HashType.MD4, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1); + AddHashCount(HashType.MD5, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1); + AddHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1); + AddHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1); + AddHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1); + AddHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1); + AddHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1); + AddHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1); + AddHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1); + } + + AddStatusCount(ItemStatus.BadDump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + AddStatusCount(ItemStatus.Good, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + AddStatusCount(ItemStatus.Nodump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + AddStatusCount(ItemStatus.Verified, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Increment the item count for a given item status + /// + /// Item type to increment + /// Amount to increment by, defaults to 1 + private void AddStatusCount(ItemStatus itemStatus, long interval = 1) + { + lock (_statusCounts) + { + if (!_statusCounts.ContainsKey(itemStatus)) + _statusCounts[itemStatus] = 0; + + _statusCounts[itemStatus] += interval; + if (_statusCounts[itemStatus] < 0) + _statusCounts[itemStatus] = 0; + } + } + + /// + /// Decrement the hash count for a given hash type + /// + /// Hash type to increment + /// Amount to increment by, defaults to 1 + private void RemoveHashCount(HashType hashType, long interval = 1) + { + lock (_hashCounts) + { + if (!_hashCounts.ContainsKey(hashType)) + return; + + _hashCounts[hashType] -= interval; + if (_hashCounts[hashType] < 0) + _hashCounts[hashType] = 0; + } + } + + /// + /// Decrement the item count for a given item type + /// + /// Item type to decrement + /// Amount to increment by, defaults to 1 + private void RemoveItemCount(ItemType itemType, long interval = 1) + { + lock (_itemCounts) + { + if (!_itemCounts.ContainsKey(itemType)) + return; + + _itemCounts[itemType] -= interval; + if (_itemCounts[itemType] < 0) + _itemCounts[itemType] = 0; + } + } + + /// + /// Remove from the statistics given a Disk + /// + /// Item to remove info for + private void RemoveItemStatistics(Disk disk) + { + if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1); + } + + RemoveStatusCount(ItemStatus.BadDump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + RemoveStatusCount(ItemStatus.Good, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + RemoveStatusCount(ItemStatus.Nodump, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + RemoveStatusCount(ItemStatus.Verified, disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Remove from the statistics given a Disk + /// + /// Item to remove info for + private void RemoveItemStatistics(Data.Models.Metadata.Disk disk) + { + if (disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.MD5Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(disk.ReadString(Data.Models.Metadata.Disk.SHA1Key)) ? 0 : 1); + } + + RemoveStatusCount(ItemStatus.BadDump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + RemoveStatusCount(ItemStatus.Good, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + RemoveStatusCount(ItemStatus.Nodump, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + RemoveStatusCount(ItemStatus.Verified, disk.ReadString(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Remove from the statistics given a File + /// + /// Item to remove info for + private void RemoveItemStatistics(File file) + { + TotalSize -= file.Size ?? 0; + RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(file.CRC) ? 0 : 1); + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(file.MD5) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(file.SHA1) ? 0 : 1); + RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(file.SHA256) ? 0 : 1); + } + + /// + /// Remove from the statistics given a Media + /// + /// Item to remove info for + private void RemoveItemStatistics(Media media) + { + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1); + RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1); + } + + /// + /// Remove from the statistics given a Media + /// + /// Item to remove info for + private void RemoveItemStatistics(Data.Models.Metadata.Media media) + { + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.MD5Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA1Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SHA256Key)) ? 0 : 1); + RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(media.ReadString(Data.Models.Metadata.Media.SpamSumKey)) ? 0 : 1); + } + + /// + /// Remove from the statistics given a Rom + /// + /// Item to remove info for + private void RemoveItemStatistics(Rom rom) + { + if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + TotalSize -= rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) ?? 0; + RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1); + RemoveHashCount(HashType.MD2, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1); + RemoveHashCount(HashType.MD4, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1); + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1); + RemoveHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1); + RemoveHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1); + RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1); + } + + RemoveStatusCount(ItemStatus.BadDump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + RemoveStatusCount(ItemStatus.Good, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + RemoveStatusCount(ItemStatus.Nodump, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + RemoveStatusCount(ItemStatus.Verified, rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Remove from the statistics given a Rom + /// + /// Item to remove info for + private void RemoveItemStatistics(Data.Models.Metadata.Rom rom) + { + if (rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump) + { + TotalSize -= rom.ReadLong(Data.Models.Metadata.Rom.SizeKey) ?? 0; + RemoveHashCount(HashType.CRC32, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.CRCKey)) ? 0 : 1); + RemoveHashCount(HashType.MD2, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD2Key)) ? 0 : 1); + RemoveHashCount(HashType.MD4, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD4Key)) ? 0 : 1); + RemoveHashCount(HashType.MD5, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.MD5Key)) ? 0 : 1); + RemoveHashCount(HashType.RIPEMD128, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD128Key)) ? 0 : 1); + RemoveHashCount(HashType.RIPEMD160, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.RIPEMD160Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA1, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA256, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA256Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA384, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA384Key)) ? 0 : 1); + RemoveHashCount(HashType.SHA512, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SHA512Key)) ? 0 : 1); + RemoveHashCount(HashType.SpamSum, string.IsNullOrEmpty(rom.ReadString(Data.Models.Metadata.Rom.SpamSumKey)) ? 0 : 1); + } + + RemoveStatusCount(ItemStatus.BadDump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.BadDump ? 1 : 0); + RemoveStatusCount(ItemStatus.Good, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Good ? 1 : 0); + RemoveStatusCount(ItemStatus.Nodump, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump ? 1 : 0); + RemoveStatusCount(ItemStatus.Verified, rom.ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Verified ? 1 : 0); + } + + /// + /// Decrement the item count for a given item status + /// + /// Item type to decrement + /// Amount to increment by, defaults to 1 + private void RemoveStatusCount(ItemStatus itemStatus, long interval = 1) + { + lock (_statusCounts) + { + if (!_statusCounts.ContainsKey(itemStatus)) + return; + + _statusCounts[itemStatus] -= interval; + if (_statusCounts[itemStatus] < 0) + _statusCounts[itemStatus] = 0; + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/DepotInformation.cs b/SabreTools.Metadata.DatFiles/DepotInformation.cs new file mode 100644 index 00000000..219b398d --- /dev/null +++ b/SabreTools.Metadata.DatFiles/DepotInformation.cs @@ -0,0 +1,66 @@ +using System; +using SabreTools.Hashing; + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Depot information wrapper + /// + public class DepotInformation : ICloneable + { + /// + /// Name or path of the Depot + /// + public string? Name { get; } + + /// + /// Whether to use this Depot or not + /// + public bool IsActive { get; } + + /// + /// Depot byte-depth + /// + public int Depth { get; } + + /// + /// Constructor + /// + /// Set active state + /// Set depth between 0 and SHA-1's byte length + public DepotInformation(bool isActive, int depth) + : this(null, isActive, depth) + { + } + + /// + /// Constructor + /// + /// Identifier for the depot + /// Set active state + /// Set depth between 0 and SHA-1's byte length + public DepotInformation(string? name, bool isActive, int depth) + { + Name = name; + IsActive = isActive; + Depth = depth; + + // Limit depth value + if (Depth == int.MinValue) + Depth = 4; + else if (Depth < 0) + Depth = 0; + else if (Depth > HashType.SHA1.ZeroBytes.Length) + Depth = HashType.SHA1.ZeroBytes.Length; + } + + #region Cloning + + /// + /// Clone the current object + /// + public object Clone() => new DepotInformation(Name, IsActive, Depth); + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/Enums.cs b/SabreTools.Metadata.DatFiles/Enums.cs new file mode 100644 index 00000000..4c0ff495 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Enums.cs @@ -0,0 +1,267 @@ +using System; + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// DAT output formats + /// + [Flags] + public enum DatFormat : ulong + { + #region XML Formats + + /// + /// Logiqx XML (using machine) + /// + Logiqx = 1 << 0, + + /// + /// Logiqx XML (using game) + /// + LogiqxDeprecated = 1 << 1, + + /// + /// MAME Softare List XML + /// + SoftwareList = 1 << 2, + + /// + /// MAME Listxml output + /// + Listxml = 1 << 3, + + /// + /// OfflineList XML + /// + OfflineList = 1 << 4, + + /// + /// SabreDAT XML + /// + SabreXML = 1 << 5, + + /// + /// openMSX Software List XML + /// + OpenMSX = 1 << 6, + + /// + /// Archive.org file list XML + /// + ArchiveDotOrg = 1 << 7, + + #endregion + + #region Propietary Formats + + /// + /// ClrMamePro custom + /// + ClrMamePro = 1 << 8, + + /// + /// RomCenter INI-based + /// + RomCenter = 1 << 9, + + /// + /// DOSCenter custom + /// + DOSCenter = 1 << 10, + + /// + /// AttractMode custom + /// + AttractMode = 1 << 11, + + #endregion + + #region Standardized Text Formats + + /// + /// ClrMamePro missfile + /// + MissFile = 1 << 12, + + /// + /// Comma-Separated Values (standardized) + /// + CSV = 1 << 13, + + /// + /// Semicolon-Separated Values (standardized) + /// + SSV = 1 << 14, + + /// + /// Tab-Separated Values (standardized) + /// + TSV = 1 << 15, + + /// + /// MAME Listrom output + /// + Listrom = 1 << 16, + + /// + /// Everdrive Packs SMDB + /// + EverdriveSMDB = 1 << 17, + + /// + /// SabreJSON + /// + SabreJSON = 1 << 18, + + #endregion + + #region SFV-similar Formats + + /// + /// CRC32 hash list + /// + RedumpSFV = 1 << 19, + + /// + /// MD2 hash list + /// + RedumpMD2 = 1 << 20, + + /// + /// MD4 hash list + /// + RedumpMD4 = 1 << 21, + + /// + /// MD5 hash list + /// + RedumpMD5 = 1 << 22, + + /// + /// RIPEMD128 hash list + /// + RedumpRIPEMD128 = 1 << 23, + + /// + /// RIPEMD160 hash list + /// + RedumpRIPEMD160 = 1 << 24, + + /// + /// SHA-1 hash list + /// + RedumpSHA1 = 1 << 25, + + /// + /// SHA-256 hash list + /// + RedumpSHA256 = 1 << 26, + + /// + /// SHA-384 hash list + /// + RedumpSHA384 = 1 << 27, + + /// + /// SHA-512 hash list + /// + RedumpSHA512 = 1 << 28, + + /// + /// SpamSum hash list + /// + RedumpSpamSum = 1 << 29, + + #endregion + + // Specialty combinations + ALL = ulong.MaxValue, + } + + /// + /// Determines merging tag handling for DAT output + /// + public enum MergingFlag + { + [Mapping("none")] + None = 0, + + [Mapping("split")] + Split, + + [Mapping("merged")] + Merged, + + [Mapping("nonmerged", "unmerged")] + NonMerged, + + /// This is not usually defined for Merging flags + [Mapping("fullmerged")] + FullMerged, + + /// This is not usually defined for Merging flags + [Mapping("device", "deviceunmerged", "devicenonmerged")] + DeviceNonMerged, + + /// This is not usually defined for Merging flags + [Mapping("full", "fullunmerged", "fullnonmerged")] + FullNonMerged, + } + + /// + /// Determines nodump tag handling for DAT output + /// + public enum NodumpFlag + { + [Mapping("none")] + None = 0, + + [Mapping("obsolete")] + Obsolete, + + [Mapping("required")] + Required, + + [Mapping("ignore")] + Ignore, + } + + /// + /// Determines packing tag handling for DAT output + /// + public enum PackingFlag + { + [Mapping("none")] + None = 0, + + /// + /// Force all sets to be in archives, except disk and media + /// + [Mapping("zip", "yes")] + Zip, + + /// + /// Force all sets to be extracted into subfolders + /// + [Mapping("unzip", "no")] + Unzip, + + /// + /// Force sets with single items to be extracted to the parent folder + /// + [Mapping("partial")] + Partial, + + /// + /// Force all sets to be extracted to the parent folder + /// + [Mapping("flat")] + Flat, + + /// + /// Force all sets to have all archives treated as files + /// + [Mapping("fileonly")] + FileOnly, + } +} diff --git a/SabreTools.Metadata.DatFiles/Extensions.cs b/SabreTools.Metadata.DatFiles/Extensions.cs new file mode 100644 index 00000000..986af2b4 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Extensions.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatFiles +{ + public static class Extensions + { + #region Private Maps + + /// + /// Set of enum to string mappings for MergingFlag + /// + private static readonly Dictionary _toMergingFlagMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for MergingFlag + /// + private static readonly Dictionary _fromMergingFlagMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of string to enum mappings for MergingFlag (secondary) + /// + private static readonly Dictionary _fromMergingFlagSecondaryMap = Converters.GenerateToString(useSecond: true); + + /// + /// Set of enum to string mappings for NodumpFlag + /// + private static readonly Dictionary _toNodumpFlagMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for NodumpFlag + /// + private static readonly Dictionary _fromNodumpFlagMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for PackingFlag + /// + private static readonly Dictionary _toPackingFlagMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for PackingFlag + /// + private static readonly Dictionary _fromPackingFlagMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of string to enum mappings for PackingFlag (secondary) + /// + private static readonly Dictionary _fromPackingFlagSecondaryMap = Converters.GenerateToString(useSecond: true); + + #endregion + + #region String to Enum + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static MergingFlag AsMergingFlag(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toMergingFlagMap.ContainsKey(value)) + return _toMergingFlagMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static NodumpFlag AsNodumpFlag(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toNodumpFlagMap.ContainsKey(value)) + return _toNodumpFlagMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static PackingFlag AsPackingFlag(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toPackingFlagMap.ContainsKey(value)) + return _toPackingFlagMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + #endregion + + #region Enum to String + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this MergingFlag value, bool useSecond = false) + { + // Try to get the value from the mappings + if (!useSecond && _fromMergingFlagMap.ContainsKey(value)) + return _fromMergingFlagMap[value]; + else if (useSecond && _fromMergingFlagSecondaryMap.ContainsKey(value)) + return _fromMergingFlagSecondaryMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this NodumpFlag value) + { + // Try to get the value from the mappings + if (_fromNodumpFlagMap.ContainsKey(value)) + return _fromNodumpFlagMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this PackingFlag value, bool useSecond = false) + { + // Try to get the value from the mappings + if (!useSecond && _fromPackingFlagMap.ContainsKey(value)) + return _fromPackingFlagMap[value]; + else if (useSecond && _fromPackingFlagSecondaryMap.ContainsKey(value)) + return _fromPackingFlagSecondaryMap[value]; + + // Otherwise, return null + return null; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/ArchiveDotOrg.cs b/SabreTools.Metadata.DatFiles/Formats/ArchiveDotOrg.cs new file mode 100644 index 00000000..be8bf1c5 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/ArchiveDotOrg.cs @@ -0,0 +1,25 @@ +using SabreTools.Metadata.DatItems; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a Archive.org file list + /// + public sealed class ArchiveDotOrg : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public ArchiveDotOrg(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.ArchiveDotOrg); + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/AttractMode.cs b/SabreTools.Metadata.DatFiles/Formats/AttractMode.cs new file mode 100644 index 00000000..23330077 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/AttractMode.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents an AttractMode DAT + /// + public sealed class AttractMode : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public AttractMode(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.AttractMode); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/ClrMamePro.cs b/SabreTools.Metadata.DatFiles/Formats/ClrMamePro.cs new file mode 100644 index 00000000..162ef6d9 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/ClrMamePro.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a ClrMamePro DAT + /// + public sealed class ClrMamePro : SerializableDatFile + { + #region Fields + + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Archive, + ItemType.BiosSet, + ItemType.Chip, + ItemType.DipSwitch, + ItemType.Disk, + ItemType.Display, + ItemType.Driver, + ItemType.Input, + ItemType.Media, + ItemType.Release, + ItemType.Rom, + ItemType.Sample, + ItemType.Sound, + ]; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public ClrMamePro(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.ClrMamePro); + } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + try + { + // Deserialize the input file + var metadataFile = new Serialization.Readers.ClrMamePro().Deserialize(filename, quotes: true); + var metadata = new Serialization.CrossModel.ClrMamePro().Serialize(metadataFile); + + // Convert to the internal format + ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner); + } + catch (Exception ex) when (!throwOnError) + { + string message = $"'{filename}' - An error occurred during parsing"; + _logger.Error(ex, message); + } + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + +#pragma warning disable IDE0010 + switch (datItem) + { + case Release release: + if (string.IsNullOrEmpty(release.GetName())) + missingFields.Add(Data.Models.Metadata.Release.NameKey); + if (string.IsNullOrEmpty(release.GetStringFieldValue(Data.Models.Metadata.Release.RegionKey))) + missingFields.Add(Data.Models.Metadata.Release.RegionKey); + break; + + case BiosSet biosset: + if (string.IsNullOrEmpty(biosset.GetName())) + missingFields.Add(Data.Models.Metadata.BiosSet.NameKey); + if (string.IsNullOrEmpty(biosset.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey))) + missingFields.Add(Data.Models.Metadata.BiosSet.DescriptionKey); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) + && string.IsNullOrEmpty(rom.GetStringFieldValue("MD2")) + && string.IsNullOrEmpty(rom.GetStringFieldValue("MD4")) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey))) + { + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + } + + break; + + case Disk disk: + if (string.IsNullOrEmpty(disk.GetName())) + missingFields.Add(Data.Models.Metadata.Disk.NameKey); + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + missingFields.Add(Data.Models.Metadata.Disk.SHA1Key); + } + + break; + + case Sample sample: + if (string.IsNullOrEmpty(sample.GetName())) + missingFields.Add(Data.Models.Metadata.Sample.NameKey); + break; + + case Archive archive: + if (string.IsNullOrEmpty(archive.GetName())) + missingFields.Add(Data.Models.Metadata.Archive.NameKey); + break; + + case Chip chip: + if (chip.GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey).AsChipType() == ChipType.NULL) + missingFields.Add(Data.Models.Metadata.Chip.ChipTypeKey); + if (string.IsNullOrEmpty(chip.GetName())) + missingFields.Add(Data.Models.Metadata.Chip.NameKey); + break; + + case Display display: + if (display.GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey).AsDisplayType() == DisplayType.NULL) + missingFields.Add(Data.Models.Metadata.Display.DisplayTypeKey); + if (display.GetInt64FieldValue(Data.Models.Metadata.Display.RotateKey) is null) + missingFields.Add(Data.Models.Metadata.Display.RotateKey); + break; + + case Sound sound: + if (sound.GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey) is null) + missingFields.Add(Data.Models.Metadata.Sound.ChannelsKey); + break; + + case Input input: + if (input.GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey) is null) + missingFields.Add(Data.Models.Metadata.Input.PlayersKey); + if (!input.ControlsSpecified) + missingFields.Add(Data.Models.Metadata.Input.ControlKey); + break; + + case DipSwitch dipswitch: + if (string.IsNullOrEmpty(dipswitch.GetName())) + missingFields.Add(Data.Models.Metadata.DipSwitch.NameKey); + break; + + case Driver driver: + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.StatusKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.EmulationKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file + var metadata = ConvertToMetadata(ignoreblanks); + var metadataFile = new Serialization.CrossModel.ClrMamePro().Deserialize(metadata); + if (!new Serialization.Writers.ClrMamePro().SerializeFile(metadataFile, outfile, quotes: true)) + { + _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; + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/DosCenter.cs b/SabreTools.Metadata.DatFiles/Formats/DosCenter.cs new file mode 100644 index 00000000..24a008e4 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/DosCenter.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents parsing and writing of a DosCenter DAT + /// + public sealed class DosCenter : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public DosCenter(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.DOSCenter); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + // if (string.IsNullOrEmpty(rom.Date)) + // missingFields.Add(Data.Models.Metadata.Rom.DateKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))) + missingFields.Add(Data.Models.Metadata.Rom.CRCKey); + // if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + // missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/EverdriveSmdb.cs b/SabreTools.Metadata.DatFiles/Formats/EverdriveSmdb.cs new file mode 100644 index 00000000..c57e3f55 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/EverdriveSmdb.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents parsing and writing of an Everdrive SMDB file + /// + public sealed class EverdriveSMDB : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public EverdriveSMDB(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.EverdriveSMDB); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA256Key); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))) + missingFields.Add(Data.Models.Metadata.Rom.MD5Key); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))) + missingFields.Add(Data.Models.Metadata.Rom.CRCKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/Hashfile.cs b/SabreTools.Metadata.DatFiles/Formats/Hashfile.cs new file mode 100644 index 00000000..82880f9d --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/Hashfile.cs @@ -0,0 +1,601 @@ +using System; +using System.Collections.Generic; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using SabreTools.Hashing; + +#pragma warning disable IDE0290 // Use primary constructor +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a hashfile such as an SFV, MD5, or SHA-1 file + /// + public abstract class Hashfile : SerializableDatFile + { + #region Fields + + // Private instance variables specific to Hashfile DATs + protected HashType _hash; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Hashfile(DatFile? datFile) : base(datFile) + { + } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + try + { + // Deserialize the input file + var hashfile = new Serialization.Readers.Hashfile().Deserialize(filename, _hash); + var metadata = new Serialization.CrossModel.Hashfile().Serialize(hashfile); + + // Convert to the internal format + ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner); + } + catch (Exception ex) when (!throwOnError) + { + string message = $"'{filename}' - An error occurred during parsing"; + _logger.Error(ex, message); + } + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file + var metadata = ConvertToMetadata(ignoreblanks); + var hashfile = new Serialization.CrossModel.Hashfile().Deserialize(metadata, _hash); + if (!new Serialization.Writers.Hashfile().SerializeFile(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; + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } + } + + /// + /// Represents an SFV (CRC-32) hashfile + /// + public sealed class SfvFile : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SfvFile(DatFile? datFile) : base(datFile) + { + _hash = HashType.CRC32; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSFV); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))) + missingFields.Add(Data.Models.Metadata.Rom.CRCKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an MD2 hashfile + /// + public sealed class Md2File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Md2File(DatFile? datFile) : base(datFile) + { + _hash = HashType.MD2; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpMD2); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key))) + missingFields.Add(Data.Models.Metadata.Rom.MD2Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an MD4 hashfile + /// + public sealed class Md4File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Md4File(DatFile? datFile) : base(datFile) + { + _hash = HashType.MD4; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpMD4); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key))) + missingFields.Add(Data.Models.Metadata.Rom.MD4Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an MD5 hashfile + /// + public sealed class Md5File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Md5File(DatFile? datFile) : base(datFile) + { + _hash = HashType.MD5; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpMD5); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Disk disk: + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))) + missingFields.Add(Data.Models.Metadata.Disk.MD5Key); + break; + + case Media medium: + if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key))) + missingFields.Add(Data.Models.Metadata.Media.MD5Key); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))) + missingFields.Add(Data.Models.Metadata.Rom.MD5Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an RIPEMD128 hashfile + /// + public sealed class RipeMD128File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public RipeMD128File(DatFile? datFile) : base(datFile) + { + _hash = HashType.RIPEMD128; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpRIPEMD128); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key))) + missingFields.Add(Data.Models.Metadata.Rom.RIPEMD128Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an RIPEMD160 hashfile + /// + public sealed class RipeMD160File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public RipeMD160File(DatFile? datFile) : base(datFile) + { + _hash = HashType.RIPEMD160; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpRIPEMD160); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key))) + missingFields.Add(Data.Models.Metadata.Rom.RIPEMD160Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an SHA-1 hashfile + /// + public sealed class Sha1File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Sha1File(DatFile? datFile) : base(datFile) + { + _hash = HashType.SHA1; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA1); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Disk disk: + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + missingFields.Add(Data.Models.Metadata.Disk.SHA1Key); + break; + + case Media medium: + if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key))) + missingFields.Add(Data.Models.Metadata.Media.SHA1Key); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an SHA-256 hashfile + /// + public sealed class Sha256File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Media, + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Sha256File(DatFile? datFile) : base(datFile) + { + _hash = HashType.SHA256; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA256); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Media medium: + if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key))) + missingFields.Add(Data.Models.Metadata.Media.SHA256Key); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA256Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an SHA-384 hashfile + /// + public sealed class Sha384File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Sha384File(DatFile? datFile) : base(datFile) + { + _hash = HashType.SHA384; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA384); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA384Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an SHA-512 hashfile + /// + public sealed class Sha512File : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Sha512File(DatFile? datFile) : base(datFile) + { + _hash = HashType.SHA512; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSHA512); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA512Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } + + /// + /// Represents an SpamSum hashfile + /// + public sealed class SpamSumFile : Hashfile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Media, + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SpamSumFile(DatFile? datFile) : base(datFile) + { + _hash = HashType.SpamSum; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RedumpSpamSum); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Media medium: + if (string.IsNullOrEmpty(medium.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey))) + missingFields.Add(Data.Models.Metadata.Media.SpamSumKey); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey))) + missingFields.Add(Data.Models.Metadata.Rom.SpamSumKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/Listrom.cs b/SabreTools.Metadata.DatFiles/Formats/Listrom.cs new file mode 100644 index 00000000..400c2ae0 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/Listrom.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a MAME Listrom file + /// + public sealed class Listrom : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Disk, + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Listrom(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Listrom); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Disk disk: + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + missingFields.Add(Data.Models.Metadata.Disk.SHA1Key); + } + + break; + + case Rom rom: + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))) + missingFields.Add(Data.Models.Metadata.Rom.CRCKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/Listxml.cs b/SabreTools.Metadata.DatFiles/Formats/Listxml.cs new file mode 100644 index 00000000..53840a22 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/Listxml.cs @@ -0,0 +1,397 @@ +using System; +using System.Collections.Generic; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a MAME/M1 XML DAT + /// + public sealed class Listxml : SerializableDatFile + { + #region Constants + + /// + /// DTD for original MAME XML DATs + /// + internal const string MAMEDTD = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]> +"; + + #endregion + + #region Fields + + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Adjuster, + ItemType.BiosSet, + ItemType.Chip, + ItemType.Condition, + ItemType.Configuration, + ItemType.Device, + ItemType.DeviceRef, + ItemType.DipSwitch, + ItemType.Disk, + ItemType.Display, + ItemType.Driver, + ItemType.Feature, + ItemType.Input, + ItemType.Port, + ItemType.RamOption, + ItemType.Rom, + ItemType.Sample, + ItemType.Slot, + ItemType.SoftwareList, + ItemType.Sound, + ]; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Listxml(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Listxml); + } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + try + { + // Deserialize the input file + var mame = new Serialization.Readers.Listxml().Deserialize(filename); + Data.Models.Metadata.MetadataFile? metadata; + if (mame is null) + { + var m1 = new Serialization.Readers.M1().Deserialize(filename); + metadata = new Serialization.CrossModel.M1().Serialize(m1); + } + else + { + metadata = new Serialization.CrossModel.Listxml().Serialize(mame); + } + + // Convert to the internal format + ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner); + } + catch (Exception ex) when (!throwOnError) + { + string message = $"'{filename}' - An error occurred during parsing"; + _logger.Error(ex, message); + } + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + +#pragma warning disable IDE0010 + switch (datItem) + { + case BiosSet biosset: + if (string.IsNullOrEmpty(biosset.GetName())) + missingFields.Add(Data.Models.Metadata.BiosSet.NameKey); + if (string.IsNullOrEmpty(biosset.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey))) + missingFields.Add(Data.Models.Metadata.BiosSet.DescriptionKey); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + { + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + } + + break; + + case Disk disk: + if (string.IsNullOrEmpty(disk.GetName())) + missingFields.Add(Data.Models.Metadata.Disk.NameKey); + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + missingFields.Add(Data.Models.Metadata.Disk.SHA1Key); + } + + break; + + case DeviceRef deviceref: + if (string.IsNullOrEmpty(deviceref.GetName())) + missingFields.Add(Data.Models.Metadata.DeviceRef.NameKey); + break; + + case Sample sample: + if (string.IsNullOrEmpty(sample.GetName())) + missingFields.Add(Data.Models.Metadata.Sample.NameKey); + break; + + case Chip chip: + if (string.IsNullOrEmpty(chip.GetName())) + missingFields.Add(Data.Models.Metadata.Chip.NameKey); + if (chip.GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey).AsChipType() == ChipType.NULL) + missingFields.Add(Data.Models.Metadata.Chip.ChipTypeKey); + break; + + case Display display: + if (display.GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey).AsDisplayType() == DisplayType.NULL) + missingFields.Add(Data.Models.Metadata.Display.DisplayTypeKey); + if (display.GetDoubleFieldValue(Data.Models.Metadata.Display.RefreshKey) is null) + missingFields.Add(Data.Models.Metadata.Display.RefreshKey); + break; + + case Sound sound: + if (sound.GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey) is null) + missingFields.Add(Data.Models.Metadata.Sound.ChannelsKey); + break; + + case Input input: + if (input.GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey) is null) + missingFields.Add(Data.Models.Metadata.Input.PlayersKey); + break; + + case DipSwitch dipswitch: + if (string.IsNullOrEmpty(dipswitch.GetName())) + missingFields.Add(Data.Models.Metadata.DipSwitch.NameKey); + if (string.IsNullOrEmpty(dipswitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.TagKey))) + missingFields.Add(Data.Models.Metadata.DipSwitch.TagKey); + break; + + case Configuration configuration: + if (string.IsNullOrEmpty(configuration.GetName())) + missingFields.Add(Data.Models.Metadata.Configuration.NameKey); + if (string.IsNullOrEmpty(configuration.GetStringFieldValue(Data.Models.Metadata.Configuration.TagKey))) + missingFields.Add(Data.Models.Metadata.Configuration.TagKey); + break; + + case Port port: + if (string.IsNullOrEmpty(port.GetStringFieldValue(Data.Models.Metadata.Port.TagKey))) + missingFields.Add(Data.Models.Metadata.Port.TagKey); + break; + + case Adjuster adjuster: + if (string.IsNullOrEmpty(adjuster.GetName())) + missingFields.Add(Data.Models.Metadata.Adjuster.NameKey); + break; + + case Driver driver: + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.StatusKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.EmulationKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.CocktailKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.SaveStateKey); + break; + + case Feature feature: + if (feature.GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey).AsFeatureType() == FeatureType.NULL) + missingFields.Add(Data.Models.Metadata.Feature.FeatureTypeKey); + break; + + case Device device: + if (device.GetStringFieldValue(Data.Models.Metadata.Device.DeviceTypeKey).AsDeviceType() == DeviceType.NULL) + missingFields.Add(Data.Models.Metadata.Device.DeviceTypeKey); + break; + + case Slot slot: + if (string.IsNullOrEmpty(slot.GetName())) + missingFields.Add(Data.Models.Metadata.Slot.NameKey); + break; + + case DatItems.Formats.SoftwareList softwarelist: + if (string.IsNullOrEmpty(softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.TagKey))) + missingFields.Add(Data.Models.Metadata.SoftwareList.TagKey); + if (string.IsNullOrEmpty(softwarelist.GetName())) + missingFields.Add(Data.Models.Metadata.SoftwareList.NameKey); + if (softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey).AsSoftwareListStatus() == SoftwareListStatus.None) + missingFields.Add(Data.Models.Metadata.SoftwareList.StatusKey); + break; + + case RamOption ramoption: + if (string.IsNullOrEmpty(ramoption.GetName())) + missingFields.Add(Data.Models.Metadata.RamOption.NameKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/Logiqx.cs b/SabreTools.Metadata.DatFiles/Formats/Logiqx.cs new file mode 100644 index 00000000..8c9cc357 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/Logiqx.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a Logiqx-derived DAT + /// + public sealed class Logiqx : SerializableDatFile + { + #region Constants + + /// + /// DTD for original Logiqx DATs + /// + /// This has been edited to reflect actual current standards + internal const string LogiqxDTD = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + /// + /// XSD for No-Intro Logiqx-derived DATs + /// + internal const string NoIntroXSD = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + #endregion + + #region Fields + + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Archive, + ItemType.BiosSet, + ItemType.DeviceRef, + ItemType.Disk, + ItemType.Driver, + ItemType.Media, + ItemType.Release, + ItemType.Rom, + ItemType.Sample, + ItemType.SoftwareList, + ]; + + /// + /// Indicates if game should be used instead of machine + /// + private readonly bool _useGame; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + /// True if the output uses "game", false if the output uses "machine" + public Logiqx(DatFile? datFile, bool useGame) : base(datFile) + { + _useGame = useGame; + if (useGame) + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.LogiqxDeprecated); + else + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.Logiqx); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + +#pragma warning disable IDE0010 + switch (datItem) + { + case Release release: + if (string.IsNullOrEmpty(release.GetName())) + missingFields.Add(Data.Models.Metadata.Release.NameKey); + if (string.IsNullOrEmpty(release.GetStringFieldValue(Data.Models.Metadata.Release.RegionKey))) + missingFields.Add(Data.Models.Metadata.Release.RegionKey); + break; + + case BiosSet biosset: + if (string.IsNullOrEmpty(biosset.GetName())) + missingFields.Add(Data.Models.Metadata.BiosSet.NameKey); + if (string.IsNullOrEmpty(biosset.GetStringFieldValue(Data.Models.Metadata.BiosSet.DescriptionKey))) + missingFields.Add(Data.Models.Metadata.BiosSet.DescriptionKey); + break; + + case Rom rom: + if (string.IsNullOrEmpty(rom.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) + && string.IsNullOrEmpty(rom.GetStringFieldValue("MD2")) + && string.IsNullOrEmpty(rom.GetStringFieldValue("MD4")) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey))) + { + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + } + + break; + + case Disk disk: + if (string.IsNullOrEmpty(disk.GetName())) + missingFields.Add(Data.Models.Metadata.Disk.NameKey); + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + missingFields.Add(Data.Models.Metadata.Disk.SHA1Key); + } + + break; + + case Media media: + if (string.IsNullOrEmpty(media.GetName())) + missingFields.Add(Data.Models.Metadata.Media.NameKey); + if (string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey))) + { + missingFields.Add(Data.Models.Metadata.Media.SHA1Key); + } + + break; + + case DeviceRef deviceref: + if (string.IsNullOrEmpty(deviceref.GetName())) + missingFields.Add(Data.Models.Metadata.DeviceRef.NameKey); + break; + + case Sample sample: + if (string.IsNullOrEmpty(sample.GetName())) + missingFields.Add(Data.Models.Metadata.Sample.NameKey); + break; + + case Archive archive: + if (string.IsNullOrEmpty(archive.GetName())) + missingFields.Add(Data.Models.Metadata.Archive.NameKey); + break; + + case Driver driver: + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.StatusKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.EmulationKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.CocktailKey); + if (driver.GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey).AsSupportStatus() == SupportStatus.NULL) + missingFields.Add(Data.Models.Metadata.Driver.SaveStateKey); + break; + + case DatItems.Formats.SoftwareList softwarelist: + if (string.IsNullOrEmpty(softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.TagKey))) + missingFields.Add(Data.Models.Metadata.SoftwareList.TagKey); + if (string.IsNullOrEmpty(softwarelist.GetName())) + missingFields.Add(Data.Models.Metadata.SoftwareList.NameKey); + if (softwarelist.GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey).AsSoftwareListStatus() == SoftwareListStatus.None) + missingFields.Add(Data.Models.Metadata.SoftwareList.StatusKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file + var metadata = ConvertToMetadata(ignoreblanks); + var datafile = new Serialization.CrossModel.Logiqx().Deserialize(metadata, _useGame); + + // TODO: Reenable doctype writing + // Only write the doctype if we don't have No-Intro data + bool success; + if (string.IsNullOrEmpty(Header.GetStringFieldValue(Data.Models.Metadata.Header.IdKey))) + success = new Serialization.Writers.Logiqx().Serialize(datafile, outfile, null, null, null, null); + else + success = new Serialization.Writers.Logiqx().Serialize(datafile, outfile, null, null, null, null); + + if (!success) + { + _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; + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/Missfile.cs b/SabreTools.Metadata.DatFiles/Formats/Missfile.cs new file mode 100644 index 00000000..7ba0fd6b --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/Missfile.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a Missfile + /// + public sealed class Missfile : DatFile + { + /// + public override ItemType[] SupportedTypes + => Enum.GetValues(typeof(ItemType)) as ItemType[] ?? []; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public Missfile(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.MissFile); + } + + /// + /// There is no consistent way to parse a missfile + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + throw new NotImplementedException(); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + // TODO: Check required fields + return null; + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + FileStream fs = File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs is null) + { + _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new(fs, new UTF8Encoding(false)); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in Items.SortedKeys) + { + List datItems = GetItemsForBucket(key, filter: true); + + // If this machine doesn't contain any writable items, skip + if (!ContainsWritable(datItems)) + continue; + + // Resolve the names in the block + datItems = 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 using machine names or we're not ignoring + if (!Modifiers.UseRomName || !ShouldIgnore(datItem, ignoreblanks)) + WriteDatItem(sw, datItem, lastgame); + + // Set the new data to compare against + lastgame = datItem.GetMachine()!.GetName(); + } + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + sw.Dispose(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Error(ex); + return false; + } + + return true; + } + + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + FileStream fs = File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs is null) + { + _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new(fs, new UTF8Encoding(false)); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in ItemsDB.SortedKeys) + { + // If this machine doesn't contain any writable items, skip + var itemsDict = GetItemsForBucketDB(key, filter: true); + if (itemsDict is null || !ContainsWritable([.. itemsDict.Values])) + continue; + + // Resolve the names in the block + var items = ResolveNamesDB([.. itemsDict]); + + foreach (var kvp in items) + { + // Check for a "null" item + var datItem = new KeyValuePair(kvp.Key, ProcessNullifiedItem(kvp.Value)); + + // Get the machine for the item + var machine = GetMachineForItemDB(datItem.Key); + + // Write out the item if we're using machine names or we're not ignoring + if (!Modifiers.UseRomName || !ShouldIgnore(datItem.Value, ignoreblanks)) + WriteDatItemDB(sw, datItem, lastgame); + + // Set the new data to compare against + lastgame = machine.Value!.GetName(); + } + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + sw.Dispose(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Error(ex); + return false; + } + + return true; + } + + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// StreamWriter to output to + /// DatItem object to be output + /// The name of the last game to be output + private void WriteDatItem(StreamWriter sw, DatItem datItem, string? lastgame) + { + var machine = datItem.GetMachine(); + WriteDatItemImpl(sw, datItem, machine!, lastgame); + } + + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// StreamWriter to output to + /// DatItem object to be output + /// The name of the last game to be output + private void WriteDatItemDB(StreamWriter sw, KeyValuePair datItem, string? lastgame) + { + var machine = GetMachineForItemDB(datItem.Key).Value; + WriteDatItemImpl(sw, datItem.Value, machine!, lastgame); + } + + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// StreamWriter to output to + /// DatItem object to be output + /// Machine object representing the set the item is in + /// The name of the last game to be output + private void WriteDatItemImpl(StreamWriter sw, DatItem datItem, Machine machine, string? lastgame) + { + // Process the item name + ProcessItemName(datItem, machine, forceRemoveQuotes: false, forceRomName: false); + + // Romba mode automatically uses item name + if (Modifiers.OutputDepot?.IsActive == true || Modifiers.UseRomName) + sw.Write($"{datItem.GetName() ?? string.Empty}\n"); + else if (!Modifiers.UseRomName && machine!.GetName() != lastgame) + sw.Write($"{machine!.GetName() ?? string.Empty}\n"); + + sw.Flush(); + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/OfflineList.cs b/SabreTools.Metadata.DatFiles/Formats/OfflineList.cs new file mode 100644 index 00000000..22e05021 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/OfflineList.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents an OfflineList XML DAT + /// + public sealed class OfflineList : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public OfflineList(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.OfflineList); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))) + missingFields.Add(Data.Models.Metadata.Rom.CRCKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/OpenMSX.cs b/SabreTools.Metadata.DatFiles/Formats/OpenMSX.cs new file mode 100644 index 00000000..7728873f --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/OpenMSX.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents an openMSX softawre list XML DAT + /// + public sealed class OpenMSX : SerializableDatFile + { + #region Constants + + /// + /// DTD for original openMSX DATs + /// + internal const string OpenMSXDTD = @" + + + + + + + + +"; + + internal const string OpenMSXCredits = @" +"; + + #endregion + + #region Fields + + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public OpenMSX(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.OpenMSX); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/RomCenter.cs b/SabreTools.Metadata.DatFiles/Formats/RomCenter.cs new file mode 100644 index 00000000..a6a0aed5 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/RomCenter.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a RomCenter INI file + /// + public sealed class RomCenter : SerializableDatFile + { + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Rom, + ]; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public RomCenter(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.RomCenter); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Rom rom: + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))) + missingFields.Add(Data.Models.Metadata.Rom.CRCKey); + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/SabreJSON.cs b/SabreTools.Metadata.DatFiles/Formats/SabreJSON.cs new file mode 100644 index 00000000..0e10af12 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/SabreJSON.cs @@ -0,0 +1,719 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents parsing and writing of a reference SabreDAT JSON + /// + /// TODO: Transform this into direct serialization and deserialization of the Metadata type + public sealed class SabreJSON : DatFile + { + /// + public override ItemType[] SupportedTypes + => Enum.GetValues(typeof(ItemType)) as ItemType[] ?? []; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SabreJSON(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SabreJSON); + } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + // Prepare all internal variables + var fs = System.IO.File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var sr = new StreamReader(fs, new UTF8Encoding(false)); + var jtr = new JsonTextReader(sr); + var source = new Source(indexId, filename); + // long sourceIndex = AddSourceDB(source); + + // If we got a null reader, just return + if (jtr is null) + return; + + // Otherwise, read the file to the end + try + { + jtr.Read(); + while (!sr.EndOfStream) + { + // Skip everything not a property name + if (jtr.TokenType != JsonToken.PropertyName) + { + jtr.Read(); + continue; + } + + switch (jtr.Value) + { + // Header value + case "header": + ReadHeader(jtr); + jtr.Read(); + break; + + // Machine array + case "machines": + ReadMachines(jtr, statsOnly, source, sourceIndex: 0, filterRunner); + jtr.Read(); + break; + + default: + jtr.Read(); + break; + } + } + } + catch (Exception ex) when (!throwOnError) + { + _logger.Warning($"Exception found while parsing '{filename}': {ex}"); + } + + jtr.Close(); + } + + /// + /// Read header information + /// + /// JsonTextReader to use to parse the header + private void ReadHeader(JsonTextReader jtr) + { + // If the reader is invalid, skip + if (jtr is null) + return; + + // Read in the header and apply any new fields + jtr.Read(); + JsonSerializer js = new(); + DatHeader? header = js.Deserialize(jtr); + SetHeader(header); + } + + /// + /// Read machine array information + /// + /// JsonTextReader to use to parse the machine + /// True to only add item statistics while parsing, false otherwise + /// Source representing the DAT + /// Index of the Source representing the DAT + /// Optional FilterRunner to filter items on parse + private void ReadMachines(JsonTextReader jtr, bool statsOnly, Source source, long sourceIndex, FilterRunner? filterRunner) + { + // If the reader is invalid, skip + if (jtr is null) + return; + + // Read in the machine array + jtr.Read(); + var js = new JsonSerializer(); + JArray machineArray = js.Deserialize(jtr) ?? []; + + // Loop through each machine object and process + foreach (JObject machineObj in machineArray.Cast()) + { + ReadMachine(machineObj, statsOnly, source, sourceIndex, filterRunner); + } + } + + /// + /// Read machine object information + /// + /// JObject representing a single machine + /// True to only add item statistics while parsing, false otherwise + /// Source representing the DAT + /// Index of the Source representing the DAT + /// Optional FilterRunner to filter items on parse + private void ReadMachine(JObject machineObj, bool statsOnly, Source source, long sourceIndex, FilterRunner? filterRunner) + { + // If object is invalid, skip it + if (machineObj is null) + return; + + // Prepare internal variables + Machine? machine = null; + + // Read the machine info, if possible + if (machineObj.ContainsKey("machine")) + machine = machineObj["machine"]?.ToObject(); + + // If the machine doesn't pass the filter + if (machine is not null && filterRunner is not null && !machine.PassesFilter(filterRunner)) + return; + + // Add the machine to the dictionary + // long machineIndex = -1; + // if (machine is not null) + // machineIndex = AddMachineDB(machine); + + // Read items, if possible + if (machineObj.ContainsKey("items")) + { + ReadItems(machineObj["items"] as JArray, + statsOnly, + source, + sourceIndex, + machine, + machineIndex: 0, + filterRunner); + } + } + + /// + /// Read item array information + /// + /// JArray representing the items list + /// True to only add item statistics while parsing, false otherwise + /// Source representing the DAT + /// Index of the Source representing the DAT + /// Machine information to add to the parsed items + /// Index of the Machine to add to the parsed items + /// Optional FilterRunner to filter items on parse + private void ReadItems( + JArray? itemsArr, + bool statsOnly, + + // Standard Dat parsing + Source source, + long sourceIndex, + + // Miscellaneous + Machine? machine, + long machineIndex, + FilterRunner? filterRunner) + { + // If the array is invalid, skip + if (itemsArr is null) + return; + + // Loop through each datitem object and process + foreach (JObject itemObj in itemsArr.Cast()) + { + ReadItem(itemObj, statsOnly, source, sourceIndex, machine, machineIndex, filterRunner); + } + } + + /// + /// Read item information + /// + /// JObject representing a single datitem + /// True to only add item statistics while parsing, false otherwise + /// Source representing the DAT + /// Index of the Source representing the DAT + /// Machine information to add to the parsed items + /// Index of the Machine to add to the parsed items + /// Optional FilterRunner to filter items on parse + private void ReadItem( + JObject itemObj, + bool statsOnly, + + // Standard Dat parsing + Source source, + long sourceIndex, + + // Miscellaneous + Machine? machine, + long machineIndex, + FilterRunner? filterRunner) + { + // If we have an empty item, skip it + if (itemObj is null) + return; + + // Prepare internal variables + DatItem? datItem = null; + + // Read the datitem info, if possible + if (itemObj.ContainsKey("datitem")) + { + JToken? datItemObj = itemObj["datitem"]; + if (datItemObj is null) + return; + +#pragma warning disable IDE0010 + switch (datItemObj.Value("type").AsItemType()) + { + case ItemType.Adjuster: + datItem = datItemObj.ToObject(); + break; + case ItemType.Analog: + datItem = datItemObj.ToObject(); + break; + case ItemType.Archive: + datItem = datItemObj.ToObject(); + break; + case ItemType.BiosSet: + datItem = datItemObj.ToObject(); + break; + case ItemType.Blank: + datItem = datItemObj.ToObject(); + break; + case ItemType.Chip: + datItem = datItemObj.ToObject(); + break; + case ItemType.Condition: + datItem = datItemObj.ToObject(); + break; + case ItemType.Configuration: + datItem = datItemObj.ToObject(); + break; + case ItemType.ConfLocation: + datItem = datItemObj.ToObject(); + break; + case ItemType.ConfSetting: + datItem = datItemObj.ToObject(); + break; + case ItemType.Control: + datItem = datItemObj.ToObject(); + break; + case ItemType.DataArea: + datItem = datItemObj.ToObject(); + break; + case ItemType.Device: + datItem = datItemObj.ToObject(); + break; + case ItemType.DeviceRef: + datItem = datItemObj.ToObject(); + break; + case ItemType.DipLocation: + datItem = datItemObj.ToObject(); + break; + case ItemType.DipValue: + datItem = datItemObj.ToObject(); + break; + case ItemType.DipSwitch: + datItem = datItemObj.ToObject(); + break; + case ItemType.Disk: + datItem = datItemObj.ToObject(); + break; + case ItemType.DiskArea: + datItem = datItemObj.ToObject(); + break; + case ItemType.Display: + datItem = datItemObj.ToObject(); + break; + case ItemType.Driver: + datItem = datItemObj.ToObject(); + break; + case ItemType.Extension: + datItem = datItemObj.ToObject(); + break; + case ItemType.Feature: + datItem = datItemObj.ToObject(); + break; + case ItemType.Info: + datItem = datItemObj.ToObject(); + break; + case ItemType.Input: + datItem = datItemObj.ToObject(); + break; + case ItemType.Instance: + datItem = datItemObj.ToObject(); + break; + case ItemType.Media: + datItem = datItemObj.ToObject(); + break; + case ItemType.Part: + datItem = datItemObj.ToObject(); + break; + case ItemType.PartFeature: + datItem = datItemObj.ToObject(); + break; + case ItemType.Port: + datItem = datItemObj.ToObject(); + break; + case ItemType.RamOption: + datItem = datItemObj.ToObject(); + break; + case ItemType.Release: + datItem = datItemObj.ToObject(); + break; + case ItemType.ReleaseDetails: + datItem = datItemObj.ToObject(); + break; + case ItemType.Rom: + datItem = datItemObj.ToObject(); + break; + case ItemType.Sample: + datItem = datItemObj.ToObject(); + break; + case ItemType.Serials: + datItem = datItemObj.ToObject(); + break; + case ItemType.SharedFeat: + datItem = datItemObj.ToObject(); + break; + case ItemType.Slot: + datItem = datItemObj.ToObject(); + break; + case ItemType.SlotOption: + datItem = datItemObj.ToObject(); + break; + case ItemType.SoftwareList: + datItem = datItemObj.ToObject(); + break; + case ItemType.Sound: + datItem = datItemObj.ToObject(); + break; + case ItemType.SourceDetails: + datItem = datItemObj.ToObject(); + break; + } +#pragma warning restore IDE0010 + } + + // If we got a valid datitem, copy machine info and add + if (datItem is not null) + { + // If the item doesn't pass the filter + if (filterRunner is not null && !datItem.PassesFilter(filterRunner)) + return; + + datItem.CopyMachineInformation(machine); + datItem.SetFieldValue(DatItem.SourceKey, source); + AddItem(datItem, statsOnly); + AddItemDB(datItem, machineIndex, sourceIndex, statsOnly); + } + } + + /// + 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 is null) + { + _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new(fs, new UTF8Encoding(false)); + JsonTextWriter jtw = new(sw) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + + // Write out the header + WriteHeader(jtw); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in Items.SortedKeys) + { + List datItems = GetItemsForBucket(key, filter: true); + + // If this machine doesn't contain any writable items, skip + if (!ContainsWritable(datItems)) + continue; + + // Resolve the names in the block + datItems = ResolveNames(datItems); + + for (int index = 0; index < datItems.Count; index++) + { + DatItem datItem = datItems[index]; + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame is not null && !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteEndGame(jtw); + + // If we have a new game, output the beginning of the new item + if (lastgame is null || !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteStartGame(jtw, datItem); + + // Check for a "null" item + datItem = ProcessNullifiedItem(datItem); + + // Write out the item if we're not ignoring + if (!ShouldIgnore(datItem, ignoreblanks)) + WriteDatItem(jtw, datItem); + + // Set the new data to compare against + lastgame = datItem.GetMachine()!.GetName(); + } + } + + // Write the file footer out + WriteFooter(jtw); + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + jtw.Close(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Error(ex); + return false; + } + + return true; + } + + /// + public override bool WriteToFileDB(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 is null) + { + _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + StreamWriter sw = new(fs, new UTF8Encoding(false)); + JsonTextWriter jtw = new(sw) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; + + // Write out the header + WriteHeader(jtw); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in ItemsDB.SortedKeys) + { + // If this machine doesn't contain any writable items, skip + var itemsDict = GetItemsForBucketDB(key, filter: true); + if (itemsDict is null || !ContainsWritable([.. itemsDict.Values])) + continue; + + // Resolve the names in the block + var items = ResolveNamesDB([.. itemsDict]); + + foreach (var kvp in items) + { + // Get the machine for the item + var machine = GetMachineForItemDB(kvp.Key); + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame is not null && !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteEndGame(jtw); + + // If we have a new game, output the beginning of the new item + if (lastgame is null || !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteStartGame(jtw, kvp.Value); + + // Check for a "null" item + var datItem = new KeyValuePair(kvp.Key, ProcessNullifiedItem(kvp.Value)); + + // Write out the item if we're not ignoring + if (!ShouldIgnore(datItem.Value, ignoreblanks)) + WriteDatItemDB(jtw, datItem); + + // Set the new data to compare against + lastgame = machine.Value!.GetName(); + } + } + + // Write the file footer out + WriteFooter(jtw); + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + jtw.Close(); + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Error(ex); + return false; + } + + return true; + } + + /// + /// Write out DAT header using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + private void WriteHeader(JsonTextWriter jtw) + { + jtw.WriteStartObject(); + + // Write the DatHeader + jtw.WritePropertyName("header"); + JsonSerializer js = new() { Formatting = Formatting.Indented }; + js.Serialize(jtw, Header); + + jtw.WritePropertyName("machines"); + jtw.WriteStartArray(); + + jtw.Flush(); + } + + /// + /// Write out Game start using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + /// DatItem object to be output + private static void WriteStartGame(JsonTextWriter jtw, DatItem datItem) + { + // No game should start with a path separator + if (!string.IsNullOrEmpty(datItem.GetMachine()!.GetName())) + datItem.GetMachine()!.SetName(datItem.GetMachine()!.GetName()!.TrimStart(Path.DirectorySeparatorChar)); + + // Build the state + jtw.WriteStartObject(); + + // Write the Machine + jtw.WritePropertyName("machine"); + JsonSerializer js = new() { Formatting = Formatting.Indented }; + js.Serialize(jtw, datItem.GetMachine()!); + + jtw.WritePropertyName("items"); + jtw.WriteStartArray(); + + jtw.Flush(); + } + + /// + /// Write out Game end using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + private static void WriteEndGame(JsonTextWriter jtw) + { + // End items + jtw.WriteEndArray(); + + // End machine + jtw.WriteEndObject(); + + jtw.Flush(); + } + + /// + /// Write out DatItem using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + /// DatItem object to be output + private void WriteDatItem(JsonTextWriter jtw, DatItem datItem) + { + // Get the machine for the item + var machine = datItem.GetMachine(); + + // Pre-process the item name + ProcessItemName(datItem, machine, forceRemoveQuotes: true, forceRomName: false); + + // Build the state + jtw.WriteStartObject(); + + // Write the DatItem + jtw.WritePropertyName("datitem"); + JsonSerializer js = new() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented }; + js.Serialize(jtw, datItem); + + // End item + jtw.WriteEndObject(); + + jtw.Flush(); + } + + /// + /// Write out DatItem using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + /// DatItem object to be output + private void WriteDatItemDB(JsonTextWriter jtw, KeyValuePair datItem) + { + // Get the machine for the item + var machine = GetMachineForItemDB(datItem.Key); + + // Pre-process the item name + ProcessItemName(datItem.Value, machine.Value, forceRemoveQuotes: true, forceRomName: false); + + // Build the state + jtw.WriteStartObject(); + + // Write the DatItem + jtw.WritePropertyName("datitem"); + JsonSerializer js = new() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented }; + js.Serialize(jtw, datItem); + + // End item + jtw.WriteEndObject(); + + jtw.Flush(); + } + + /// + /// Write out DAT footer using the supplied JsonTextWriter + /// + /// JsonTextWriter to output to + private static void WriteFooter(JsonTextWriter jtw) + { + // End items + jtw.WriteEndArray(); + + // End machine + jtw.WriteEndObject(); + + // End machines + jtw.WriteEndArray(); + + // End file + jtw.WriteEndObject(); + + jtw.Flush(); + } + + // https://github.com/dotnet/runtime/issues/728 + private class BaseFirstContractResolver : DefaultContractResolver + { + public BaseFirstContractResolver() + { + NamingStrategy = new CamelCaseNamingStrategy(); + } + + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + return [.. base.CreateProperties(type, memberSerialization) + .Where(p => p is not null) + .OrderBy(p => BaseTypesAndSelf(p.DeclaringType).Count())]; + + static IEnumerable BaseTypesAndSelf(Type? t) + { + while (t is not null) + { + yield return t; + t = t.BaseType; + } + } + } + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/SabreXML.cs b/SabreTools.Metadata.DatFiles/Formats/SabreXML.cs new file mode 100644 index 00000000..25636402 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/SabreXML.cs @@ -0,0 +1,522 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; + +#pragma warning disable IDE0060 // Remove unused parameter +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents parsing and writing of a SabreDAT XML + /// + /// TODO: Transform this into direct serialization and deserialization of the Metadata type + public sealed class SabreXML : DatFile + { + /// + public override ItemType[] SupportedTypes + => Enum.GetValues(typeof(ItemType)) as ItemType[] ?? []; + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SabreXML(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SabreXML); + } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + // Prepare all internal variables + XmlReader? xtr = XmlReader.Create(filename, new XmlReaderSettings + { + CheckCharacters = false, +#if NET40_OR_GREATER + DtdProcessing = DtdProcessing.Ignore, +#endif + IgnoreComments = true, + IgnoreWhitespace = true, + ValidationFlags = XmlSchemaValidationFlags.None, + ValidationType = ValidationType.None, + }); + var source = new Source(indexId, filename); + long sourceIndex = AddSourceDB(source); + + // If we got a null reader, just return + if (xtr is null) + return; + + // Otherwise, read the file to the end + try + { + xtr.MoveToContent(); + while (!xtr.EOF) + { + // We only want elements + if (xtr.NodeType != XmlNodeType.Element) + { + xtr.Read(); + continue; + } + + switch (xtr.Name) + { + case "header": + XmlSerializer xs = new(typeof(DatHeader)); + DatHeader? header = xs.Deserialize(xtr.ReadSubtree()) as DatHeader; + SetHeader(header); + xtr.Skip(); + break; + + case "directory": + ReadDirectory(xtr.ReadSubtree(), statsOnly, source, sourceIndex, filterRunner); + + // Skip the directory node now that we've processed it + xtr.Read(); + break; + default: + xtr.Read(); + break; + } + } + } + catch (Exception ex) when (!throwOnError) + { + _logger.Warning(ex, $"Exception found while parsing '{filename}'"); + + // For XML errors, just skip the affected node + xtr?.Read(); + } + +#if NET452_OR_GREATER + xtr?.Dispose(); +#endif + } + + /// + /// Read directory information + /// + /// XmlReader to use to parse the header + /// True to only add item statistics while parsing, false otherwise + /// Source representing the DAT + /// Index of the Source representing the DAT + /// Optional FilterRunner to filter items on parse + private void ReadDirectory(XmlReader xtr, + bool statsOnly, + Source source, + long sourceIndex, + FilterRunner? filterRunner) + { + // If the reader is invalid, skip + if (xtr is null) + return; + + // Prepare internal variables + Machine? machine = null; + long machineIndex = -1; + + // Otherwise, read the directory + xtr.MoveToContent(); + while (!xtr.EOF) + { + // We only want elements + if (xtr.NodeType != XmlNodeType.Element) + { + xtr.Read(); + continue; + } + + switch (xtr.Name) + { + case "machine": + XmlSerializer xs = new(typeof(Machine)); + machine = xs?.Deserialize(xtr.ReadSubtree()) as Machine; + + // If the machine doesn't pass the filter + if (machine is not null && filterRunner is not null && !machine.PassesFilter(filterRunner)) + machine = null; + + if (machine is not null) + machineIndex = AddMachineDB(machine); + + xtr.Skip(); + break; + + case "files": + ReadFiles(xtr.ReadSubtree(), + machine, + machineIndex, + statsOnly, + source, + sourceIndex, + filterRunner); + + // Skip the directory node now that we've processed it + xtr.Read(); + break; + default: + xtr.Read(); + break; + } + } + } + + /// + /// Read Files information + /// + /// XmlReader to use to parse the header + /// Machine to copy information from + /// Index of the Machine to add to the parsed items + /// True to only add item statistics while parsing, false otherwise + /// Source representing the DAT + /// Index of the Source representing the DAT + /// Optional FilterRunner to filter items on parse + private void ReadFiles(XmlReader xtr, + Machine? machine, + long machineIndex, + bool statsOnly, + Source source, + long sourceIndex, + FilterRunner? filterRunner) + { + // If the reader is invalid, skip + if (xtr is null) + return; + + // Otherwise, read the items + xtr.MoveToContent(); + while (!xtr.EOF) + { + // We only want elements + if (xtr.NodeType != XmlNodeType.Element) + { + xtr.Read(); + continue; + } + + switch (xtr.Name) + { + case "datitem": + XmlSerializer xs = new(typeof(DatItem)); + if (xs.Deserialize(xtr.ReadSubtree()) is DatItem item) + { + // If the item doesn't pass the filter + if (filterRunner is not null && !item.PassesFilter(filterRunner)) + { + xtr.Skip(); + break; + } + + item.CopyMachineInformation(machine); + item.SetFieldValue(DatItem.SourceKey, source); + AddItem(item, statsOnly); + // AddItemDB(item, machineIndex, sourceIndex, statsOnly); + } + + xtr.Skip(); + break; + default: + xtr.Read(); + break; + } + } + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + FileStream fs = File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs is null) + { + _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + XmlTextWriter xtw = new(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1, + }; + + // Write out the header + WriteHeader(xtw); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in Items.SortedKeys) + { + List datItems = GetItemsForBucket(key, filter: true); + + // If this machine doesn't contain any writable items, skip + if (!ContainsWritable(datItems)) + continue; + + // Resolve the names in the block + datItems = ResolveNames(datItems); + + for (int index = 0; index < datItems.Count; index++) + { + DatItem datItem = datItems[index]; + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame is not null && !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteEndGame(xtw); + + // If we have a new game, output the beginning of the new item + if (lastgame is null || !string.Equals(lastgame, datItem.GetMachine()!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteStartGame(xtw, datItem); + + // Check for a "null" item + datItem = ProcessNullifiedItem(datItem); + + // Write out the item if we're not ignoring + if (!ShouldIgnore(datItem, ignoreblanks)) + WriteDatItem(xtw, datItem); + + // Set the new data to compare against + lastgame = datItem.GetMachine()!.GetName(); + } + } + + // Write the file footer out + WriteFooter(xtw); + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); +#if NET452_OR_GREATER + xtw.Dispose(); +#endif + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Error(ex); + return false; + } + + return true; + } + + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + FileStream fs = File.Create(outfile); + + // If we get back null for some reason, just log and return + if (fs is null) + { + _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); + return false; + } + + XmlTextWriter xtw = new(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1, + }; + + // Write out the header + WriteHeader(xtw); + + // Write out each of the machines and roms + string? lastgame = null; + + // Use a sorted list of games to output + foreach (string key in ItemsDB.SortedKeys) + { + // If this machine doesn't contain any writable items, skip + var itemsDict = GetItemsForBucketDB(key, filter: true); + if (itemsDict is null || !ContainsWritable([.. itemsDict.Values])) + continue; + + // Resolve the names in the block + var items = ResolveNamesDB([.. itemsDict]); + + foreach (var kvp in items) + { + // Get the machine for the item + var machine = GetMachineForItemDB(kvp.Key); + + // If we have a different game and we're not at the start of the list, output the end of last item + if (lastgame is not null && !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteEndGame(xtw); + + // If we have a new game, output the beginning of the new item + if (lastgame is null || !string.Equals(lastgame, machine.Value!.GetName(), StringComparison.OrdinalIgnoreCase)) + WriteStartGame(xtw, kvp.Value); + + // Check for a "null" item + var datItem = new KeyValuePair(kvp.Key, ProcessNullifiedItem(kvp.Value)); + + // Write out the item if we're not ignoring + if (!ShouldIgnore(datItem.Value, ignoreblanks)) + WriteDatItemDB(xtw, datItem); + + // Set the new data to compare against + lastgame = machine.Value!.GetName(); + } + } + + // Write the file footer out + WriteFooter(xtw); + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); +#if NET452_OR_GREATER + xtw.Dispose(); +#endif + fs.Dispose(); + } + catch (Exception ex) when (!throwOnError) + { + _logger.Error(ex); + return false; + } + + return true; + } + + /// + /// Write out DAT header using the supplied StreamWriter + /// + /// XmlTextWriter to output to + private void WriteHeader(XmlTextWriter xtw) + { + xtw.WriteStartDocument(); + + xtw.WriteStartElement("datafile"); + + XmlSerializer xs = new(typeof(DatHeader)); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + xs.Serialize(xtw, Header, ns); + + xtw.WriteStartElement("data"); + + xtw.Flush(); + } + + /// + /// Write out Game start using the supplied StreamWriter + /// + /// XmlTextWriter to output to + /// DatItem object to be output + private static void WriteStartGame(XmlTextWriter xtw, DatItem datItem) + { + // No game should start with a path separator + datItem.GetMachine()!.SetName(datItem.GetMachine()!.GetName()?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty); + + // Write the machine + xtw.WriteStartElement("directory"); + XmlSerializer xs = new(typeof(Machine)); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + xs.Serialize(xtw, datItem.GetMachine(), ns); + + xtw.WriteStartElement("files"); + + xtw.Flush(); + } + + /// + /// Write out Game start using the supplied StreamWriter + /// + /// XmlTextWriter to output to + private static void WriteEndGame(XmlTextWriter xtw) + { + // End files + xtw.WriteEndElement(); + + // End directory + xtw.WriteEndElement(); + + xtw.Flush(); + } + + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// XmlTextWriter to output to + /// DatItem object to be output + private void WriteDatItem(XmlTextWriter xtw, DatItem datItem) + { + // Get the machine for the item + var machine = datItem.GetMachine(); + + // Pre-process the item name + ProcessItemName(datItem, machine, forceRemoveQuotes: true, forceRomName: false); + + // Write the DatItem + XmlSerializer xs = new(typeof(DatItem)); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + xs.Serialize(xtw, datItem, ns); + + xtw.Flush(); + } + + /// + /// Write out DatItem using the supplied StreamWriter + /// + /// XmlTextWriter to output to + /// DatItem object to be output + private void WriteDatItemDB(XmlTextWriter xtw, KeyValuePair datItem) + { + // Get the machine for the item + var machine = GetMachineForItemDB(datItem.Key); + + // Pre-process the item name + ProcessItemName(datItem.Value, machine.Value, forceRemoveQuotes: true, forceRomName: false); + + // Write the DatItem + XmlSerializer xs = new(typeof(DatItem)); + XmlSerializerNamespaces ns = new(); + ns.Add("", ""); + xs.Serialize(xtw, datItem, ns); + + xtw.Flush(); + } + + /// + /// Write out DAT footer using the supplied StreamWriter + /// + /// XmlTextWriter to output to + private static void WriteFooter(XmlTextWriter xtw) + { + // End files + xtw.WriteEndElement(); + + // End directory + xtw.WriteEndElement(); + + // End data + xtw.WriteEndElement(); + + // End datafile + xtw.WriteEndElement(); + + xtw.Flush(); + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/SeparatedValue.cs b/SabreTools.Metadata.DatFiles/Formats/SeparatedValue.cs new file mode 100644 index 00000000..777d153c --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/SeparatedValue.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +#pragma warning disable IDE0290 // Use primary constructor +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a value-separated DAT + /// + public abstract class SeparatedValue : SerializableDatFile + { + #region Fields + + /// + public override ItemType[] SupportedTypes + => [ + ItemType.Disk, + ItemType.Media, + ItemType.Rom, + ]; + + /// + /// Represents the delimiter between fields + /// + protected char _delim; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SeparatedValue(DatFile? datFile) : base(datFile) + { + } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + try + { + // Deserialize the input file + var metadataFile = new Serialization.Readers.SeparatedValue().Deserialize(filename, _delim); + var metadata = new Serialization.CrossModel.SeparatedValue().Serialize(metadataFile); + + // Convert to the internal format + ConvertFromMetadata(metadata, filename, indexId, keep, statsOnly, filterRunner); + } + catch (Exception ex) when (!throwOnError) + { + string message = $"'{filename}' - An error occurred during parsing"; + _logger.Error(ex, message); + } + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + + // Check item name + if (string.IsNullOrEmpty(datItem.GetName())) + missingFields.Add(Data.Models.Metadata.Rom.NameKey); + +#pragma warning disable IDE0010 + switch (datItem) + { + case Disk disk: + if (string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + missingFields.Add(Data.Models.Metadata.Disk.SHA1Key); + } + + break; + + case Media media: + if (string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey))) + { + missingFields.Add(Data.Models.Metadata.Media.SHA1Key); + } + + break; + + case Rom rom: + if (rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is null || rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) < 0) + missingFields.Add(Data.Models.Metadata.Rom.SizeKey); + if (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)) + && string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey))) + { + missingFields.Add(Data.Models.Metadata.Rom.SHA1Key); + } + + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file + var metadata = ConvertToMetadata(ignoreblanks); + var metadataFile = new Serialization.CrossModel.SeparatedValue().Deserialize(metadata); + if (!new Serialization.Writers.SeparatedValue().SerializeFile(metadataFile, outfile, _delim, longHeader: false)) + { + _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; + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } + } + + /// + /// Represents a comma-separated value file + /// + public sealed class CommaSeparatedValue : SeparatedValue + { + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public CommaSeparatedValue(DatFile? datFile) : base(datFile) + { + _delim = ','; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.CSV); + } + } + + /// + /// Represents a semicolon-separated value file + /// + public sealed class SemicolonSeparatedValue : SeparatedValue + { + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SemicolonSeparatedValue(DatFile? datFile) : base(datFile) + { + _delim = ';'; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SSV); + } + } + + /// + /// Represents a tab-separated value file + /// + public sealed class TabSeparatedValue : SeparatedValue + { + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public TabSeparatedValue(DatFile? datFile) : base(datFile) + { + _delim = '\t'; + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.TSV); + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/SerializableDatFile.cs b/SabreTools.Metadata.DatFiles/Formats/SerializableDatFile.cs new file mode 100644 index 00000000..90a2f37f --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/SerializableDatFile.cs @@ -0,0 +1,121 @@ +using System; +using SabreTools.Metadata.Filter; +using SabreTools.Data.Models.Metadata; +using SabreTools.Serialization.CrossModel; +using SabreTools.Serialization.Readers; +using SabreTools.Serialization.Writers; + +#pragma warning disable IDE0290 // Use primary constructor +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents a DAT that can be serialized + /// + /// Base internal model for the DAT type + /// IFileReader type to use for conversion + /// IFileWriter type to use for conversion + /// ICrossModel for cross-model serialization + public abstract class SerializableDatFile : DatFile + where TFileReader : IFileReader + where TFileWriter : IFileWriter + where TCrossModel : ICrossModel + { + #region Static Serialization Instances + + /// + /// File deserializer instance + /// + private static readonly TFileReader FileDeserializer = Activator.CreateInstance(); + + /// + /// File serializer instance + /// + private static readonly TFileWriter FileSerializer = Activator.CreateInstance(); + + /// + /// Cross-model serializer instance + /// + private static readonly TCrossModel CrossModelSerializer = Activator.CreateInstance(); + + #endregion + + /// + protected SerializableDatFile(DatFile? datFile) : base(datFile) { } + + /// + public override void ParseFile(string filename, + int indexId, + bool keep, + bool statsOnly = false, + FilterRunner? filterRunner = null, + bool throwOnError = false) + { + try + { + // Deserialize the input file in two steps + var specificFormat = FileDeserializer.Deserialize(filename); + var internalFormat = CrossModelSerializer.Serialize(specificFormat); + + // Convert to the internal format + ConvertFromMetadata(internalFormat, filename, indexId, keep, statsOnly, filterRunner); + } + catch (Exception ex) when (!throwOnError) + { + string message = $"'{filename}' - An error occurred during parsing"; + _logger.Error(ex, message); + } + } + + /// + public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file in two steps + var internalFormat = ConvertToMetadata(ignoreblanks); + var specificFormat = CrossModelSerializer.Deserialize(internalFormat); + if (!FileSerializer.SerializeFile(specificFormat, outfile)) + { + _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; + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } + + /// + public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) + { + try + { + _logger.User($"Writing to '{outfile}'..."); + + // Serialize the input file in two steps + var internalFormat = ConvertToMetadataDB(ignoreblanks); + var specificFormat = CrossModelSerializer.Deserialize(internalFormat); + if (!FileSerializer.SerializeFile(specificFormat, outfile)) + { + _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; + } + + _logger.User($"'{outfile}' written!{Environment.NewLine}"); + return true; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/Formats/SoftwareList.cs b/SabreTools.Metadata.DatFiles/Formats/SoftwareList.cs new file mode 100644 index 00000000..02dde29d --- /dev/null +++ b/SabreTools.Metadata.DatFiles/Formats/SoftwareList.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; + +namespace SabreTools.Metadata.DatFiles.Formats +{ + /// + /// Represents parsing and writing of a SoftwareList + /// + public sealed class SoftwareList : SerializableDatFile + { + #region Constants + + /// + /// DTD for original MAME Software List DATs + /// + /// + /// TODO: See if there's an updated DTD and then check for required fields + /// + internal const string SoftwareListDTD = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + #endregion + + #region Fields + + /// + public override ItemType[] SupportedTypes + => [ + ItemType.DipSwitch, + ItemType.Disk, + ItemType.Info, + ItemType.PartFeature, + ItemType.Rom, + ItemType.SharedFeat, + ]; + + #endregion + + /// + /// Constructor designed for casting a base DatFile + /// + /// Parent DatFile to copy from + public SoftwareList(DatFile? datFile) : base(datFile) + { + Header.SetFieldValue(DatHeader.DatFormatKey, DatFormat.SoftwareList); + } + + /// + protected internal override List? GetMissingRequiredFields(DatItem datItem) + { + List missingFields = []; + +#pragma warning disable IDE0010 + switch (datItem) + { + case DipSwitch dipSwitch: + if (!dipSwitch.PartSpecified) + { + missingFields.Add(Data.Models.Metadata.Part.NameKey); + missingFields.Add(Data.Models.Metadata.Part.InterfaceKey); + } + else + { + if (string.IsNullOrEmpty(dipSwitch.GetFieldValue(DipSwitch.PartKey)!.GetName())) + missingFields.Add(Data.Models.Metadata.Part.NameKey); + if (string.IsNullOrEmpty(dipSwitch.GetFieldValue(DipSwitch.PartKey)!.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey))) + missingFields.Add(Data.Models.Metadata.Part.InterfaceKey); + } + + if (string.IsNullOrEmpty(dipSwitch.GetName())) + missingFields.Add(Data.Models.Metadata.DipSwitch.NameKey); + if (string.IsNullOrEmpty(dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.TagKey))) + missingFields.Add(Data.Models.Metadata.DipSwitch.TagKey); + if (string.IsNullOrEmpty(dipSwitch.GetStringFieldValue(Data.Models.Metadata.DipSwitch.MaskKey))) + missingFields.Add(Data.Models.Metadata.DipSwitch.MaskKey); + if (dipSwitch.ValuesSpecified) + { + var dipValues = dipSwitch.GetFieldValue(Data.Models.Metadata.DipSwitch.DipValueKey); + if (Array.Find(dipValues!, dv => string.IsNullOrEmpty(dv.GetName())) is not null) + missingFields.Add(Data.Models.Metadata.DipValue.NameKey); + if (Array.Find(dipValues!, dv => string.IsNullOrEmpty(dv.GetStringFieldValue(Data.Models.Metadata.DipValue.ValueKey))) is not null) + missingFields.Add(Data.Models.Metadata.DipValue.ValueKey); + } + + break; + + case Disk disk: + if (!disk.PartSpecified) + { + missingFields.Add(Data.Models.Metadata.Part.NameKey); + missingFields.Add(Data.Models.Metadata.Part.InterfaceKey); + } + else + { + if (string.IsNullOrEmpty(disk.GetFieldValue(Disk.PartKey)!.GetName())) + missingFields.Add(Data.Models.Metadata.Part.NameKey); + if (string.IsNullOrEmpty(disk.GetFieldValue(Disk.PartKey)!.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey))) + missingFields.Add(Data.Models.Metadata.Part.InterfaceKey); + } + + if (!disk.DiskAreaSpecified) + { + missingFields.Add(Data.Models.Metadata.DiskArea.NameKey); + } + else + { + if (string.IsNullOrEmpty(disk.GetFieldValue(Disk.DiskAreaKey)!.GetName())) + missingFields.Add(Data.Models.Metadata.DiskArea.NameKey); + } + + if (string.IsNullOrEmpty(disk.GetName())) + missingFields.Add(Data.Models.Metadata.Disk.NameKey); + break; + + case Info info: + if (string.IsNullOrEmpty(info.GetName())) + missingFields.Add(Data.Models.Metadata.Info.NameKey); + break; + + case Rom rom: + if (!rom.PartSpecified) + { + missingFields.Add(Data.Models.Metadata.Part.NameKey); + missingFields.Add(Data.Models.Metadata.Part.InterfaceKey); + } + else + { + if (string.IsNullOrEmpty(rom.GetFieldValue(Rom.PartKey)!.GetName())) + missingFields.Add(Data.Models.Metadata.Part.NameKey); + if (string.IsNullOrEmpty(rom.GetFieldValue(Rom.PartKey)!.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey))) + missingFields.Add(Data.Models.Metadata.Part.InterfaceKey); + } + + if (!rom.DataAreaSpecified) + { + missingFields.Add(Data.Models.Metadata.DataArea.NameKey); + missingFields.Add(Data.Models.Metadata.DataArea.SizeKey); + } + else + { + if (string.IsNullOrEmpty(rom.GetFieldValue(Rom.DataAreaKey)!.GetName())) + missingFields.Add(Data.Models.Metadata.DataArea.NameKey); + if (rom.GetFieldValue(Rom.DataAreaKey)!.GetInt64FieldValue(Data.Models.Metadata.DataArea.SizeKey) is null) + missingFields.Add(Data.Models.Metadata.DataArea.SizeKey); + } + + break; + + case SharedFeat sharedFeat: + if (string.IsNullOrEmpty(sharedFeat.GetName())) + missingFields.Add(Data.Models.Metadata.SharedFeat.NameKey); + break; + } +#pragma warning restore IDE0010 + + return missingFields; + } + } +} diff --git a/SabreTools.Metadata.DatFiles/ItemDictionary.cs b/SabreTools.Metadata.DatFiles/ItemDictionary.cs new file mode 100644 index 00000000..18913b17 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/ItemDictionary.cs @@ -0,0 +1,931 @@ +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Collections.Concurrent; +#endif +using System.Collections.Generic; +using System.IO; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Threading.Tasks; +#endif +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using SabreTools.Hashing; +using SabreTools.IO.Logging; +using SabreTools.Text.Compare; + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Item dictionary with statistics, bucketing, and sorting + /// + [JsonObject("items"), XmlRoot("items")] + public class ItemDictionary + { + #region Private instance variables + + /// + /// Determine the bucketing key for all items + /// + private ItemKey _bucketedBy = ItemKey.NULL; + + /// + /// Internal dictionary for the class + /// +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary?> _items = []; +#else + private readonly Dictionary?> _items = []; +#endif + + /// + /// Logging object + /// + private readonly Logger _logger; + + #endregion + + #region Fields + + /// + /// Get the keys in sorted order from the file dictionary + /// + /// List of the keys in sorted order + [JsonIgnore, XmlIgnore] + public string[] SortedKeys + { + get + { + List keys = [.. _items.Keys]; + keys.Sort(new NaturalComparer()); + return [.. keys]; + } + } + + /// + /// DAT statistics + /// + [JsonIgnore, XmlIgnore] + public DatStatistics DatStatistics { get; } = new DatStatistics(); + + #endregion + + #region Constructors + + /// + /// Generic constructor + /// + public ItemDictionary() + { + _logger = new Logger(this); + } + + #endregion + + #region Accessors + + /// + /// 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 + public string AddItem(DatItem item, bool statsOnly) + { + // If we have a Disk, File, Media, or Rom, clean the hash data + if (item is Disk disk) + { + // If the file has aboslutely no hashes, skip and log + if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + _logger.Verbose($"Incomplete entry for '{disk.GetName()}' will be output as nodump"); + disk.SetFieldValue(Data.Models.Metadata.Disk.StatusKey, ItemStatus.Nodump.AsStringValue()); + } + + item = disk; + } + else if (item is DatItems.Formats.File file) + { + // If the file has aboslutely no hashes, skip and log + if (string.IsNullOrEmpty(file.CRC) + && string.IsNullOrEmpty(file.MD5) + && string.IsNullOrEmpty(file.SHA1) + && string.IsNullOrEmpty(file.SHA256)) + { + _logger.Verbose($"Incomplete entry for '{file.GetName()}' will be output as nodump"); + } + + item = file; + } + else if (item is Media media) + { + // If the file has aboslutely no hashes, skip and log + if (string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey))) + { + _logger.Verbose($"Incomplete entry for '{media.GetName()}' will be output as nodump"); + } + + item = media; + } + else if (item is Rom rom) + { + long? size = rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey); + + // If we have the case where there is SHA-1 and nothing else, we don't fill in any other part of the data + if (size is null && !string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + { + // No-op, just catch it so it doesn't go further + //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Entry with only SHA-1 found - '{rom.GetName()}'"); + } + + // If we have a rom and it's missing size AND the hashes match a 0-byte file, fill in the rest of the info + else if ((size == 0 || size is null) + && (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) || rom.HasZeroHash())) + { + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "0"); + rom.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, null); // HashType.MD2.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, null); // HashType.MD4.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, HashType.MD5.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, null); // HashType.RIPEMD128.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, null); // HashType.RIPEMD160.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, null); // HashType.SHA256.ZeroString; + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, null); // HashType.SHA384.ZeroString; + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, null); // HashType.SHA512.ZeroString; + rom.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, null); // HashType.SpamSum.ZeroString; + } + + // If the file has no size and it's not the above case, skip and log + else if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump && (size == 0 || size is null)) + { + //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump"); + rom.SetFieldValue(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue()); + } + + // If the file has a size but aboslutely no hashes, skip and log + else if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump + && size is not null && size > 0 + && !rom.HasHashes()) + { + //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump"); + rom.SetFieldValue(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue()); + } + + item = rom; + } + + // Get the key and add the file + string key = GetBucketKey(item, _bucketedBy, lower: true, norename: true); + + // If only adding statistics, we add an empty key for games and then just item stats + if (statsOnly) + { + EnsureBucketingKey(key); + DatStatistics.AddItemStatistics(item); + } + else + { + AddItem(key, item); + } + + return key; + } + + /// + /// Remove all items marked for removal + /// + public void ClearMarked() + { + string[] keys = [.. SortedKeys]; +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(keys, Core.Globals.ParallelOptions, key => +#elif NET40_OR_GREATER + Parallel.ForEach(keys, key => +#else + foreach (var key in keys) +#endif + { + var list = GetItemsForBucket(key, filter: true); + RemoveBucket(key); + list.ForEach(item => AddItem(key, item)); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Get the items associated with a bucket name + /// + /// Name of the bucket to retrive items for + /// Indicates if RemoveKey filtering is performed + /// List representing the bucket items, empty on missing + public List GetItemsForBucket(string? bucketName, bool filter = false) + { + if (bucketName is null) + return []; + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryGetValue(bucketName, out var items)) + return []; +#else + if (!_items.ContainsKey(bucketName)) + return []; + + var items = _items[bucketName]; +#endif + + if (items is null || !filter) + return [.. items ?? []]; + + var datItems = new List(); + foreach (DatItem item in items) + { + if (item.GetBoolFieldValue(DatItem.RemoveKey) != true) + datItems.Add(item); + } + + return datItems; + } + + /// + /// Remove a key from the file dictionary if it exists + /// + /// Key in the dictionary to remove + public bool RemoveBucket(string key) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + bool removed = _items.TryRemove(key, out var list); +#else + if (!_items.ContainsKey(key)) + return false; + + bool removed = true; + var list = _items[key]; + _items.Remove(key); +#endif + if (list is null) + return removed; + + foreach (var item in list) + { + DatStatistics.RemoveItemStatistics(item); + } + + return removed; + } + + /// + /// Remove the indexed instance of a value from the file dictionary if it exists + /// + /// Key in the dictionary to remove from + /// Value to remove from the dictionary + /// Index of the item to be removed + public bool RemoveItem(string key, DatItem value, int index) + { + // Explicit lock for some weird corner cases + lock (key) + { + // If the key doesn't exist, return +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryGetValue(key, out var list) || list is null) + return false; +#else + if (!_items.ContainsKey(key)) + return false; + + var list = _items[key]; + if (list is null) + return false; +#endif + + // If the value doesn't exist in the key, assume it has been removed + if (index < 0) + return false; + + // Remove the statistics first + DatStatistics.RemoveItemStatistics(value); + + list.RemoveAt(index); + return true; + } + } + + /// + /// Override the internal ItemKey value + /// + /// + public void SetBucketedBy(ItemKey newBucket) + { + _bucketedBy = newBucket; + } + + /// + /// Add a value to the file dictionary + /// + /// Key in the dictionary to add to + /// Value to add to the dictionary + internal void AddItem(string key, DatItem value) + { + // Explicit lock for some weird corner cases + lock (key) + { + // Ensure the key exists + EnsureBucketingKey(key); + + // If item is null, don't add it + if (value is null) + return; + + // Now add the value + _items[key]!.Add(value); + + // Now update the statistics + DatStatistics.AddItemStatistics(value); + } + } + + #endregion + + #region Bucketing + + /// + /// Take the arbitrarily bucketed Files Dictionary and convert to one bucketed by a user-defined method + /// + /// ItemKey enum representing how to bucket the individual items + /// True if the key should be lowercased (default), false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + public void BucketBy(ItemKey bucketBy, bool lower = true, bool norename = true) + { + // If we have a situation where there's no dictionary or no keys at all, we skip + if (_items is null || _items.Count == 0) + return; + + // If the sorted type isn't the same, we want to sort the dictionary accordingly + if (_bucketedBy != bucketBy && bucketBy != ItemKey.NULL) + { + _logger.User($"Organizing roms by {bucketBy}"); + PerformBucketing(bucketBy, lower, norename); + } + + // Sort the dictionary to be consistent + _logger.User($"Sorting roms by {bucketBy}"); + PerformSorting(norename); + } + + /// + /// Perform deduplication on the current sorted dictionary + /// + public void Deduplicate() + { +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(SortedKeys, Core.Globals.ParallelOptions, key => +#elif NET40_OR_GREATER + Parallel.ForEach(SortedKeys, key => +#else + foreach (var key in SortedKeys) +#endif + { + // Get the possibly unsorted list + List sortedList = GetItemsForBucket(key); + + // Sort and merge the list + Sort(ref sortedList, norename: false); + sortedList = Merge(sortedList); + + // Add the list back to the dictionary + RemoveBucket(key); + sortedList.ForEach(item => AddItem(key, item)); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Return the duplicate status of two items + /// + /// Current DatItem + /// DatItem to check against + /// The DupeType corresponding to the relationship between the two + public DupeType GetDuplicateStatus(DatItem? self, DatItem? last) + { + DupeType output = 0x00; + + // If either item is null + if (self is null || last is null) + return output; + + // If we don't have a duplicate at all, return none + if (!self.Equals(last)) + return output; + + // Get the sources for comparison + var selfSource = self.GetFieldValue(DatItem.SourceKey); + var lastSource = last.GetFieldValue(DatItem.SourceKey); + + // Get the machines for comparison + var selfMachine = self.GetMachine(); + string? selfMachineName = selfMachine?.GetName(); + var lastMachine = last.GetMachine(); + string? lastMachineName = lastMachine?.GetName(); + + // If the duplicate is external already +#if NET20 || NET35 + if ((last.GetFieldValue(DatItem.DupeTypeKey) & DupeType.External) != 0) +#else + if (last.GetFieldValue(DatItem.DupeTypeKey).HasFlag(DupeType.External)) +#endif + output |= DupeType.External; + + // If the duplicate should be external + else if (lastSource?.Index != selfSource?.Index) + output |= DupeType.External; + + // Otherwise, it's considered an internal dupe + else + output |= DupeType.Internal; + + // If the item and machine names match + if (lastMachineName == selfMachineName && last.GetName() == self.GetName()) + output |= DupeType.All; + + // Otherwise, hash match is assumed + else + output |= DupeType.Hash; + + return output; + } + + /// + /// Merge an arbitrary set of DatItems based on the supplied information + /// + /// List of DatItem objects representing the items to be merged + /// A List of DatItem objects representing the merged items + /// TODO: Make this internal like the DB counterpart + public static List Merge(List? items) + { + // Check for null or blank inputs first + if (items is null || items.Count == 0) + return []; + + // Create placeholder object for checking duplicates + var dupDict = new ItemDictionary(); + + // Create output list + List output = []; + + // Then deduplicate them by checking to see if data matches previous saved roms + int nodumpCount = 0; + foreach (DatItem datItem in items) + { + // If we don't have a Disk, File, Media, or Rom, we skip checking for duplicates + if (datItem is not Disk && datItem is not DatItems.Formats.File && datItem is not Media && datItem is not Rom) + continue; + + // If it's a nodump, add and skip + if (datItem is Rom rom && rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump) + { + output.Add(datItem); + nodumpCount++; + continue; + } + else if (datItem is Disk disk && disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump) + { + output.Add(datItem); + nodumpCount++; + continue; + } + + // If it's the first non-nodump item in the list, don't touch it + if (output.Count == nodumpCount) + { + output.Add(datItem); + continue; + } + + // Find the index of the first duplicate, if one exists + int pos = output.FindIndex(lastItem => dupDict.GetDuplicateStatus(datItem, lastItem) != 0x00); + if (pos < 0) + { + output.Add(datItem); + continue; + } + + // Get the duplicate item + DatItem savedItem = output[pos]; + DupeType dupetype = dupDict.GetDuplicateStatus(datItem, savedItem); + + // Disks, File, Media, and Roms have more information to fill + if (datItem is Disk diskItem && savedItem is Disk savedDisk) + savedDisk.FillMissingInformation(diskItem); + else if (datItem is DatItems.Formats.File fileItem && savedItem is DatItems.Formats.File savedFile) + savedFile.FillMissingInformation(fileItem); + else if (datItem is Media mediaItem && savedItem is Media savedMedia) + savedMedia.FillMissingInformation(mediaItem); + else if (datItem is Rom romItem && savedItem is Rom savedRom) + savedRom.FillMissingInformation(romItem); + + // Set the duplicate type on the saved item + savedItem.SetFieldValue(DatItem.DupeTypeKey, dupetype); + + // Get the sources associated with the items + var savedSource = savedItem.GetFieldValue(DatItem.SourceKey); + var itemSource = datItem.GetFieldValue(DatItem.SourceKey); + + // Get the machines associated with the items + var savedMachine = savedItem.GetMachine(); + var itemMachine = datItem.GetMachine(); + + // If the current source has a lower ID than the saved, use the saved source + if (itemSource?.Index < savedSource?.Index) + { + datItem.SetFieldValue(DatItem.SourceKey, savedSource.Clone() as Source); + savedItem.CopyMachineInformation(datItem); + savedItem.SetName(datItem.GetName()); + } + + // If the saved machine is a child of the current machine, use the current machine instead + if (savedMachine?.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey) == itemMachine?.GetName() + || savedMachine?.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey) == itemMachine?.GetName()) + { + savedItem.CopyMachineInformation(datItem); + savedItem.SetName(datItem.GetName()); + } + + // Replace the original item in the list + output.RemoveAt(pos); + output.Insert(pos, savedItem); + } + + // Then return the result + return output; + } + + /// + /// List all duplicates found in a DAT based on a DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// List of matched DatItem objects + /// This also sets the remove flag on any duplicates found + /// TODO: Figure out if removal should be a flag or just removed entirely + internal List GetDuplicates(DatItem datItem, bool sorted = false) + { + // Check for an empty rom list first + if (DatStatistics.TotalCount == 0) + return []; + + // We want to get the proper key for the DatItem + string key = SortAndGetKey(datItem, sorted); + + // Get the items for the current key, if possible + List items = GetItemsForBucket(key, filter: false); + if (items.Count == 0) + return []; + + // Try to find duplicates + List output = []; + foreach (DatItem other in items) + { + // Skip items marked for removal + if (other.GetBoolFieldValue(DatItem.RemoveKey) == true) + continue; + + // Mark duplicates for future removal + if (datItem.Equals(other)) + { + other.SetFieldValue(DatItem.RemoveKey, true); + output.Add(other); + } + } + + // Return any matching items + return output; + } + + /// + /// Check if a DAT contains the given DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// True if it contains the rom, false otherwise + internal bool HasDuplicates(DatItem datItem, bool sorted = false) + { + // Check for an empty rom list first + if (DatStatistics.TotalCount == 0) + return false; + + // We want to get the proper key for the DatItem + string key = SortAndGetKey(datItem, sorted); + + // Try to find duplicates + List roms = GetItemsForBucket(key); + if (roms.Count == 0) + return false; + + return roms.FindIndex(datItem.Equals) > -1; + } + + /// + /// Ensure the key exists in the items dictionary + /// + /// Key to ensure + private void EnsureBucketingKey(string key) + { + // If the key is missing from the dictionary, add it +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _items.GetOrAdd(key, []); +#else + if (!_items.ContainsKey(key)) + _items[key] = []; +#endif + } + + /// + /// Get the highest-order Field value that represents the statistics + /// + private ItemKey GetBestAvailable() + { + // Get the required counts + long diskCount = DatStatistics.GetItemCount(ItemType.Disk); + long mediaCount = DatStatistics.GetItemCount(ItemType.Media); + long romCount = DatStatistics.GetItemCount(ItemType.Rom); + long nodumpCount = DatStatistics.GetStatusCount(ItemStatus.Nodump); + + // If all items are supposed to have a SHA-512, we bucket by that + if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA512)) + return ItemKey.SHA512; + + // If all items are supposed to have a SHA-384, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA384)) + return ItemKey.SHA384; + + // If all items are supposed to have a SHA-256, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA256)) + return ItemKey.SHA256; + + // If all items are supposed to have a SHA-1, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA1)) + return ItemKey.SHA1; + + // If all items are supposed to have a RIPEMD160, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.RIPEMD160)) + return ItemKey.RIPEMD160; + + // If all items are supposed to have a RIPEMD128, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.RIPEMD128)) + return ItemKey.RIPEMD128; + + // If all items are supposed to have a MD5, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD5)) + return ItemKey.MD5; + + // If all items are supposed to have a MD4, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD4)) + return ItemKey.MD4; + + // If all items are supposed to have a MD2, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD2)) + return ItemKey.MD2; + + // Otherwise, we bucket by CRC + else + return ItemKey.CRC; + } + + /// + /// Get the bucketing key for a given item + /// The current item + /// ItemKey value representing what key to get + /// True if the key should be lowercased, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// + private static string GetBucketKey(DatItem datItem, ItemKey bucketBy, bool lower, bool norename) + { + if (datItem is null) + return string.Empty; + + // Treat NULL like machine + if (bucketBy == ItemKey.NULL) + bucketBy = ItemKey.Machine; + + // Get the machine and source + var machine = datItem.GetMachine(); + var source = datItem.GetFieldValue(DatItem.SourceKey); + + // Get the bucket key + return datItem.GetKey(bucketBy, machine, source, lower, norename); + } + + /// + /// Perform bucketing based on the item key provided + /// + /// ItemKey enum representing how to bucket the individual items + /// True if the key should be lowercased, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + private void PerformBucketing(ItemKey bucketBy, bool lower, bool norename) + { + // Set the sorted type + _bucketedBy = bucketBy; + + // First do the initial sort of all of the roms inplace + List oldkeys = [.. SortedKeys]; + +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.For(0, oldkeys.Count, Core.Globals.ParallelOptions, k => +#elif NET40_OR_GREATER + Parallel.For(0, oldkeys.Count, k => +#else + for (int k = 0; k < oldkeys.Count; k++) +#endif + { + string key = oldkeys[k]; + if (GetItemsForBucket(key, filter: true).Count == 0) + RemoveBucket(key); + + // Now add each of the roms to their respective keys + for (int i = 0; i < GetItemsForBucket(key).Count; i++) + { + DatItem item = GetItemsForBucket(key)[i]; + if (item is null || item.GetBoolFieldValue(DatItem.RemoveKey) == true) + continue; + + // Get the machine and source + var machine = item.GetMachine(); + var source = item.GetFieldValue(DatItem.SourceKey); + + // We want to get the key most appropriate for the given sorting type + string newkey = item.GetKey(bucketBy, machine, source, lower, norename); + + // If the key is different, move the item to the new key + if (newkey != key) + { + AddItem(newkey, item); + bool removed = RemoveItem(key, item, i); + if (!removed) + continue; + + i--; // This make sure that the pointer stays on the correct since one was removed + } + } + + // If the key is now empty, remove it + if (GetItemsForBucket(key, filter: true).Count == 0) + RemoveBucket(key); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Perform inplace sorting of the dictionary + /// + private void PerformSorting(bool norename) + { +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(SortedKeys, Core.Globals.ParallelOptions, key => +#elif NET40_OR_GREATER + Parallel.ForEach(SortedKeys, key => +#else + foreach (var key in SortedKeys) +#endif + { + // Get the possibly unsorted list + List sortedList = GetItemsForBucket(key); + + // Sort the list of items to be consistent + Sort(ref sortedList, norename); + + // Add the list back to the dictionary + RemoveBucket(key); + sortedList.ForEach(item => AddItem(key, item)); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Sort a list of DatItem objects by SourceID, Game, and Name (in order) + /// + /// List of DatItem objects representing the items to be sorted + /// True if files are not renamed, false otherwise + /// True if it sorted correctly, false otherwise + private bool Sort(ref List items, bool norename) + { + // Create the comparer extenal to the delegate + var nc = new NaturalComparer(); + + // Sort by machine, type, item name, and source + items.Sort(delegate (DatItem x, DatItem y) + { + try + { + // Compare on source if renaming + if (!norename) + { + int xSourceIndex = x.GetFieldValue(DatItem.SourceKey)?.Index ?? 0; + int ySourceIndex = y.GetFieldValue(DatItem.SourceKey)?.Index ?? 0; + if (xSourceIndex != ySourceIndex) + return xSourceIndex - ySourceIndex; + } + + // Get the machines + Machine? xMachine = x.GetMachine(); + Machine? yMachine = y.GetMachine(); + + // If machine names don't match + string? xMachineName = xMachine?.GetName(); + string? yMachineName = yMachine?.GetName(); + if (xMachineName != yMachineName) + return nc.Compare(xMachineName, yMachineName); + + // If types don't match + string? xType = x.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + string? yType = y.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + if (xType != yType) + return xType.AsItemType() - yType.AsItemType(); + + // If directory names don't match + string? xDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(x.GetName() ?? string.Empty)); + string? yDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(y.GetName() ?? string.Empty)); + if (xDirectoryName != yDirectoryName) + return nc.Compare(xDirectoryName, yDirectoryName); + + // If item names don't match + string? xName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(x.GetName() ?? string.Empty)); + string? yName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(y.GetName() ?? string.Empty)); + return nc.Compare(xName, yName); + } + catch + { + // Absorb the error + return 0; + } + }); + + return true; + } + + /// + /// Sort the input DAT and get the key to be used by the item + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// Key to try to use + private string SortAndGetKey(DatItem datItem, bool sorted = false) + { + // If we're not already sorted, take care of it + if (!sorted) + BucketBy(GetBestAvailable()); + + // Now that we have the sorted type, we get the proper key + return GetBucketKey(datItem, _bucketedBy, lower: true, norename: true); + } + + #endregion + + #region Statistics + + /// + /// Recalculate the statistics for the Dat + /// + public void RecalculateStats() + { + // Wipe out any stats already there + DatStatistics.ResetStatistics(); + + // If we have a blank Dat in any way, return + if (_items is null || _items.Count == 0) + return; + + // Loop through and add + foreach (string key in _items.Keys) + { + List? datItems = _items[key]; + if (datItems is null) + continue; + + foreach (DatItem item in datItems) + { + DatStatistics.AddItemStatistics(item); + } + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/ItemDictionaryDB.cs b/SabreTools.Metadata.DatFiles/ItemDictionaryDB.cs new file mode 100644 index 00000000..1197ae8a --- /dev/null +++ b/SabreTools.Metadata.DatFiles/ItemDictionaryDB.cs @@ -0,0 +1,1263 @@ +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Collections.Concurrent; +#endif +using System.Collections.Generic; +using System.IO; +using System.Linq; +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER +using System.Threading; +using System.Threading.Tasks; +#endif +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; +using SabreTools.Metadata.DatItems; +using SabreTools.Metadata.DatItems.Formats; +using SabreTools.Hashing; +using SabreTools.IO.Logging; +using SabreTools.Text.Compare; + +/* + * Planning Notes: + * + * In order for this in-memory "database" design to work, there need to be a few things: + * - Feature parity with all existing item dictionary operations + * - A way to transition between the two item dictionaries (a flag?) + * - Helper methods that target the "database" version instead of assuming the standard dictionary + * + * Notable changes include: + * - Separation of Machine from DatItem, leading to a mapping instead + * + Should DatItem include an index reference to the machine? Or should that be all external? + * - Adding machines to the dictionary distinctly from the items + * - Having a separate "bucketing" system that only reorders indicies and not full items; quicker? + * - Non-key-based add/remove of values; use explicit methods instead of dictionary-style accessors +*/ + +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Item dictionary with statistics, bucketing, and sorting + /// + [JsonObject("items"), XmlRoot("items")] + public class ItemDictionaryDB + { + #region Private instance variables + + /// + /// Internal dictionary for all items + /// + [JsonIgnore, XmlIgnore] +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary _items = []; +#else + private readonly Dictionary _items = []; +#endif + + /// + /// Current highest available item index + /// + [JsonIgnore, XmlIgnore] + private long _itemIndex = 0; + + /// + /// Internal dictionary for all machines + /// + [JsonIgnore, XmlIgnore] +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary _machines = []; +#else + private readonly Dictionary _machines = []; +#endif + + /// + /// Current highest available machine index + /// + [JsonIgnore, XmlIgnore] + private long _machineIndex = 0; + + /// + /// Internal dictionary for all sources + /// + [JsonIgnore, XmlIgnore] +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary _sources = []; +#else + private readonly Dictionary _sources = []; +#endif + + /// + /// Current highest available source index + /// + [JsonIgnore, XmlIgnore] + private long _sourceIndex = 0; + + /// + /// Internal dictionary for item to machine mappings + /// + [JsonIgnore, XmlIgnore] +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary _itemToMachineMapping = []; +#else + private readonly Dictionary _itemToMachineMapping = []; +#endif + + /// + /// Internal dictionary for item to source mappings + /// + [JsonIgnore, XmlIgnore] +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary _itemToSourceMapping = []; +#else + private readonly Dictionary _itemToSourceMapping = []; +#endif + + /// + /// Internal dictionary representing the current buckets + /// + [JsonIgnore, XmlIgnore] +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + private readonly ConcurrentDictionary> _buckets = []; +#else + private readonly Dictionary> _buckets = []; +#endif + + /// + /// Current bucketed by value + /// + private ItemKey _bucketedBy = ItemKey.NULL; + + /// + /// Logging object + /// + private readonly Logger _logger; + + #endregion + + #region Fields + + /// + /// Get the keys in sorted order from the file dictionary + /// + /// List of the keys in sorted order + [JsonIgnore, XmlIgnore] + public string[] SortedKeys + { + get + { + List keys = [.. _buckets.Keys]; + keys.Sort(new NaturalComparer()); + return [.. keys]; + } + } + + /// + /// DAT statistics + /// + [JsonIgnore, XmlIgnore] + public DatStatistics DatStatistics { get; } = new DatStatistics(); + + #endregion + + #region Constructors + + /// + /// Generic constructor + /// + public ItemDictionaryDB() + { + _logger = new Logger(this); + } + + #endregion + + #region Accessors + + /// + /// Add a DatItem to the dictionary after validation + /// + /// Item data to validate + /// Index of the machine related to the item + /// Index of the source related to the item + /// True to only add item statistics while parsing, false otherwise + /// The index for the added item, -1 on error + public long AddItem(DatItem item, long machineIndex, long sourceIndex, bool statsOnly) + { + // If we have a Disk, File, Media, or Rom, clean the hash data + if (item is Disk disk) + { + // If the file has aboslutely no hashes, skip and log + if (disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() != ItemStatus.Nodump + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)) + && string.IsNullOrEmpty(disk.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))) + { + _logger.Verbose($"Incomplete entry for '{disk.GetName()}' will be output as nodump"); + disk.SetFieldValue(Data.Models.Metadata.Disk.StatusKey, ItemStatus.Nodump.AsStringValue()); + } + + item = disk; + } + else if (item is DatItems.Formats.File file) + { + // If the file has aboslutely no hashes, skip and log + if (string.IsNullOrEmpty(file.CRC) + && string.IsNullOrEmpty(file.MD5) + && string.IsNullOrEmpty(file.SHA1) + && string.IsNullOrEmpty(file.SHA256)) + { + _logger.Verbose($"Incomplete entry for '{file.GetName()}' will be output as nodump"); + } + + item = file; + } + else if (item is Media media) + { + // If the file has aboslutely no hashes, skip and log + if (string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)) + && string.IsNullOrEmpty(media.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey))) + { + _logger.Verbose($"Incomplete entry for '{media.GetName()}' will be output as nodump"); + } + + item = media; + } + else if (item is Rom rom) + { + long? size = rom.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey); + + // If we have the case where there is SHA-1 and nothing else, we don't fill in any other part of the data + if (size is null && !string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))) + { + // No-op, just catch it so it doesn't go further + //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Entry with only SHA-1 found - '{rom.GetName()}'"); + } + + // If we have a rom and it's missing size AND the hashes match a 0-byte file, fill in the rest of the info + else if ((size == 0 || size is null) + && (string.IsNullOrEmpty(rom.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)) || rom.HasZeroHash())) + { + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, "0"); + rom.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, null); // HashType.MD2.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, null); // HashType.MD4.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, HashType.MD5.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, null); // HashType.RIPEMD128.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, null); // HashType.RIPEMD160.ZeroString + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, null); // HashType.SHA256.ZeroString; + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, null); // HashType.SHA384.ZeroString; + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, null); // HashType.SHA512.ZeroString; + rom.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, null); // HashType.SpamSum.ZeroString; + } + + // If the file has no size and it's not the above case, skip and log + else if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump && (size == 0 || size is null)) + { + //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump"); + rom.SetFieldValue(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue()); + } + + // If the file has a size but aboslutely no hashes, skip and log + else if (rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() != ItemStatus.Nodump + && size is not null && size > 0 + && !rom.HasHashes()) + { + //logger.Verbose($"{Header.GetStringFieldValue(DatHeader.FileNameKey)}: Incomplete entry for '{rom.GetName()}' will be output as nodump"); + rom.SetFieldValue(Data.Models.Metadata.Rom.StatusKey, ItemStatus.Nodump.AsStringValue()); + } + + item = rom; + } + + // If only adding statistics, we add just item stats + if (statsOnly) + { + DatStatistics.AddItemStatistics(item); + return -1; + } + else + { + return AddItem(item, machineIndex, sourceIndex); + } + } + + /// + /// Add a machine, returning the insert index + /// + public long AddMachine(Machine machine) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + long index = Interlocked.Increment(ref _machineIndex) - 1; + _machines.TryAdd(index, machine); + return index; +#else + long index = _machineIndex++ - 1; + _machines[index] = machine; + return index; +#endif + } + + /// + /// Add a source, returning the insert index + /// + public long AddSource(Source source) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + long index = Interlocked.Increment(ref _sourceIndex) - 1; + _sources.TryAdd(index, source); + return index; +#else + long index = _sourceIndex++ - 1; + _sources[index] = source; + return index; +#endif + } + + /// + /// Remove all items marked for removal + /// + public void ClearMarked() + { + long[] itemIndices = [.. _items.Keys]; + foreach (long itemIndex in itemIndices) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryGetValue(itemIndex, out var datItem) || datItem is null) + continue; +#else + var datItem = _items[itemIndex]; +#endif + + if (datItem.GetBoolFieldValue(DatItem.RemoveKey) != true) + continue; + + RemoveItem(itemIndex); + } + } + + /// + /// Get all items and their indicies + /// + public IDictionary GetItems() => _items; + + /// + /// Get the indices and items associated with a bucket name + /// + public Dictionary GetItemsForBucket(string? bucketName, bool filter = false) + { + if (bucketName is null) + return []; + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_buckets.TryGetValue(bucketName, out var itemIds)) + return []; +#else + if (!_buckets.ContainsKey(bucketName)) + return []; + + var itemIds = _buckets[bucketName]; +#endif + + var datItems = new Dictionary(); + foreach (long itemId in itemIds) + { + // Ignore missing IDs +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryGetValue(itemId, out var datItem) || datItem is null) + continue; +#else + if (!_items.ContainsKey(itemId)) + continue; + + var datItem = _items[itemId]; + if (datItem is null) + continue; +#endif + + if (!filter || datItem.GetBoolFieldValue(DatItem.RemoveKey) != true) + datItems[itemId] = datItem; + } + + return datItems; + } + + /// + /// Get a machine based on the index + /// + public Machine? GetMachine(long index) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_machines.TryGetValue(index, out var machine)) + return null; + + return machine; +#else + if (!_machines.ContainsKey(index)) + return null; + + return _machines[index]; +#endif + } + + /// + /// Get a machine based on the name + /// + /// This assume that all machines have unique names + public KeyValuePair GetMachine(string? name) + { + if (string.IsNullOrEmpty(name)) + return new KeyValuePair(-1, null); + + var machine = _machines.FirstOrDefault(m => m.Value.GetName() == name); + return new KeyValuePair(machine.Key, machine.Value); + } + + /// + /// Get the index and machine associated with an item index + /// + public KeyValuePair GetMachineForItem(long itemIndex) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_itemToMachineMapping.TryGetValue(itemIndex, out long machineIndex)) + return new KeyValuePair(-1, null); + + if (!_machines.TryGetValue(machineIndex, out var machine)) + return new KeyValuePair(-1, null); + + return new KeyValuePair(machineIndex, machine); +#else + if (!_itemToMachineMapping.ContainsKey(itemIndex)) + return new KeyValuePair(-1, null); + + long machineIndex = _itemToMachineMapping[itemIndex]; + if (!_machines.ContainsKey(machineIndex)) + return new KeyValuePair(-1, null); + + var machine = _machines[machineIndex]; + return new KeyValuePair(machineIndex, machine); +#endif + } + + /// + /// Get all machines and their indicies + /// + public IDictionary GetMachines() => _machines; + + /// + /// Get a source based on the index + /// + public Source? GetSource(long index) + { + if (!_sources.ContainsKey(index)) + return null; + + return _sources[index]; + } + + /// + /// Get the index and source associated with an item index + /// + public KeyValuePair GetSourceForItem(long itemIndex) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_itemToSourceMapping.TryGetValue(itemIndex, out long sourceIndex)) + return new KeyValuePair(-1, null); + + if (!_sources.TryGetValue(sourceIndex, out var source)) + return new KeyValuePair(-1, null); + + return new KeyValuePair(sourceIndex, source); +#else + if (!_itemToSourceMapping.ContainsKey(itemIndex)) + return new KeyValuePair(-1, null); + + long sourceIndex = _itemToSourceMapping[itemIndex]; + if (!_sources.ContainsKey(sourceIndex)) + return new KeyValuePair(-1, null); + + var source = _sources[sourceIndex]; + return new KeyValuePair(sourceIndex, source); +#endif + } + + /// + /// Get all sources and their indicies + /// + public IDictionary GetSources() => _sources; + + /// + /// Remap an item to a new machine index without validation + /// + /// Current item index + /// New machine index + public void RemapDatItemToMachine(long itemIndex, long machineIndex) + { + lock (_itemToMachineMapping) + { + _itemToMachineMapping[itemIndex] = machineIndex; + } + } + + /// + /// Remove a key from the file dictionary if it exists + /// + /// Key in the dictionary to remove + public bool RemoveBucket(string key) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + bool removed = _buckets.TryRemove(key, out var list); +#else + if (!_buckets.ContainsKey(key)) + return false; + + bool removed = true; + var list = _buckets[key]; + _buckets.Remove(key); +#endif + if (list is null) + return removed; + + foreach (var index in list) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryGetValue(index, out var datItem) || datItem is null) + continue; +#else + if (!_items.ContainsKey(index)) + continue; + + var datItem = _items[index]; +#endif + + RemoveItem(index); + } + + return removed; + } + + /// + /// Remove an item, returning if it could be removed + /// + public bool RemoveItem(long itemIndex) + { + // If the key doesn't exist, return +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryRemove(itemIndex, out var datItem)) + return false; +#else + if (!_items.ContainsKey(itemIndex)) + return false; + + var datItem = _items[itemIndex]; + _items.Remove(itemIndex); +#endif + + // Remove statistics, if possible + if (datItem is not null) + DatStatistics.RemoveItemStatistics(datItem); + + // Remove the machine mapping +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _itemToMachineMapping.TryRemove(itemIndex, out _); +#else + if (_itemToMachineMapping.ContainsKey(itemIndex)) + _itemToMachineMapping.Remove(itemIndex); +#endif + + // Remove the source mapping +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _itemToSourceMapping.TryRemove(itemIndex, out _); +#else + if (_itemToSourceMapping.ContainsKey(itemIndex)) + _itemToSourceMapping.Remove(itemIndex); +#endif + + return true; + } + + /// + /// Remove a machine, returning if it could be removed + /// + public bool RemoveMachine(long machineIndex) + { + if (!_machines.ContainsKey(machineIndex)) + return false; + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _machines.TryRemove(machineIndex, out _); +#else + _machines.Remove(machineIndex); +#endif + + var itemIds = _itemToMachineMapping + .Where(mapping => mapping.Value == machineIndex) + .Select(mapping => mapping.Key); + + foreach (long itemId in itemIds) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _itemToMachineMapping.TryRemove(itemId, out _); +#else + _itemToMachineMapping.Remove(itemId); +#endif + } + + return true; + } + + /// + /// Remove a machine, returning if it could be removed + /// + public bool RemoveMachine(string machineName) + { + if (string.IsNullOrEmpty(machineName)) + return false; + + var machine = _machines.FirstOrDefault(m => m.Value.GetName() == machineName); + return RemoveMachine(machine.Key); + } + + /// + /// Add an item, returning the insert index + /// + internal long AddItem(DatItem item, long machineIndex, long sourceIndex) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + // Add the item with a new index + long index = Interlocked.Increment(ref _itemIndex) - 1; + _items.TryAdd(index, item); + + // Add the machine mapping + _itemToMachineMapping.TryAdd(index, machineIndex); + + // Add the source mapping + _itemToSourceMapping.TryAdd(index, sourceIndex); +#else + // Add the item with a new index + long index = _itemIndex++ - 1; + _items[index] = item; + + // Add the machine mapping + _itemToMachineMapping[index] = machineIndex; + + // Add the source mapping + _itemToSourceMapping[index] = sourceIndex; +#endif + + // Add the item statistics + DatStatistics.AddItemStatistics(item); + + // Add the item to the default bucket + PerformItemBucketing(index, _bucketedBy, lower: true, norename: true); + + // Return the used index + return index; + } + + #endregion + + #region Bucketing + + /// + /// Update the bucketing dictionary + /// + /// ItemKey enum representing how to bucket the individual items + /// True if the key should be lowercased (default), false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// + public void BucketBy(ItemKey bucketBy, bool lower = true, bool norename = true) + { + // If the sorted type isn't the same, we want to sort the dictionary accordingly + if (_bucketedBy != bucketBy && bucketBy != ItemKey.NULL) + { + _logger.User($"Organizing roms by {bucketBy}"); + PerformBucketing(bucketBy, lower, norename); + } + + // Sort the dictionary to be consistent + _logger.User($"Sorting roms by {bucketBy}"); + PerformSorting(norename); + } + + /// + /// Perform deduplication on the current sorted dictionary + /// + public void Deduplicate() + { +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.ForEach(SortedKeys, Core.Globals.ParallelOptions, key => +#elif NET40_OR_GREATER + Parallel.ForEach(SortedKeys, key => +#else + foreach (var key in SortedKeys) +#endif + { + // Get the possibly unsorted list + List> sortedList = [.. GetItemsForBucket(key)]; + + // Sort and merge the list + Sort(ref sortedList, false); + sortedList = Merge(sortedList); + + // Get all existing mappings + List currentMappings = sortedList.ConvertAll(item => + { + return new ItemMappings( + item.Value, + GetMachineForItem(item.Key).Key, + GetSourceForItem(item.Key).Key + ); + }); + + // Add the list back to the dictionary + RemoveBucket(key); + currentMappings.ForEach(map => + AddItem(map.Item, map.MachineId, map.SourceId)); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Return the duplicate status of two items + /// + /// Current DatItem + /// Source associated with this item + /// DatItem to check against + /// Source associated with the last item + /// The DupeType corresponding to the relationship between the two + public DupeType GetDuplicateStatus(KeyValuePair? selfItem, Source? selfSource, KeyValuePair? lastItem, Source? lastSource) + { + DupeType output = 0x00; + + // If either item is null + if (selfItem is null || lastItem is null) + return output; + + // If we don't have a duplicate at all, return none + if (!selfItem.Value.Value.Equals(lastItem.Value.Value)) + return output; + + // Get the machines for comparison + var selfMachine = GetMachineForItem(selfItem.Value.Key).Value; + string? selfMachineName = selfMachine?.GetName(); + var lastMachine = GetMachineForItem(lastItem.Value.Key).Value; + string? lastMachineName = lastMachine?.GetName(); + + // If the duplicate is external already +#if NET20 || NET35 + if ((lastItem.Value.Value.GetFieldValue(DatItem.DupeTypeKey) & DupeType.External) != 0) +#else + if (lastItem.Value.Value.GetFieldValue(DatItem.DupeTypeKey).HasFlag(DupeType.External)) +#endif + output |= DupeType.External; + + // If the duplicate should be external + else if (lastSource?.Index != selfSource?.Index) + output |= DupeType.External; + + // Otherwise, it's considered an internal dupe + else + output |= DupeType.Internal; + + // If the item and machine names match + if (lastMachineName == selfMachineName && lastItem.Value.Value.GetName() == selfItem.Value.Value.GetName()) + output |= DupeType.All; + + // Otherwise, hash match is assumed + else + output |= DupeType.Hash; + + return output; + } + + /// + /// List all duplicates found in a DAT based on a DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// List of matched DatItem objects + /// This also sets the remove flag on any duplicates found + /// TODO: Figure out if removal should be a flag or just removed entirely + internal Dictionary GetDuplicates(KeyValuePair datItem, bool sorted = false) + { + // Check for an empty rom list first + if (DatStatistics.TotalCount == 0) + return []; + + // We want to get the proper key for the DatItem, ignoring the index + _ = SortAndGetKey(datItem, sorted); + var machine = GetMachineForItem(datItem.Key); + var source = GetSourceForItem(datItem.Key); + string key = datItem.Value.GetKey(_bucketedBy, machine.Value, source.Value); + + // If the key doesn't exist, return the empty list + var items = GetItemsForBucket(key); + if (items.Count == 0) + return []; + + // Try to find duplicates + Dictionary output = []; + foreach (var rom in items) + { + // Skip items marked for removal + if (rom.Value.GetBoolFieldValue(DatItem.RemoveKey) == true) + continue; + + // Mark duplicates for future removal + if (datItem.Value.Equals(rom.Value)) + { + rom.Value.SetFieldValue(DatItem.RemoveKey, true); + output[rom.Key] = rom.Value; + } + } + + // Return any matching items + return output; + } + + /// + /// Check if a DAT contains the given DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// True if it contains the rom, false otherwise + internal bool HasDuplicates(KeyValuePair datItem, bool sorted = false) + { + // Check for an empty rom list first + if (DatStatistics.TotalCount == 0) + return false; + + // We want to get the proper key for the DatItem, ignoring the index + _ = SortAndGetKey(datItem, sorted); + var machine = GetMachineForItem(datItem.Key); + var source = GetSourceForItem(datItem.Key); + string key = datItem.Value.GetKey(_bucketedBy, machine.Value, source.Value); + + // If the key doesn't exist + var roms = GetItemsForBucket(key); + if (roms is null || roms.Count == 0) + return false; + + // Try to find duplicates + return roms.Values.Any(datItem.Value.Equals); + } + + /// + /// Merge an arbitrary set of item pairs based on the supplied information + /// + /// List of pairs representing the items to be merged + private List> Merge(List> itemMappings) + { + // Check for null or blank roms first + if (itemMappings is null || itemMappings.Count == 0) + return []; + + // Create output list + List> output = []; + + // Then deduplicate them by checking to see if data matches previous saved roms + int nodumpCount = 0; + foreach (var kvp in itemMappings) + { + long itemIndex = kvp.Key; + DatItem datItem = kvp.Value; + + // If we don't have a Disk, File, Media, or Rom, we skip checking for duplicates + if (datItem is not Disk && datItem is not DatItems.Formats.File && datItem is not Media && datItem is not Rom) + continue; + + // If it's a nodump, add and skip + if (datItem is Rom rom && rom.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus() == ItemStatus.Nodump) + { + output.Add(new KeyValuePair(itemIndex, datItem)); + nodumpCount++; + continue; + } + else if (datItem is Disk disk && disk.GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus() == ItemStatus.Nodump) + { + output.Add(new KeyValuePair(itemIndex, datItem)); + nodumpCount++; + continue; + } + + // If it's the first non-nodump rom in the list, don't touch it + if (output.Count == nodumpCount) + { + output.Add(new KeyValuePair(itemIndex, datItem)); + continue; + } + + // Find the index of the first duplicate, if one exists + var datItemSource = GetSourceForItem(itemIndex); + int pos = output.FindIndex(lastItem => + { + var lastItemSource = GetSourceForItem(lastItem.Key); + return GetDuplicateStatus(kvp, datItemSource.Value, lastItem, lastItemSource.Value) != 0x00; + }); + if (pos < 0) + { + output.Add(new KeyValuePair(itemIndex, datItem)); + continue; + } + + // Get the duplicate item + long savedIndex = output[pos].Key; + DatItem savedItem = output[pos].Value; + var savedItemSource = GetSourceForItem(savedIndex); + DupeType dupetype = GetDuplicateStatus(kvp, datItemSource.Value, output[pos], savedItemSource.Value); + + // Disks, Media, and Roms have more information to fill + if (datItem is Disk diskItem && savedItem is Disk savedDisk) + savedDisk.FillMissingInformation(diskItem); + else if (datItem is DatItems.Formats.File fileItem && savedItem is DatItems.Formats.File savedFile) + savedFile.FillMissingInformation(fileItem); + else if (datItem is Media mediaItem && savedItem is Media savedMedia) + savedMedia.FillMissingInformation(mediaItem); + else if (datItem is Rom romItem && savedItem is Rom savedRom) + savedRom.FillMissingInformation(romItem); + + savedItem.SetFieldValue(DatItem.DupeTypeKey, dupetype); + + // Get the sources associated with the items + var savedSource = _sources[_itemToSourceMapping[savedIndex]]; + var itemSource = _sources[_itemToSourceMapping[itemIndex]]; + + // Get the machines associated with the items + var savedMachine = _machines[_itemToMachineMapping[savedIndex]]; + var itemMachine = _machines[_itemToMachineMapping[itemIndex]]; + + // If the current source has a lower ID than the saved, use the saved source + if (itemSource?.Index < savedSource?.Index) + { + _itemToSourceMapping[itemIndex] = _itemToSourceMapping[savedIndex]; + _machines[_itemToMachineMapping[savedIndex]] = (itemMachine.Clone() as Machine)!; + savedItem.SetName(datItem.GetName()); + } + + // If the saved machine is a child of the current machine, use the current machine instead + if (savedMachine.GetStringFieldValue(Data.Models.Metadata.Machine.CloneOfKey) == itemMachine.GetName() + || savedMachine.GetStringFieldValue(Data.Models.Metadata.Machine.RomOfKey) == itemMachine.GetName()) + { + _machines[_itemToMachineMapping[savedIndex]] = (itemMachine.Clone() as Machine)!; + savedItem.SetName(datItem.GetName()); + } + + // Replace the original item in the list + output.RemoveAt(pos); + output.Insert(pos, new KeyValuePair(savedIndex, savedItem)); + } + + return output; + } + + /// + /// Ensure the key exists in the items dictionary + /// + private void EnsureBucketingKey(string key) + { + // If the key is missing from the dictionary, add it +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _buckets.GetOrAdd(key, []); +#else + if (!_buckets.ContainsKey(key)) + _buckets[key] = []; +#endif + } + + /// + /// Get the highest-order Field value that represents the statistics + /// + private ItemKey GetBestAvailable() + { + // Get the required counts + long diskCount = DatStatistics.GetItemCount(ItemType.Disk); + long mediaCount = DatStatistics.GetItemCount(ItemType.Media); + long romCount = DatStatistics.GetItemCount(ItemType.Rom); + long nodumpCount = DatStatistics.GetStatusCount(ItemStatus.Nodump); + + // If all items are supposed to have a SHA-512, we bucket by that + if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA512)) + return ItemKey.SHA512; + + // If all items are supposed to have a SHA-384, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA384)) + return ItemKey.SHA384; + + // If all items are supposed to have a SHA-256, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA256)) + return ItemKey.SHA256; + + // If all items are supposed to have a SHA-1, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.SHA1)) + return ItemKey.SHA1; + + // If all items are supposed to have a RIPEMD160, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.RIPEMD160)) + return ItemKey.RIPEMD160; + + // If all items are supposed to have a RIPEMD128, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.RIPEMD128)) + return ItemKey.RIPEMD128; + + // If all items are supposed to have a MD5, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD5)) + return ItemKey.MD5; + + // If all items are supposed to have a MD4, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD4)) + return ItemKey.MD4; + + // If all items are supposed to have a MD2, we bucket by that + else if (diskCount + mediaCount + romCount - nodumpCount == DatStatistics.GetHashCount(HashType.MD2)) + return ItemKey.MD2; + + // Otherwise, we bucket by CRC + else + return ItemKey.CRC; + } + + /// + /// Get the bucketing key for a given item index + /// Index of the current item + /// ItemKey value representing what key to get + /// True if the key should be lowercased, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// + private string GetBucketKey(long itemIndex, ItemKey bucketBy, bool lower, bool norename) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_items.TryGetValue(itemIndex, out var datItem) || datItem is null) + return string.Empty; +#else + if (!_items.ContainsKey(itemIndex)) + return string.Empty; + + var datItem = _items[itemIndex]; + if (datItem is null) + return string.Empty; +#endif + + var source = GetSourceForItem(itemIndex); + var machine = GetMachineForItem(itemIndex); + + // Treat NULL like machine + if (bucketBy == ItemKey.NULL) + bucketBy = ItemKey.Machine; + + // Get the bucket key + return datItem.GetKey(bucketBy, machine.Value, source.Value, lower, norename); + } + + /// + /// Perform bucketing based on the item key provided + /// + /// ItemKey enum representing how to bucket the individual items + /// True if the key should be lowercased, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + private void PerformBucketing(ItemKey bucketBy, bool lower, bool norename) + { + // Reset the bucketing values + _bucketedBy = bucketBy; + _buckets.Clear(); + + // Get the current list of item indicies + long[] itemIndicies = [.. _items.Keys]; + +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.For(0, itemIndicies.Length, Core.Globals.ParallelOptions, i => +#elif NET40_OR_GREATER + Parallel.For(0, itemIndicies.Length, i => +#else + for (int i = 0; i < itemIndicies.Length; i++) +#endif + { + PerformItemBucketing(i, bucketBy, lower, norename); +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + }); +#else + } +#endif + } + + /// + /// Bucket a single DatItem + /// + /// Index of the item to bucket + /// ItemKey enum representing how to bucket the individual items + /// True if the key should be lowercased, false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + private void PerformItemBucketing(long itemIndex, ItemKey bucketBy, bool lower, bool norename) + { + string? bucketKey = GetBucketKey(itemIndex, bucketBy, lower, norename); + lock (bucketKey) + { + EnsureBucketingKey(bucketKey); + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + if (!_buckets.TryGetValue(bucketKey, out var bucket) || bucket is null) + return; + + bucket.Add(itemIndex); +#else + _buckets[bucketKey].Add(itemIndex); +#endif + } + } + + /// + /// Sort existing buckets for consistency + /// + private void PerformSorting(bool norename) + { + // Get the current list of bucket keys + string[] bucketKeys = [.. _buckets.Keys]; + +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + Parallel.For(0, bucketKeys.Length, Core.Globals.ParallelOptions, i => +#elif NET40_OR_GREATER + Parallel.For(0, bucketKeys.Length, i => +#else + for (int i = 0; i < bucketKeys.Length; i++) +#endif + { +#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _buckets.TryGetValue(bucketKeys[i], out var itemIndices); +#else + var itemIndices = _buckets[bucketKeys[i]]; +#endif + if (itemIndices is null || itemIndices.Count == 0) + { +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _buckets.TryRemove(bucketKeys[i], out _); + return; +#else + _buckets.Remove(bucketKeys[i]); + continue; +#endif + } + + var datItems = itemIndices + .FindAll(i => _items.ContainsKey(i)) + .ConvertAll(i => new KeyValuePair(i, _items[i])); + + Sort(ref datItems, norename); + +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER + _buckets.TryAdd(bucketKeys[i], datItems.ConvertAll(kvp => kvp.Key)); + }); +#else + _buckets[bucketKeys[i]] = datItems.ConvertAll(kvp => kvp.Key); + } +#endif + } + + /// + /// Sort a list of item pairs by SourceID, Game, and Name (in order) + /// + /// List of pairs representing the items to be sorted + /// True if files are not renamed, false otherwise + /// True if it sorted correctly, false otherwise + private bool Sort(ref List> itemMappings, bool norename) + { + // Create the comparer extenal to the delegate + var nc = new NaturalComparer(); + + itemMappings.Sort(delegate (KeyValuePair x, KeyValuePair y) + { + try + { + // Compare on source if renaming + if (!norename) + { + int xSourceIndex = GetSourceForItem(x.Key).Value?.Index ?? 0; + int ySourceIndex = GetSourceForItem(y.Key).Value?.Index ?? 0; + if (xSourceIndex != ySourceIndex) + return xSourceIndex - ySourceIndex; + } + + // Get the machines + Machine? xMachine = _machines[_itemToMachineMapping[x.Key]]; + Machine? yMachine = _machines[_itemToMachineMapping[y.Key]]; + + // If machine names don't match + string? xMachineName = xMachine?.GetName(); + string? yMachineName = yMachine?.GetName(); + if (xMachineName != yMachineName) + return nc.Compare(xMachineName, yMachineName); + + // If types don't match + string? xType = x.Value.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + string? yType = y.Value.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey); + if (xType != yType) + return xType.AsItemType() - yType.AsItemType(); + + // If directory names don't match + string? xDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(x.Value.GetName())); + string? yDirectoryName = Path.GetDirectoryName(TextHelper.RemovePathUnsafeCharacters(y.Value.GetName())); + if (xDirectoryName != yDirectoryName) + return nc.Compare(xDirectoryName, yDirectoryName); + + // If item names don't match + string? xName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(x.Value.GetName())); + string? yName = Path.GetFileName(TextHelper.RemovePathUnsafeCharacters(y.Value.GetName())); + return nc.Compare(xName, yName); + } + catch + { + // Absorb the error + return 0; + } + }); + + return true; + } + + /// + /// Sort the input DAT and get the key to be used by the item + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// Key to try to use + private string SortAndGetKey(KeyValuePair datItem, bool sorted = false) + { + // If we're not already sorted, take care of it + if (!sorted) + BucketBy(GetBestAvailable()); + + // Now that we have the sorted type, we get the proper key + return GetBucketKey(datItem.Key, _bucketedBy, lower: true, norename: true); + } + + #endregion + + #region Statistics + + /// + /// Recalculate the statistics for the Dat + /// + public void RecalculateStats() + { + // Wipe out any stats already there + DatStatistics.ResetStatistics(); + + // If there are no items + if (_items is null || _items.Count == 0) + return; + + // Loop through and add + foreach (var item in _items.Values) + { + if (item is null) + continue; + + DatStatistics.AddItemStatistics(item); + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatFiles/ItemMappings.cs b/SabreTools.Metadata.DatFiles/ItemMappings.cs new file mode 100644 index 00000000..0ccfcdfe --- /dev/null +++ b/SabreTools.Metadata.DatFiles/ItemMappings.cs @@ -0,0 +1,12 @@ +namespace SabreTools.Metadata.DatFiles +{ + /// + /// Class used during deduplication + /// + public struct ItemMappings(DatItems.DatItem item, long machineId, long sourceId) + { + public DatItems.DatItem Item = item; + public long MachineId = machineId; + public long SourceId = sourceId; + } +} diff --git a/SabreTools.Metadata.DatFiles/SabreTools.Metadata.DatFiles.csproj b/SabreTools.Metadata.DatFiles/SabreTools.Metadata.DatFiles.csproj new file mode 100644 index 00000000..84b16516 --- /dev/null +++ b/SabreTools.Metadata.DatFiles/SabreTools.Metadata.DatFiles.csproj @@ -0,0 +1,53 @@ + + + + + net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 + true + false + false + true + latest + enable + true + snupkg + true + 2.3.0 + + + Matt Nadareski + DatFile specific functionality for metadata file processing + Copyright (c) Matt Nadareski 2016-2026 + https://github.com/SabreTools/ + README.md + https://github.com/SabreTools/SabreTools.Serialization + git + metadata dat datfile + MIT + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SabreTools.Metadata.DatItems.Test/ConvertersTests.cs b/SabreTools.Metadata.DatItems.Test/ConvertersTests.cs new file mode 100644 index 00000000..465eb986 --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/ConvertersTests.cs @@ -0,0 +1,37 @@ +using SabreTools.Metadata.Tools; +using Xunit; + +namespace SabreTools.Metadata.DatItems.Test +{ + public class ConvertersTests + { + #region Generators + + [Theory] + [InlineData(ChipType.NULL, 2)] + [InlineData(ControlType.NULL, 15)] + [InlineData(DeviceType.NULL, 21)] + [InlineData(DisplayType.NULL, 5)] + [InlineData(Endianness.NULL, 2)] + [InlineData(FeatureStatus.NULL, 2)] + [InlineData(FeatureType.NULL, 14)] + [InlineData(ItemStatus.NULL, 7)] + [InlineData(ItemType.NULL, 54)] + [InlineData(LoadFlag.NULL, 14)] + [InlineData(MachineType.None, 6)] + [InlineData(OpenMSXSubType.NULL, 3)] + [InlineData(Relation.NULL, 6)] + [InlineData(Runnable.NULL, 3)] + [InlineData(SoftwareListStatus.None, 3)] + [InlineData(Supported.NULL, 5)] + [InlineData(SupportStatus.NULL, 3)] + public void GenerateToEnumTest(T value, int expected) + { + var actual = Converters.GenerateToEnum(); + Assert.Equal(default, value); + Assert.Equal(expected, actual.Keys.Count); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/DatItemTests.cs b/SabreTools.Metadata.DatItems.Test/DatItemTests.cs new file mode 100644 index 00000000..ca8fee7c --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/DatItemTests.cs @@ -0,0 +1,539 @@ +using SabreTools.Metadata.DatItems.Formats; +using Xunit; + +namespace SabreTools.Metadata.DatItems.Test +{ + public class DatItemTests + { + #region Private Testing Classes + + /// + /// Testing implementation of Data.Models.Metadata.DatItem + /// + private class TestDatItemModel : Data.Models.Metadata.DatItem + { + public const string NameKey = "__NAME__"; + } + + /// + /// Testing implementation of DatItem + /// + private class TestDatItem : DatItem + { + private readonly string? _nameKey; + + protected override ItemType ItemType => ItemType.Blank; + + public TestDatItem() => _nameKey = TestDatItemModel.NameKey; + + public TestDatItem(string? nameKey) => _nameKey = nameKey; + + /// + public override string? GetName() => _nameKey is not null ? _internal.ReadString(_nameKey) : null; + + /// + public override void SetName(string? name) + { + if (_nameKey is not null) + _internal[_nameKey] = name; + } + } + + #endregion + + #region CopyMachineInformation + + [Fact] + public void CopyMachineInformation_NewItem_Overwrite() + { + Machine? machineA = new Machine(); + machineA.SetName("machineA"); + + var romA = new Rom(); + + var romB = new Rom(); + romB.RemoveField(DatItem.MachineKey); + + romA.CopyMachineInformation(romB); + var actualMachineA = romA.GetMachine(); + Assert.NotNull(actualMachineA); + Assert.Null(actualMachineA.GetName()); + } + + [Fact] + public void CopyMachineInformation_EmptyItem_NoChange() + { + Machine? machineA = new Machine(); + machineA.SetName("machineA"); + + var romA = new Rom(); + romA.SetFieldValue(DatItem.MachineKey, machineA); + + var romB = new Rom(); + romB.RemoveField(DatItem.MachineKey); + + romA.CopyMachineInformation(romB); + var actualMachineA = romA.GetMachine(); + Assert.NotNull(actualMachineA); + Assert.Equal("machineA", actualMachineA.GetName()); + } + + [Fact] + public void CopyMachineInformation_NullMachine_NoChange() + { + Machine? machineA = new Machine(); + machineA.SetName("machineA"); + + Machine? machineB = null; + + var romA = new Rom(); + romA.SetFieldValue(DatItem.MachineKey, machineA); + + var romB = new Rom(); + romB.SetFieldValue(DatItem.MachineKey, machineB); + + romA.CopyMachineInformation(romB); + var actualMachineA = romA.GetMachine(); + Assert.NotNull(actualMachineA); + Assert.Equal("machineA", actualMachineA.GetName()); + } + + [Fact] + public void CopyMachineInformation_EmptyMachine_Overwrite() + { + Machine? machineA = new Machine(); + machineA.SetName("machineA"); + + Machine? machineB = new Machine(); + + var romA = new Rom(); + romA.SetFieldValue(DatItem.MachineKey, machineA); + + var romB = new Rom(); + romB.SetFieldValue(DatItem.MachineKey, machineB); + + romA.CopyMachineInformation(romB); + var actualMachineA = romA.GetMachine(); + Assert.NotNull(actualMachineA); + Assert.Null(actualMachineA.GetName()); + } + + [Fact] + public void CopyMachineInformation_FilledMachine_Overwrite() + { + Machine? machineA = new Machine(); + machineA.SetName("machineA"); + + Machine? machineB = new Machine(); + machineB.SetName("machineB"); + + var romA = new Rom(); + romA.SetFieldValue(DatItem.MachineKey, machineA); + + var romB = new Rom(); + romB.SetFieldValue(DatItem.MachineKey, machineB); + + romA.CopyMachineInformation(romB); + var actualMachineA = romA.GetMachine(); + Assert.NotNull(actualMachineA); + Assert.Equal("machineB", actualMachineA.GetName()); + } + + [Fact] + public void CopyMachineInformation_MismatchedType_Overwrite() + { + Machine? machineA = new Machine(); + machineA.SetName("machineA"); + + Machine? machineB = new Machine(); + machineB.SetName("machineB"); + + var romA = new Rom(); + romA.SetFieldValue(DatItem.MachineKey, machineA); + + var diskB = new Disk(); + diskB.SetFieldValue(DatItem.MachineKey, machineB); + + romA.CopyMachineInformation(diskB); + var actualMachineA = romA.GetMachine(); + Assert.NotNull(actualMachineA); + Assert.Equal("machineB", actualMachineA.GetName()); + } + + #endregion + + #region CompareTo + + [Fact] + public void CompareTo_NullOther_Returns1() + { + DatItem self = new Rom(); + DatItem? other = null; + + int actual = self.CompareTo(other); + Assert.Equal(1, actual); + } + + [Fact] + public void CompareTo_DifferentOther_Returns1() + { + DatItem self = new Rom(); + self.SetName("name"); + + DatItem? other = new Disk(); + other.SetName("name"); + + int actual = self.CompareTo(other); + Assert.Equal(1, actual); + } + + [Fact] + public void CompareTo_Empty_Returns1() + { + DatItem self = new Rom(); + DatItem? other = new Rom(); + + int actual = self.CompareTo(other); + Assert.Equal(1, actual); + } + + [Theory] + [InlineData(null, null, 0)] + [InlineData("name", null, 1)] + [InlineData("name", "other", -1)] + [InlineData(null, "name", -1)] + [InlineData("other", "name", 1)] + [InlineData("name", "name", 0)] + public void CompareTo_NamesOnly(string? selfName, string? otherName, int expected) + { + DatItem self = new Rom(); + self.SetName(selfName); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + + DatItem? other = new Rom(); + other.SetName(otherName); + other.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + + int actual = self.CompareTo(other); + Assert.Equal(expected, actual); + } + + #endregion + + #region Equals + + [Fact] + public void Equals_Null_False() + { + DatItem self = new TestDatItem(); + DatItem? other = null; + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_MismatchedType_False() + { + DatItem self = new TestDatItem(); + DatItem? other = new Rom(); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_DefaultInternal_True() + { + DatItem self = new TestDatItem(); + DatItem? other = new TestDatItem(); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + [Fact] + public void Equals_MismatchedInternal_False() + { + DatItem self = new TestDatItem(); + self.SetName("self"); + + DatItem? other = new TestDatItem(); + other.SetName("other"); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_EqualInternal_True() + { + DatItem self = new TestDatItem(); + self.SetName("name"); + + DatItem? other = new TestDatItem(); + other.SetName("name"); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + #endregion + + // TODO: Change when Machine retrieval gets fixed + #region GetKey + + [Theory] + [InlineData(ItemKey.NULL, false, false, "")] + [InlineData(ItemKey.NULL, false, true, "")] + [InlineData(ItemKey.NULL, true, false, "")] + [InlineData(ItemKey.NULL, true, true, "")] + [InlineData(ItemKey.Machine, false, false, "0000000000-Machine")] + [InlineData(ItemKey.Machine, false, true, "Machine")] + [InlineData(ItemKey.Machine, true, false, "0000000000-machine")] + [InlineData(ItemKey.Machine, true, true, "machine")] + [InlineData(ItemKey.CRC, false, false, "00000000")] + [InlineData(ItemKey.CRC, false, true, "00000000")] + [InlineData(ItemKey.CRC, true, false, "00000000")] + [InlineData(ItemKey.CRC, true, true, "00000000")] + [InlineData(ItemKey.MD2, false, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, false, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD4, false, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, false, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD5, false, false, "d41d8cd98f00b204e9800998ecf8427e")] + [InlineData(ItemKey.MD5, false, true, "d41d8cd98f00b204e9800998ecf8427e")] + [InlineData(ItemKey.MD5, true, false, "d41d8cd98f00b204e9800998ecf8427e")] + [InlineData(ItemKey.MD5, true, true, "d41d8cd98f00b204e9800998ecf8427e")] + [InlineData(ItemKey.RIPEMD128, false, false, "cdf26213a150dc3ecb610f18f6b38b46")] + [InlineData(ItemKey.RIPEMD128, false, true, "cdf26213a150dc3ecb610f18f6b38b46")] + [InlineData(ItemKey.RIPEMD128, true, false, "cdf26213a150dc3ecb610f18f6b38b46")] + [InlineData(ItemKey.RIPEMD128, true, true, "cdf26213a150dc3ecb610f18f6b38b46")] + [InlineData(ItemKey.RIPEMD160, false, false, "9c1185a5c5e9fc54612808977ee8f548b2258d31")] + [InlineData(ItemKey.RIPEMD160, false, true, "9c1185a5c5e9fc54612808977ee8f548b2258d31")] + [InlineData(ItemKey.RIPEMD160, true, false, "9c1185a5c5e9fc54612808977ee8f548b2258d31")] + [InlineData(ItemKey.RIPEMD160, true, true, "9c1185a5c5e9fc54612808977ee8f548b2258d31")] + [InlineData(ItemKey.SHA1, false, false, "da39a3ee5e6b4b0d3255bfef95601890afd80709")] + [InlineData(ItemKey.SHA1, false, true, "da39a3ee5e6b4b0d3255bfef95601890afd80709")] + [InlineData(ItemKey.SHA1, true, false, "da39a3ee5e6b4b0d3255bfef95601890afd80709")] + [InlineData(ItemKey.SHA1, true, true, "da39a3ee5e6b4b0d3255bfef95601890afd80709")] + [InlineData(ItemKey.SHA256, false, false, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA256, false, true, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA256, true, false, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA256, true, true, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA384, false, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, false, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA512, false, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, false, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SpamSum, false, false, "3::")] + [InlineData(ItemKey.SpamSum, false, true, "3::")] + [InlineData(ItemKey.SpamSum, true, false, "3::")] + [InlineData(ItemKey.SpamSum, true, true, "3::")] + public void GetKeyDB_DefaultImplementation(ItemKey bucketedBy, bool lower, bool norename, string expected) + { + Source source = new Source(0); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "Machine"); + + DatItem datItem = new Blank(); + + string actual = datItem.GetKey(bucketedBy, machine, source, lower, norename); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(ItemKey.NULL, false, false, "")] + [InlineData(ItemKey.NULL, false, true, "")] + [InlineData(ItemKey.NULL, true, false, "")] + [InlineData(ItemKey.NULL, true, true, "")] + [InlineData(ItemKey.Machine, false, false, "0000000000-Machine")] + [InlineData(ItemKey.Machine, false, true, "Machine")] + [InlineData(ItemKey.Machine, true, false, "0000000000-machine")] + [InlineData(ItemKey.Machine, true, true, "machine")] + [InlineData(ItemKey.CRC, false, false, "DEADBEEF")] + [InlineData(ItemKey.CRC, false, true, "DEADBEEF")] + [InlineData(ItemKey.CRC, true, false, "deadbeef")] + [InlineData(ItemKey.CRC, true, true, "deadbeef")] + [InlineData(ItemKey.MD2, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD2, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD2, true, false, "deadbeef")] + [InlineData(ItemKey.MD2, true, true, "deadbeef")] + [InlineData(ItemKey.MD4, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD4, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD4, true, false, "deadbeef")] + [InlineData(ItemKey.MD4, true, true, "deadbeef")] + [InlineData(ItemKey.MD5, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD5, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD5, true, false, "deadbeef")] + [InlineData(ItemKey.MD5, true, true, "deadbeef")] + [InlineData(ItemKey.RIPEMD128, false, false, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD128, false, true, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD128, true, false, "deadbeef")] + [InlineData(ItemKey.RIPEMD128, true, true, "deadbeef")] + [InlineData(ItemKey.RIPEMD160, false, false, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD160, false, true, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD160, true, false, "deadbeef")] + [InlineData(ItemKey.RIPEMD160, true, true, "deadbeef")] + [InlineData(ItemKey.SHA1, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA1, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA1, true, false, "deadbeef")] + [InlineData(ItemKey.SHA1, true, true, "deadbeef")] + [InlineData(ItemKey.SHA256, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA256, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA256, true, false, "deadbeef")] + [InlineData(ItemKey.SHA256, true, true, "deadbeef")] + [InlineData(ItemKey.SHA384, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA384, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA384, true, false, "deadbeef")] + [InlineData(ItemKey.SHA384, true, true, "deadbeef")] + [InlineData(ItemKey.SHA512, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA512, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA512, true, false, "deadbeef")] + [InlineData(ItemKey.SHA512, true, true, "deadbeef")] + [InlineData(ItemKey.SpamSum, false, false, "BASE64")] + [InlineData(ItemKey.SpamSum, false, true, "BASE64")] + [InlineData(ItemKey.SpamSum, true, false, "base64")] + [InlineData(ItemKey.SpamSum, true, true, "base64")] + public void GetKeyDB_CustomImplementation(ItemKey bucketedBy, bool lower, bool norename, string expected) + { + Source source = new Source(0); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "Machine"); + + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "BASE64"); + + string actual = datItem.GetKey(bucketedBy, machine, source, lower, norename); + Assert.Equal(expected, actual); + } + + #endregion + + #region GetName + + [Fact] + public void GetName_NoNameKey_Null() + { + DatItem item = new TestDatItem(nameKey: null); + item.SetFieldValue(TestDatItemModel.NameKey, "name"); + + string? actual = item.GetName(); + Assert.Null(actual); + } + + [Fact] + public void GetName_EmptyNameKey_Null() + { + DatItem item = new TestDatItem(nameKey: string.Empty); + item.SetFieldValue(TestDatItemModel.NameKey, "name"); + + string? actual = item.GetName(); + Assert.Null(actual); + } + + [Fact] + public void GetName_NameKeyNotExists_Null() + { + DatItem item = new TestDatItem(nameKey: "INVALID"); + item.SetFieldValue(TestDatItemModel.NameKey, "name"); + + string? actual = item.GetName(); + Assert.Null(actual); + } + + [Fact] + public void GetName_NameKeyExists_Filled() + { + DatItem item = new TestDatItem(nameKey: TestDatItemModel.NameKey); + item.SetFieldValue(TestDatItemModel.NameKey, "name"); + + string? actual = item.GetName(); + Assert.Equal("name", actual); + } + + #endregion + + #region SetName + + [Fact] + public void SetName_NoNameKey_Null() + { + DatItem item = new TestDatItem(nameKey: null); + item.SetName("name"); + + string? actual = item.GetName(); + Assert.Null(actual); + } + + [Fact] + public void SetName_EmptyNameKey_Null() + { + DatItem item = new TestDatItem(nameKey: string.Empty); + item.SetName("name"); + + string? actual = item.GetName(); + Assert.Null(actual); + } + + [Fact] + public void SetName_NameKeyNonEmpty_Filled() + { + DatItem item = new TestDatItem(nameKey: TestDatItemModel.NameKey); + item.SetName("name"); + + string? actual = item.GetName(); + Assert.Equal("name", actual); + } + + #endregion + + #region Clone + + [Fact] + public void CloneTest() + { + DatItem item = new Sample(); + item.SetName("name"); + + object clone = item.Clone(); + Sample? actual = clone as Sample; + Assert.NotNull(actual); + Assert.Equal("name", actual.GetName()); + } + + #endregion + + #region GetInternalClone + + [Fact] + public void GetInternalCloneTest() + { + DatItem item = new TestDatItem(); + item.SetName("name"); + + TestDatItemModel actual = item.GetInternalClone(); + Assert.Equal("name", actual[TestDatItemModel.NameKey]); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/ExtensionsTests.cs b/SabreTools.Metadata.DatItems.Test/ExtensionsTests.cs new file mode 100644 index 00000000..1ce4b2e3 --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/ExtensionsTests.cs @@ -0,0 +1,590 @@ +using Xunit; + +namespace SabreTools.Metadata.DatItems.Test +{ + public class ExtensionsTests + { + #region String to Enum + + [Theory] + [InlineData(null, ChipType.NULL)] + [InlineData("cpu", ChipType.CPU)] + [InlineData("audio", ChipType.Audio)] + public void AsChipTypeTest(string? field, ChipType expected) + { + ChipType actual = field.AsChipType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, ControlType.NULL)] + [InlineData("joy", ControlType.Joy)] + [InlineData("stick", ControlType.Stick)] + [InlineData("paddle", ControlType.Paddle)] + [InlineData("pedal", ControlType.Pedal)] + [InlineData("lightgun", ControlType.Lightgun)] + [InlineData("positional", ControlType.Positional)] + [InlineData("dial", ControlType.Dial)] + [InlineData("trackball", ControlType.Trackball)] + [InlineData("mouse", ControlType.Mouse)] + [InlineData("only_buttons", ControlType.OnlyButtons)] + [InlineData("keypad", ControlType.Keypad)] + [InlineData("keyboard", ControlType.Keyboard)] + [InlineData("mahjong", ControlType.Mahjong)] + [InlineData("hanafuda", ControlType.Hanafuda)] + [InlineData("gambling", ControlType.Gambling)] + public void AsControlTypeTest(string? field, ControlType expected) + { + ControlType actual = field.AsControlType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, DeviceType.NULL)] + [InlineData("unknown", DeviceType.Unknown)] + [InlineData("cartridge", DeviceType.Cartridge)] + [InlineData("floppydisk", DeviceType.FloppyDisk)] + [InlineData("harddisk", DeviceType.HardDisk)] + [InlineData("cylinder", DeviceType.Cylinder)] + [InlineData("cassette", DeviceType.Cassette)] + [InlineData("punchcard", DeviceType.PunchCard)] + [InlineData("punchtape", DeviceType.PunchTape)] + [InlineData("printout", DeviceType.Printout)] + [InlineData("serial", DeviceType.Serial)] + [InlineData("parallel", DeviceType.Parallel)] + [InlineData("snapshot", DeviceType.Snapshot)] + [InlineData("quickload", DeviceType.QuickLoad)] + [InlineData("memcard", DeviceType.MemCard)] + [InlineData("cdrom", DeviceType.CDROM)] + [InlineData("magtape", DeviceType.MagTape)] + [InlineData("romimage", DeviceType.ROMImage)] + [InlineData("midiin", DeviceType.MIDIIn)] + [InlineData("midiout", DeviceType.MIDIOut)] + [InlineData("picture", DeviceType.Picture)] + [InlineData("vidfile", DeviceType.VidFile)] + public void AsDeviceTypeTest(string? field, DeviceType expected) + { + DeviceType actual = field.AsDeviceType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, DisplayType.NULL)] + [InlineData("raster", DisplayType.Raster)] + [InlineData("vector", DisplayType.Vector)] + [InlineData("lcd", DisplayType.LCD)] + [InlineData("svg", DisplayType.SVG)] + [InlineData("unknown", DisplayType.Unknown)] + public void AsDisplayTypeTest(string? field, DisplayType expected) + { + DisplayType actual = field.AsDisplayType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, Endianness.NULL)] + [InlineData("big", Endianness.Big)] + [InlineData("little", Endianness.Little)] + public void AsEndiannessTest(string? field, Endianness expected) + { + Endianness actual = field.AsEndianness(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, FeatureStatus.NULL)] + [InlineData("unemulated", FeatureStatus.Unemulated)] + [InlineData("imperfect", FeatureStatus.Imperfect)] + public void AsFeatureStatusTest(string? field, FeatureStatus expected) + { + FeatureStatus actual = field.AsFeatureStatus(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, FeatureType.NULL)] + [InlineData("protection", FeatureType.Protection)] + [InlineData("palette", FeatureType.Palette)] + [InlineData("graphics", FeatureType.Graphics)] + [InlineData("sound", FeatureType.Sound)] + [InlineData("controls", FeatureType.Controls)] + [InlineData("keyboard", FeatureType.Keyboard)] + [InlineData("mouse", FeatureType.Mouse)] + [InlineData("microphone", FeatureType.Microphone)] + [InlineData("camera", FeatureType.Camera)] + [InlineData("disk", FeatureType.Disk)] + [InlineData("printer", FeatureType.Printer)] + [InlineData("lan", FeatureType.Lan)] + [InlineData("wan", FeatureType.Wan)] + [InlineData("timing", FeatureType.Timing)] + public void AsFeatureTypeTest(string? field, FeatureType expected) + { + FeatureType actual = field.AsFeatureType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, ItemStatus.NULL)] + [InlineData("none", ItemStatus.None)] + [InlineData("no", ItemStatus.None)] + [InlineData("good", ItemStatus.Good)] + [InlineData("baddump", ItemStatus.BadDump)] + [InlineData("nodump", ItemStatus.Nodump)] + [InlineData("yes", ItemStatus.Nodump)] + [InlineData("verified", ItemStatus.Verified)] + public void AsItemStatusTest(string? field, ItemStatus expected) + { + ItemStatus actual = field.AsItemStatus(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, ItemType.NULL)] + [InlineData("adjuster", ItemType.Adjuster)] + [InlineData("analog", ItemType.Analog)] + [InlineData("archive", ItemType.Archive)] + [InlineData("biosset", ItemType.BiosSet)] + [InlineData("blank", ItemType.Blank)] + [InlineData("chip", ItemType.Chip)] + [InlineData("condition", ItemType.Condition)] + [InlineData("configuration", ItemType.Configuration)] + [InlineData("conflocation", ItemType.ConfLocation)] + [InlineData("confsetting", ItemType.ConfSetting)] + [InlineData("control", ItemType.Control)] + [InlineData("dataarea", ItemType.DataArea)] + [InlineData("device", ItemType.Device)] + [InlineData("deviceref", ItemType.DeviceRef)] + [InlineData("device_ref", ItemType.DeviceRef)] + [InlineData("diplocation", ItemType.DipLocation)] + [InlineData("dipswitch", ItemType.DipSwitch)] + [InlineData("dipvalue", ItemType.DipValue)] + [InlineData("disk", ItemType.Disk)] + [InlineData("diskarea", ItemType.DiskArea)] + [InlineData("display", ItemType.Display)] + [InlineData("driver", ItemType.Driver)] + [InlineData("extension", ItemType.Extension)] + [InlineData("feature", ItemType.Feature)] + [InlineData("file", ItemType.File)] + [InlineData("info", ItemType.Info)] + [InlineData("input", ItemType.Input)] + [InlineData("instance", ItemType.Instance)] + [InlineData("media", ItemType.Media)] + [InlineData("part", ItemType.Part)] + [InlineData("partfeature", ItemType.PartFeature)] + [InlineData("part_feature", ItemType.PartFeature)] + [InlineData("port", ItemType.Port)] + [InlineData("ramoption", ItemType.RamOption)] + [InlineData("ram_option", ItemType.RamOption)] + [InlineData("release", ItemType.Release)] + [InlineData("releasedetails", ItemType.ReleaseDetails)] + [InlineData("release_details", ItemType.ReleaseDetails)] + [InlineData("rom", ItemType.Rom)] + [InlineData("sample", ItemType.Sample)] + [InlineData("serials", ItemType.Serials)] + [InlineData("sharedfeat", ItemType.SharedFeat)] + [InlineData("shared_feat", ItemType.SharedFeat)] + [InlineData("sharedfeature", ItemType.SharedFeat)] + [InlineData("shared_feature", ItemType.SharedFeat)] + [InlineData("slot", ItemType.Slot)] + [InlineData("slotoption", ItemType.SlotOption)] + [InlineData("slot_option", ItemType.SlotOption)] + [InlineData("softwarelist", ItemType.SoftwareList)] + [InlineData("software_list", ItemType.SoftwareList)] + [InlineData("sound", ItemType.Sound)] + [InlineData("sourcedetails", ItemType.SourceDetails)] + [InlineData("source_details", ItemType.SourceDetails)] + public void AsItemTypeTest(string? field, ItemType expected) + { + ItemType actual = field.AsItemType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, LoadFlag.NULL)] + [InlineData("load16_byte", LoadFlag.Load16Byte)] + [InlineData("load16_word", LoadFlag.Load16Word)] + [InlineData("load16_word_swap", LoadFlag.Load16WordSwap)] + [InlineData("load32_byte", LoadFlag.Load32Byte)] + [InlineData("load32_word", LoadFlag.Load32Word)] + [InlineData("load32_word_swap", LoadFlag.Load32WordSwap)] + [InlineData("load32_dword", LoadFlag.Load32DWord)] + [InlineData("load64_word", LoadFlag.Load64Word)] + [InlineData("load64_word_swap", LoadFlag.Load64WordSwap)] + [InlineData("reload", LoadFlag.Reload)] + [InlineData("fill", LoadFlag.Fill)] + [InlineData("continue", LoadFlag.Continue)] + [InlineData("reload_plain", LoadFlag.ReloadPlain)] + [InlineData("ignore", LoadFlag.Ignore)] + public void AsLoadFlagTest(string? field, LoadFlag expected) + { + LoadFlag actual = field.AsLoadFlag(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, MachineType.None)] + [InlineData("none", MachineType.None)] + [InlineData("bios", MachineType.Bios)] + [InlineData("dev", MachineType.Device)] + [InlineData("device", MachineType.Device)] + [InlineData("mech", MachineType.Mechanical)] + [InlineData("mechanical", MachineType.Mechanical)] + public void AsMachineTypeTest(string? field, MachineType expected) + { + MachineType actual = field.AsMachineType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, OpenMSXSubType.NULL)] + [InlineData("rom", OpenMSXSubType.Rom)] + [InlineData("megarom", OpenMSXSubType.MegaRom)] + [InlineData("sccpluscart", OpenMSXSubType.SCCPlusCart)] + public void AsOpenMSXSubTypeTest(string? field, OpenMSXSubType expected) + { + OpenMSXSubType actual = field.AsOpenMSXSubType(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, Relation.NULL)] + [InlineData("eq", Relation.Equal)] + [InlineData("ne", Relation.NotEqual)] + [InlineData("gt", Relation.GreaterThan)] + [InlineData("le", Relation.LessThanOrEqual)] + [InlineData("lt", Relation.LessThan)] + [InlineData("ge", Relation.GreaterThanOrEqual)] + public void AsRelationTest(string? field, Relation expected) + { + Relation actual = field.AsRelation(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, Runnable.NULL)] + [InlineData("no", Runnable.No)] + [InlineData("partial", Runnable.Partial)] + [InlineData("yes", Runnable.Yes)] + public void AsRunnableTest(string? field, Runnable expected) + { + Runnable actual = field.AsRunnable(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, SoftwareListStatus.None)] + [InlineData("none", SoftwareListStatus.None)] + [InlineData("original", SoftwareListStatus.Original)] + [InlineData("compatible", SoftwareListStatus.Compatible)] + public void AsSoftwareListStatusTest(string? field, SoftwareListStatus expected) + { + SoftwareListStatus actual = field.AsSoftwareListStatus(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, Supported.NULL)] + [InlineData("no", Supported.No)] + [InlineData("unsupported", Supported.No)] + [InlineData("partial", Supported.Partial)] + [InlineData("yes", Supported.Yes)] + [InlineData("supported", Supported.Yes)] + public void AsSupportedTest(string? field, Supported expected) + { + Supported actual = field.AsSupported(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, SupportStatus.NULL)] + [InlineData("good", SupportStatus.Good)] + [InlineData("imperfect", SupportStatus.Imperfect)] + [InlineData("preliminary", SupportStatus.Preliminary)] + public void AsSupportStatusTest(string? field, SupportStatus expected) + { + SupportStatus actual = field.AsSupportStatus(); + Assert.Equal(expected, actual); + } + + #endregion + + #region Enum to String + + [Theory] + [InlineData(ChipType.NULL, null)] + [InlineData(ChipType.CPU, "cpu")] + [InlineData(ChipType.Audio, "audio")] + public void FromChipTypeTest(ChipType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(ControlType.NULL, null)] + [InlineData(ControlType.Joy, "joy")] + [InlineData(ControlType.Stick, "stick")] + [InlineData(ControlType.Paddle, "paddle")] + [InlineData(ControlType.Pedal, "pedal")] + [InlineData(ControlType.Lightgun, "lightgun")] + [InlineData(ControlType.Positional, "positional")] + [InlineData(ControlType.Dial, "dial")] + [InlineData(ControlType.Trackball, "trackball")] + [InlineData(ControlType.Mouse, "mouse")] + [InlineData(ControlType.OnlyButtons, "only_buttons")] + [InlineData(ControlType.Keypad, "keypad")] + [InlineData(ControlType.Keyboard, "keyboard")] + [InlineData(ControlType.Mahjong, "mahjong")] + [InlineData(ControlType.Hanafuda, "hanafuda")] + [InlineData(ControlType.Gambling, "gambling")] + public void FromControlTypeTest(ControlType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(DeviceType.NULL, null)] + [InlineData(DeviceType.Unknown, "unknown")] + [InlineData(DeviceType.Cartridge, "cartridge")] + [InlineData(DeviceType.FloppyDisk, "floppydisk")] + [InlineData(DeviceType.HardDisk, "harddisk")] + [InlineData(DeviceType.Cylinder, "cylinder")] + [InlineData(DeviceType.Cassette, "cassette")] + [InlineData(DeviceType.PunchCard, "punchcard")] + [InlineData(DeviceType.PunchTape, "punchtape")] + [InlineData(DeviceType.Printout, "printout")] + [InlineData(DeviceType.Serial, "serial")] + [InlineData(DeviceType.Parallel, "parallel")] + [InlineData(DeviceType.Snapshot, "snapshot")] + [InlineData(DeviceType.QuickLoad, "quickload")] + [InlineData(DeviceType.MemCard, "memcard")] + [InlineData(DeviceType.CDROM, "cdrom")] + [InlineData(DeviceType.MagTape, "magtape")] + [InlineData(DeviceType.ROMImage, "romimage")] + [InlineData(DeviceType.MIDIIn, "midiin")] + [InlineData(DeviceType.MIDIOut, "midiout")] + [InlineData(DeviceType.Picture, "picture")] + [InlineData(DeviceType.VidFile, "vidfile")] + public void FromDeviceTypeTest(DeviceType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(DisplayType.NULL, null)] + [InlineData(DisplayType.Raster, "raster")] + [InlineData(DisplayType.Vector, "vector")] + [InlineData(DisplayType.LCD, "lcd")] + [InlineData(DisplayType.SVG, "svg")] + [InlineData(DisplayType.Unknown, "unknown")] + public void FromDisplayTypeTest(DisplayType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(Endianness.NULL, null)] + [InlineData(Endianness.Big, "big")] + [InlineData(Endianness.Little, "little")] + public void FromEndiannessTest(Endianness field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(FeatureStatus.NULL, null)] + [InlineData(FeatureStatus.Unemulated, "unemulated")] + [InlineData(FeatureStatus.Imperfect, "imperfect")] + public void FromFeatureStatusTest(FeatureStatus field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(FeatureType.NULL, null)] + [InlineData(FeatureType.Protection, "protection")] + [InlineData(FeatureType.Palette, "palette")] + [InlineData(FeatureType.Graphics, "graphics")] + [InlineData(FeatureType.Sound, "sound")] + [InlineData(FeatureType.Controls, "controls")] + [InlineData(FeatureType.Keyboard, "keyboard")] + [InlineData(FeatureType.Mouse, "mouse")] + [InlineData(FeatureType.Microphone, "microphone")] + [InlineData(FeatureType.Camera, "camera")] + [InlineData(FeatureType.Disk, "disk")] + [InlineData(FeatureType.Printer, "printer")] + [InlineData(FeatureType.Lan, "lan")] + [InlineData(FeatureType.Wan, "wan")] + [InlineData(FeatureType.Timing, "timing")] + public void FromFeatureTypeTest(FeatureType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(ItemStatus.NULL, null)] + [InlineData(ItemStatus.None, "none")] + [InlineData(ItemStatus.Good, "good")] + [InlineData(ItemStatus.BadDump, "baddump")] + [InlineData(ItemStatus.Nodump, "nodump")] + [InlineData(ItemStatus.Verified, "verified")] + public void FromItemStatusTest(ItemStatus field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(ItemType.NULL, null)] + [InlineData(ItemType.Adjuster, "adjuster")] + [InlineData(ItemType.Analog, "analog")] + [InlineData(ItemType.Archive, "archive")] + [InlineData(ItemType.BiosSet, "biosset")] + [InlineData(ItemType.Blank, "blank")] + [InlineData(ItemType.Chip, "chip")] + [InlineData(ItemType.Condition, "condition")] + [InlineData(ItemType.Configuration, "configuration")] + [InlineData(ItemType.ConfLocation, "conflocation")] + [InlineData(ItemType.ConfSetting, "confsetting")] + [InlineData(ItemType.Control, "control")] + [InlineData(ItemType.DataArea, "dataarea")] + [InlineData(ItemType.Device, "device")] + [InlineData(ItemType.DeviceRef, "device_ref")] + [InlineData(ItemType.DipLocation, "diplocation")] + [InlineData(ItemType.DipSwitch, "dipswitch")] + [InlineData(ItemType.DipValue, "dipvalue")] + [InlineData(ItemType.Disk, "disk")] + [InlineData(ItemType.DiskArea, "diskarea")] + [InlineData(ItemType.Display, "display")] + [InlineData(ItemType.Driver, "driver")] + [InlineData(ItemType.Extension, "extension")] + [InlineData(ItemType.Feature, "feature")] + [InlineData(ItemType.File, "file")] + [InlineData(ItemType.Info, "info")] + [InlineData(ItemType.Input, "input")] + [InlineData(ItemType.Instance, "instance")] + [InlineData(ItemType.Media, "media")] + [InlineData(ItemType.Part, "part")] + [InlineData(ItemType.PartFeature, "part_feature")] + [InlineData(ItemType.Port, "port")] + [InlineData(ItemType.RamOption, "ramoption")] + [InlineData(ItemType.Release, "release")] + [InlineData(ItemType.ReleaseDetails, "release_details")] + [InlineData(ItemType.Rom, "rom")] + [InlineData(ItemType.Sample, "sample")] + [InlineData(ItemType.Serials, "serials")] + [InlineData(ItemType.SharedFeat, "sharedfeat")] + [InlineData(ItemType.Slot, "slot")] + [InlineData(ItemType.SlotOption, "slotoption")] + [InlineData(ItemType.SoftwareList, "softwarelist")] + [InlineData(ItemType.Sound, "sound")] + [InlineData(ItemType.SourceDetails, "source_details")] + public void FromItemTypeTest(ItemType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(LoadFlag.NULL, null)] + [InlineData(LoadFlag.Load16Byte, "load16_byte")] + [InlineData(LoadFlag.Load16Word, "load16_word")] + [InlineData(LoadFlag.Load16WordSwap, "load16_word_swap")] + [InlineData(LoadFlag.Load32Byte, "load32_byte")] + [InlineData(LoadFlag.Load32Word, "load32_word")] + [InlineData(LoadFlag.Load32WordSwap, "load32_word_swap")] + [InlineData(LoadFlag.Load32DWord, "load32_dword")] + [InlineData(LoadFlag.Load64Word, "load64_word")] + [InlineData(LoadFlag.Load64WordSwap, "load64_word_swap")] + [InlineData(LoadFlag.Reload, "reload")] + [InlineData(LoadFlag.Fill, "fill")] + [InlineData(LoadFlag.Continue, "continue")] + [InlineData(LoadFlag.ReloadPlain, "reload_plain")] + [InlineData(LoadFlag.Ignore, "ignore")] + public void FromLoadFlagTest(LoadFlag field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(OpenMSXSubType.NULL, null)] + [InlineData(OpenMSXSubType.Rom, "rom")] + [InlineData(OpenMSXSubType.MegaRom, "megarom")] + [InlineData(OpenMSXSubType.SCCPlusCart, "sccpluscart")] + public void FromOpenMSXSubTypeTest(OpenMSXSubType field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(Relation.NULL, null)] + [InlineData(Relation.Equal, "eq")] + [InlineData(Relation.NotEqual, "ne")] + [InlineData(Relation.GreaterThan, "gt")] + [InlineData(Relation.LessThanOrEqual, "le")] + [InlineData(Relation.LessThan, "lt")] + [InlineData(Relation.GreaterThanOrEqual, "ge")] + public void FromRelationTest(Relation field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(Runnable.NULL, null)] + [InlineData(Runnable.No, "no")] + [InlineData(Runnable.Partial, "partial")] + [InlineData(Runnable.Yes, "yes")] + public void FromRunnableTest(Runnable field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(SoftwareListStatus.None, "none")] + [InlineData(SoftwareListStatus.Original, "original")] + [InlineData(SoftwareListStatus.Compatible, "compatible")] + public void FromSoftwareListStatusTest(SoftwareListStatus field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(Supported.NULL, true, null)] + [InlineData(Supported.NULL, false, null)] + [InlineData(Supported.No, true, "unsupported")] + [InlineData(Supported.No, false, "no")] + [InlineData(Supported.Partial, true, "partial")] + [InlineData(Supported.Partial, false, "partial")] + [InlineData(Supported.Yes, true, "supported")] + [InlineData(Supported.Yes, false, "yes")] + public void FromSupportedTest(Supported field, bool useSecond, string? expected) + { + string? actual = field.AsStringValue(useSecond); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(SupportStatus.NULL, null)] + [InlineData(SupportStatus.Good, "good")] + [InlineData(SupportStatus.Imperfect, "imperfect")] + [InlineData(SupportStatus.Preliminary, "preliminary")] + public void FromSupportStatusTest(SupportStatus field, string? expected) + { + string? actual = field.AsStringValue(); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/Formats/DiskTests.cs b/SabreTools.Metadata.DatItems.Test/Formats/DiskTests.cs new file mode 100644 index 00000000..dc7261aa --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/Formats/DiskTests.cs @@ -0,0 +1,269 @@ +using SabreTools.Hashing; +using Xunit; + +namespace SabreTools.Metadata.DatItems.Formats.Test +{ + public class DiskTests + { + #region ConvertToRom + + [Fact] + public void ConvertToRomTest() + { + DiskArea diskArea = new DiskArea(); + diskArea.SetName("XXXXXX"); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "XXXXXX"); + + Part part = new Part(); + part.SetName("XXXXXX"); + + Source source = new Source(0, "XXXXXX"); + + Disk disk = new Disk(); + disk.SetName("XXXXXX"); + disk.SetFieldValue(Disk.DiskAreaKey, diskArea); + disk.SetFieldValue(Data.Models.Metadata.Disk.MergeKey, "XXXXXX"); + disk.SetFieldValue(Data.Models.Metadata.Disk.RegionKey, "XXXXXX"); + disk.SetFieldValue(Data.Models.Metadata.Disk.StatusKey, "good"); + disk.SetFieldValue(Data.Models.Metadata.Disk.OptionalKey, "XXXXXX"); + disk.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, HashType.MD5.ZeroString); + disk.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, HashType.SHA1.ZeroString); + disk.SetFieldValue(DatItem.DupeTypeKey, DupeType.All | DupeType.External); + disk.SetFieldValue(DatItem.MachineKey, machine); + disk.SetFieldValue(Disk.PartKey, part); + disk.SetFieldValue(DatItem.RemoveKey, (bool?)false); + disk.SetFieldValue(DatItem.SourceKey, source); + + Rom actual = disk.ConvertToRom(); + + Assert.Equal("XXXXXX.chd", actual.GetName()); + Assert.Equal("XXXXXX", actual.GetStringFieldValue(Data.Models.Metadata.Rom.MergeKey)); + Assert.Equal("XXXXXX", actual.GetStringFieldValue(Data.Models.Metadata.Rom.RegionKey)); + Assert.Equal("good", actual.GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey)); + Assert.Equal("XXXXXX", actual.GetStringFieldValue(Data.Models.Metadata.Rom.OptionalKey)); + Assert.Equal(HashType.MD5.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Equal(HashType.SHA1.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal(DupeType.All | DupeType.External, actual.GetFieldValue(DatItem.DupeTypeKey)); + + DataArea? actualDataArea = actual.GetFieldValue(Rom.DataAreaKey); + Assert.NotNull(actualDataArea); + Assert.Equal("XXXXXX", actualDataArea.GetStringFieldValue(Data.Models.Metadata.DataArea.NameKey)); + + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("XXXXXX", actualMachine.GetName()); + + Assert.Equal(false, actual.GetBoolFieldValue(DatItem.RemoveKey)); + + Part? actualPart = actual.GetFieldValue(Rom.PartKey); + Assert.NotNull(actualPart); + Assert.Equal("XXXXXX", actualPart.GetStringFieldValue(Data.Models.Metadata.Part.NameKey)); + + Source? actualSource = actual.GetFieldValue(DatItem.SourceKey); + Assert.NotNull(actualSource); + Assert.Equal(0, actualSource.Index); + Assert.Equal("XXXXXX", actualSource.Name); + } + + #endregion + + #region FillMissingInformation + + [Fact] + public void FillMissingInformation_BothEmpty() + { + Disk self = new Disk(); + Disk other = new Disk(); + + self.FillMissingInformation(other); + + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)); + } + + [Fact] + public void FillMissingInformation_AllMissing() + { + Disk self = new Disk(); + + Disk other = new Disk(); + other.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "XXXXXX"); + + self.FillMissingInformation(other); + + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key)); + } + + #endregion + + #region HasHashes + + [Fact] + public void HasHashes_NoHash_False() + { + Disk self = new Disk(); + bool actual = self.HasHashes(); + Assert.False(actual); + } + + [Fact] + public void HasHashes_MD5_True() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA1_True() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "XXXXXX"); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_All_True() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "XXXXXX"); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + #endregion + + #region HasZeroHash + + [Fact] + public void HasZeroHash_NoHash_True() + { + Disk self = new Disk(); + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_NonZeroHash_False() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, "DEADBEEF"); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "DEADBEEF"); + + bool actual = self.HasZeroHash(); + Assert.False(actual); + } + + [Fact] + public void HasZeroHash_ZeroMD5_True() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, HashType.MD5.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA1_True() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, HashType.SHA1.ZeroString); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroAll_True() + { + Disk self = new Disk(); + self.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, HashType.MD5.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, HashType.SHA1.ZeroString); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + #endregion + + // TODO: Change when Machine retrieval gets fixed + #region GetKey + + [Theory] + [InlineData(ItemKey.NULL, false, false, "")] + [InlineData(ItemKey.NULL, false, true, "")] + [InlineData(ItemKey.NULL, true, false, "")] + [InlineData(ItemKey.NULL, true, true, "")] + [InlineData(ItemKey.Machine, false, false, "0000000000-Machine")] + [InlineData(ItemKey.Machine, false, true, "Machine")] + [InlineData(ItemKey.Machine, true, false, "0000000000-machine")] + [InlineData(ItemKey.Machine, true, true, "machine")] + [InlineData(ItemKey.CRC, false, false, "00000000")] + [InlineData(ItemKey.CRC, false, true, "00000000")] + [InlineData(ItemKey.CRC, true, false, "00000000")] + [InlineData(ItemKey.CRC, true, true, "00000000")] + [InlineData(ItemKey.MD2, false, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, false, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD4, false, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, false, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD5, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD5, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD5, true, false, "deadbeef")] + [InlineData(ItemKey.MD5, true, true, "deadbeef")] + [InlineData(ItemKey.SHA1, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA1, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA1, true, false, "deadbeef")] + [InlineData(ItemKey.SHA1, true, true, "deadbeef")] + [InlineData(ItemKey.SHA256, false, false, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA256, false, true, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA256, true, false, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA256, true, true, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData(ItemKey.SHA384, false, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, false, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA512, false, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, false, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SpamSum, false, false, "3::")] + [InlineData(ItemKey.SpamSum, false, true, "3::")] + [InlineData(ItemKey.SpamSum, true, false, "3::")] + [InlineData(ItemKey.SpamSum, true, true, "3::")] + public void GetKeyDBTest(ItemKey bucketedBy, bool lower, bool norename, string expected) + { + Source source = new Source(0); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "Machine"); + + DatItem datItem = new Disk(); + datItem.SetFieldValue(Data.Models.Metadata.Disk.MD5Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, "DEADBEEF"); + + string actual = datItem.GetKey(bucketedBy, machine, source, lower, norename); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/Formats/FileTests.cs b/SabreTools.Metadata.DatItems.Test/Formats/FileTests.cs new file mode 100644 index 00000000..e59a60dd --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/Formats/FileTests.cs @@ -0,0 +1,360 @@ +using SabreTools.Hashing; +using Xunit; + +namespace SabreTools.Metadata.DatItems.Formats.Test +{ + public class FileTests + { + #region ConvertToRom + + [Fact] + public void ConvertToRomTest() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "XXXXXX"); + + Source source = new Source(0, "XXXXXX"); + + var file = new File + { + Id = "XXXXXX", + Extension = "XXXXXX", + Size = 12345, + CRC = "DEADBEEF", + MD5 = "DEADBEEF", + SHA1 = "DEADBEEF", + SHA256 = "DEADBEEF", + Format = "XXXXXX" + }; + file.SetFieldValue(DatItem.DupeTypeKey, DupeType.All | DupeType.External); + file.SetFieldValue(DatItem.MachineKey, machine); + file.SetFieldValue(DatItem.RemoveKey, (bool?)false); + file.SetFieldValue(DatItem.SourceKey, source); + file.SetFieldValue(DatItem.MachineKey, machine); + file.SetFieldValue(DatItem.SourceKey, source); + + Rom actual = file.ConvertToRom(); + + Assert.Equal("XXXXXX.XXXXXX", actual.GetName()); + Assert.Equal(12345, actual.GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey)); + Assert.Equal("deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Equal("000000000000000000000000deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Equal("00000000000000000000000000000000deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("00000000000000000000000000000000000000000000000000000000deadbeef", actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Equal(DupeType.All | DupeType.External, actual.GetFieldValue(DatItem.DupeTypeKey)); + + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("XXXXXX", actualMachine.GetName()); + + Assert.Equal(false, actual.GetBoolFieldValue(DatItem.RemoveKey)); + + Source? actualSource = actual.GetFieldValue(DatItem.SourceKey); + Assert.NotNull(actualSource); + Assert.Equal(0, actualSource.Index); + Assert.Equal("XXXXXX", actualSource.Name); + } + + #endregion + + #region FillMissingInformation + + [Fact] + public void FillMissingInformation_BothEmpty() + { + File self = new File(); + File other = new File(); + + self.FillMissingInformation(other); + + Assert.Null(self.Size); + Assert.Null(self.CRC); + Assert.Null(self.MD5); + Assert.Null(self.SHA1); + Assert.Null(self.SHA256); + } + + [Fact] + public void FillMissingInformation_AllMissing() + { + File self = new File(); + + File other = new File + { + Size = 12345, + CRC = "DEADBEEF", + MD5 = "DEADBEEF", + SHA1 = "DEADBEEF", + SHA256 = "DEADBEEF", + }; + + self.FillMissingInformation(other); + + Assert.Equal(12345, self.Size); + Assert.Equal("deadbeef", self.CRC); + Assert.Equal("000000000000000000000000deadbeef", self.MD5); + Assert.Equal("00000000000000000000000000000000deadbeef", self.SHA1); + Assert.Equal("00000000000000000000000000000000000000000000000000000000deadbeef", self.SHA256); + } + + #endregion + + #region HasHashes + + [Fact] + public void HasHashes_NoHash_False() + { + File self = new File(); + bool actual = self.HasHashes(); + Assert.False(actual); + } + + [Fact] + public void HasHashes_CRC_True() + { + File self = new File + { + CRC = "deadbeef", + MD5 = string.Empty, + SHA1 = string.Empty, + SHA256 = string.Empty, + }; + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_MD5_True() + { + File self = new File + { + CRC = string.Empty, + MD5 = "deadbeef", + SHA1 = string.Empty, + SHA256 = string.Empty, + }; + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA1_True() + { + File self = new File + { + CRC = string.Empty, + MD5 = string.Empty, + SHA1 = "deadbeef", + SHA256 = string.Empty, + }; + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA256_True() + { + File self = new File + { + CRC = string.Empty, + MD5 = string.Empty, + SHA1 = string.Empty, + SHA256 = "deadbeef", + }; + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_All_True() + { + File self = new File + { + CRC = "deadbeef", + MD5 = "deadbeef", + SHA1 = "deadbeef", + SHA256 = "deadbeef", + }; + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + #endregion + + #region HasZeroHash + + [Fact] + public void HasZeroHash_NoHash_True() + { + File self = new File(); + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_NonZeroHash_False() + { + File self = new File + { + CRC = "deadbeef", + MD5 = "deadbeef", + SHA1 = "deadbeef", + SHA256 = "deadbeef", + }; + + bool actual = self.HasZeroHash(); + Assert.False(actual); + } + + [Fact] + public void HasZeroHash_ZeroCRC_True() + { + File self = new File + { + CRC = HashType.CRC32.ZeroString, + MD5 = string.Empty, + SHA1 = string.Empty, + SHA256 = string.Empty, + }; + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroMD5_True() + { + File self = new File + { + CRC = string.Empty, + MD5 = HashType.MD5.ZeroString, + SHA1 = string.Empty, + SHA256 = string.Empty, + }; + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA1_True() + { + File self = new File + { + CRC = string.Empty, + MD5 = string.Empty, + SHA1 = HashType.SHA1.ZeroString, + SHA256 = string.Empty, + }; + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA256_True() + { + File self = new File + { + CRC = string.Empty, + MD5 = string.Empty, + SHA1 = string.Empty, + SHA256 = HashType.SHA256.ZeroString, + }; + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroAll_True() + { + File self = new File + { + CRC = HashType.CRC32.ZeroString, + MD5 = HashType.MD5.ZeroString, + SHA1 = HashType.SHA1.ZeroString, + SHA256 = HashType.SHA256.ZeroString, + }; + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + #endregion + + // TODO: Change when Machine retrieval gets fixed + #region GetKey + + [Theory] + [InlineData(ItemKey.NULL, false, false, "")] + [InlineData(ItemKey.NULL, false, true, "")] + [InlineData(ItemKey.NULL, true, false, "")] + [InlineData(ItemKey.NULL, true, true, "")] + [InlineData(ItemKey.Machine, false, false, "0000000000-Machine")] + [InlineData(ItemKey.Machine, false, true, "Machine")] + [InlineData(ItemKey.Machine, true, false, "0000000000-machine")] + [InlineData(ItemKey.Machine, true, true, "machine")] + [InlineData(ItemKey.CRC, false, false, "deadbeef")] + [InlineData(ItemKey.CRC, false, true, "deadbeef")] + [InlineData(ItemKey.CRC, true, false, "deadbeef")] + [InlineData(ItemKey.CRC, true, true, "deadbeef")] + [InlineData(ItemKey.MD2, false, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, false, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD4, false, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, false, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD5, false, false, "000000000000000000000000deadbeef")] + [InlineData(ItemKey.MD5, false, true, "000000000000000000000000deadbeef")] + [InlineData(ItemKey.MD5, true, false, "000000000000000000000000deadbeef")] + [InlineData(ItemKey.MD5, true, true, "000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA1, false, false, "00000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA1, false, true, "00000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA1, true, false, "00000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA1, true, true, "00000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA256, false, false, "00000000000000000000000000000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA256, false, true, "00000000000000000000000000000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA256, true, false, "00000000000000000000000000000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA256, true, true, "00000000000000000000000000000000000000000000000000000000deadbeef")] + [InlineData(ItemKey.SHA384, false, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, false, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA512, false, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, false, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SpamSum, false, false, "3::")] + [InlineData(ItemKey.SpamSum, false, true, "3::")] + [InlineData(ItemKey.SpamSum, true, false, "3::")] + [InlineData(ItemKey.SpamSum, true, true, "3::")] + public void GetKeyDBTest(ItemKey bucketedBy, bool lower, bool norename, string expected) + { + Source source = new Source(0); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "Machine"); + + DatItem datItem = new File + { + CRC = "DEADBEEF", + MD5 = "DEADBEEF", + SHA1 = "DEADBEEF", + SHA256 = "DEADBEEF", + }; + + string actual = datItem.GetKey(bucketedBy, machine, source, lower, norename); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/Formats/MediaTests.cs b/SabreTools.Metadata.DatItems.Test/Formats/MediaTests.cs new file mode 100644 index 00000000..487c0100 --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/Formats/MediaTests.cs @@ -0,0 +1,323 @@ +using SabreTools.Hashing; +using Xunit; + +namespace SabreTools.Metadata.DatItems.Formats.Test +{ + public class MediaTests + { + #region ConvertToRom + + [Fact] + public void ConvertToRomTest() + { + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "XXXXXX"); + + Source source = new Source(0, "XXXXXX"); + + Media media = new Media(); + media.SetName("XXXXXX"); + media.SetFieldValue(Data.Models.Metadata.Media.MD5Key, HashType.MD5.ZeroString); + media.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, HashType.SHA1.ZeroString); + media.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, HashType.SHA256.ZeroString); + media.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, HashType.SpamSum.ZeroString); + media.SetFieldValue(DatItem.DupeTypeKey, DupeType.All | DupeType.External); + media.SetFieldValue(DatItem.MachineKey, machine); + media.SetFieldValue(DatItem.RemoveKey, (bool?)false); + media.SetFieldValue(DatItem.SourceKey, source); + + Rom actual = media.ConvertToRom(); + + Assert.Equal("XXXXXX.aaruf", actual.GetName()); + Assert.Equal(HashType.MD5.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Equal(HashType.SHA1.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal(HashType.SHA256.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Equal(HashType.SpamSum.ZeroString, actual.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + Assert.Equal(DupeType.All | DupeType.External, actual.GetFieldValue(DatItem.DupeTypeKey)); + + Machine? actualMachine = actual.GetMachine(); + Assert.NotNull(actualMachine); + Assert.Equal("XXXXXX", actualMachine.GetName()); + + Assert.Equal(false, actual.GetBoolFieldValue(DatItem.RemoveKey)); + + Source? actualSource = actual.GetFieldValue(DatItem.SourceKey); + Assert.NotNull(actualSource); + Assert.Equal(0, actualSource.Index); + Assert.Equal("XXXXXX", actualSource.Name); + } + + #endregion + + #region FillMissingInformation + + [Fact] + public void FillMissingInformation_BothEmpty() + { + Media self = new Media(); + Media other = new Media(); + + self.FillMissingInformation(other); + + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)); + } + + [Fact] + public void FillMissingInformation_AllMissing() + { + Media self = new Media(); + + Media other = new Media(); + other.SetFieldValue(Data.Models.Metadata.Media.MD5Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, "XXXXXX"); + + self.FillMissingInformation(other); + + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Media.MD5Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey)); + } + + #endregion + + #region HasHashes + + [Fact] + public void HasHashes_NoHash_False() + { + Media self = new Media(); + bool actual = self.HasHashes(); + Assert.False(actual); + } + + [Fact] + public void HasHashes_MD5_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA1_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA256_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SpamSum_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, "XXXXXX"); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_All_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, "XXXXXX"); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + #endregion + + #region HasZeroHash + + [Fact] + public void HasZeroHash_NoHash_True() + { + Media self = new Media(); + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_NonZeroHash_False() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, "XXXXXX"); + + bool actual = self.HasZeroHash(); + Assert.False(actual); + } + + [Fact] + public void HasZeroHash_ZeroMD5_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, HashType.MD5.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA1_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, HashType.SHA1.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA256_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, HashType.SHA256.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSpamSum_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, HashType.SpamSum.ZeroString); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroAll_True() + { + Media self = new Media(); + self.SetFieldValue(Data.Models.Metadata.Media.MD5Key, HashType.MD5.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, HashType.SHA1.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, HashType.SHA256.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, HashType.SpamSum.ZeroString); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + #endregion + + // TODO: Change when Machine retrieval gets fixed + #region GetKey + + [Theory] + [InlineData(ItemKey.NULL, false, false, "")] + [InlineData(ItemKey.NULL, false, true, "")] + [InlineData(ItemKey.NULL, true, false, "")] + [InlineData(ItemKey.NULL, true, true, "")] + [InlineData(ItemKey.Machine, false, false, "0000000000-Machine")] + [InlineData(ItemKey.Machine, false, true, "Machine")] + [InlineData(ItemKey.Machine, true, false, "0000000000-machine")] + [InlineData(ItemKey.Machine, true, true, "machine")] + [InlineData(ItemKey.CRC, false, false, "00000000")] + [InlineData(ItemKey.CRC, false, true, "00000000")] + [InlineData(ItemKey.CRC, true, false, "00000000")] + [InlineData(ItemKey.CRC, true, true, "00000000")] + [InlineData(ItemKey.MD2, false, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, false, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, false, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD2, true, true, "8350e5a3e24c153df2275c9f80692773")] + [InlineData(ItemKey.MD4, false, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, false, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, false, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD4, true, true, "31d6cfe0d16ae931b73c59d7e0c089c0")] + [InlineData(ItemKey.MD5, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD5, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD5, true, false, "deadbeef")] + [InlineData(ItemKey.MD5, true, true, "deadbeef")] + [InlineData(ItemKey.SHA1, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA1, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA1, true, false, "deadbeef")] + [InlineData(ItemKey.SHA1, true, true, "deadbeef")] + [InlineData(ItemKey.SHA256, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA256, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA256, true, false, "deadbeef")] + [InlineData(ItemKey.SHA256, true, true, "deadbeef")] + [InlineData(ItemKey.SHA384, false, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, false, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, false, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA384, true, true, "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")] + [InlineData(ItemKey.SHA512, false, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, false, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, false, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SHA512, true, true, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData(ItemKey.SpamSum, false, false, "DEADBEEF")] + [InlineData(ItemKey.SpamSum, false, true, "DEADBEEF")] + [InlineData(ItemKey.SpamSum, true, false, "deadbeef")] + [InlineData(ItemKey.SpamSum, true, true, "deadbeef")] + public void GetKeyDBTest(ItemKey bucketedBy, bool lower, bool norename, string expected) + { + Source source = new Source(0); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "Machine"); + + DatItem datItem = new Media(); + datItem.SetFieldValue(Data.Models.Metadata.Media.MD5Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Media.SHA1Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Media.SHA256Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Media.SpamSumKey, "DEADBEEF"); + + string actual = datItem.GetKey(bucketedBy, machine, source, lower, norename); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/Formats/RomTests.cs b/SabreTools.Metadata.DatItems.Test/Formats/RomTests.cs new file mode 100644 index 00000000..18bcea6c --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/Formats/RomTests.cs @@ -0,0 +1,672 @@ +using SabreTools.Hashing; +using Xunit; + +namespace SabreTools.Metadata.DatItems.Formats.Test +{ + public class RomTests + { + #region FillMissingInformation + + [Fact] + public void FillMissingInformation_BothEmpty() + { + Rom self = new Rom(); + Rom other = new Rom(); + + self.FillMissingInformation(other); + + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Null(self.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + } + + [Fact] + public void FillMissingInformation_AllMissing() + { + Rom self = new Rom(); + + Rom other = new Rom(); + other.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "XXXXXX"); + other.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "XXXXXX"); + + self.FillMissingInformation(other); + + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key)); + Assert.Equal("XXXXXX", self.GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey)); + } + + #endregion + + #region HasHashes + + [Fact] + public void HasHashes_NoHash_False() + { + Rom self = new Rom(); + bool actual = self.HasHashes(); + Assert.False(actual); + } + + [Fact] + public void HasHashes_CRC_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_MD2_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_MD4_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_MD5_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_RIPEMD128_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_RIPEMD160_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA1_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA256_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA384_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SHA512_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_SpamSum_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "XXXXXX"); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + [Fact] + public void HasHashes_All_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "XXXXXX"); + + bool actual = self.HasHashes(); + Assert.True(actual); + } + + #endregion + + #region HasZeroHash + + [Fact] + public void HasZeroHash_NoHash_True() + { + Rom self = new Rom(); + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_NonZeroHash_False() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "XXXXXX"); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "XXXXXX"); + + bool actual = self.HasZeroHash(); + Assert.False(actual); + } + + [Fact] + public void HasZeroHash_ZeroCRC_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroMD2_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, HashType.MD2.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroMD4_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, HashType.MD4.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroMD5_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, HashType.MD5.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroRIPEMD128_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, HashType.RIPEMD128.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroRIPEMD160_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, HashType.RIPEMD160.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA1_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA256_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, HashType.SHA256.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA384_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, HashType.SHA384.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSHA512_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, HashType.SHA512.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, string.Empty); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroSpamSum_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, string.Empty); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, HashType.SpamSum.ZeroString); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + [Fact] + public void HasZeroHash_ZeroAll_True() + { + Rom self = new Rom(); + self.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, HashType.CRC32.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, HashType.MD2.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, HashType.MD4.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, HashType.MD5.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, HashType.RIPEMD128.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, HashType.RIPEMD160.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, HashType.SHA1.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, HashType.SHA256.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, HashType.SHA384.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, HashType.SHA512.ZeroString); + self.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, HashType.SpamSum.ZeroString); + + bool actual = self.HasZeroHash(); + Assert.True(actual); + } + + #endregion + + // TODO: Change when Machine retrieval gets fixed + #region GetKey + + [Theory] + [InlineData(ItemKey.NULL, false, false, "")] + [InlineData(ItemKey.NULL, false, true, "")] + [InlineData(ItemKey.NULL, true, false, "")] + [InlineData(ItemKey.NULL, true, true, "")] + [InlineData(ItemKey.Machine, false, false, "0000000000-Machine")] + [InlineData(ItemKey.Machine, false, true, "Machine")] + [InlineData(ItemKey.Machine, true, false, "0000000000-machine")] + [InlineData(ItemKey.Machine, true, true, "machine")] + [InlineData(ItemKey.CRC, false, false, "DEADBEEF")] + [InlineData(ItemKey.CRC, false, true, "DEADBEEF")] + [InlineData(ItemKey.CRC, true, false, "deadbeef")] + [InlineData(ItemKey.CRC, true, true, "deadbeef")] + [InlineData(ItemKey.MD2, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD2, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD2, true, false, "deadbeef")] + [InlineData(ItemKey.MD2, true, true, "deadbeef")] + [InlineData(ItemKey.MD4, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD4, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD4, true, false, "deadbeef")] + [InlineData(ItemKey.MD4, true, true, "deadbeef")] + [InlineData(ItemKey.MD5, false, false, "DEADBEEF")] + [InlineData(ItemKey.MD5, false, true, "DEADBEEF")] + [InlineData(ItemKey.MD5, true, false, "deadbeef")] + [InlineData(ItemKey.MD5, true, true, "deadbeef")] + [InlineData(ItemKey.RIPEMD128, false, false, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD128, false, true, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD128, true, false, "deadbeef")] + [InlineData(ItemKey.RIPEMD128, true, true, "deadbeef")] + [InlineData(ItemKey.RIPEMD160, false, false, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD160, false, true, "DEADBEEF")] + [InlineData(ItemKey.RIPEMD160, true, false, "deadbeef")] + [InlineData(ItemKey.RIPEMD160, true, true, "deadbeef")] + [InlineData(ItemKey.SHA1, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA1, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA1, true, false, "deadbeef")] + [InlineData(ItemKey.SHA1, true, true, "deadbeef")] + [InlineData(ItemKey.SHA256, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA256, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA256, true, false, "deadbeef")] + [InlineData(ItemKey.SHA256, true, true, "deadbeef")] + [InlineData(ItemKey.SHA384, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA384, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA384, true, false, "deadbeef")] + [InlineData(ItemKey.SHA384, true, true, "deadbeef")] + [InlineData(ItemKey.SHA512, false, false, "DEADBEEF")] + [InlineData(ItemKey.SHA512, false, true, "DEADBEEF")] + [InlineData(ItemKey.SHA512, true, false, "deadbeef")] + [InlineData(ItemKey.SHA512, true, true, "deadbeef")] + [InlineData(ItemKey.SpamSum, false, false, "DEADBEEF")] + [InlineData(ItemKey.SpamSum, false, true, "DEADBEEF")] + [InlineData(ItemKey.SpamSum, true, false, "deadbeef")] + [InlineData(ItemKey.SpamSum, true, true, "deadbeef")] + public void GetKeyDBTest(ItemKey bucketedBy, bool lower, bool norename, string expected) + { + Source source = new Source(0); + + Machine machine = new Machine(); + machine.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "Machine"); + + DatItem datItem = new Rom(); + datItem.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD2Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD4Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, "DEADBEEF"); + datItem.SetFieldValue(Data.Models.Metadata.Rom.SpamSumKey, "DEADBEEF"); + + string actual = datItem.GetKey(bucketedBy, machine, source, lower, norename); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/MachineTests.cs b/SabreTools.Metadata.DatItems.Test/MachineTests.cs new file mode 100644 index 00000000..7bdb5904 --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/MachineTests.cs @@ -0,0 +1,87 @@ +using Xunit; + +namespace SabreTools.Metadata.DatItems.Test +{ + public class MachineTests + { + #region Clone + + [Fact] + public void CloneTest() + { + Machine item = new Machine(); + item.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "name"); + + object clone = item.Clone(); + Machine? actual = clone as Machine; + Assert.NotNull(actual); + Assert.Equal("name", actual.GetName()); + } + + #endregion + + #region GetInternalClone + + [Fact] + public void GetInternalCloneTest() + { + Machine item = new Machine(); + item.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "name"); + + Data.Models.Metadata.Machine actual = item.GetInternalClone(); + Assert.Equal("name", actual[Data.Models.Metadata.Machine.NameKey]); + } + + #endregion + + #region Equals + + [Fact] + public void Equals_Null_False() + { + Machine self = new Machine(); + Machine? other = null; + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_DefaultInternal_True() + { + Machine self = new Machine(); + Machine? other = new Machine(); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + [Fact] + public void Equals_MismatchedInternal_False() + { + Machine self = new Machine(); + self.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "self"); + + Machine? other = new Machine(); + other.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "other"); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_EqualInternal_True() + { + Machine self = new Machine(); + self.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "name"); + + Machine? other = new Machine(); + other.SetFieldValue(Data.Models.Metadata.Machine.NameKey, "name"); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems.Test/SabreTools.Metadata.DatItems.Test.csproj b/SabreTools.Metadata.DatItems.Test/SabreTools.Metadata.DatItems.Test.csproj new file mode 100644 index 00000000..e029bb79 --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/SabreTools.Metadata.DatItems.Test.csproj @@ -0,0 +1,22 @@ + + + + net8.0;net9.0;net10.0 + false + latest + enable + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SabreTools.Metadata.DatItems.Test/SourceTests.cs b/SabreTools.Metadata.DatItems.Test/SourceTests.cs new file mode 100644 index 00000000..af057edd --- /dev/null +++ b/SabreTools.Metadata.DatItems.Test/SourceTests.cs @@ -0,0 +1,23 @@ +using Xunit; + +namespace SabreTools.Metadata.DatItems.Test +{ + public class SourceTests + { + #region Clone + + [Fact] + public void CloneTest() + { + Source item = new Source(1, source: "src"); + + object clone = item.Clone(); + Source? actual = clone as Source; + Assert.NotNull(actual); + Assert.Equal(1, actual.Index); + Assert.Equal("src", actual.Name); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/DatItem.cs b/SabreTools.Metadata.DatItems/DatItem.cs new file mode 100644 index 00000000..0f811747 --- /dev/null +++ b/SabreTools.Metadata.DatItems/DatItem.cs @@ -0,0 +1,346 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Hashing; +using SabreTools.IO.Logging; +using SabreTools.Metadata.DatItems.Formats; +using SabreTools.Metadata.Filter; + +namespace SabreTools.Metadata.DatItems +{ + /// + /// Base class for all items included in a set + /// + [JsonObject("datitem"), XmlRoot("datitem")] + [XmlInclude(typeof(Adjuster))] + [XmlInclude(typeof(Analog))] + [XmlInclude(typeof(Archive))] + [XmlInclude(typeof(BiosSet))] + [XmlInclude(typeof(Blank))] + [XmlInclude(typeof(Chip))] + [XmlInclude(typeof(Condition))] + [XmlInclude(typeof(Configuration))] + [XmlInclude(typeof(ConfLocation))] + [XmlInclude(typeof(ConfSetting))] + [XmlInclude(typeof(Control))] + [XmlInclude(typeof(DataArea))] + [XmlInclude(typeof(Device))] + [XmlInclude(typeof(DeviceRef))] + [XmlInclude(typeof(DipLocation))] + [XmlInclude(typeof(DipSwitch))] + [XmlInclude(typeof(DipValue))] + [XmlInclude(typeof(Disk))] + [XmlInclude(typeof(DiskArea))] + [XmlInclude(typeof(Display))] + [XmlInclude(typeof(Driver))] + [XmlInclude(typeof(Extension))] + [XmlInclude(typeof(Feature))] + [XmlInclude(typeof(Info))] + [XmlInclude(typeof(Input))] + [XmlInclude(typeof(Instance))] + [XmlInclude(typeof(Media))] + [XmlInclude(typeof(Part))] + [XmlInclude(typeof(PartFeature))] + [XmlInclude(typeof(Port))] + [XmlInclude(typeof(RamOption))] + [XmlInclude(typeof(Release))] + [XmlInclude(typeof(Rom))] + [XmlInclude(typeof(Sample))] + [XmlInclude(typeof(SharedFeat))] + [XmlInclude(typeof(Slot))] + [XmlInclude(typeof(SlotOption))] + [XmlInclude(typeof(SoftwareList))] + [XmlInclude(typeof(Sound))] + public abstract class DatItem : ModelBackedItem, IEquatable, IComparable, ICloneable + { + #region Constants + + /// + /// Duplicate type when compared to another item + /// + public const string DupeTypeKey = "DUPETYPE"; + + /// + /// Machine associated with the item + /// + public const string MachineKey = "MACHINE"; + + /// + /// Flag if item should be removed + /// + public const string RemoveKey = "REMOVE"; + + /// + /// Source information + /// + public const string SourceKey = "SOURCE"; + + #endregion + + #region Fields + + /// + /// Item type for the object + /// + protected abstract ItemType ItemType { get; } + + #endregion + + #region Logging + + /// + /// Static logger for static methods + /// + [JsonIgnore, XmlIgnore] + protected static readonly Logger _staticLogger = new(); + + #endregion + + #region Accessors + + /// + /// Get the machine for a DatItem + /// + /// Machine if available, null otherwise + /// Relies on + public Machine? GetMachine() => _internal.Read(MachineKey); + + /// + /// Gets the name to use for a DatItem + /// + /// Name if available, null otherwise + public virtual string? GetName() => _internal.GetName(); + + /// + /// Sets the name to use for a DatItem + /// + /// Name to set for the item + public virtual void SetName(string? name) => _internal.SetName(name); + + #endregion + + #region Cloning Methods + + /// + /// Clone the DatItem + /// + /// Clone of the DatItem + public abstract object Clone(); + + /// + /// Copy all machine information over in one shot + /// + /// Existing item to copy information from + public void CopyMachineInformation(DatItem item) + { + // If there is no machine + if (!item._internal.ContainsKey(MachineKey)) + return; + + var machine = item.GetMachine(); + CopyMachineInformation(machine); + } + + /// + /// Copy all machine information over in one shot + /// + /// Existing machine to copy information from + public void CopyMachineInformation(Machine? machine) + { + if (machine is null) + return; + + if (machine.Clone() is Machine cloned) + SetFieldValue(MachineKey, cloned); + } + + #endregion + + #region Comparision Methods + + /// + public int CompareTo(DatItem? other) + { + // If the other item doesn't exist + if (other is null) + return 1; + + // Get the names to avoid changing values + string? selfName = GetName(); + string? otherName = other.GetName(); + + // If the names are equal + if (selfName == otherName) + return Equals(other) ? 0 : 1; + + // If `otherName` is null, Compare will return > 0 + // If `selfName` is null, Compare will return < 0 + return string.Compare(selfName, otherName, StringComparison.Ordinal); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not DatItem otherItem) + return false; + + // Compare internal models + return _internal.Equals(otherItem); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not DatItem otherItem) + return false; + + // Compare internal models + return _internal.Equals(otherItem); + } + + /// + /// Determine if an item is a duplicate using partial matching logic + /// + /// DatItem to use as a baseline + /// True if the items are duplicates, false otherwise + public virtual bool Equals(DatItem? other) + { + // If the other item is null + if (other is null) + return false; + + // Get the types for comparison + ItemType selfType = GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType(); + ItemType otherType = other.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType(); + + // If we don't have a matched type, return false + if (selfType != otherType) + return false; + + // Compare the internal models + return _internal.EqualTo(other._internal); + } + + #endregion + + #region Manipulation + + /// + /// Runs a filter and determines if it passes or not + /// + /// Filter runner to use for checking + /// True if the item and its machine passes the filter, false otherwise + public bool PassesFilter(FilterRunner filterRunner) + { + var machine = GetMachine(); + if (machine is not null && !machine.PassesFilter(filterRunner)) + return false; + + return filterRunner.Run(_internal); + } + + /// + /// Runs a filter and determines if it passes or not + /// + /// Filter runner to use for checking + /// True if the item passes the filter, false otherwise + public bool PassesFilterDB(FilterRunner filterRunner) + => filterRunner.Run(_internal); + + #endregion + + #region Sorting and Merging + + /// + /// Get the dictionary key that should be used for a given item and bucketing type + /// + /// ItemKey value representing what key to get + /// Machine associated with the item for renaming + /// Source associated with the item for renaming + /// True if the key should be lowercased (default), false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// String representing the key to be used for the DatItem + public virtual string GetKey(ItemKey bucketedBy, Machine? machine, Source? source, bool lower = true, bool norename = true) + { + // Set the output key as the default blank string + string key = string.Empty; + + string sourceKeyPadded = source?.Index.ToString().PadLeft(10, '0') + '-'; + string machineName = machine?.GetName() ?? "Default"; + +#pragma warning disable IDE0010 + // Now determine what the key should be based on the bucketedBy value + switch (bucketedBy) + { + case ItemKey.CRC: + key = HashType.CRC32.ZeroString; + break; + + case ItemKey.Machine: + key = (norename ? string.Empty : sourceKeyPadded) + machineName; + break; + + case ItemKey.MD2: + key = HashType.MD2.ZeroString; + break; + + case ItemKey.MD4: + key = HashType.MD4.ZeroString; + break; + + case ItemKey.MD5: + key = HashType.MD5.ZeroString; + break; + + case ItemKey.RIPEMD128: + key = HashType.RIPEMD128.ZeroString; + break; + + case ItemKey.RIPEMD160: + key = HashType.RIPEMD160.ZeroString; + break; + + case ItemKey.SHA1: + key = HashType.SHA1.ZeroString; + break; + + case ItemKey.SHA256: + key = HashType.SHA256.ZeroString; + break; + + case ItemKey.SHA384: + key = HashType.SHA384.ZeroString; + break; + + case ItemKey.SHA512: + key = HashType.SHA512.ZeroString; + break; + + case ItemKey.SpamSum: + key = HashType.SpamSum.ZeroString; + break; + } +#pragma warning restore IDE0010 + + // Double and triple check the key for corner cases + key ??= string.Empty; + if (lower) + key = key.ToLowerInvariant(); + + return key; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/DatItemT.cs b/SabreTools.Metadata.DatItems/DatItemT.cs new file mode 100644 index 00000000..5b72964e --- /dev/null +++ b/SabreTools.Metadata.DatItems/DatItemT.cs @@ -0,0 +1,112 @@ +using System; +using System.Reflection; + +namespace SabreTools.Metadata.DatItems +{ + /// + /// Base class for all items included in a set that are backed by an internal model + /// + public abstract class DatItem : DatItem, IEquatable>, IComparable>, ICloneable where T : Data.Models.Metadata.DatItem + { + #region Constructors + + /// + /// Create a default, empty object + /// + public DatItem() + { + _internal = Activator.CreateInstance(); + + SetName(string.Empty); + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType); + SetFieldValue(MachineKey, new Machine()); + } + + /// + /// Create an object from the internal model + /// + public DatItem(T item) + { + _internal = item; + + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType); + SetFieldValue(MachineKey, new Machine()); + } + + #endregion + + #region Cloning Methods + + /// + /// Clone the DatItem + /// + /// Clone of the DatItem + /// + /// Throws an exception if there is a DatItem implementation + /// that is not a part of this library. + /// + public override object Clone() + { + var concrete = Array.Find(Assembly.GetExecutingAssembly().GetTypes(), + t => !t.IsAbstract && t.IsClass && t.BaseType == typeof(DatItem)); + + var clone = Activator.CreateInstance(concrete!); + (clone as DatItem)!._internal = _internal?.Clone() as T ?? Activator.CreateInstance(); + return clone; + } + + /// + /// Get a clone of the current internal model + /// + public virtual T GetInternalClone() => (_internal.Clone() as T)!; + + #endregion + + #region Comparision Methods + + /// + public int CompareTo(DatItem? other) + { + // If the other item doesn't exist + if (other is null) + return 1; + + // Get the names to avoid changing values + string? selfName = GetName(); + string? otherName = other.GetName(); + + // If the names are equal + if (selfName == otherName) + return Equals(other) ? 0 : 1; + + // If `otherName` is null, Compare will return > 0 + // If `selfName` is null, Compare will return < 0 + return string.Compare(selfName, otherName, StringComparison.Ordinal); + } + + /// + /// Determine if an item is a duplicate using partial matching logic + /// + /// DatItem to use as a baseline + /// True if the items are duplicates, false otherwise + public virtual bool Equals(DatItem? other) + { + // If the other value is null + if (other is null) + return false; + + // Get the types for comparison + ItemType selfType = GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType(); + ItemType otherType = other.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType(); + + // If we don't have a matched type, return false + if (selfType != otherType) + return false; + + // Compare the internal models + return _internal.EqualTo(other._internal); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Enums.cs b/SabreTools.Metadata.DatItems/Enums.cs new file mode 100644 index 00000000..efb40015 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Enums.cs @@ -0,0 +1,685 @@ +using System; + +namespace SabreTools.Metadata.DatItems +{ + /// + /// Determine the chip type + /// + [Flags] + public enum ChipType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("cpu")] + CPU = 1 << 0, + + [Mapping("audio")] + Audio = 1 << 1, + } + + /// + /// Determine the control type + /// + [Flags] + public enum ControlType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("joy")] + Joy = 1 << 0, + + [Mapping("stick")] + Stick = 1 << 1, + + [Mapping("paddle")] + Paddle = 1 << 2, + + [Mapping("pedal")] + Pedal = 1 << 3, + + [Mapping("lightgun")] + Lightgun = 1 << 4, + + [Mapping("positional")] + Positional = 1 << 5, + + [Mapping("dial")] + Dial = 1 << 6, + + [Mapping("trackball")] + Trackball = 1 << 7, + + [Mapping("mouse")] + Mouse = 1 << 8, + + [Mapping("only_buttons")] + OnlyButtons = 1 << 9, + + [Mapping("keypad")] + Keypad = 1 << 10, + + [Mapping("keyboard")] + Keyboard = 1 << 11, + + [Mapping("mahjong")] + Mahjong = 1 << 12, + + [Mapping("hanafuda")] + Hanafuda = 1 << 13, + + [Mapping("gambling")] + Gambling = 1 << 14, + } + + /// + /// Determine the device type + /// + [Flags] + public enum DeviceType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("unknown")] + Unknown = 1 << 0, + + [Mapping("cartridge")] + Cartridge = 1 << 1, + + [Mapping("floppydisk")] + FloppyDisk = 1 << 2, + + [Mapping("harddisk")] + HardDisk = 1 << 3, + + [Mapping("cylinder")] + Cylinder = 1 << 4, + + [Mapping("cassette")] + Cassette = 1 << 5, + + [Mapping("punchcard")] + PunchCard = 1 << 6, + + [Mapping("punchtape")] + PunchTape = 1 << 7, + + [Mapping("printout")] + Printout = 1 << 8, + + [Mapping("serial")] + Serial = 1 << 9, + + [Mapping("parallel")] + Parallel = 1 << 10, + + [Mapping("snapshot")] + Snapshot = 1 << 11, + + [Mapping("quickload")] + QuickLoad = 1 << 12, + + [Mapping("memcard")] + MemCard = 1 << 13, + + [Mapping("cdrom")] + CDROM = 1 << 14, + + [Mapping("magtape")] + MagTape = 1 << 15, + + [Mapping("romimage")] + ROMImage = 1 << 16, + + [Mapping("midiin")] + MIDIIn = 1 << 17, + + [Mapping("midiout")] + MIDIOut = 1 << 18, + + [Mapping("picture")] + Picture = 1 << 19, + + [Mapping("vidfile")] + VidFile = 1 << 20, + } + + /// + /// Determine the display type + /// + [Flags] + public enum DisplayType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("raster")] + Raster = 1 << 0, + + [Mapping("vector")] + Vector = 1 << 1, + + [Mapping("lcd")] + LCD = 1 << 2, + + [Mapping("svg")] + SVG = 1 << 3, + + [Mapping("unknown")] + Unknown = 1 << 4, + } + + /// + /// Determines which type of duplicate a file is + /// + [Flags] + public enum DupeType + { + // Type of match + Hash = 1 << 0, + All = 1 << 1, + + // Location of match + Internal = 1 << 2, + External = 1 << 3, + } + + /// + /// Determine the endianness + /// + [Flags] + public enum Endianness + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("big")] + Big = 1 << 0, + + [Mapping("little")] + Little = 1 << 1, + } + + /// + /// Determine the emulation status + /// + [Flags] + public enum FeatureStatus + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("unemulated")] + Unemulated = 1 << 0, + + [Mapping("imperfect")] + Imperfect = 1 << 1, + } + + /// + /// Determine the feature type + /// + [Flags] + public enum FeatureType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("protection")] + Protection = 1 << 0, + + [Mapping("palette")] + Palette = 1 << 1, + + [Mapping("graphics")] + Graphics = 1 << 2, + + [Mapping("sound")] + Sound = 1 << 3, + + [Mapping("controls")] + Controls = 1 << 4, + + [Mapping("keyboard")] + Keyboard = 1 << 5, + + [Mapping("mouse")] + Mouse = 1 << 6, + + [Mapping("microphone")] + Microphone = 1 << 7, + + [Mapping("camera")] + Camera = 1 << 8, + + [Mapping("disk")] + Disk = 1 << 9, + + [Mapping("printer")] + Printer = 1 << 10, + + [Mapping("lan")] + Lan = 1 << 11, + + [Mapping("wan")] + Wan = 1 << 12, + + [Mapping("timing")] + Timing = 1 << 13, + } + + /// + /// A subset of fields that can be used as keys + /// + public enum ItemKey + { + NULL = 0, + + Machine, + + CRC, + MD2, + MD4, + MD5, + RIPEMD128, + RIPEMD160, + SHA1, + SHA256, + SHA384, + SHA512, + SpamSum, + } + + /// + /// Determine the status of the item + /// + [Flags] + public enum ItemStatus + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("none", "no")] + None = 1 << 0, + + [Mapping("good")] + Good = 1 << 1, + + [Mapping("baddump")] + BadDump = 1 << 2, + + [Mapping("nodump", "yes")] + Nodump = 1 << 3, + + [Mapping("verified")] + Verified = 1 << 4, + } + + /// + /// Determine what type of file an item is + /// + public enum ItemType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + // "Actionable" item types + + [Mapping("rom")] + Rom, + + [Mapping("disk")] + Disk, + + [Mapping("file")] + File, + + [Mapping("media")] + Media, + + // "Auxiliary" item types + + [Mapping("adjuster")] + Adjuster, + + [Mapping("analog")] + Analog, + + [Mapping("archive")] + Archive, + + [Mapping("biosset")] + BiosSet, + + [Mapping("chip")] + Chip, + + [Mapping("condition")] + Condition, + + [Mapping("configuration")] + Configuration, + + [Mapping("conflocation")] + ConfLocation, + + [Mapping("confsetting")] + ConfSetting, + + [Mapping("control")] + Control, + + [Mapping("dataarea")] + DataArea, + + [Mapping("device")] + Device, + + [Mapping("device_ref", "deviceref")] + DeviceRef, + + [Mapping("diplocation")] + DipLocation, + + [Mapping("dipswitch")] + DipSwitch, + + [Mapping("dipvalue")] + DipValue, + + [Mapping("diskarea")] + DiskArea, + + [Mapping("display")] + Display, + + [Mapping("driver")] + Driver, + + [Mapping("extension")] + Extension, + + [Mapping("feature")] + Feature, + + [Mapping("info")] + Info, + + [Mapping("input")] + Input, + + [Mapping("instance")] + Instance, + + [Mapping("original")] + Original, + + [Mapping("part")] + Part, + + [Mapping("part_feature", "partfeature")] + PartFeature, + + [Mapping("port")] + Port, + + [Mapping("ramoption", "ram_option")] + RamOption, + + [Mapping("release")] + Release, + + [Mapping("release_details", "releasedetails")] + ReleaseDetails, + + [Mapping("sample")] + Sample, + + [Mapping("serials")] + Serials, + + [Mapping("sharedfeat", "shared_feat", "sharedfeature", "shared_feature")] + SharedFeat, + + [Mapping("slot")] + Slot, + + [Mapping("slotoption", "slot_option")] + SlotOption, + + [Mapping("softwarelist", "software_list")] + SoftwareList, + + [Mapping("sound")] + Sound, + + [Mapping("source_details", "sourcedetails")] + SourceDetails, + + [Mapping("blank")] + Blank = 99, // This is not a real type, only used internally + } + + /// + /// Determine the loadflag value + /// + [Flags] + public enum LoadFlag + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("load16_byte")] + Load16Byte = 1 << 0, + + [Mapping("load16_word")] + Load16Word = 1 << 1, + + [Mapping("load16_word_swap")] + Load16WordSwap = 1 << 2, + + [Mapping("load32_byte")] + Load32Byte = 1 << 3, + + [Mapping("load32_word")] + Load32Word = 1 << 4, + + [Mapping("load32_word_swap")] + Load32WordSwap = 1 << 5, + + [Mapping("load32_dword")] + Load32DWord = 1 << 6, + + [Mapping("load64_word")] + Load64Word = 1 << 7, + + [Mapping("load64_word_swap")] + Load64WordSwap = 1 << 8, + + [Mapping("reload")] + Reload = 1 << 9, + + [Mapping("fill")] + Fill = 1 << 10, + + [Mapping("continue")] + Continue = 1 << 11, + + [Mapping("reload_plain")] + ReloadPlain = 1 << 12, + + [Mapping("ignore")] + Ignore = 1 << 13, + } + + /// + /// Determine what type of machine it is + /// + [Flags] + public enum MachineType + { + [Mapping("none")] + None = 0, + + [Mapping("bios")] + Bios = 1 << 0, + + [Mapping("device", "dev")] + Device = 1 << 1, + + [Mapping("mechanical", "mech")] + Mechanical = 1 << 2, + } + + /// + /// Determine which OpenMSX subtype an item is + /// + [Flags] + public enum OpenMSXSubType + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("rom")] + Rom = 1 << 0, + + [Mapping("megarom")] + MegaRom = 1 << 1, + + [Mapping("sccpluscart")] + SCCPlusCart = 1 << 2, + } + + /// + /// Determine relation of value to condition + /// + [Flags] + public enum Relation + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("eq")] + Equal = 1 << 0, + + [Mapping("ne")] + NotEqual = 1 << 1, + + [Mapping("gt")] + GreaterThan = 1 << 2, + + [Mapping("le")] + LessThanOrEqual = 1 << 3, + + [Mapping("lt")] + LessThan = 1 << 4, + + [Mapping("ge")] + GreaterThanOrEqual = 1 << 5, + } + + /// + /// Determine machine runnable status + /// + [Flags] + public enum Runnable + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("no")] + No = 1 << 0, + + [Mapping("partial")] + Partial = 1 << 1, + + [Mapping("yes")] + Yes = 1 << 2, + } + + /// + /// Determine software list status + /// + [Flags] + public enum SoftwareListStatus + { + [Mapping("none")] + None = 0, + + [Mapping("original")] + Original = 1 << 0, + + [Mapping("compatible")] + Compatible = 1 << 1, + } + + /// + /// Determine machine support status + /// + [Flags] + public enum Supported + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("no", "unsupported")] + No = 1 << 0, + + [Mapping("partial")] + Partial = 1 << 1, + + [Mapping("yes", "supported")] + Yes = 1 << 2, + } + + /// + /// Determine driver support statuses + /// + [Flags] + public enum SupportStatus + { + /// + /// This is a fake flag that is used for filter only + /// + NULL = 0, + + [Mapping("good")] + Good = 1 << 0, + + [Mapping("imperfect")] + Imperfect = 1 << 1, + + [Mapping("preliminary")] + Preliminary = 1 << 2, + } +} diff --git a/SabreTools.Metadata.DatItems/Extensions.cs b/SabreTools.Metadata.DatItems/Extensions.cs new file mode 100644 index 00000000..c522ff7a --- /dev/null +++ b/SabreTools.Metadata.DatItems/Extensions.cs @@ -0,0 +1,788 @@ +using System.Collections.Generic; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems +{ + public static class Extensions + { + #region Private Maps + + /// + /// Set of enum to string mappings for ChipType + /// + private static readonly Dictionary _toChipTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for ChipType + /// + private static readonly Dictionary _fromChipTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for ControlType + /// + private static readonly Dictionary _toControlTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for ControlType + /// + private static readonly Dictionary _fromControlTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for DeviceType + /// + private static readonly Dictionary _toDeviceTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for DeviceType + /// + private static readonly Dictionary _fromDeviceTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for DisplayType + /// + private static readonly Dictionary _toDisplayTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for DisplayType + /// + private static readonly Dictionary _fromDisplayTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for Endianness + /// + private static readonly Dictionary _toEndiannessMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for Endianness + /// + private static readonly Dictionary _fromEndiannessMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for FeatureStatus + /// + private static readonly Dictionary _toFeatureStatusMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for FeatureStatus + /// + private static readonly Dictionary _fromFeatureStatusMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for FeatureType + /// + private static readonly Dictionary _toFeatureTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for FeatureType + /// + private static readonly Dictionary _fromFeatureTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for ItemStatus + /// + private static readonly Dictionary _toItemStatusMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for ItemStatus + /// + private static readonly Dictionary _fromItemStatusMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for ItemType + /// + private static readonly Dictionary _toItemTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for ItemType + /// + private static readonly Dictionary _fromItemTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for LoadFlag + /// + private static readonly Dictionary _toLoadFlagMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for LoadFlag + /// + private static readonly Dictionary _fromLoadFlagMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for MachineType + /// + private static readonly Dictionary _toMachineTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of enum to string mappings for OpenMSXSubType + /// + private static readonly Dictionary _toOpenMSXSubTypeMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for OpenMSXSubType + /// + private static readonly Dictionary _fromOpenMSXSubTypeMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for Relation + /// + private static readonly Dictionary _toRelationMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for Relation + /// + private static readonly Dictionary _fromRelationMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for Runnable + /// + private static readonly Dictionary _toRunnableMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for Runnable + /// + private static readonly Dictionary _fromRunnableMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for SoftwareListStatus + /// + private static readonly Dictionary _toSoftwareListStatusMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for SoftwareListStatus + /// + private static readonly Dictionary _fromSoftwareListStatusMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of enum to string mappings for Supported + /// + private static readonly Dictionary _toSupportedMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for Supported + /// + private static readonly Dictionary _fromSupportedMap = Converters.GenerateToString(useSecond: false); + + /// + /// Set of string to enum mappings for Supported (secondary) + /// + private static readonly Dictionary _fromSupportedSecondaryMap = Converters.GenerateToString(useSecond: true); + + /// + /// Set of enum to string mappings for SupportStatus + /// + private static readonly Dictionary _toSupportStatusMap = Converters.GenerateToEnum(); + + /// + /// Set of string to enum mappings for SupportStatus + /// + private static readonly Dictionary _fromSupportStatusMap = Converters.GenerateToString(useSecond: false); + + #endregion + + #region String to Enum + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static ChipType AsChipType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toChipTypeMap.ContainsKey(value)) + return _toChipTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static ControlType AsControlType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toControlTypeMap.ContainsKey(value)) + return _toControlTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static DeviceType AsDeviceType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toDeviceTypeMap.ContainsKey(value)) + return _toDeviceTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static DisplayType AsDisplayType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toDisplayTypeMap.ContainsKey(value)) + return _toDisplayTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static Endianness AsEndianness(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toEndiannessMap.ContainsKey(value)) + return _toEndiannessMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static FeatureStatus AsFeatureStatus(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toFeatureStatusMap.ContainsKey(value)) + return _toFeatureStatusMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static FeatureType AsFeatureType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toFeatureTypeMap.ContainsKey(value)) + return _toFeatureTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static ItemStatus AsItemStatus(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toItemStatusMap.ContainsKey(value)) + return _toItemStatusMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static ItemType AsItemType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toItemTypeMap.ContainsKey(value)) + return _toItemTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static LoadFlag AsLoadFlag(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toLoadFlagMap.ContainsKey(value)) + return _toLoadFlagMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static MachineType AsMachineType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toMachineTypeMap.ContainsKey(value)) + return _toMachineTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static OpenMSXSubType AsOpenMSXSubType(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toOpenMSXSubTypeMap.ContainsKey(value)) + return _toOpenMSXSubTypeMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static Relation AsRelation(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toRelationMap.ContainsKey(value)) + return _toRelationMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static Runnable AsRunnable(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toRunnableMap.ContainsKey(value)) + return _toRunnableMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static SoftwareListStatus AsSoftwareListStatus(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toSoftwareListStatusMap.ContainsKey(value)) + return _toSoftwareListStatusMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static Supported AsSupported(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toSupportedMap.ContainsKey(value)) + return _toSupportedMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + /// + /// Get the enum value for an input string, if possible + /// + /// String value to parse/param> + /// Enum value representing the input, default on error + public static SupportStatus AsSupportStatus(this string? value) + { + // Normalize the input value + value = value?.ToLowerInvariant(); + if (value is null) + return default; + + // Try to get the value from the mappings + if (_toSupportStatusMap.ContainsKey(value)) + return _toSupportStatusMap[value]; + + // Otherwise, return the default value for the enum + return default; + } + + #endregion + + #region Enum to String + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this ChipType value) + { + // Try to get the value from the mappings + if (_fromChipTypeMap.ContainsKey(value)) + return _fromChipTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this ControlType value) + { + // Try to get the value from the mappings + if (_fromControlTypeMap.ContainsKey(value)) + return _fromControlTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this DeviceType value) + { + // Try to get the value from the mappings + if (_fromDeviceTypeMap.ContainsKey(value)) + return _fromDeviceTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this DisplayType value) + { + // Try to get the value from the mappings + if (_fromDisplayTypeMap.ContainsKey(value)) + return _fromDisplayTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this Endianness value) + { + // Try to get the value from the mappings + if (_fromEndiannessMap.ContainsKey(value)) + return _fromEndiannessMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this FeatureStatus value) + { + // Try to get the value from the mappings + if (_fromFeatureStatusMap.ContainsKey(value)) + return _fromFeatureStatusMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this FeatureType value) + { + // Try to get the value from the mappings + if (_fromFeatureTypeMap.ContainsKey(value)) + return _fromFeatureTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this ItemStatus value) + { + // Try to get the value from the mappings + if (_fromItemStatusMap.ContainsKey(value)) + return _fromItemStatusMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this ItemType value) + { + // Try to get the value from the mappings + if (_fromItemTypeMap.ContainsKey(value)) + return _fromItemTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this LoadFlag value) + { + // Try to get the value from the mappings + if (_fromLoadFlagMap.ContainsKey(value)) + return _fromLoadFlagMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this OpenMSXSubType value) + { + // Try to get the value from the mappings + if (_fromOpenMSXSubTypeMap.ContainsKey(value)) + return _fromOpenMSXSubTypeMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this Relation value) + { + // Try to get the value from the mappings + if (_fromRelationMap.ContainsKey(value)) + return _fromRelationMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this Runnable value) + { + // Try to get the value from the mappings + if (_fromRunnableMap.ContainsKey(value)) + return _fromRunnableMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this SoftwareListStatus value) + { + // Try to get the value from the mappings + if (_fromSoftwareListStatusMap.ContainsKey(value)) + return _fromSoftwareListStatusMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this Supported value, bool useSecond = false) + { + // Try to get the value from the mappings + if (!useSecond && _fromSupportedMap.ContainsKey(value)) + return _fromSupportedMap[value]; + else if (useSecond && _fromSupportedSecondaryMap.ContainsKey(value)) + return _fromSupportedSecondaryMap[value]; + + // Otherwise, return null + return null; + } + + /// + /// Get the string value for an input enum, if possible + /// + /// Enum value to parse/param> + /// True to use the second mapping option, if it exists + /// String value representing the input, default on error + public static string? AsStringValue(this SupportStatus value) + { + // Try to get the value from the mappings + if (_fromSupportStatusMap.ContainsKey(value)) + return _fromSupportStatusMap[value]; + + // Otherwise, return null + return null; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Adjuster.cs b/SabreTools.Metadata.DatItems/Formats/Adjuster.cs new file mode 100644 index 00000000..0706bbc8 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Adjuster.cs @@ -0,0 +1,70 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which Adjuster(s) is associated with a set + /// + [JsonObject("adjuster"), XmlRoot("adjuster")] + public sealed class Adjuster : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Adjuster; + + [JsonIgnore] + public bool ConditionsSpecified + { + get + { + var conditions = GetFieldValue(Data.Models.Metadata.Adjuster.ConditionKey); + return conditions is not null && conditions.Length > 0; + } + } + + #endregion + + #region Constructors + + public Adjuster() : base() { } + + public Adjuster(Data.Models.Metadata.Adjuster item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Adjuster.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.Adjuster.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.Adjuster.DefaultKey).FromYesNo()); + + // Handle subitems + var condition = item.Read(Data.Models.Metadata.Adjuster.ConditionKey); + if (condition is not null) + SetFieldValue(Data.Models.Metadata.Adjuster.ConditionKey, new Condition(condition)); + } + + public Adjuster(Data.Models.Metadata.Adjuster item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.Adjuster GetInternalClone() + { + var adjusterItem = base.GetInternalClone(); + + var condition = GetFieldValue(Data.Models.Metadata.Adjuster.ConditionKey); + if (condition is not null) + adjusterItem[Data.Models.Metadata.Adjuster.ConditionKey] = condition.GetInternalClone(); + + return adjusterItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Analog.cs b/SabreTools.Metadata.DatItems/Formats/Analog.cs new file mode 100644 index 00000000..5c110660 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Analog.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single analog item + /// + [JsonObject("analog"), XmlRoot("analog")] + public sealed class Analog : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Analog; + + #endregion + + #region Constructors + + public Analog() : base() { } + + public Analog(Data.Models.Metadata.Analog item) : base(item) { } + + public Analog(Data.Models.Metadata.Analog item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Archive.cs b/SabreTools.Metadata.DatItems/Formats/Archive.cs new file mode 100644 index 00000000..e8398538 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Archive.cs @@ -0,0 +1,100 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents generic archive files to be included in a set + /// + [JsonObject("archive"), XmlRoot("archive")] + public sealed class Archive : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Archive; + + // TODO: None of the following are used or checked + + /// + /// Archive ID number + /// + /// TODO: No-Intro database export only + [JsonProperty("number"), XmlElement("number")] + public string? Number { get; set; } + + /// + /// Clone value + /// + /// TODO: No-Intro database export only + [JsonProperty("clone"), XmlElement("clone")] + public string? CloneValue { get; set; } + + /// + /// Regional parent value + /// + /// TODO: No-Intro database export only + [JsonProperty("regparent"), XmlElement("regparent")] + public string? RegParent { get; set; } + + /// + /// Region value + /// + /// TODO: No-Intro database export only + [JsonProperty("region"), XmlElement("region")] + public string? Region { get; set; } + + /// + /// Languages value + /// + /// TODO: No-Intro database export only + [JsonProperty("languages"), XmlElement("languages")] + public string? Languages { get; set; } + + /// + /// Development status value + /// + /// TODO: No-Intro database export only + [JsonProperty("devstatus"), XmlElement("devstatus")] + public string? DevStatus { get; set; } + + /// + /// Physical value + /// + /// TODO: No-Intro database export only + /// TODO: Is this numeric or a flag? + [JsonProperty("physical"), XmlElement("physical")] + public string? Physical { get; set; } + + /// + /// Complete value + /// + /// TODO: No-Intro database export only + /// TODO: Is this numeric or a flag? + [JsonProperty("complete"), XmlElement("complete")] + public string? Complete { get; set; } + + /// + /// Categories value + /// + /// TODO: No-Intro database export only + [JsonProperty("categories"), XmlElement("categories")] + public string? Categories { get; set; } + + #endregion + + #region Constructors + + public Archive() : base() { } + + public Archive(Data.Models.Metadata.Archive item) : base(item) { } + + public Archive(Data.Models.Metadata.Archive item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/BiosSet.cs b/SabreTools.Metadata.DatItems/Formats/BiosSet.cs new file mode 100644 index 00000000..8462abe6 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/BiosSet.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which BIOS(es) is associated with a set + /// + [JsonObject("biosset"), XmlRoot("biosset")] + public sealed class BiosSet : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.BiosSet; + + #endregion + + #region Constructors + + public BiosSet() : base() { } + + public BiosSet(Data.Models.Metadata.BiosSet item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.BiosSet.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.BiosSet.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.BiosSet.DefaultKey).FromYesNo()); + } + + public BiosSet(Data.Models.Metadata.BiosSet item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Blank.cs b/SabreTools.Metadata.DatItems/Formats/Blank.cs new file mode 100644 index 00000000..f05c8522 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Blank.cs @@ -0,0 +1,95 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a blank set from an input DAT + /// + [JsonObject("blank"), XmlRoot("blank")] + public sealed class Blank : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Blank; + + #endregion + + #region Constructors + + /// + /// Create a default, empty Blank object + /// + public Blank() + { + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType); + } + + #endregion + + #region Cloning Methods + + /// + public override object Clone() + { + var blank = new Blank(); + blank.SetFieldValue(MachineKey, GetMachine()); + blank.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + blank.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + blank.SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue()); + + return blank; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not DatItem otherItem) + return false; + + // Compare internal models + return Equals(otherItem); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not DatItem otherItem) + return false; + + // Compare internal models + return Equals(otherItem); + } + + /// + public override bool Equals(DatItem? other) + { + // If we don't have a blank, return false + if (GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) != other?.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey)) + return false; + + // Otherwise, treat it as a Blank + Blank? newOther = other as Blank; + + // If the machine information matches + return GetMachine() == newOther!.GetMachine(); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Chip.cs b/SabreTools.Metadata.DatItems/Formats/Chip.cs new file mode 100644 index 00000000..127a9239 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Chip.cs @@ -0,0 +1,41 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which Chip(s) is associated with a set + /// + [JsonObject("chip"), XmlRoot("chip")] + public sealed class Chip : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Chip; + + #endregion + + #region Constructors + + public Chip() : base() { } + + public Chip(Data.Models.Metadata.Chip item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Chip.SoundOnlyKey) is not null) + SetFieldValue(Data.Models.Metadata.Chip.SoundOnlyKey, GetBoolFieldValue(Data.Models.Metadata.Chip.SoundOnlyKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey) is not null) + SetFieldValue(Data.Models.Metadata.Chip.ChipTypeKey, GetStringFieldValue(Data.Models.Metadata.Chip.ChipTypeKey).AsChipType().AsStringValue()); + } + + public Chip(Data.Models.Metadata.Chip item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Condition.cs b/SabreTools.Metadata.DatItems/Formats/Condition.cs new file mode 100644 index 00000000..41f3a993 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Condition.cs @@ -0,0 +1,38 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a condition on a machine or other item + /// + [JsonObject("condition"), XmlRoot("condition")] + public sealed class Condition : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Condition; + + #endregion + + #region Constructors + + public Condition() : base() { } + + public Condition(Data.Models.Metadata.Condition item) : base(item) + { + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.Condition.RelationKey) is not null) + SetFieldValue(Data.Models.Metadata.Condition.RelationKey, GetStringFieldValue(Data.Models.Metadata.Condition.RelationKey).AsRelation().AsStringValue()); + } + + public Condition(Data.Models.Metadata.Condition item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/ConfLocation.cs b/SabreTools.Metadata.DatItems/Formats/ConfLocation.cs new file mode 100644 index 00000000..7e9fe37f --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/ConfLocation.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one conflocation + /// + [JsonObject("conflocation"), XmlRoot("conflocation")] + public sealed class ConfLocation : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.ConfLocation; + + #endregion + + #region Constructors + + public ConfLocation() : base() { } + + public ConfLocation(Data.Models.Metadata.ConfLocation item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.ConfLocation.InvertedKey) is not null) + SetFieldValue(Data.Models.Metadata.ConfLocation.InvertedKey, GetBoolFieldValue(Data.Models.Metadata.ConfLocation.InvertedKey).FromYesNo()); + } + + public ConfLocation(Data.Models.Metadata.ConfLocation item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/ConfSetting.cs b/SabreTools.Metadata.DatItems/Formats/ConfSetting.cs new file mode 100644 index 00000000..26d4b6f4 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/ConfSetting.cs @@ -0,0 +1,71 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one ListXML confsetting + /// + [JsonObject("confsetting"), XmlRoot("confsetting")] + public sealed class ConfSetting : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.ConfSetting; + + [JsonIgnore] + public bool ConditionsSpecified + { + get + { + var conditions = GetFieldValue(Data.Models.Metadata.ConfSetting.ConditionKey); + return conditions is not null && conditions.Length > 0; + } + } + + #endregion + + #region Constructors + + public ConfSetting() : base() { } + + public ConfSetting(Data.Models.Metadata.ConfSetting item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.ConfSetting.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.ConfSetting.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.ConfSetting.DefaultKey).FromYesNo()); + + // Handle subitems + var condition = GetFieldValue(Data.Models.Metadata.ConfSetting.ConditionKey); + if (condition is not null) + SetFieldValue(Data.Models.Metadata.ConfSetting.ConditionKey, new Condition(condition)); + } + + public ConfSetting(Data.Models.Metadata.ConfSetting item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.ConfSetting GetInternalClone() + { + var confSettingItem = base.GetInternalClone(); + + // Handle subitems + var condition = GetFieldValue(Data.Models.Metadata.ConfSetting.ConditionKey); + if (condition is not null) + confSettingItem[Data.Models.Metadata.ConfSetting.ConditionKey] = condition.GetInternalClone(); + + return confSettingItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Configuration.cs b/SabreTools.Metadata.DatItems/Formats/Configuration.cs new file mode 100644 index 00000000..5808a0a7 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Configuration.cs @@ -0,0 +1,115 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which Configuration(s) is associated with a set + /// + [JsonObject("configuration"), XmlRoot("configuration")] + public sealed class Configuration : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Configuration; + + [JsonIgnore] + public bool ConditionsSpecified + { + get + { + var conditions = GetFieldValue(Data.Models.Metadata.Configuration.ConditionKey); + return conditions is not null && conditions.Length > 0; + } + } + + [JsonIgnore] + public bool LocationsSpecified + { + get + { + var locations = GetFieldValue(Data.Models.Metadata.Configuration.ConfLocationKey); + return locations is not null && locations.Length > 0; + } + } + + [JsonIgnore] + public bool SettingsSpecified + { + get + { + var settings = GetFieldValue(Data.Models.Metadata.Configuration.ConfSettingKey); + return settings is not null && settings.Length > 0; + } + } + + #endregion + + #region Constructors + + public Configuration() : base() { } + + public Configuration(Data.Models.Metadata.Configuration item) : base(item) + { + // Handle subitems + var condition = item.Read(Data.Models.Metadata.Configuration.ConditionKey); + if (condition is not null) + SetFieldValue(Data.Models.Metadata.Configuration.ConditionKey, new Condition(condition)); + + var confLocations = item.ReadItemArray(Data.Models.Metadata.Configuration.ConfLocationKey); + if (confLocations is not null) + { + ConfLocation[] confLocationItems = Array.ConvertAll(confLocations, confLocation => new ConfLocation(confLocation)); + SetFieldValue(Data.Models.Metadata.Configuration.ConfLocationKey, confLocationItems); + } + + var confSettings = item.ReadItemArray(Data.Models.Metadata.Configuration.ConfSettingKey); + if (confSettings is not null) + { + ConfSetting[] confSettingItems = Array.ConvertAll(confSettings, confSetting => new ConfSetting(confSetting)); + SetFieldValue(Data.Models.Metadata.Configuration.ConfSettingKey, confSettingItems); + } + } + + public Configuration(Data.Models.Metadata.Configuration item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.Configuration GetInternalClone() + { + var configurationItem = base.GetInternalClone(); + + var condition = GetFieldValue(Data.Models.Metadata.Configuration.ConditionKey); + if (condition is not null) + configurationItem[Data.Models.Metadata.Configuration.ConditionKey] = condition.GetInternalClone(); + + var confLocations = GetFieldValue(Data.Models.Metadata.Configuration.ConfLocationKey); + if (confLocations is not null) + { + Data.Models.Metadata.ConfLocation[] confLocationItems = Array.ConvertAll(confLocations, confLocation => confLocation.GetInternalClone()); + configurationItem[Data.Models.Metadata.Configuration.ConfLocationKey] = confLocationItems; + } + + var confSettings = GetFieldValue(Data.Models.Metadata.Configuration.ConfSettingKey); + if (confSettings is not null) + { + Data.Models.Metadata.ConfSetting[] confSettingItems = Array.ConvertAll(confSettings, confSetting => confSetting.GetInternalClone()); + configurationItem[Data.Models.Metadata.Configuration.ConfSettingKey] = confSettingItems; + } + + return configurationItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Control.cs b/SabreTools.Metadata.DatItems/Formats/Control.cs new file mode 100644 index 00000000..f530d328 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Control.cs @@ -0,0 +1,55 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents control for an input + /// + [JsonObject("control"), XmlRoot("control")] + public sealed class Control : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Control; + + #endregion + + #region Constructors + + public Control() : base() { } + + public Control(Data.Models.Metadata.Control item) : base(item) + { + // Process flag values + if (GetInt64FieldValue(Data.Models.Metadata.Control.ButtonsKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.ButtonsKey, GetInt64FieldValue(Data.Models.Metadata.Control.ButtonsKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Control.KeyDeltaKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.KeyDeltaKey, GetInt64FieldValue(Data.Models.Metadata.Control.KeyDeltaKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Control.MaximumKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.MaximumKey, GetInt64FieldValue(Data.Models.Metadata.Control.MaximumKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Control.MinimumKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.MinimumKey, GetInt64FieldValue(Data.Models.Metadata.Control.MinimumKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Control.PlayerKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.PlayerKey, GetInt64FieldValue(Data.Models.Metadata.Control.PlayerKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Control.ReqButtonsKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.ReqButtonsKey, GetInt64FieldValue(Data.Models.Metadata.Control.ReqButtonsKey).ToString()); + if (GetBoolFieldValue(Data.Models.Metadata.Control.ReverseKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.ReverseKey, GetBoolFieldValue(Data.Models.Metadata.Control.ReverseKey).FromYesNo()); + if (GetInt64FieldValue(Data.Models.Metadata.Control.SensitivityKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.SensitivityKey, GetInt64FieldValue(Data.Models.Metadata.Control.SensitivityKey).ToString()); + if (GetStringFieldValue(Data.Models.Metadata.Control.ControlTypeKey) is not null) + SetFieldValue(Data.Models.Metadata.Control.ControlTypeKey, GetStringFieldValue(Data.Models.Metadata.Control.ControlTypeKey).AsControlType().AsStringValue()); + } + + public Control(Data.Models.Metadata.Control item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/DataArea.cs b/SabreTools.Metadata.DatItems/Formats/DataArea.cs new file mode 100644 index 00000000..1d0adfa0 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/DataArea.cs @@ -0,0 +1,43 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// SoftwareList dataarea information + /// + /// One DataArea can contain multiple Rom items + [JsonObject("dataarea"), XmlRoot("dataarea")] + public sealed class DataArea : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.DataArea; + + #endregion + + #region Constructors + + public DataArea() : base() { } + + public DataArea(Data.Models.Metadata.DataArea item) : base(item) + { + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.DataArea.EndiannessKey) is not null) + SetFieldValue(Data.Models.Metadata.DataArea.EndiannessKey, GetStringFieldValue(Data.Models.Metadata.DataArea.EndiannessKey).AsEndianness().AsStringValue()); + if (GetInt64FieldValue(Data.Models.Metadata.DataArea.SizeKey) is not null) + SetFieldValue(Data.Models.Metadata.DataArea.SizeKey, GetInt64FieldValue(Data.Models.Metadata.DataArea.SizeKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.DataArea.WidthKey) is not null) + SetFieldValue(Data.Models.Metadata.DataArea.WidthKey, GetInt64FieldValue(Data.Models.Metadata.DataArea.WidthKey).ToString()); + } + + public DataArea(Data.Models.Metadata.DataArea item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Device.cs b/SabreTools.Metadata.DatItems/Formats/Device.cs new file mode 100644 index 00000000..cfdb825c --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Device.cs @@ -0,0 +1,98 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single device on the machine + /// + [JsonObject("device"), XmlRoot("device")] + public sealed class Device : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Device; + + [JsonIgnore] + public bool InstancesSpecified + { + get + { + var instances = GetFieldValue(Data.Models.Metadata.Device.InstanceKey); + return instances is not null && instances.Length > 0; + } + } + + [JsonIgnore] + public bool ExtensionsSpecified + { + get + { + var extensions = GetFieldValue(Data.Models.Metadata.Device.ExtensionKey); + return extensions is not null && extensions.Length > 0; + } + } + + #endregion + + #region Constructors + + public Device() : base() { } + + public Device(Data.Models.Metadata.Device item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Device.MandatoryKey) is not null) + SetFieldValue(Data.Models.Metadata.Device.MandatoryKey, GetBoolFieldValue(Data.Models.Metadata.Device.MandatoryKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Device.DeviceTypeKey) is not null) + SetFieldValue(Data.Models.Metadata.Device.DeviceTypeKey, GetStringFieldValue(Data.Models.Metadata.Device.DeviceTypeKey).AsDeviceType().AsStringValue()); + + // Handle subitems + var instance = item.Read(Data.Models.Metadata.Device.InstanceKey); + if (instance is not null) + SetFieldValue(Data.Models.Metadata.Device.InstanceKey, new Instance(instance)); + + var extensions = item.ReadItemArray(Data.Models.Metadata.Device.ExtensionKey); + if (extensions is not null) + { + Extension[] extensionItems = Array.ConvertAll(extensions, extension => new Extension(extension)); + SetFieldValue(Data.Models.Metadata.Device.ExtensionKey, extensionItems); + } + } + + public Device(Data.Models.Metadata.Device item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.Device GetInternalClone() + { + var deviceItem = base.GetInternalClone(); + + var instance = GetFieldValue(Data.Models.Metadata.Device.InstanceKey); + if (instance is not null) + deviceItem[Data.Models.Metadata.Device.InstanceKey] = instance.GetInternalClone(); + + var extensions = GetFieldValue(Data.Models.Metadata.Device.ExtensionKey); + if (extensions is not null) + { + Data.Models.Metadata.Extension[] extensionItems = Array.ConvertAll(extensions, extension => extension.GetInternalClone()); + deviceItem[Data.Models.Metadata.Device.ExtensionKey] = extensionItems; + } + + return deviceItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/DeviceRef.cs b/SabreTools.Metadata.DatItems/Formats/DeviceRef.cs new file mode 100644 index 00000000..b1155cfd --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/DeviceRef.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which Device Reference(s) is associated with a set + /// + [JsonObject("device_ref"), XmlRoot("device_ref")] + public sealed class DeviceRef : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.DeviceRef; + + #endregion + + #region Constructors + + public DeviceRef() : base() { } + + public DeviceRef(Data.Models.Metadata.DeviceRef item) : base(item) { } + + public DeviceRef(Data.Models.Metadata.DeviceRef item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/DipLocation.cs b/SabreTools.Metadata.DatItems/Formats/DipLocation.cs new file mode 100644 index 00000000..6bf2b730 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/DipLocation.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one diplocation + /// + [JsonObject("diplocation"), XmlRoot("diplocation")] + public sealed class DipLocation : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.DipLocation; + + #endregion + + #region Constructors + + public DipLocation() : base() { } + + public DipLocation(Data.Models.Metadata.DipLocation item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.DipLocation.InvertedKey) is not null) + SetFieldValue(Data.Models.Metadata.DipLocation.InvertedKey, GetBoolFieldValue(Data.Models.Metadata.DipLocation.InvertedKey).FromYesNo()); + } + + public DipLocation(Data.Models.Metadata.DipLocation item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/DipSwitch.cs b/SabreTools.Metadata.DatItems/Formats/DipSwitch.cs new file mode 100644 index 00000000..0dfdcf5c --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/DipSwitch.cs @@ -0,0 +1,141 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which DIP Switch(es) is associated with a set + /// + [JsonObject("dipswitch"), XmlRoot("dipswitch")] + public sealed class DipSwitch : DatItem + { + #region Constants + + /// + /// Non-standard key for inverted logic + /// + public const string PartKey = "PART"; + + #endregion + + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.DipSwitch; + + [JsonIgnore] + public bool ConditionsSpecified + { + get + { + var conditions = GetFieldValue(Data.Models.Metadata.DipSwitch.ConditionKey); + return conditions is not null && conditions.Length > 0; + } + } + + [JsonIgnore] + public bool LocationsSpecified + { + get + { + var locations = GetFieldValue(Data.Models.Metadata.DipSwitch.DipLocationKey); + return locations is not null && locations.Length > 0; + } + } + + [JsonIgnore] + public bool ValuesSpecified + { + get + { + var values = GetFieldValue(Data.Models.Metadata.DipSwitch.DipValueKey); + return values is not null && values.Length > 0; + } + } + + [JsonIgnore] + public bool PartSpecified + { + get + { + var part = GetFieldValue(PartKey); + return part is not null + && (!string.IsNullOrEmpty(part.GetName()) + || !string.IsNullOrEmpty(part.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey))); + } + } + + #endregion + + #region Constructors + + public DipSwitch() : base() { } + + public DipSwitch(Data.Models.Metadata.DipSwitch item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.DipSwitch.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.DipSwitch.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.DipSwitch.DefaultKey).FromYesNo()); + + // Handle subitems + var condition = item.Read(Data.Models.Metadata.DipSwitch.ConditionKey); + if (condition is not null) + SetFieldValue(Data.Models.Metadata.DipSwitch.ConditionKey, new Condition(condition)); + + var dipLocations = item.ReadItemArray(Data.Models.Metadata.DipSwitch.DipLocationKey); + if (dipLocations is not null) + { + DipLocation[] dipLocationItems = Array.ConvertAll(dipLocations, dipLocation => new DipLocation(dipLocation)); + SetFieldValue(Data.Models.Metadata.DipSwitch.DipLocationKey, dipLocationItems); + } + + var dipValues = item.ReadItemArray(Data.Models.Metadata.DipSwitch.DipValueKey); + if (dipValues is not null) + { + DipValue[] dipValueItems = Array.ConvertAll(dipValues, dipValue => new DipValue(dipValue)); + SetFieldValue(Data.Models.Metadata.DipSwitch.DipValueKey, dipValueItems); + } + } + + public DipSwitch(Data.Models.Metadata.DipSwitch item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.DipSwitch GetInternalClone() + { + var dipSwitchItem = base.GetInternalClone(); + + var condition = GetFieldValue(Data.Models.Metadata.DipSwitch.ConditionKey); + if (condition is not null) + dipSwitchItem[Data.Models.Metadata.DipSwitch.ConditionKey] = condition.GetInternalClone(); + + var dipLocations = GetFieldValue(Data.Models.Metadata.DipSwitch.DipLocationKey); + if (dipLocations is not null) + { + Data.Models.Metadata.DipLocation[] dipLocationItems = Array.ConvertAll(dipLocations, dipLocation => dipLocation.GetInternalClone()); + dipSwitchItem[Data.Models.Metadata.DipSwitch.DipLocationKey] = dipLocationItems; + } + + var dipValues = GetFieldValue(Data.Models.Metadata.DipSwitch.DipValueKey); + if (dipValues is not null) + { + Data.Models.Metadata.DipValue[] dipValueItems = Array.ConvertAll(dipValues, dipValue => dipValue.GetInternalClone()); + dipSwitchItem[Data.Models.Metadata.DipSwitch.DipValueKey] = dipValueItems; + } + + return dipSwitchItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/DipValue.cs b/SabreTools.Metadata.DatItems/Formats/DipValue.cs new file mode 100644 index 00000000..1969f8d9 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/DipValue.cs @@ -0,0 +1,71 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one ListXML dipvalue + /// + [JsonObject("dipvalue"), XmlRoot("dipvalue")] + public sealed class DipValue : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.DipValue; + + [JsonIgnore] + public bool ConditionsSpecified + { + get + { + var conditions = GetFieldValue(Data.Models.Metadata.DipValue.ConditionKey); + return conditions is not null && conditions.Length > 0; + } + } + + #endregion + + #region Constructors + + public DipValue() : base() { } + + public DipValue(Data.Models.Metadata.DipValue item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.DipValue.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.DipValue.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.DipValue.DefaultKey).FromYesNo()); + + // Handle subitems + var condition = GetFieldValue(Data.Models.Metadata.DipValue.ConditionKey); + if (condition is not null) + SetFieldValue(Data.Models.Metadata.DipValue.ConditionKey, new Condition(condition)); + } + + public DipValue(Data.Models.Metadata.DipValue item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.DipValue GetInternalClone() + { + var dipValueItem = base.GetInternalClone(); + + // Handle subitems + var subCondition = GetFieldValue(Data.Models.Metadata.DipValue.ConditionKey); + if (subCondition is not null) + dipValueItem[Data.Models.Metadata.DipValue.ConditionKey] = subCondition.GetInternalClone(); + + return dipValueItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Disk.cs b/SabreTools.Metadata.DatItems/Formats/Disk.cs new file mode 100644 index 00000000..ee9cf0b2 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Disk.cs @@ -0,0 +1,184 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents Compressed Hunks of Data (CHD) formatted disks which use internal hashes + /// + [JsonObject("disk"), XmlRoot("disk")] + public sealed class Disk : DatItem + { + #region Constants + + /// + /// Non-standard key for inverted logic + /// + public const string DiskAreaKey = "DISKAREA"; + + /// + /// Non-standard key for inverted logic + /// + public const string PartKey = "PART"; + + #endregion + + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Disk; + + [JsonIgnore] + public bool DiskAreaSpecified + { + get + { + var diskArea = GetFieldValue(DiskAreaKey); + return diskArea is not null && !string.IsNullOrEmpty(diskArea.GetName()); + } + } + + [JsonIgnore] + public bool PartSpecified + { + get + { + var part = GetFieldValue(PartKey); + return part is not null + && (!string.IsNullOrEmpty(part.GetName()) + || !string.IsNullOrEmpty(part.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey))); + } + } + + #endregion + + #region Constructors + + public Disk() : base() + { + SetFieldValue(DupeTypeKey, 0x00); + SetFieldValue(Data.Models.Metadata.Disk.StatusKey, ItemStatus.None.AsStringValue()); + } + + public Disk(Data.Models.Metadata.Disk item) : base(item) + { + SetFieldValue(DupeTypeKey, 0x00); + + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Disk.OptionalKey) is not null) + SetFieldValue(Data.Models.Metadata.Disk.OptionalKey, GetBoolFieldValue(Data.Models.Metadata.Disk.OptionalKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey) is not null) + SetFieldValue(Data.Models.Metadata.Disk.StatusKey, GetStringFieldValue(Data.Models.Metadata.Disk.StatusKey).AsItemStatus().AsStringValue()); + if (GetBoolFieldValue(Data.Models.Metadata.Disk.WritableKey) is not null) + SetFieldValue(Data.Models.Metadata.Disk.WritableKey, GetBoolFieldValue(Data.Models.Metadata.Disk.WritableKey).FromYesNo()); + + // Process hash values + if (GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key) is not null) + SetFieldValue(Data.Models.Metadata.Disk.MD5Key, TextHelper.NormalizeMD5(GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key))); + if (GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key) is not null) + SetFieldValue(Data.Models.Metadata.Disk.SHA1Key, TextHelper.NormalizeSHA1(GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key))); + } + + public Disk(Data.Models.Metadata.Disk item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + /// Convert a disk to the closest Rom approximation + /// + /// + public Rom ConvertToRom() + { + var rom = new Rom(_internal.ConvertToRom()!); + + // Create a DataArea if there was an existing DiskArea + var diskArea = GetFieldValue(DiskAreaKey); + if (diskArea is not null) + { + var dataArea = new DataArea(); + + string? diskAreaName = diskArea.GetStringFieldValue(Data.Models.Metadata.DiskArea.NameKey); + dataArea.SetFieldValue(Data.Models.Metadata.DataArea.NameKey, diskAreaName); + + rom.SetFieldValue(Rom.DataAreaKey, dataArea); + } + + rom.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + rom.SetFieldValue(MachineKey, GetMachine()?.Clone() as Machine); + rom.SetFieldValue(Rom.PartKey, GetFieldValue(PartKey)?.Clone() as Part); + rom.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + rom.SetFieldValue(SourceKey, GetFieldValue(SourceKey)?.Clone() as Source); + + return rom; + } + + #endregion + + #region Comparision Methods + + /// + /// Fill any missing size and hash information from another Disk + /// + /// Disk to fill information from + public void FillMissingInformation(Disk other) + => _internal.FillMissingHashes(other._internal); + + /// + /// Returns if the Rom contains any hashes + /// + /// True if any hash exists, false otherwise + public bool HasHashes() => _internal.HasHashes(); + + /// + /// Returns if all of the hashes are set to their 0-byte values + /// + /// True if any hash matches the 0-byte value, false otherwise + public bool HasZeroHash() => _internal.HasZeroHash(); + + #endregion + + #region Sorting and Merging + + /// + public override string GetKey(ItemKey bucketedBy, Machine? machine, Source? source, bool lower = true, bool norename = true) + { + // Set the output key as the default blank string + string? key; + +#pragma warning disable IDE0010 + // Now determine what the key should be based on the bucketedBy value + switch (bucketedBy) + { + case ItemKey.MD5: + key = GetStringFieldValue(Data.Models.Metadata.Disk.MD5Key); + break; + + case ItemKey.SHA1: + key = GetStringFieldValue(Data.Models.Metadata.Disk.SHA1Key); + break; + + // Let the base handle generic stuff + default: + return base.GetKey(bucketedBy, machine, source, lower, norename); + } +#pragma warning restore IDE0010 + + // Double and triple check the key for corner cases + key ??= string.Empty; + if (lower) + key = key.ToLowerInvariant(); + + return key; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/DiskArea.cs b/SabreTools.Metadata.DatItems/Formats/DiskArea.cs new file mode 100644 index 00000000..617a97a2 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/DiskArea.cs @@ -0,0 +1,34 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// SoftwareList diskarea information + /// + /// One DiskArea can contain multiple Disk items + [JsonObject("diskarea"), XmlRoot("diskarea")] + public sealed class DiskArea : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.DiskArea; + + #endregion + + #region Constructors + + public DiskArea() : base() { } + + public DiskArea(Data.Models.Metadata.DiskArea item) : base(item) { } + + public DiskArea(Data.Models.Metadata.DiskArea item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Display.cs b/SabreTools.Metadata.DatItems/Formats/Display.cs new file mode 100644 index 00000000..20313df6 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Display.cs @@ -0,0 +1,106 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one machine display + /// + [JsonObject("display"), XmlRoot("display")] + public sealed class Display : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Display; + + #endregion + + #region Constructors + + public Display() : base() { } + + public Display(Data.Models.Metadata.Display item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Display.FlipXKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.FlipXKey, GetBoolFieldValue(Data.Models.Metadata.Display.FlipXKey).FromYesNo()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.HBEndKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.HBEndKey, GetInt64FieldValue(Data.Models.Metadata.Display.HBEndKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.HBStartKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.HBStartKey, GetInt64FieldValue(Data.Models.Metadata.Display.HBStartKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.HeightKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.HeightKey, GetInt64FieldValue(Data.Models.Metadata.Display.HeightKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.HTotalKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.HTotalKey, GetInt64FieldValue(Data.Models.Metadata.Display.HTotalKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.PixClockKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.PixClockKey, GetInt64FieldValue(Data.Models.Metadata.Display.PixClockKey).ToString()); + if (GetDoubleFieldValue(Data.Models.Metadata.Display.RefreshKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.RefreshKey, GetDoubleFieldValue(Data.Models.Metadata.Display.RefreshKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.RotateKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.RotateKey, GetInt64FieldValue(Data.Models.Metadata.Display.RotateKey).ToString()); + if (GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.DisplayTypeKey, GetStringFieldValue(Data.Models.Metadata.Display.DisplayTypeKey).AsDisplayType().AsStringValue()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.VBEndKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.VBEndKey, GetInt64FieldValue(Data.Models.Metadata.Display.VBEndKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.VBStartKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.VBStartKey, GetInt64FieldValue(Data.Models.Metadata.Display.VBStartKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.VTotalKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.VTotalKey, GetInt64FieldValue(Data.Models.Metadata.Display.VTotalKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Display.WidthKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.WidthKey, GetInt64FieldValue(Data.Models.Metadata.Display.WidthKey).ToString()); + } + + public Display(Data.Models.Metadata.Display item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + public Display(Data.Models.Metadata.Video item) : base() + { + SetFieldValue(Data.Models.Metadata.Video.AspectXKey, NumberHelper.ConvertToInt64(item.ReadString(Data.Models.Metadata.Video.AspectXKey))); + SetFieldValue(Data.Models.Metadata.Video.AspectYKey, NumberHelper.ConvertToInt64(item.ReadString(Data.Models.Metadata.Video.AspectYKey))); + SetFieldValue(Data.Models.Metadata.Display.DisplayTypeKey, item.ReadString(Data.Models.Metadata.Video.ScreenKey).AsDisplayType().AsStringValue()); + SetFieldValue(Data.Models.Metadata.Display.HeightKey, NumberHelper.ConvertToInt64(item.ReadString(Data.Models.Metadata.Video.HeightKey))); + SetFieldValue(Data.Models.Metadata.Display.RefreshKey, NumberHelper.ConvertToDouble(item.ReadString(Data.Models.Metadata.Video.RefreshKey))); + SetFieldValue(Data.Models.Metadata.Display.WidthKey, NumberHelper.ConvertToInt64(item.ReadString(Data.Models.Metadata.Video.WidthKey))); + + switch (item.ReadString(Data.Models.Metadata.Video.OrientationKey)) + { + case "horizontal": + SetFieldValue(Data.Models.Metadata.Display.RotateKey, 0); + break; + case "vertical": + SetFieldValue(Data.Models.Metadata.Display.RotateKey, 90); + break; + default: + // TODO: Log invalid values + break; + } + + // Process flag values + if (GetInt64FieldValue(Data.Models.Metadata.Video.AspectXKey) is not null) + SetFieldValue(Data.Models.Metadata.Video.AspectXKey, GetInt64FieldValue(Data.Models.Metadata.Video.AspectXKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Video.AspectYKey) is not null) + SetFieldValue(Data.Models.Metadata.Video.AspectYKey, GetInt64FieldValue(Data.Models.Metadata.Video.AspectYKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Video.HeightKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.HeightKey, GetInt64FieldValue(Data.Models.Metadata.Video.HeightKey).ToString()); + if (GetDoubleFieldValue(Data.Models.Metadata.Video.RefreshKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.RefreshKey, GetDoubleFieldValue(Data.Models.Metadata.Video.RefreshKey).ToString()); + if (GetStringFieldValue(Data.Models.Metadata.Video.ScreenKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.DisplayTypeKey, GetStringFieldValue(Data.Models.Metadata.Video.ScreenKey).AsDisplayType().AsStringValue()); + if (GetInt64FieldValue(Data.Models.Metadata.Video.WidthKey) is not null) + SetFieldValue(Data.Models.Metadata.Display.WidthKey, GetInt64FieldValue(Data.Models.Metadata.Video.WidthKey).ToString()); + } + + public Display(Data.Models.Metadata.Video item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Driver.cs b/SabreTools.Metadata.DatItems/Formats/Driver.cs new file mode 100644 index 00000000..30ed9988 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Driver.cs @@ -0,0 +1,59 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents the a driver of the machine + /// + [JsonObject("driver"), XmlRoot("driver")] + public sealed class Driver : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Driver; + + #endregion + + #region Constructors + + public Driver() : base() { } + + public Driver(Data.Models.Metadata.Driver item) : base(item) + { + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.CocktailKey, GetStringFieldValue(Data.Models.Metadata.Driver.CocktailKey).AsSupportStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Driver.ColorKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.ColorKey, GetStringFieldValue(Data.Models.Metadata.Driver.ColorKey).AsSupportStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.EmulationKey, GetStringFieldValue(Data.Models.Metadata.Driver.EmulationKey).AsSupportStatus().AsStringValue()); + if (GetBoolFieldValue(Data.Models.Metadata.Driver.IncompleteKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.IncompleteKey, GetBoolFieldValue(Data.Models.Metadata.Driver.IncompleteKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Driver.NoSoundHardwareKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.NoSoundHardwareKey, GetBoolFieldValue(Data.Models.Metadata.Driver.NoSoundHardwareKey).FromYesNo()); + if (GetInt64FieldValue(Data.Models.Metadata.Driver.PaletteSizeKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.PaletteSizeKey, GetInt64FieldValue(Data.Models.Metadata.Driver.PaletteSizeKey).ToString()); + if (GetBoolFieldValue(Data.Models.Metadata.Driver.RequiresArtworkKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.RequiresArtworkKey, GetBoolFieldValue(Data.Models.Metadata.Driver.RequiresArtworkKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.SaveStateKey, GetStringFieldValue(Data.Models.Metadata.Driver.SaveStateKey).AsSupported().AsStringValue(useSecond: true)); + if (GetStringFieldValue(Data.Models.Metadata.Driver.SoundKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.SoundKey, GetStringFieldValue(Data.Models.Metadata.Driver.SoundKey).AsSupportStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.StatusKey, GetStringFieldValue(Data.Models.Metadata.Driver.StatusKey).AsSupportStatus().AsStringValue()); + if (GetBoolFieldValue(Data.Models.Metadata.Driver.UnofficialKey) is not null) + SetFieldValue(Data.Models.Metadata.Driver.UnofficialKey, GetBoolFieldValue(Data.Models.Metadata.Driver.UnofficialKey).FromYesNo()); + } + + public Driver(Data.Models.Metadata.Driver item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Extension.cs b/SabreTools.Metadata.DatItems/Formats/Extension.cs new file mode 100644 index 00000000..a6db27d4 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Extension.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a matchable extension + /// + [JsonObject("extension"), XmlRoot("extension")] + public sealed class Extension : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Extension; + + #endregion + + #region Constructors + + public Extension() : base() { } + + public Extension(Data.Models.Metadata.Extension item) : base(item) { } + + public Extension(Data.Models.Metadata.Extension item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Feature.cs b/SabreTools.Metadata.DatItems/Formats/Feature.cs new file mode 100644 index 00000000..c8b22729 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Feature.cs @@ -0,0 +1,42 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents the a feature of the machine + /// + [JsonObject("feature"), XmlRoot("feature")] + public sealed class Feature : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Feature; + + #endregion + + #region Constructors + + public Feature() : base() { } + + public Feature(Data.Models.Metadata.Feature item) : base(item) + { + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.Feature.OverallKey) is not null) + SetFieldValue(Data.Models.Metadata.Feature.OverallKey, GetStringFieldValue(Data.Models.Metadata.Feature.OverallKey).AsFeatureStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Feature.StatusKey) is not null) + SetFieldValue(Data.Models.Metadata.Feature.StatusKey, GetStringFieldValue(Data.Models.Metadata.Feature.StatusKey).AsFeatureStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey) is not null) + SetFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey, GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey).AsFeatureType().AsStringValue()); + } + + public Feature(Data.Models.Metadata.Feature item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/File.cs b/SabreTools.Metadata.DatItems/Formats/File.cs new file mode 100644 index 00000000..75242228 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/File.cs @@ -0,0 +1,323 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Hashing; +using SabreTools.IO.Extensions; +using SabreTools.Metadata.Tools; + +// TODO: Add item mappings for all fields +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single file item + /// + [JsonObject("file"), XmlRoot("file")] + public sealed class File : DatItem + { + #region Private instance variables + + private byte[]? _crc; // 8 bytes + private byte[]? _md5; // 16 bytes + private byte[]? _sha1; // 20 bytes + private byte[]? _sha256; // 32 bytes + + #endregion + + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.File; + + /// + /// ID value + /// + [JsonProperty("id", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("id")] + public string? Id { get; set; } + + /// + /// Extension value + /// + [JsonProperty("extension", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("extension")] + public string? Extension { get; set; } + + /// + /// Byte size of the rom + /// + [JsonProperty("size", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("size")] + public long? Size { get; set; } = null; + + /// + /// File CRC32 hash + /// + [JsonProperty("crc", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("crc")] + public string? CRC + { + get { return _crc.ToHexString(); } + set { _crc = value == "null" ? HashType.CRC32.ZeroBytes : TextHelper.NormalizeCRC32(value).FromHexString(); } + } + + /// + /// File MD5 hash + /// + [JsonProperty("md5", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("md5")] + public string? MD5 + { + get { return _md5.ToHexString(); } + set { _md5 = TextHelper.NormalizeMD5(value).FromHexString(); } + } + + /// + /// File SHA-1 hash + /// + [JsonProperty("sha1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha1")] + public string? SHA1 + { + get { return _sha1.ToHexString(); } + set { _sha1 = TextHelper.NormalizeSHA1(value).FromHexString(); } + } + + /// + /// File SHA-256 hash + /// + [JsonProperty("sha256", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha256")] + public string? SHA256 + { + get { return _sha256.ToHexString(); } + set { _sha256 = TextHelper.NormalizeSHA256(value).FromHexString(); } + } + + /// + /// Format value + /// + [JsonProperty("format", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("format")] + public string? Format { get; set; } + + #endregion + + #region Constructors + + /// + /// Create a default, empty File object + /// + public File() + { + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType); + } + + #endregion + + #region Cloning Methods + + /// + public override object Clone() + { + var file = new File() + { + Id = this.Id, + Extension = this.Extension, + Size = this.Size, + _crc = this._crc, + _md5 = this._md5, + _sha1 = this._sha1, + _sha256 = this._sha256, + Format = this.Format, + }; + file.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + file.SetFieldValue(MachineKey, GetMachine()!.Clone() as Machine ?? new Machine()); + file.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + file.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + + return file; + } + + /// + /// Convert a disk to the closest Rom approximation + /// + /// + public Rom ConvertToRom() + { + var rom = new Rom(); + + rom.SetName($"{Id}.{Extension}"); + rom.SetFieldValue(Data.Models.Metadata.Rom.SizeKey, Size); + rom.SetFieldValue(Data.Models.Metadata.Rom.CRCKey, CRC); + rom.SetFieldValue(Data.Models.Metadata.Rom.MD5Key, MD5); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, SHA1); + rom.SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, SHA256); + + rom.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + rom.SetFieldValue(MachineKey, GetMachine()?.Clone() as Machine); + rom.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + rom.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + + return rom; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(DatItem? other) + { + bool dupefound = false; + + // If we don't have a file, return false + if (GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) != other?.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey)) + return dupefound; + + // Otherwise, treat it as a File + File? newOther = other as File; + + // If all hashes are empty, then they're dupes + if (!HasHashes() && !newOther!.HasHashes()) + { + dupefound = true; + } + + // If we have a file that has no known size, rely on the hashes only + else if (Size is null && HashMatch(newOther!)) + { + dupefound = true; + } + + // Otherwise if we get a partial match + else if (Size == newOther!.Size && HashMatch(newOther)) + { + dupefound = true; + } + + return dupefound; + } + + /// + /// Fill any missing size and hash information from another Rom + /// + /// File to fill information from + public void FillMissingInformation(File other) + { + if (Size is null && other.Size is not null) + Size = other.Size; + + if (_crc.IsNullOrEmpty() && !other._crc.IsNullOrEmpty()) + _crc = other._crc; + + if (_md5.IsNullOrEmpty() && !other._md5.IsNullOrEmpty()) + _md5 = other._md5; + + if (_sha1.IsNullOrEmpty() && !other._sha1.IsNullOrEmpty()) + _sha1 = other._sha1; + + if (_sha256.IsNullOrEmpty() && !other._sha256.IsNullOrEmpty()) + _sha256 = other._sha256; + } + + /// + /// Returns if the File contains any hashes + /// + /// True if any hash exists, false otherwise + public bool HasHashes() + { + return !_crc.IsNullOrEmpty() + || !_md5.IsNullOrEmpty() + || !_sha1.IsNullOrEmpty() + || !_sha256.IsNullOrEmpty(); + } + + /// + /// Returns if all of the hashes are set to their 0-byte values + /// + /// True if any hash matches the 0-byte value, false otherwise + public bool HasZeroHash() + { + bool crcNull = string.IsNullOrEmpty(CRC) || string.Equals(CRC, HashType.CRC32.ZeroString, StringComparison.OrdinalIgnoreCase); + bool md5Null = string.IsNullOrEmpty(MD5) || string.Equals(MD5, HashType.MD5.ZeroString, StringComparison.OrdinalIgnoreCase); + bool sha1Null = string.IsNullOrEmpty(SHA1) || string.Equals(SHA1, HashType.SHA1.ZeroString, StringComparison.OrdinalIgnoreCase); + bool sha256Null = string.IsNullOrEmpty(SHA256) || string.Equals(SHA256, HashType.SHA256.ZeroString, StringComparison.OrdinalIgnoreCase); + + return crcNull && md5Null && sha1Null && sha256Null; + } + + /// + /// Returns if there are no, non-empty hashes in common with another File + /// + /// File to compare against + /// True if at least one hash is not mutually exclusive, false otherwise + private bool HasCommonHash(File other) + { + return !(_crc.IsNullOrEmpty() ^ other._crc.IsNullOrEmpty()) + || !(_md5.IsNullOrEmpty() ^ other._md5.IsNullOrEmpty()) + || !(_sha1.IsNullOrEmpty() ^ other._sha1.IsNullOrEmpty()) + || !(_sha256.IsNullOrEmpty() ^ other._sha256.IsNullOrEmpty()); + } + + /// + /// Returns if any hashes are common with another File + /// + /// File to compare against + /// True if any hashes are in common, false otherwise + private bool HashMatch(File other) + { + // If either have no hashes, we return false, otherwise this would be a false positive + if (!HasHashes() || !other.HasHashes()) + return false; + + // If neither have hashes in common, we return false, otherwise this would be a false positive + if (!HasCommonHash(other)) + return false; + + // Return if all hashes match according to merge rules + return MetadataExtensions.ConditionalHashEquals(_crc, other._crc) + && MetadataExtensions.ConditionalHashEquals(_md5, other._md5) + && MetadataExtensions.ConditionalHashEquals(_sha1, other._sha1) + && MetadataExtensions.ConditionalHashEquals(_sha256, other._sha256); + } + + #endregion + + #region Sorting and Merging + + /// + public override string GetKey(ItemKey bucketedBy, Machine? machine, Source? source, bool lower = true, bool norename = true) + { + // Set the output key as the default blank string + string? key; + +#pragma warning disable IDE0010 + // Now determine what the key should be based on the bucketedBy value + switch (bucketedBy) + { + case ItemKey.CRC: + key = CRC; + break; + + case ItemKey.MD5: + key = MD5; + break; + + case ItemKey.SHA1: + key = SHA1; + break; + + case ItemKey.SHA256: + key = SHA256; + break; + + // Let the base handle generic stuff + default: + return base.GetKey(bucketedBy, machine, source, lower, norename); + } +#pragma warning restore IDE0010 + + // Double and triple check the key for corner cases + key ??= string.Empty; + if (lower) + key = key.ToLowerInvariant(); + + return key; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Info.cs b/SabreTools.Metadata.DatItems/Formats/Info.cs new file mode 100644 index 00000000..04f56b49 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Info.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents special information about a machine + /// + [JsonObject("info"), XmlRoot("info")] + public sealed class Info : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Info; + + #endregion + + #region Constructors + + public Info() : base() { } + + public Info(Data.Models.Metadata.Info item) : base(item) { } + + public Info(Data.Models.Metadata.Info item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Input.cs b/SabreTools.Metadata.DatItems/Formats/Input.cs new file mode 100644 index 00000000..3f968764 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Input.cs @@ -0,0 +1,86 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one ListXML input + /// + [JsonObject("input"), XmlRoot("input")] + public sealed class Input : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Input; + + [JsonIgnore] + public bool ControlsSpecified + { + get + { + var controls = GetFieldValue(Data.Models.Metadata.Input.ControlKey); + return controls is not null && controls.Length > 0; + } + } + + #endregion + + #region Constructors + + public Input() : base() { } + + public Input(Data.Models.Metadata.Input item) : base(item) + { + // Process flag values + if (GetInt64FieldValue(Data.Models.Metadata.Input.ButtonsKey) is not null) + SetFieldValue(Data.Models.Metadata.Input.ButtonsKey, GetInt64FieldValue(Data.Models.Metadata.Input.ButtonsKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Input.CoinsKey) is not null) + SetFieldValue(Data.Models.Metadata.Input.CoinsKey, GetInt64FieldValue(Data.Models.Metadata.Input.CoinsKey).ToString()); + if (GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey) is not null) + SetFieldValue(Data.Models.Metadata.Input.PlayersKey, GetInt64FieldValue(Data.Models.Metadata.Input.PlayersKey).ToString()); + if (GetBoolFieldValue(Data.Models.Metadata.Input.ServiceKey) is not null) + SetFieldValue(Data.Models.Metadata.Input.ServiceKey, GetBoolFieldValue(Data.Models.Metadata.Input.ServiceKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Input.TiltKey) is not null) + SetFieldValue(Data.Models.Metadata.Input.TiltKey, GetBoolFieldValue(Data.Models.Metadata.Input.TiltKey).FromYesNo()); + + // Handle subitems + var controls = item.ReadItemArray(Data.Models.Metadata.Input.ControlKey); + if (controls is not null) + { + Control[] controlItems = Array.ConvertAll(controls, control => new Control(control)); + SetFieldValue(Data.Models.Metadata.Input.ControlKey, controlItems); + } + } + + public Input(Data.Models.Metadata.Input item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.Input GetInternalClone() + { + var inputItem = base.GetInternalClone(); + + var controls = GetFieldValue(Data.Models.Metadata.Input.ControlKey); + if (controls is not null) + { + Data.Models.Metadata.Control[] controlItems = Array.ConvertAll(controls, control => control.GetInternalClone()); + inputItem[Data.Models.Metadata.Input.ControlKey] = controlItems; + } + + return inputItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Instance.cs b/SabreTools.Metadata.DatItems/Formats/Instance.cs new file mode 100644 index 00000000..b9e691dc --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Instance.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single instance of another item + /// + [JsonObject("instance"), XmlRoot("instance")] + public sealed class Instance : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Instance; + + #endregion + + #region Constructors + + public Instance() : base() { } + + public Instance(Data.Models.Metadata.Instance item) : base(item) { } + + public Instance(Data.Models.Metadata.Instance item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Media.cs b/SabreTools.Metadata.DatItems/Formats/Media.cs new file mode 100644 index 00000000..ca26b9fc --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Media.cs @@ -0,0 +1,136 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents Aaruformat images which use internal hashes + /// + [JsonObject("media"), XmlRoot("media")] + public sealed class Media : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Media; + + #endregion + + #region Constructors + + public Media() : base() + { + SetFieldValue(DupeTypeKey, 0x00); + } + + public Media(Data.Models.Metadata.Media item) : base(item) + { + SetFieldValue(DupeTypeKey, 0x00); + + // Process hash values + if (GetStringFieldValue(Data.Models.Metadata.Media.MD5Key) is not null) + SetFieldValue(Data.Models.Metadata.Media.MD5Key, TextHelper.NormalizeMD5(GetStringFieldValue(Data.Models.Metadata.Media.MD5Key))); + if (GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key) is not null) + SetFieldValue(Data.Models.Metadata.Media.SHA1Key, TextHelper.NormalizeSHA1(GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key))); + if (GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key) is not null) + SetFieldValue(Data.Models.Metadata.Media.SHA256Key, TextHelper.NormalizeSHA256(GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key))); + } + + public Media(Data.Models.Metadata.Media item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + /// Convert a media to the closest Rom approximation + /// + /// + public Rom ConvertToRom() + { + var rom = new Rom(_internal.ConvertToRom()!); + + rom.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + rom.SetFieldValue(MachineKey, GetMachine()); + rom.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + rom.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + + return rom; + } + + #endregion + + #region Comparision Methods + + /// + /// Fill any missing size and hash information from another Media + /// + /// Media to fill information from + public void FillMissingInformation(Media other) + => _internal.FillMissingHashes(other._internal); + + /// + /// Returns if the Rom contains any hashes + /// + /// True if any hash exists, false otherwise + public bool HasHashes() => _internal.HasHashes(); + + /// + /// Returns if all of the hashes are set to their 0-byte values + /// + /// True if any hash matches the 0-byte value, false otherwise + public bool HasZeroHash() => _internal.HasZeroHash(); + + #endregion + + #region Sorting and Merging + + /// + public override string GetKey(ItemKey bucketedBy, Machine? machine, Source? source, bool lower = true, bool norename = true) + { + // Set the output key as the default blank string + string? key; + +#pragma warning disable IDE0010 + // Now determine what the key should be based on the bucketedBy value + switch (bucketedBy) + { + case ItemKey.MD5: + key = GetStringFieldValue(Data.Models.Metadata.Media.MD5Key); + break; + + case ItemKey.SHA1: + key = GetStringFieldValue(Data.Models.Metadata.Media.SHA1Key); + break; + + case ItemKey.SHA256: + key = GetStringFieldValue(Data.Models.Metadata.Media.SHA256Key); + break; + + case ItemKey.SpamSum: + key = GetStringFieldValue(Data.Models.Metadata.Media.SpamSumKey); + break; + + // Let the base handle generic stuff + default: + return base.GetKey(bucketedBy, machine, source, lower, norename); + } +#pragma warning restore IDE0010 + + // Double and triple check the key for corner cases + key ??= string.Empty; + if (lower) + key = key.ToLowerInvariant(); + + return key; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Original.cs b/SabreTools.Metadata.DatItems/Formats/Original.cs new file mode 100644 index 00000000..9504719e --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Original.cs @@ -0,0 +1,32 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents the OpenMSX original value + /// + [JsonObject("original"), XmlRoot("original")] + public sealed class Original + { + [JsonProperty("value"), XmlElement("value")] + public bool? Value + { + get => _internal.ReadBool(Data.Models.Metadata.Original.ValueKey); + set => _internal[Data.Models.Metadata.Original.ValueKey] = value; + } + + [JsonProperty("content"), XmlElement("content")] + public string? Content + { + get => _internal.ReadString(Data.Models.Metadata.Original.ContentKey); + set => _internal[Data.Models.Metadata.Original.ContentKey] = value; + } + + /// + /// Internal Original model + /// + [JsonIgnore] + private readonly Data.Models.Metadata.Original _internal = []; + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Part.cs b/SabreTools.Metadata.DatItems/Formats/Part.cs new file mode 100644 index 00000000..7a505ee3 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Part.cs @@ -0,0 +1,44 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// SoftwareList part information + /// + /// One Part can contain multiple PartFeature, DataArea, DiskArea, and DipSwitch items + [JsonObject("part"), XmlRoot("part")] + public sealed class Part : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Part; + + [JsonIgnore] + public bool FeaturesSpecified + { + get + { + var features = GetFieldValue(Data.Models.Metadata.Part.FeatureKey); + return features is not null && features.Length > 0; + } + } + + #endregion + + #region Constructors + + public Part() : base() { } + + public Part(Data.Models.Metadata.Part item) : base(item) { } + + public Part(Data.Models.Metadata.Part item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/PartFeature.cs b/SabreTools.Metadata.DatItems/Formats/PartFeature.cs new file mode 100644 index 00000000..4db54bb7 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/PartFeature.cs @@ -0,0 +1,51 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one part feature object + /// + [JsonObject("part_feature"), XmlRoot("part_feature")] + public sealed class PartFeature : DatItem + { + #region Constants + + /// + /// Non-standard key for inverted logic + /// + public const string PartKey = "PART"; + + #endregion + + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.PartFeature; + + #endregion + + #region Constructors + + public PartFeature() : base() { } + + public PartFeature(Data.Models.Metadata.Feature item) : base(item) + { + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.Feature.OverallKey) is not null) + SetFieldValue(Data.Models.Metadata.Feature.OverallKey, GetStringFieldValue(Data.Models.Metadata.Feature.OverallKey).AsFeatureStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Feature.StatusKey) is not null) + SetFieldValue(Data.Models.Metadata.Feature.StatusKey, GetStringFieldValue(Data.Models.Metadata.Feature.StatusKey).AsFeatureStatus().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey) is not null) + SetFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey, GetStringFieldValue(Data.Models.Metadata.Feature.FeatureTypeKey).AsFeatureType().AsStringValue()); + } + + public PartFeature(Data.Models.Metadata.Feature item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Port.cs b/SabreTools.Metadata.DatItems/Formats/Port.cs new file mode 100644 index 00000000..9f73eecc --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Port.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single port on a machine + /// + [JsonObject("port"), XmlRoot("port")] + public sealed class Port : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Port; + + [JsonIgnore] + public bool AnalogsSpecified + { + get + { + var analogs = GetFieldValue(Data.Models.Metadata.Port.AnalogKey); + return analogs is not null && analogs.Length > 0; + } + } + + #endregion + + #region Constructors + + public Port() : base() { } + + public Port(Data.Models.Metadata.Port item) : base(item) + { + // Handle subitems + var analogs = item.ReadItemArray(Data.Models.Metadata.Port.AnalogKey); + if (analogs is not null) + { + Analog[] analogItems = Array.ConvertAll(analogs, analog => new Analog(analog)); + SetFieldValue(Data.Models.Metadata.Port.AnalogKey, analogItems); + } + } + + public Port(Data.Models.Metadata.Port item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.Port GetInternalClone() + { + var portItem = base.GetInternalClone(); + + var analogs = GetFieldValue(Data.Models.Metadata.Port.AnalogKey); + if (analogs is not null) + { + Data.Models.Metadata.Analog[] analogItems = Array.ConvertAll(analogs, analog => analog.GetInternalClone()); + portItem[Data.Models.Metadata.Port.AnalogKey] = analogItems; + } + + return portItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/RamOption.cs b/SabreTools.Metadata.DatItems/Formats/RamOption.cs new file mode 100644 index 00000000..ceda917d --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/RamOption.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which RAM option(s) is associated with a set + /// + [JsonObject("ramoption"), XmlRoot("ramoption")] + public sealed class RamOption : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.RamOption; + + #endregion + + #region Constructors + + public RamOption() : base() { } + + public RamOption(Data.Models.Metadata.RamOption item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.RamOption.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.RamOption.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.RamOption.DefaultKey).FromYesNo()); + } + + public RamOption(Data.Models.Metadata.RamOption item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Release.cs b/SabreTools.Metadata.DatItems/Formats/Release.cs new file mode 100644 index 00000000..f81eb9af --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Release.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents release information about a set + /// + [JsonObject("release"), XmlRoot("release")] + public sealed class Release : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Release; + + #endregion + + #region Constructors + + public Release() : base() { } + + public Release(Data.Models.Metadata.Release item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Release.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.Release.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.Release.DefaultKey).FromYesNo()); + } + + public Release(Data.Models.Metadata.Release item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/ReleaseDetails.cs b/SabreTools.Metadata.DatItems/Formats/ReleaseDetails.cs new file mode 100644 index 00000000..c3c8445b --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/ReleaseDetails.cs @@ -0,0 +1,189 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +// TODO: Add item mappings for all fields +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single release details item + /// + [JsonObject("release_details"), XmlRoot("release_details")] + public sealed class ReleaseDetails : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.ReleaseDetails; + + /// + /// Id value + /// + /// TODO: Is this required? + [JsonProperty("id", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("id")] + public string? Id { get; set; } + + /// + /// Directory name value + /// + [JsonProperty("dirname", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("dirname")] + public string? DirName { get; set; } + + /// + /// Rom info value + /// + [JsonProperty("rominfo", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("rominfo")] + public string? RomInfo { get; set; } + + /// + /// Category value + /// + [JsonProperty("category", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("category")] + public string? Category { get; set; } + + /// + /// NFO name value + /// + [JsonProperty("nfoname", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("nfoname")] + public string? NfoName { get; set; } + + /// + /// NFO size value + /// + [JsonProperty("nfosize", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("nfosize")] + public long? NfoSize { get; set; } + + /// + /// NFO CRC value + /// + [JsonProperty("nfocrc", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("nfocrc")] + public string? NfoCrc { get; set; } + + /// + /// Archive name value + /// + [JsonProperty("archivename", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("archivename")] + public string? ArchiveName { get; set; } + + /// + /// Original format value + /// + [JsonProperty("originalformat", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("originalformat")] + public string? OriginalFormat { get; set; } + + /// + /// Date value + /// + [JsonProperty("date", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("date")] + public string? Date { get; set; } + + /// + /// Grpup value + /// + [JsonProperty("group", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("group")] + public string? Group { get; set; } + + /// + /// Comment value + /// + [JsonProperty("comment", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("comment")] + public string? Comment { get; set; } + + /// + /// Tool value + /// + [JsonProperty("tool", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("tool")] + public string? Tool { get; set; } + + /// + /// Region value + /// + [JsonProperty("region", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("region")] + public string? Region { get; set; } + + /// + /// Origin value + /// + [JsonProperty("origin", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("origin")] + public string? Origin { get; set; } + + #endregion + + #region Constructors + + /// + /// Create a default, empty ReleaseDetails object + /// + public ReleaseDetails() + { + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType); + } + + #endregion + + #region Cloning Methods + + /// + public override object Clone() + { + var releaseDetails = new ReleaseDetails() + { + Id = this.Id, + DirName = this.DirName, + RomInfo = this.RomInfo, + Category = this.Category, + NfoName = this.NfoName, + NfoSize = this.NfoSize, + NfoCrc = this.NfoCrc, + ArchiveName = this.ArchiveName, + OriginalFormat = this.OriginalFormat, + Date = this.Date, + Group = this.Group, + Comment = this.Comment, + Tool = this.Tool, + Region = this.Region, + Origin = this.Origin, + }; + releaseDetails.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + releaseDetails.SetFieldValue(MachineKey, GetMachine()); + releaseDetails.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + releaseDetails.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + releaseDetails.SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue()); + + return releaseDetails; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(DatItem? other) + { + // If we don't have a Details, return false + if (GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) != other?.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey)) + return false; + + // Otherwise, treat it as a Details + ReleaseDetails? newOther = other as ReleaseDetails; + + // If the Details information matches + return Id == newOther!.Id + && DirName == newOther.DirName + && RomInfo == newOther.RomInfo + && Category == newOther.Category + && NfoName == newOther.NfoName + && NfoSize == newOther.NfoSize + && NfoCrc == newOther.NfoCrc + && ArchiveName == newOther.ArchiveName + && OriginalFormat == newOther.OriginalFormat + && Date == newOther.Date + && Group == newOther.Group + && Comment == newOther.Comment + && Tool == newOther.Tool + && Region == newOther.Region + && Origin == newOther.Origin; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Rom.cs b/SabreTools.Metadata.DatItems/Formats/Rom.cs new file mode 100644 index 00000000..12f93c4b --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Rom.cs @@ -0,0 +1,312 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a generic file within a set + /// + [JsonObject("rom"), XmlRoot("rom")] + public sealed class Rom : DatItem + { + #region Constants + + /// + /// Non-standard key for inverted logic + /// + public const string DataAreaKey = "DATAAREA"; + + /// + /// Non-standard key for inverted logic + /// + public const string PartKey = "PART"; + + #endregion + + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Rom; + + [JsonIgnore] + public bool ItemStatusSpecified + { + get + { + var status = GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus(); + return status != ItemStatus.NULL && status != ItemStatus.None; + } + } + + [JsonIgnore] + public bool OriginalSpecified + { + get + { + var original = GetFieldValue("ORIGINAL"); + return original is not null && original != default; + } + } + + [JsonIgnore] + public bool DataAreaSpecified + { + get + { + var dataArea = GetFieldValue(DataAreaKey); + return dataArea is not null + && (!string.IsNullOrEmpty(dataArea.GetName()) + || dataArea.GetInt64FieldValue(Data.Models.Metadata.DataArea.SizeKey) is not null + || dataArea.GetInt64FieldValue(Data.Models.Metadata.DataArea.WidthKey) is not null + || dataArea.GetStringFieldValue(Data.Models.Metadata.DataArea.EndiannessKey).AsEndianness() != Endianness.NULL); + } + } + + [JsonIgnore] + public bool PartSpecified + { + get + { + var part = GetFieldValue(PartKey); + return part is not null + && (!string.IsNullOrEmpty(part.GetName()) + || !string.IsNullOrEmpty(part.GetStringFieldValue(Data.Models.Metadata.Part.InterfaceKey))); + } + } + + #endregion + + #region Constructors + + public Rom() : base() + { + SetFieldValue(DupeTypeKey, 0x00); + SetFieldValue(Data.Models.Metadata.Rom.StatusKey, ItemStatus.None.AsStringValue()); + } + + public Rom(Data.Models.Metadata.Dump item, Machine machine, Source source, int index) + { + // If we don't have rom data, we can't do anything + Data.Models.Metadata.Rom? rom = null; + OpenMSXSubType subType = OpenMSXSubType.NULL; + if (item.Read(Data.Models.Metadata.Dump.RomKey) is not null) + { + rom = item.Read(Data.Models.Metadata.Dump.RomKey); + subType = OpenMSXSubType.Rom; + } + else if (item.Read(Data.Models.Metadata.Dump.MegaRomKey) is not null) + { + rom = item.Read(Data.Models.Metadata.Dump.MegaRomKey); + subType = OpenMSXSubType.MegaRom; + } + else if (item.Read(Data.Models.Metadata.Dump.SCCPlusCartKey) is not null) + { + rom = item.Read(Data.Models.Metadata.Dump.SCCPlusCartKey); + subType = OpenMSXSubType.SCCPlusCart; + } + + // Just return if nothing valid was found + if (rom is null) + return; + + string name = $"{machine.GetName()}_{index++}{(!string.IsNullOrEmpty(rom!.ReadString(Data.Models.Metadata.Rom.RemarkKey)) ? $" {rom.ReadString(Data.Models.Metadata.Rom.RemarkKey)}" : string.Empty)}"; + + SetName(name); + SetFieldValue(Data.Models.Metadata.Rom.OffsetKey, rom.ReadString(Data.Models.Metadata.Rom.StartKey)); + SetFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType, subType.AsStringValue()); + SetFieldValue(Data.Models.Metadata.Rom.OpenMSXType, rom.ReadString(Data.Models.Metadata.Rom.OpenMSXType) ?? rom.ReadString(Data.Models.Metadata.DatItem.TypeKey)); + SetFieldValue(Data.Models.Metadata.Rom.RemarkKey, rom.ReadString(Data.Models.Metadata.Rom.RemarkKey)); + SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)); + SetFieldValue(Data.Models.Metadata.Rom.StartKey, rom.ReadString(Data.Models.Metadata.Rom.StartKey)); + SetFieldValue(SourceKey, source); + + if (item.Read(Data.Models.Metadata.Dump.OriginalKey) is not null) + { + var original = item.Read(Data.Models.Metadata.Dump.OriginalKey)!; + SetFieldValue("ORIGINAL", new Original + { + Value = original.ReadBool(Data.Models.Metadata.Original.ValueKey), + Content = original.ReadString(Data.Models.Metadata.Original.ContentKey), + }); + } + + CopyMachineInformation(machine); + + // Process hash values + if (GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SizeKey, GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey).ToString()); + if (GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.CRCKey, TextHelper.NormalizeCRC32(GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MD2Key, TextHelper.NormalizeMD2(GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MD4Key, TextHelper.NormalizeMD5(GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MD5Key, TextHelper.NormalizeMD5(GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, TextHelper.NormalizeRIPEMD128(GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, TextHelper.NormalizeRIPEMD160(GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, TextHelper.NormalizeSHA1(GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, TextHelper.NormalizeSHA256(GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, TextHelper.NormalizeSHA384(GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, TextHelper.NormalizeSHA512(GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key))); + } + + public Rom(Data.Models.Metadata.Rom item) : base(item) + { + SetFieldValue(DupeTypeKey, 0x00); + + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.Rom.DisposeKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.DisposeKey, GetBoolFieldValue(Data.Models.Metadata.Rom.DisposeKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Rom.InvertedKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.InvertedKey, GetBoolFieldValue(Data.Models.Metadata.Rom.InvertedKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Rom.LoadFlagKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.LoadFlagKey, GetStringFieldValue(Data.Models.Metadata.Rom.LoadFlagKey).AsLoadFlag().AsStringValue()); + if (GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType) is not null) + SetFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType, GetStringFieldValue(Data.Models.Metadata.Rom.OpenMSXMediaType).AsOpenMSXSubType().AsStringValue()); + if (GetBoolFieldValue(Data.Models.Metadata.Rom.MIAKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MIAKey, GetBoolFieldValue(Data.Models.Metadata.Rom.MIAKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Rom.OptionalKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.OptionalKey, GetBoolFieldValue(Data.Models.Metadata.Rom.OptionalKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Rom.SoundOnlyKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SoundOnlyKey, GetBoolFieldValue(Data.Models.Metadata.Rom.SoundOnlyKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.StatusKey, GetStringFieldValue(Data.Models.Metadata.Rom.StatusKey).AsItemStatus().AsStringValue()); + + // Process hash values + if (GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SizeKey, GetInt64FieldValue(Data.Models.Metadata.Rom.SizeKey).ToString()); + if (GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey) is not null) + SetFieldValue(Data.Models.Metadata.Rom.CRCKey, TextHelper.NormalizeCRC32(GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MD2Key, TextHelper.NormalizeMD2(GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MD4Key, TextHelper.NormalizeMD4(GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.MD5Key, TextHelper.NormalizeMD5(GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key, TextHelper.NormalizeRIPEMD128(GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key, TextHelper.NormalizeRIPEMD160(GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA1Key, TextHelper.NormalizeSHA1(GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA256Key, TextHelper.NormalizeSHA256(GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA384Key, TextHelper.NormalizeSHA384(GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key))); + if (GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key) is not null) + SetFieldValue(Data.Models.Metadata.Rom.SHA512Key, TextHelper.NormalizeSHA512(GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key))); + } + + public Rom(Data.Models.Metadata.Rom item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Comparision Methods + + /// + /// Fill any missing size and hash information from another Rom + /// + /// Rom to fill information from + public void FillMissingInformation(Rom other) + => _internal.FillMissingHashes(other._internal); + + /// + /// Returns if the Rom contains any hashes + /// + /// True if any hash exists, false otherwise + public bool HasHashes() => _internal.HasHashes(); + + /// + /// Returns if all of the hashes are set to their 0-byte values + /// + /// True if any hash matches the 0-byte value, false otherwise + public bool HasZeroHash() => _internal.HasZeroHash(); + + #endregion + + #region Sorting and Merging + + /// + public override string GetKey(ItemKey bucketedBy, Machine? machine, Source? source, bool lower = true, bool norename = true) + { + // Set the output key as the default blank string + string? key; + +#pragma warning disable IDE0010 + // Now determine what the key should be based on the bucketedBy value + switch (bucketedBy) + { + case ItemKey.CRC: + key = GetStringFieldValue(Data.Models.Metadata.Rom.CRCKey); + break; + + case ItemKey.MD2: + key = GetStringFieldValue(Data.Models.Metadata.Rom.MD2Key); + break; + + case ItemKey.MD4: + key = GetStringFieldValue(Data.Models.Metadata.Rom.MD4Key); + break; + + case ItemKey.MD5: + key = GetStringFieldValue(Data.Models.Metadata.Rom.MD5Key); + break; + + case ItemKey.RIPEMD128: + key = GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD128Key); + break; + + case ItemKey.RIPEMD160: + key = GetStringFieldValue(Data.Models.Metadata.Rom.RIPEMD160Key); + break; + + case ItemKey.SHA1: + key = GetStringFieldValue(Data.Models.Metadata.Rom.SHA1Key); + break; + + case ItemKey.SHA256: + key = GetStringFieldValue(Data.Models.Metadata.Rom.SHA256Key); + break; + + case ItemKey.SHA384: + key = GetStringFieldValue(Data.Models.Metadata.Rom.SHA384Key); + break; + + case ItemKey.SHA512: + key = GetStringFieldValue(Data.Models.Metadata.Rom.SHA512Key); + break; + + case ItemKey.SpamSum: + key = GetStringFieldValue(Data.Models.Metadata.Rom.SpamSumKey); + break; + + // Let the base handle generic stuff + default: + return base.GetKey(bucketedBy, machine, source, lower, norename); + } +#pragma warning restore IDE0010 + + // Double and triple check the key for corner cases + key ??= string.Empty; + if (lower) + key = key.ToLowerInvariant(); + + return key; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Sample.cs b/SabreTools.Metadata.DatItems/Formats/Sample.cs new file mode 100644 index 00000000..fa7da685 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Sample.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a (usually WAV-formatted) sample to be included for use in the set + /// + [JsonObject("sample"), XmlRoot("sample")] + public class Sample : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Sample; + + #endregion + + #region Constructors + + public Sample() : base() { } + + public Sample(Data.Models.Metadata.Sample item) : base(item) { } + + public Sample(Data.Models.Metadata.Sample item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Serials.cs b/SabreTools.Metadata.DatItems/Formats/Serials.cs new file mode 100644 index 00000000..4622250e --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Serials.cs @@ -0,0 +1,180 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +// TODO: Add item mappings for all fields +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single serials item + /// + [JsonObject("serials"), XmlRoot("serials")] + public sealed class Serials : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Serials; + + /// + /// Digital serial 1 value + /// + [JsonProperty("digital_serial1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("digital_serial1")] + public string? DigitalSerial1 { get; set; } + + /// + /// Digital serial 2 value + /// + [JsonProperty("digital_serial2", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("digital_serial2")] + public string? DigitalSerial2 { get; set; } + + /// + /// Media serial 1 value + /// + [JsonProperty("media_serial1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("media_serial1")] + public string? MediaSerial1 { get; set; } + + /// + /// Media serial 2 value + /// + [JsonProperty("media_serial2", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("media_serial2")] + public string? MediaSerial2 { get; set; } + + /// + /// Media serial 3 value + /// + [JsonProperty("media_serial3", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("media_serial3")] + public string? MediaSerial3 { get; set; } + + /// + /// PCB serial value + /// + [JsonProperty("pcb_serial", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("pcb_serial")] + public string? PcbSerial { get; set; } + + /// + /// Rom chip serial 1 value + /// + [JsonProperty("romchip_serial1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("romchip_serial1")] + public string? RomChipSerial1 { get; set; } + + /// + /// Rom chip serial 2 value + /// + [JsonProperty("romchip_serial2", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("romchip_serial2")] + public string? RomChipSerial2 { get; set; } + + /// + /// Lockout serial value + /// + [JsonProperty("lockout_serial", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("lockout_serial")] + public string? LockoutSerial { get; set; } + + /// + /// Save chip serial value + /// + [JsonProperty("savechip_serial", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("savechip_serial")] + public string? SaveChipSerial { get; set; } + + /// + /// Chip serial value + /// + [JsonProperty("chip_serial", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("chip_serial")] + public string? ChipSerial { get; set; } + + /// + /// Box serial value + /// + [JsonProperty("box_serial", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("box_serial")] + public string? BoxSerial { get; set; } + + /// + /// Media stamp value + /// + [JsonProperty("mediastamp", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("mediastamp")] + public string? MediaStamp { get; set; } + + /// + /// Box barcode value + /// + [JsonProperty("box_barcode", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("box_barcode")] + public string? BoxBarcode { get; set; } + + #endregion + + #region Constructors + + /// + /// Create a default, empty Serials object + /// + public Serials() + { + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType); + } + + #endregion + + #region Cloning Methods + + /// + public override object Clone() + { + var serials = new Serials() + { + DigitalSerial1 = this.DigitalSerial1, + DigitalSerial2 = this.DigitalSerial2, + MediaSerial1 = this.MediaSerial1, + MediaSerial2 = this.MediaSerial2, + MediaSerial3 = this.MediaSerial3, + PcbSerial = this.PcbSerial, + RomChipSerial1 = this.RomChipSerial1, + RomChipSerial2 = this.RomChipSerial2, + LockoutSerial = this.LockoutSerial, + SaveChipSerial = this.SaveChipSerial, + ChipSerial = this.ChipSerial, + BoxSerial = this.BoxSerial, + MediaStamp = this.MediaStamp, + BoxBarcode = this.BoxBarcode, + }; + serials.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + serials.SetFieldValue(MachineKey, GetMachine()); + serials.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + serials.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + serials.SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue()); + + return serials; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(DatItem? other) + { + // If we don't have a Serials, return false + if (GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) != other?.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey)) + return false; + + // Otherwise, treat it as a Serials + Serials? newOther = other as Serials; + + // If the Serials information matches + return DigitalSerial1 == newOther!.DigitalSerial1 + && DigitalSerial2 == newOther.DigitalSerial2 + && MediaSerial1 == newOther.MediaSerial1 + && MediaSerial2 == newOther.MediaSerial2 + && MediaSerial3 == newOther.MediaSerial3 + && PcbSerial == newOther.PcbSerial + && RomChipSerial1 == newOther.RomChipSerial1 + && RomChipSerial2 == newOther.RomChipSerial2 + && LockoutSerial == newOther.LockoutSerial + && SaveChipSerial == newOther.SaveChipSerial + && ChipSerial == newOther.ChipSerial + && BoxSerial == newOther.BoxSerial + && MediaStamp == newOther.MediaStamp + && BoxBarcode == newOther.BoxBarcode; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/SharedFeat.cs b/SabreTools.Metadata.DatItems/Formats/SharedFeat.cs new file mode 100644 index 00000000..0bae9d8b --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/SharedFeat.cs @@ -0,0 +1,33 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one shared feature object + /// + [JsonObject("sharedfeat"), XmlRoot("sharedfeat")] + public sealed class SharedFeat : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.SharedFeat; + + #endregion + + #region Constructors + + public SharedFeat() : base() { } + + public SharedFeat(Data.Models.Metadata.SharedFeat item) : base(item) { } + + public SharedFeat(Data.Models.Metadata.SharedFeat item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Slot.cs b/SabreTools.Metadata.DatItems/Formats/Slot.cs new file mode 100644 index 00000000..1e329ee2 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Slot.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which Slot(s) is associated with a set + /// + [JsonObject("slot"), XmlRoot("slot")] + public sealed class Slot : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Slot; + + [JsonIgnore] + public bool SlotOptionsSpecified + { + get + { + var slotOptions = GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey); + return slotOptions is not null && slotOptions.Length > 0; + } + } + + #endregion + + #region Constructors + + public Slot() : base() { } + + public Slot(Data.Models.Metadata.Slot item) : base(item) + { + // Handle subitems + var slotOptions = item.ReadItemArray(Data.Models.Metadata.Slot.SlotOptionKey); + if (slotOptions is not null) + { + SlotOption[] slotOptionItems = Array.ConvertAll(slotOptions, slotOption => new SlotOption(slotOption)); + SetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey, slotOptionItems); + } + } + + public Slot(Data.Models.Metadata.Slot item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + + #region Cloning Methods + + /// + public override Data.Models.Metadata.Slot GetInternalClone() + { + var slotItem = base.GetInternalClone(); + + var slotOptions = GetFieldValue(Data.Models.Metadata.Slot.SlotOptionKey); + if (slotOptions is not null) + { + Data.Models.Metadata.SlotOption[] slotOptionItems = Array.ConvertAll(slotOptions, slotOption => slotOption.GetInternalClone()); + slotItem[Data.Models.Metadata.Slot.SlotOptionKey] = slotOptionItems; + } + + return slotItem; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/SlotOption.cs b/SabreTools.Metadata.DatItems/Formats/SlotOption.cs new file mode 100644 index 00000000..c4a55b8f --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/SlotOption.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents one ListXML slotoption + /// + [JsonObject("slotoption"), XmlRoot("slotoption")] + public sealed class SlotOption : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.SlotOption; + + #endregion + + #region Constructors + + public SlotOption() : base() { } + + public SlotOption(Data.Models.Metadata.SlotOption item) : base(item) + { + // Process flag values + if (GetBoolFieldValue(Data.Models.Metadata.SlotOption.DefaultKey) is not null) + SetFieldValue(Data.Models.Metadata.SlotOption.DefaultKey, GetBoolFieldValue(Data.Models.Metadata.SlotOption.DefaultKey).FromYesNo()); + } + + public SlotOption(Data.Models.Metadata.SlotOption item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/SoftwareList.cs b/SabreTools.Metadata.DatItems/Formats/SoftwareList.cs new file mode 100644 index 00000000..55d7128d --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/SoftwareList.cs @@ -0,0 +1,41 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents which SoftwareList(s) is associated with a set + /// + [JsonObject("softwarelist"), XmlRoot("softwarelist")] + public sealed class SoftwareList : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.SoftwareList; + + #endregion + + #region Constructors + + public SoftwareList() : base() { } + + public SoftwareList(Data.Models.Metadata.SoftwareList item) : base(item) + { + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey) is not null) + SetFieldValue(Data.Models.Metadata.SoftwareList.StatusKey, GetStringFieldValue(Data.Models.Metadata.SoftwareList.StatusKey).AsSoftwareListStatus().AsStringValue()); + + // Handle subitems + // TODO: Handle the Software subitem + } + + public SoftwareList(Data.Models.Metadata.SoftwareList item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/Sound.cs b/SabreTools.Metadata.DatItems/Formats/Sound.cs new file mode 100644 index 00000000..8305809e --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/Sound.cs @@ -0,0 +1,38 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents the sound output for a machine + /// + [JsonObject("sound"), XmlRoot("sound")] + public sealed class Sound : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.Sound; + + #endregion + + #region Constructors + + public Sound() : base() { } + + public Sound(Data.Models.Metadata.Sound item) : base(item) + { + // Process flag values + if (GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey) is not null) + SetFieldValue(Data.Models.Metadata.Sound.ChannelsKey, GetInt64FieldValue(Data.Models.Metadata.Sound.ChannelsKey).ToString()); + } + + public Sound(Data.Models.Metadata.Sound item, Machine machine, Source source) : this(item) + { + SetFieldValue(SourceKey, source); + CopyMachineInformation(machine); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Formats/SourceDetails.cs b/SabreTools.Metadata.DatItems/Formats/SourceDetails.cs new file mode 100644 index 00000000..3dcf2b9d --- /dev/null +++ b/SabreTools.Metadata.DatItems/Formats/SourceDetails.cs @@ -0,0 +1,230 @@ +using System.Xml.Serialization; +using Newtonsoft.Json; + +// TODO: Add item mappings for all fields +namespace SabreTools.Metadata.DatItems.Formats +{ + /// + /// Represents a single source details item + /// + [JsonObject("source_details"), XmlRoot("source_details")] + public sealed class SourceDetails : DatItem + { + #region Fields + + /// /> + protected override ItemType ItemType => ItemType.SourceDetails; + + /// + /// Id value + /// + /// TODO: Is this required? + [JsonProperty("id", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("id")] + public string? Id { get; set; } + + /// + /// Section value + /// + [JsonProperty("section", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("section")] + public string? Section { get; set; } + + /// + /// Rom info value + /// + [JsonProperty("rominfo", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("rominfo")] + public string? RomInfo { get; set; } + + /// + /// Dumping date value + /// + [JsonProperty("d_date", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("d_date")] + public string? DDate { get; set; } + + /// + /// Dumping date info value + /// + [JsonProperty("d_date_info", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("d_date_info")] + public string? DDateInfo { get; set; } + + /// + /// Release date value + /// + [JsonProperty("r_date", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("r_date")] + public string? RDate { get; set; } + + /// + /// Release date info value + /// + [JsonProperty("r_date_info", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("r_date_info")] + public string? RDateInfo { get; set; } + + /// + /// Origin value + /// + [JsonProperty("origin", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("origin")] + public string? Origin { get; set; } + + /// + /// Region value + /// + [JsonProperty("region", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("region")] + public string? Region { get; set; } + + /// + /// Media title value + /// + [JsonProperty("media_title", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("media_title")] + public string? MediaTitle { get; set; } + + /// + /// Dumper value + /// + [JsonProperty("dumper", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("dumper")] + public string? Dumper { get; set; } + + /// + /// Project value + /// + [JsonProperty("project", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("project")] + public string? Project { get; set; } + + /// + /// Original format value + /// + [JsonProperty("originalformat", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("originalformat")] + public string? OriginalFormat { get; set; } + + /// + /// Nodump value + /// + [JsonProperty("nodump", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("nodump")] + public string? Nodump { get; set; } + + /// + /// Tool value + /// + [JsonProperty("tool", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("tool")] + public string? Tool { get; set; } + + /// + /// Comment 1 value + /// + [JsonProperty("comment1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("comment1")] + public string? Comment1 { get; set; } + + /// + /// Link 2 value + /// + [JsonProperty("comment2", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("comment2")] + public string? Comment2 { get; set; } + + /// + /// Link 1 value + /// + [JsonProperty("link1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("link1")] + public string? Link1 { get; set; } + + /// + /// Link 2 value + /// + [JsonProperty("link2", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("link2")] + public string? Link2 { get; set; } + + /// + /// Link 3 value + /// + [JsonProperty("link3", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("link3")] + public string? Link3 { get; set; } + + #endregion + + #region Constructors + + /// + /// Create a default, empty SourceDetails object + /// + public SourceDetails() + { + SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, ItemType.SourceDetails); + } + + #endregion + + #region Cloning Methods + + /// + public override object Clone() + { + var sourceDetails = new SourceDetails() + { + Id = this.Id, + Section = this.Section, + RomInfo = this.RomInfo, + DDate = this.DDate, + DDateInfo = this.DDateInfo, + RDate = this.RDate, + RDateInfo = this.RDateInfo, + Origin = this.Origin, + Region = this.Region, + MediaTitle = this.MediaTitle, + Dumper = this.Dumper, + Project = this.Project, + OriginalFormat = this.OriginalFormat, + Nodump = this.Nodump, + Tool = this.Tool, + Comment1 = this.Comment1, + Comment2 = this.Comment2, + Link1 = this.Link1, + Link2 = this.Link2, + Link3 = this.Link3, + }; + sourceDetails.SetFieldValue(DupeTypeKey, GetFieldValue(DupeTypeKey)); + sourceDetails.SetFieldValue(MachineKey, GetMachine()); + sourceDetails.SetFieldValue(RemoveKey, GetBoolFieldValue(RemoveKey)); + sourceDetails.SetFieldValue(SourceKey, GetFieldValue(SourceKey)); + sourceDetails.SetFieldValue(Data.Models.Metadata.DatItem.TypeKey, GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey).AsItemType().AsStringValue()); + + return sourceDetails; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(DatItem? other) + { + // If we don't have a SourceDetails, return false + if (GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey) != other?.GetStringFieldValue(Data.Models.Metadata.DatItem.TypeKey)) + return false; + + // Otherwise, treat it as a SourceDetails + SourceDetails? newOther = other as SourceDetails; + + // If the Details information matches + return Id == newOther!.Id + && Section == newOther.Section + && RomInfo == newOther.RomInfo + && DDate == newOther.DDate + && DDateInfo == newOther.DDateInfo + && RomInfo == newOther.RomInfo + && RDate == newOther.RDate + && RDateInfo == newOther.RDateInfo + && Origin == newOther.Origin + && Region == newOther.Region + && MediaTitle == newOther.MediaTitle + && Dumper == newOther.Dumper + && Project == newOther.Project + && OriginalFormat == newOther.OriginalFormat + && Nodump == newOther.Nodump + && Tool == newOther.Tool + && Comment1 == newOther.Comment1 + && Comment2 == newOther.Comment2 + && Link1 == newOther.Link1 + && Link2 == newOther.Link2 + && Link3 == newOther.Link3; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Machine.cs b/SabreTools.Metadata.DatItems/Machine.cs new file mode 100644 index 00000000..670d9e93 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Machine.cs @@ -0,0 +1,156 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Data.Extensions; +using SabreTools.Metadata.Filter; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems +{ + /// + /// Represents the information specific to a set/game/machine + /// + [JsonObject("machine"), XmlRoot("machine")] + public sealed class Machine : ModelBackedItem, ICloneable, IEquatable + { + #region Constructors + + public Machine() + { + _internal = []; + } + + public Machine(Data.Models.Metadata.Machine machine) + { + // Get all fields to automatically copy without processing + var nonItemFields = TypeHelper.GetConstants(typeof(Data.Models.Metadata.Machine)); + if (nonItemFields is null) + return; + + // Populate the internal machine from non-filter fields + _internal = []; + foreach (string fieldName in nonItemFields) + { + if (machine.TryGetValue(fieldName, out var value)) + _internal[fieldName] = value; + } + + // Process flag values + if (GetStringFieldValue(Data.Models.Metadata.Machine.Im1CRCKey) is not null) + SetFieldValue(Data.Models.Metadata.Machine.Im1CRCKey, TextHelper.NormalizeCRC32(GetStringFieldValue(Data.Models.Metadata.Machine.Im1CRCKey))); + if (GetStringFieldValue(Data.Models.Metadata.Machine.Im2CRCKey) is not null) + SetFieldValue(Data.Models.Metadata.Machine.Im2CRCKey, TextHelper.NormalizeCRC32(GetStringFieldValue(Data.Models.Metadata.Machine.Im2CRCKey))); + if (GetBoolFieldValue(Data.Models.Metadata.Machine.IsBiosKey) is not null) + SetFieldValue(Data.Models.Metadata.Machine.IsBiosKey, GetBoolFieldValue(Data.Models.Metadata.Machine.IsBiosKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Machine.IsDeviceKey) is not null) + SetFieldValue(Data.Models.Metadata.Machine.IsDeviceKey, GetBoolFieldValue(Data.Models.Metadata.Machine.IsDeviceKey).FromYesNo()); + if (GetBoolFieldValue(Data.Models.Metadata.Machine.IsMechanicalKey) is not null) + SetFieldValue(Data.Models.Metadata.Machine.IsMechanicalKey, GetBoolFieldValue(Data.Models.Metadata.Machine.IsMechanicalKey).FromYesNo()); + if (GetStringFieldValue(Data.Models.Metadata.Machine.SupportedKey) is not null) + SetFieldValue(Data.Models.Metadata.Machine.SupportedKey, GetStringFieldValue(Data.Models.Metadata.Machine.SupportedKey).AsSupported().AsStringValue()); + + // Handle Trurip object, if it exists + if (machine.ContainsKey(Data.Models.Metadata.Machine.TruripKey)) + { + var truripItem = machine.Read(Data.Models.Metadata.Machine.TruripKey); + if (truripItem is not null) + SetFieldValue(Data.Models.Metadata.Machine.TruripKey, new Trurip(truripItem)); + } + } + + #endregion + + #region Accessors + + /// + /// Gets the name to use for a Machine + /// + /// Name if available, null otherwise + public string? GetName() => _internal.GetName(); + + /// + /// Sets the name to use for a Machine + /// + /// Name to set for the item + public void SetName(string? name) => _internal.SetName(name); + + #endregion + + #region Cloning methods + + /// + /// Create a clone of the current machine + /// + /// New machine with the same values as the current one + public object Clone() + { + return new Machine() + { + _internal = _internal.Clone() as Data.Models.Metadata.Machine ?? [], + }; + } + + /// + /// Get a clone of the current internal model + /// + public Data.Models.Metadata.Machine GetInternalClone() => (_internal.Clone() as Data.Models.Metadata.Machine)!; + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not Machine otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not Machine otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + /// + public bool Equals(Machine? other) + { + // If other is null + if (other is null) + return false; + + // Compare internal models + return _internal.EqualTo(other._internal); + } + + #endregion + + #region Manipulation + + /// + /// Runs a filter and determines if it passes or not + /// + /// Filter runner to use for checking + /// True if the Machine passes the filter, false otherwise + public bool PassesFilter(FilterRunner filterRunner) => filterRunner.Run(_internal); + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/SabreTools.Metadata.DatItems.csproj b/SabreTools.Metadata.DatItems/SabreTools.Metadata.DatItems.csproj new file mode 100644 index 00000000..32969615 --- /dev/null +++ b/SabreTools.Metadata.DatItems/SabreTools.Metadata.DatItems.csproj @@ -0,0 +1,45 @@ + + + + + net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 + true + false + false + true + latest + enable + true + snupkg + true + 2.3.0 + + + Matt Nadareski + DatItem specific functionality for metadata file processing + Copyright (c) Matt Nadareski 2016-2026 + https://github.com/SabreTools/ + README.md + https://github.com/SabreTools/SabreTools.Serialization + git + metadata dat datfile + MIT + + + + + + + + + + + + + + + + + + + diff --git a/SabreTools.Metadata.DatItems/Source.cs b/SabreTools.Metadata.DatItems/Source.cs new file mode 100644 index 00000000..383cf9e8 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Source.cs @@ -0,0 +1,41 @@ +using System; + +#pragma warning disable IDE0290 // Use primary constructor +namespace SabreTools.Metadata.DatItems +{ + /// + /// Source information wrapper + /// + public class Source : ICloneable + { + /// + /// Source index + /// + public readonly int Index; + + /// + /// Source name + /// + public readonly string? Name; + + /// + /// Constructor + /// + /// Source ID + /// Source name, optional + public Source(int id, string? source = null) + { + Index = id; + Name = source; + } + + #region Cloning + + /// + /// Clone the current object + /// + public object Clone() => new Source(Index, Name); + + #endregion + } +} diff --git a/SabreTools.Metadata.DatItems/Trurip.cs b/SabreTools.Metadata.DatItems/Trurip.cs new file mode 100644 index 00000000..e6927cf9 --- /dev/null +++ b/SabreTools.Metadata.DatItems/Trurip.cs @@ -0,0 +1,146 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.DatItems +{ + /// + /// Represents TruRip/EmuArc-specific values on a machine + /// + [JsonObject("trurip"), XmlRoot("trurip")] + public sealed class Trurip : ICloneable + { + #region Fields + + /// + /// Title ID + /// + [JsonProperty("titleid", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("titleid")] + public string? TitleID { get; set; } = null; + + /// + /// Machine developer + /// + [JsonProperty("developer", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("developer")] + public string? Developer { get; set; } = null; + + /// + /// Game genre + /// + [JsonProperty("genre", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("genre")] + public string? Genre { get; set; } = null; + + /// + /// Game subgenre + /// + [JsonProperty("subgenre", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("subgenre")] + public string? Subgenre { get; set; } = null; + + /// + /// Game ratings + /// + [JsonProperty("ratings", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("ratings")] + public string? Ratings { get; set; } = null; + + /// + /// Game score + /// + [JsonProperty("score", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("score")] + public string? Score { get; set; } = null; + + /// + /// Is the machine enabled + /// + [JsonProperty("enabled", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("enabled")] + public string? Enabled { get; set; } = null; // bool? + + /// + /// Does the game have a CRC check + /// + [JsonProperty("hascrc", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("hascrc")] + public bool? Crc { get; set; } = null; + + [JsonIgnore] + public bool CrcSpecified { get { return Crc is not null; } } + + /// + /// Machine relations + /// + [JsonProperty("relatedto", DefaultValueHandling = DefaultValueHandling.Ignore)] + [XmlElement("relatedto")] + public string? RelatedTo { get; set; } = null; + + #endregion + + #region Constructors + + public Trurip() { } + + public Trurip(Data.Models.Logiqx.Trurip trurip) + { + TitleID = trurip.TitleID; + Developer = trurip.Developer; + Genre = trurip.Genre; + Subgenre = trurip.Subgenre; + Ratings = trurip.Ratings; + Score = trurip.Score; + Enabled = trurip.Enabled; + Crc = trurip.CRC.AsYesNo(); + RelatedTo = trurip.RelatedTo; + } + + #endregion + + #region Cloning methods + + /// + /// Create a clone of the current object + /// + /// New object with the same values as the current one + public object Clone() + { + return new Trurip() + { + TitleID = this.TitleID, + Developer = this.Developer, + Genre = this.Genre, + Subgenre = this.Subgenre, + Ratings = this.Ratings, + Score = this.Score, + Enabled = this.Enabled, + Crc = this.Crc, + RelatedTo = this.RelatedTo, + }; + } + + /// + /// Convert to the internal Logiqx model + /// + public Data.Models.Logiqx.Trurip ConvertToLogiqx() + { + return new Data.Models.Logiqx.Trurip() + { + TitleID = this.TitleID, + Developer = this.Developer, + Genre = this.Genre, + Subgenre = this.Subgenre, + Ratings = this.Ratings, + Score = this.Score, + Enabled = this.Enabled, + CRC = this.Crc.FromYesNo(), + RelatedTo = this.RelatedTo, + }; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Filter.Test/ExtraIniItemTests.cs b/SabreTools.Metadata.Filter.Test/ExtraIniItemTests.cs new file mode 100644 index 00000000..1c6d3af7 --- /dev/null +++ b/SabreTools.Metadata.Filter.Test/ExtraIniItemTests.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace SabreTools.Metadata.Filter.Test +{ + public class ExtraIniItemTests + { + [Fact] + public void Constructor_EmptyPath_NoMappings() + { + string iniPath = string.Empty; + ExtraIniItem extraIniItem = new ExtraIniItem("Sample.Name", iniPath); + Assert.Empty(extraIniItem.Mappings); + } + + [Fact] + public void Constructor_InvalidPath_NoMappings() + { + string iniPath = "INVALID"; + ExtraIniItem extraIniItem = new ExtraIniItem("Sample.Name", iniPath); + Assert.Empty(extraIniItem.Mappings); + } + + [Fact] + public void Constructor_ValidPath_Mappings() + { + string iniPath = Path.Combine(Environment.CurrentDirectory, "TestData", "extra.ini"); + ExtraIniItem extraIniItem = new ExtraIniItem("Sample.Name", iniPath); + + Dictionary mappings = extraIniItem.Mappings; + Assert.NotEmpty(mappings); + Assert.Equal("true", mappings["useBool"]); + Assert.Equal("Value", mappings["useValue"]); + Assert.Equal("Other", mappings["useOther"]); + } + } +} diff --git a/SabreTools.Metadata.Filter.Test/FilterKeyTests.cs b/SabreTools.Metadata.Filter.Test/FilterKeyTests.cs new file mode 100644 index 00000000..482c68d3 --- /dev/null +++ b/SabreTools.Metadata.Filter.Test/FilterKeyTests.cs @@ -0,0 +1,81 @@ +using System; +using Xunit; + +namespace SabreTools.Metadata.Filter.Test +{ + public class FilterKeyTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("ItemName")] + [InlineData("ItemName.FieldName.Extra")] + [InlineData("InvalidItemName.FieldName")] + [InlineData("DatItem.InvalidFieldName")] + [InlineData("Item.InvalidFieldName")] + [InlineData("Sample.InvalidFieldName")] + public void Constructor_InvalidKey_Throws(string? key) + { + Assert.Throws(() => new FilterKey(key)); + } + + [Theory] + [InlineData("header.name", "header", "name")] + [InlineData("HEADER.NAME", "header", "name")] + [InlineData("game.name", "machine", "name")] + [InlineData("GAME.NAME", "machine", "name")] + [InlineData("machine.name", "machine", "name")] + [InlineData("MACHINE.NAME", "machine", "name")] + [InlineData("resource.name", "machine", "name")] + [InlineData("RESOURCE.NAME", "machine", "name")] + [InlineData("set.name", "machine", "name")] + [InlineData("SET.NAME", "machine", "name")] + [InlineData("datitem.name", "item", "name")] + [InlineData("DATITEM.NAME", "item", "name")] + [InlineData("item.name", "item", "name")] + [InlineData("ITEM.NAME", "item", "name")] + [InlineData("sample.name", "sample", "name")] + [InlineData("SAMPLE.NAME", "sample", "name")] + public void Constructor_ValidKey_Sets(string? key, string expectedItemName, string expectedFieldName) + { + FilterKey filterKey = new FilterKey(key); + Assert.Equal(expectedItemName, filterKey.ItemName); + Assert.Equal(expectedFieldName, filterKey.FieldName); + } + + [Theory] + [InlineData("", "FieldName")] + [InlineData("ItemName", "")] + [InlineData("DatItem", "InvalidFieldName")] + [InlineData("Item", "InvalidFieldName")] + [InlineData("Sample", "InvalidFieldName")] + public void Constructor_InvalidNames_Throws(string itemName, string fieldName) + { + Assert.Throws(() => new FilterKey(itemName, fieldName)); + } + + [Theory] + [InlineData("header", "name", "header", "name")] + [InlineData("HEADER", "NAME", "header", "name")] + [InlineData("game", "name", "machine", "name")] + [InlineData("GAME", "NAME", "machine", "name")] + [InlineData("machine", "name", "machine", "name")] + [InlineData("MACHINE", "NAME", "machine", "name")] + [InlineData("resource", "name", "machine", "name")] + [InlineData("RESOURCE", "NAME", "machine", "name")] + [InlineData("set", "name", "machine", "name")] + [InlineData("SET", "NAME", "machine", "name")] + [InlineData("datitem", "name", "item", "name")] + [InlineData("DATITEM", "NAME", "item", "name")] + [InlineData("item", "name", "item", "name")] + [InlineData("ITEM", "NAME", "item", "name")] + [InlineData("sample", "name", "sample", "name")] + [InlineData("SAMPLE", "NAME", "sample", "name")] + public void Constructor_ValidNames_Sets(string itemName, string fieldName, string expectedItemName, string expectedFieldName) + { + FilterKey filterKey = new FilterKey(itemName, fieldName); + Assert.Equal(expectedItemName, filterKey.ItemName); + Assert.Equal(expectedFieldName, filterKey.FieldName); + } + } +} diff --git a/SabreTools.Metadata.Filter.Test/FilterObjectTests.cs b/SabreTools.Metadata.Filter.Test/FilterObjectTests.cs new file mode 100644 index 00000000..34b96bf7 --- /dev/null +++ b/SabreTools.Metadata.Filter.Test/FilterObjectTests.cs @@ -0,0 +1,476 @@ +using System; +using SabreTools.Data.Models.Metadata; +using Xunit; + +namespace SabreTools.Metadata.Filter.Test +{ + public class FilterObjectTests + { + #region Constructor + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("Sample.Name")] + [InlineData("Sample.Name++")] + public void Constructor_InvalidKey_Throws(string? filterString) + { + Assert.Throws(() => new FilterObject(filterString)); + } + + [Theory] + [InlineData("Sample.Name=XXXXXX", Operation.Equals)] + [InlineData("Sample.Name==XXXXXX", Operation.Equals)] + [InlineData("Sample.Name:XXXXXX", Operation.Equals)] + [InlineData("Sample.Name::XXXXXX", Operation.Equals)] + [InlineData("Sample.Name!XXXXXX", Operation.NotEquals)] + [InlineData("Sample.Name!=XXXXXX", Operation.NotEquals)] + [InlineData("Sample.Name!:XXXXXX", Operation.NotEquals)] + [InlineData("Sample.Name>XXXXXX", Operation.GreaterThan)] + [InlineData("Sample.Name>=XXXXXX", Operation.GreaterThanOrEqual)] + [InlineData("Sample.NameXXXXXX", Operation.NONE)] + [InlineData("Sample.Name=XXXXXX", Operation.NONE)] + [InlineData("Sample.Name>", Operation.GreaterThan)] + [InlineData("Sample.Name", "XXXXXX", ">=", Operation.GreaterThanOrEqual)] + [InlineData("Sample.Name", "XXXXXX", "<", Operation.LessThan)] + [InlineData("Sample.Name", "XXXXXX", "<=", Operation.LessThanOrEqual)] + [InlineData("Sample.Name", "XXXXXX", "@@", Operation.NONE)] + [InlineData("Sample.Name", "XXXXXX", ":!", Operation.NONE)] + [InlineData("Sample.Name", "XXXXXX", "=>", Operation.NONE)] + [InlineData("Sample.Name", "XXXXXX", "=<", Operation.NONE)] + public void Constructor_TripleString(string itemField, string? value, string? operation, Operation expected) + { + FilterObject filterObject = new FilterObject(itemField, value, operation); + Assert.Equal("sample", filterObject.Key.ItemName); + Assert.Equal("name", filterObject.Key.FieldName); + Assert.Equal("XXXXXX", filterObject.Value); + Assert.Equal(expected, filterObject.Operation); + } + + #endregion + + #region MatchesEqual + + [Fact] + public void MatchesEqual_NoKey() + { + var sample = new Sample(); + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesEqual_NoValue() + { + var sample = new Sample { [Sample.NameKey] = null }; + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesEqual_BoolValue() + { + var sample = new Sample { [Sample.NameKey] = "true" }; + FilterObject filterObject = new FilterObject("Sample.Name", "yes", Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesEqual_Int64Value() + { + var sample = new Sample { [Sample.NameKey] = "12345" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12345", Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesEqual_DoubleValue() + { + var sample = new Sample { [Sample.NameKey] = "12.345" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12.345", Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesEqual_RegexValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "^XXXXXX$", Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesEqual_StringValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "XXXXXX", Operation.Equals); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + #endregion + + #region MatchesNotEqual + + [Fact] + public void MatchesNotEqual_NoKey() + { + var sample = new Sample(); + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesNotEqual_NoValue() + { + var sample = new Sample { [Sample.NameKey] = null }; + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesNotEqual_BoolValue() + { + var sample = new Sample { [Sample.NameKey] = "true" }; + FilterObject filterObject = new FilterObject("Sample.Name", "yes", Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesNotEqual_Int64Value() + { + var sample = new Sample { [Sample.NameKey] = "12345" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12345", Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesNotEqual_DoubleValue() + { + var sample = new Sample { [Sample.NameKey] = "12.345" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12.345", Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesNotEqual_RegexValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "^XXXXXX$", Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesNotEqual_StringValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "XXXXXX", Operation.NotEquals); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + #endregion + + #region MatchesGreaterThan + + [Fact] + public void MatchesGreaterThan_NoKey() + { + var sample = new Sample(); + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThan_NoValue() + { + var sample = new Sample { [Sample.NameKey] = null }; + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThan_BoolValue() + { + var sample = new Sample { [Sample.NameKey] = "true" }; + FilterObject filterObject = new FilterObject("Sample.Name", "yes", Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThan_Int64Value() + { + var sample = new Sample { [Sample.NameKey] = "12346" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12345", Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesGreaterThan_DoubleValue() + { + var sample = new Sample { [Sample.NameKey] = "12.346" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12.345", Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesGreaterThan_RegexValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "^XXXXXX$", Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThan_StringValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "XXXXXX", Operation.GreaterThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + #endregion + + #region MatchesGreaterThanOrEqual + + [Fact] + public void MatchesGreaterThanOrEqual_NoKey() + { + var sample = new Sample(); + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThanOrEqual_NoValue() + { + var sample = new Sample { [Sample.NameKey] = null }; + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThanOrEqual_BoolValue() + { + var sample = new Sample { [Sample.NameKey] = "true" }; + FilterObject filterObject = new FilterObject("Sample.Name", "yes", Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThanOrEqual_Int64Value() + { + var sample = new Sample { [Sample.NameKey] = "12346" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12345", Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesGreaterThanOrEqual_DoubleValue() + { + var sample = new Sample { [Sample.NameKey] = "12.346" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12.345", Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesGreaterThanOrEqual_RegexValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "^XXXXXX$", Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesGreaterThanOrEqual_StringValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "XXXXXX", Operation.GreaterThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + #endregion + + #region MatchesLessThan + + [Fact] + public void MatchesLessThan_NoKey() + { + var sample = new Sample(); + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThan_NoValue() + { + var sample = new Sample { [Sample.NameKey] = null }; + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThan_BoolValue() + { + var sample = new Sample { [Sample.NameKey] = "true" }; + FilterObject filterObject = new FilterObject("Sample.Name", "yes", Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThan_Int64Value() + { + var sample = new Sample { [Sample.NameKey] = "12344" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12345", Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesLessThan_DoubleValue() + { + var sample = new Sample { [Sample.NameKey] = "12.344" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12.345", Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesLessThan_RegexValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "^XXXXXX$", Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThan_StringValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "XXXXXX", Operation.LessThan); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + #endregion + + #region MatchesLessThanOrEqual + + [Fact] + public void MatchesLessThanOrEqual_NoKey() + { + var sample = new Sample(); + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThanOrEqual_NoValue() + { + var sample = new Sample { [Sample.NameKey] = null }; + FilterObject filterObject = new FilterObject("Sample.Name", null, Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThanOrEqual_BoolValue() + { + var sample = new Sample { [Sample.NameKey] = "true" }; + FilterObject filterObject = new FilterObject("Sample.Name", "yes", Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThanOrEqual_Int64Value() + { + var sample = new Sample { [Sample.NameKey] = "12344" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12345", Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesLessThanOrEqual_DoubleValue() + { + var sample = new Sample { [Sample.NameKey] = "12.344" }; + FilterObject filterObject = new FilterObject("Sample.Name", "12.345", Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.True(actual); + } + + [Fact] + public void MatchesLessThanOrEqual_RegexValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "^XXXXXX$", Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + [Fact] + public void MatchesLessThanOrEqual_StringValue() + { + var sample = new Sample { [Sample.NameKey] = "XXXXXX" }; + FilterObject filterObject = new FilterObject("Sample.Name", "XXXXXX", Operation.LessThanOrEqual); + bool actual = filterObject.Matches(sample); + Assert.False(actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Filter.Test/FilterRunnerTests.cs b/SabreTools.Metadata.Filter.Test/FilterRunnerTests.cs new file mode 100644 index 00000000..cfbaf684 --- /dev/null +++ b/SabreTools.Metadata.Filter.Test/FilterRunnerTests.cs @@ -0,0 +1,219 @@ +using SabreTools.Data.Models.Metadata; +using Xunit; + +namespace SabreTools.Metadata.Filter.Test +{ + public class FilterRunnerTests + { + private static readonly FilterRunner _filterRunner; + + static FilterRunnerTests() + { + FilterObject[] filters = + [ + new FilterObject("header.author", "auth", Operation.Equals), + new FilterObject("machine.description", "desc", Operation.Equals), + new FilterObject("item.name", "name", Operation.Equals), + new FilterObject("rom.crc", "crc", Operation.Equals), + ]; + + _filterRunner = new FilterRunner(filters); + } + + #region Header + + [Fact] + public void Header_Missing_False() + { + var header = new Header(); + bool actual = _filterRunner.Run(header); + Assert.False(actual); + } + + [Fact] + public void Header_Null_False() + { + var header = new Header { [Header.AuthorKey] = null }; + bool actual = _filterRunner.Run(header); + Assert.False(actual); + } + + [Fact] + public void Header_Empty_False() + { + var header = new Header { [Header.AuthorKey] = "" }; + bool actual = _filterRunner.Run(header); + Assert.False(actual); + } + + [Fact] + public void Header_Incorrect_False() + { + var header = new Header { [Header.AuthorKey] = "NO_MATCH" }; + bool actual = _filterRunner.Run(header); + Assert.False(actual); + } + + [Fact] + public void Header_Correct_True() + { + var header = new Header { [Header.AuthorKey] = "auth" }; + bool actual = _filterRunner.Run(header); + Assert.True(actual); + } + + #endregion + + #region Machine + + [Fact] + public void Machine_Missing_False() + { + var machine = new Machine(); + bool actual = _filterRunner.Run(machine); + Assert.False(actual); + } + + [Fact] + public void Machine_Null_False() + { + var machine = new Machine { [Machine.DescriptionKey] = null }; + bool actual = _filterRunner.Run(machine); + Assert.False(actual); + } + + [Fact] + public void Machine_Empty_False() + { + var machine = new Machine { [Machine.DescriptionKey] = "" }; + bool actual = _filterRunner.Run(machine); + Assert.False(actual); + } + + [Fact] + public void Machine_Incorrect_False() + { + var machine = new Machine { [Machine.DescriptionKey] = "NO_MATCH" }; + bool actual = _filterRunner.Run(machine); + Assert.False(actual); + } + + [Fact] + public void Machine_Correct_True() + { + var machine = new Machine { [Machine.DescriptionKey] = "desc" }; + bool actual = _filterRunner.Run(machine); + Assert.True(actual); + } + + #endregion + + #region DatItem (General) + + [Fact] + public void DatItem_Missing_False() + { + DatItem datItem = new Sample(); + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void DatItem_Null_False() + { + DatItem datItem = new Sample { [Sample.NameKey] = null }; + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void DatItem_Empty_False() + { + DatItem datItem = new Sample { [Sample.NameKey] = "" }; + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void DatItem_Incorrect_False() + { + DatItem datItem = new Sample { [Sample.NameKey] = "NO_MATCH" }; + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void DatItem_Correct_True() + { + DatItem datItem = new Sample { [Sample.NameKey] = "name" }; + bool actual = _filterRunner.Run(datItem); + Assert.True(actual); + } + + #endregion + + #region DatItem (Specific) + + [Fact] + public void Rom_Missing_False() + { + DatItem datItem = new Rom(); + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void Rom_Null_False() + { + DatItem datItem = new Rom + { + [Rom.NameKey] = "name", + [Rom.CRCKey] = null, + }; + + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void Rom_Empty_False() + { + DatItem datItem = new Rom + { + [Rom.NameKey] = "name", + [Rom.CRCKey] = "", + }; + + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void Rom_Incorrect_False() + { + DatItem datItem = new Rom + { + [Rom.NameKey] = "name", + [Rom.CRCKey] = "NO_MATCH", + }; + + bool actual = _filterRunner.Run(datItem); + Assert.False(actual); + } + + [Fact] + public void Rom_Correct_True() + { + DatItem datItem = new Rom + { + [Rom.NameKey] = "name", + [Rom.CRCKey] = "crc", + }; + + bool actual = _filterRunner.Run(datItem); + Assert.True(actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Filter.Test/SabreTools.Metadata.Filter.Test.csproj b/SabreTools.Metadata.Filter.Test/SabreTools.Metadata.Filter.Test.csproj new file mode 100644 index 00000000..6f45b24e --- /dev/null +++ b/SabreTools.Metadata.Filter.Test/SabreTools.Metadata.Filter.Test.csproj @@ -0,0 +1,32 @@ + + + + net8.0;net9.0;net10.0 + false + latest + enable + + + + + + + + + Always + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SabreTools.Metadata.Filter.Test/TestData/extra.ini b/SabreTools.Metadata.Filter.Test/TestData/extra.ini new file mode 100644 index 00000000..2fe31da3 --- /dev/null +++ b/SabreTools.Metadata.Filter.Test/TestData/extra.ini @@ -0,0 +1,12 @@ +[FOLDER_SETTINGS] +RootFolderIcon mame +SubFolderIcon folder + +[ROOT_FOLDER] +useBool + +[Value] +useValue + +[Other] +useOther diff --git a/SabreTools.Metadata.Filter/Enums.cs b/SabreTools.Metadata.Filter/Enums.cs new file mode 100644 index 00000000..a4b0fddd --- /dev/null +++ b/SabreTools.Metadata.Filter/Enums.cs @@ -0,0 +1,41 @@ +namespace SabreTools.Metadata.Filter +{ + /// + /// Determines how a filter group should be applied + /// + public enum GroupType + { + /// + /// Default value, does nothing + /// + NONE, + + /// + /// All must pass for the group to pass + /// + AND, + + /// + /// Any must pass for the group to pass + /// + OR, + } + + /// + /// Determines what operation is being done + /// + public enum Operation + { + /// + /// Default value, does nothing + /// + NONE, + + Equals, + NotEquals, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + } +} diff --git a/SabreTools.Metadata.Filter/ExtraIniItem.cs b/SabreTools.Metadata.Filter/ExtraIniItem.cs new file mode 100644 index 00000000..f82c6935 --- /dev/null +++ b/SabreTools.Metadata.Filter/ExtraIniItem.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SabreTools.IO.Logging; +using SabreTools.IO.Readers; + +namespace SabreTools.Metadata.Filter +{ + public class ExtraIniItem + { + #region Fields + + /// + /// Item type and field to update with INI information + /// + public readonly FilterKey Key; + + /// + /// Mappings from machine names to value + /// + public readonly Dictionary Mappings = []; + + #endregion + + #region Constructors + + public ExtraIniItem(string key, string iniPath) + { + Key = new FilterKey(key); + if (!PopulateFromFile(iniPath)) + Mappings.Clear(); + } + + public ExtraIniItem(string itemName, string fieldName, string iniPath) + { + Key = new FilterKey(itemName, fieldName); + if (!PopulateFromFile(iniPath)) + Mappings.Clear(); + } + + #endregion + + #region Extras Population + + /// + /// Populate the dictionary from an INI file + /// + /// Path to INI file to populate from + /// + /// The INI file format that is supported here is not exactly the same + /// as a traditional one. This expects a MAME extras format, which usually + /// doesn't contain key value pairs and always at least contains one section + /// called `ROOT_FOLDER`. If that's the name of a section, then we assume + /// the value is boolean. If there's another section name, then that is set + /// as the value instead. + /// + private bool PopulateFromFile(string iniPath) + { + // Validate the path + if (iniPath.Length == 0) + return false; + else if (!File.Exists(iniPath)) + return false; + + // Prepare all internal variables + var ir = new IniReader(iniPath) { ValidateRows = false }; + bool foundRootFolder = false; + + // If we got a null reader, just return + if (ir is null) + return false; + + // Otherwise, read the file to the end + try + { + while (!ir.EndOfStream) + { + // Read in the next line and process + ir.ReadNextLine(); + + // We don't care about whitespace or comments + if (ir.RowType == IniRowType.None || ir.RowType == IniRowType.Comment) + continue; + + // If we have a section, just read it in + if (ir.RowType == IniRowType.SectionHeader) + { + // If we've found the start of the extras, set the flag + if (string.Equals(ir.Section, "ROOT_FOLDER", StringComparison.OrdinalIgnoreCase)) + foundRootFolder = true; + + continue; + } + + // If we have a value, then we start populating the dictionary + else if (foundRootFolder) + { + // Get the value and machine name + string? value = ir.Section; + string? machineName = ir.CurrentLine?.Trim(); + + // If the section is "ROOT_FOLDER", then we use the value "true" instead. + // This is done because some INI files use the name of the file as the + // category to be assigned to the items included. + if (value == "ROOT_FOLDER") + value = "true"; + + // Add the new mapping + if (machineName is not null && value is not null) + Mappings[machineName] = value; + } + } + } + catch (Exception ex) + { + LoggerImpl.Warning(ex, $"Exception found while parsing '{iniPath}'"); + return false; + } + + ir.Dispose(); + return true; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Filter/FilterGroup.cs b/SabreTools.Metadata.Filter/FilterGroup.cs new file mode 100644 index 00000000..6987d165 --- /dev/null +++ b/SabreTools.Metadata.Filter/FilterGroup.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using SabreTools.Data.Models.Metadata; + +namespace SabreTools.Metadata.Filter +{ + /// + /// Represents a set of filters and groups + /// + public class FilterGroup + { + /// + /// How to apply the group filters + /// + public readonly GroupType GroupType; + + /// + /// All standalone filters in the group + /// + private readonly List _subfilters = []; + + /// + /// All filter groups contained in the group + /// + private readonly List _subgroups = []; + + public FilterGroup(GroupType groupType) + { + GroupType = groupType; + } + + public FilterGroup(FilterObject[] filters, GroupType groupType) + { + _subfilters.AddRange(filters); + GroupType = groupType; + } + + public FilterGroup(FilterGroup[] groups, GroupType groupType) + { + _subgroups.AddRange(groups); + GroupType = groupType; + } + + public FilterGroup(FilterObject[] filters, FilterGroup[] groups, GroupType groupType) + { + _subfilters.AddRange(filters); + _subgroups.AddRange(groups); + GroupType = groupType; + } + + #region Accessors + + /// + /// Add a FilterObject to the set + /// + public void AddFilter(FilterObject filter) => _subfilters.Add(filter); + + /// + /// Add a FilterGroup to the set + /// + public void AddGroup(FilterGroup group) => _subgroups.Add(group); + + #endregion + + #region Matching + + /// + /// Determine if a DictionaryBase object matches the group + /// + public bool Matches(DictionaryBase dictionaryBase) + { + return GroupType switch + { + GroupType.AND => MatchesAnd(dictionaryBase), + GroupType.OR => MatchesOr(dictionaryBase), + + GroupType.NONE => false, + _ => false, + }; + } + + /// + /// Determines if a value matches all filters + /// + private bool MatchesAnd(DictionaryBase dictionaryBase) + { + // Run standalone filters + foreach (var filter in _subfilters) + { + // One failed match fails the group + if (!filter.Matches(dictionaryBase)) + return false; + } + + // Run filter subgroups + foreach (var group in _subgroups) + { + // One failed match fails the group + if (!group.Matches(dictionaryBase)) + return false; + } + + return true; + } + + /// + /// Determines if a value matches any filters + /// + private bool MatchesOr(DictionaryBase dictionaryBase) + { + // Run standalone filters + foreach (var filter in _subfilters) + { + // One successful match passes the group + if (filter.Matches(dictionaryBase)) + return true; + } + + // Run filter subgroups + foreach (var group in _subgroups) + { + // One successful match passes the group + if (group.Matches(dictionaryBase)) + return true; + } + + return false; + } + + #endregion + + #region Helpers + +#pragma warning disable IDE0051 + /// + /// Derive a group type from the input string, if possible + /// + private static GroupType GetGroupType(string? groupType) + { + return groupType?.ToLowerInvariant() switch + { + "&" => GroupType.AND, + "&&" => GroupType.AND, + + "|" => GroupType.OR, + "||" => GroupType.OR, + + _ => GroupType.NONE, + }; + } +#pragma warning restore IDE0051 + +#pragma warning disable IDE0051 + /// + /// Parse an input string into a filter group + /// + private static void Parse(string? input) + { + // Tokenize the string + string[] tokens = Tokenize(input); + if (tokens.Length == 0) + return; + + // Loop through the tokens and parse + for (int i = 0; i < tokens.Length; i++) + { + // TODO: Implement parsing + // - Opening parenthesis means a new group + // - Closing parenthesis means finalize group and return it + // - Current starting and ending with a parenthesis strips them off + // - Unbalanced parenthesis can only be found on parse + // - Failed parsing of FilterObjects(?) + // - Invalid FilterObjects(?) + } + } +#pragma warning restore IDE0051 + + /// + /// Tokenize an input string for parsing + /// + private static string[] Tokenize(string? input) + { + // Null inputs are ignored + if (input is null) + return []; + + // Split the string into parseable pieces + // - Left and right parenthesis are separate + // - Operators & and | are separate + // - Key-value pairs are enforced for statements + // - Numbers can be a value without quotes + // - All other values require quotes + return Regex.Split(input, @"(\(|\)|[&|]{1,2}|[^\s()""]+[:!=]\d+|[^\s()""]+[:!=]{1,2}""[^""]*"")", RegexOptions.Compiled); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Filter/FilterKey.cs b/SabreTools.Metadata.Filter/FilterKey.cs new file mode 100644 index 00000000..5efa1686 --- /dev/null +++ b/SabreTools.Metadata.Filter/FilterKey.cs @@ -0,0 +1,214 @@ +using System; +using SabreTools.Data.Models.Metadata; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.Filter +{ + /// + /// Represents a single filter key + /// + public class FilterKey + { + /// + /// Item name associated with the filter + /// + public readonly string ItemName; + + /// + /// Field name associated with the filter + /// + public readonly string FieldName; + + /// + /// Validating combined key constructor + /// + public FilterKey(string? key) + { + if (!ParseFilterId(key, out string itemName, out string fieldName)) + throw new ArgumentException($"{nameof(key)} could not be parsed", nameof(key)); + + ItemName = itemName; + FieldName = fieldName; + } + + /// + /// Validating discrete value constructor + /// + public FilterKey(string itemName, string fieldName) + { + if (!ParseFilterId(ref itemName, ref fieldName)) + throw new ArgumentException($"{nameof(itemName)} was not recognized", nameof(itemName)); + + ItemName = itemName; + FieldName = fieldName; + } + + /// + public override string ToString() => $"{ItemName}.{FieldName}"; + + /// + /// Parse a filter ID string into the item name and field name, if possible + /// + private static bool ParseFilterId(string? itemFieldString, out string itemName, out string fieldName) + { + // Set default values + itemName = string.Empty; fieldName = string.Empty; + + // If we don't have a filter ID, we can't do anything + if (string.IsNullOrEmpty(itemFieldString)) + return false; + + // If we only have one part, we can't do anything + string[] splitFilter = itemFieldString!.Split('.'); + if (splitFilter.Length != 2) + return false; + + // Set and sanitize the filter ID + itemName = splitFilter[0]; + fieldName = splitFilter[1]; + return ParseFilterId(ref itemName, ref fieldName); + } + + /// + /// Parse a filter ID string into the item name and field name, if possible + /// + private static bool ParseFilterId(ref string itemName, ref string fieldName) + { + // If we don't have a filter ID, we can't do anything + if (string.IsNullOrEmpty(itemName) || string.IsNullOrEmpty(fieldName)) + return false; + + // Return santized values based on the split ID + return itemName.ToLowerInvariant() switch + { + // Header + "header" => ParseHeaderFilterId(ref itemName, ref fieldName), + + // Machine + "game" => ParseMachineFilterId(ref itemName, ref fieldName), + "machine" => ParseMachineFilterId(ref itemName, ref fieldName), + "resource" => ParseMachineFilterId(ref itemName, ref fieldName), + "set" => ParseMachineFilterId(ref itemName, ref fieldName), + + // DatItem + "datitem" => ParseDatItemFilterId(ref itemName, ref fieldName), + "item" => ParseDatItemFilterId(ref itemName, ref fieldName), + _ => ParseDatItemFilterId(ref itemName, ref fieldName), + }; + } + + /// + /// Parse and validate header fields + /// + private static bool ParseHeaderFilterId(ref string itemName, ref string fieldName) + { + // Get the set of constants + var constants = TypeHelper.GetConstants(typeof(Header)); + if (constants is null) + return false; + + // Get if there's a match to the constant + string localFieldName = fieldName; + string? constantMatch = Array.Find(constants, c => string.Equals(c, localFieldName, StringComparison.OrdinalIgnoreCase)); + if (constantMatch is null) + return false; + + // Return the sanitized ID + itemName = MetadataFile.HeaderKey; + fieldName = constantMatch; + return true; + } + + /// + /// Parse and validate machine/game fields + /// + private static bool ParseMachineFilterId(ref string itemName, ref string fieldName) + { + // Get the set of constants + var constants = TypeHelper.GetConstants(typeof(Machine)); + if (constants is null) + return false; + + // Get if there's a match to the constant + string localFieldName = fieldName; + string? constantMatch = Array.Find(constants, c => string.Equals(c, localFieldName, StringComparison.OrdinalIgnoreCase)); + if (constantMatch is null) + return false; + + // Return the sanitized ID + itemName = MetadataFile.MachineKey; + fieldName = constantMatch; + return true; + } + + /// + /// Parse and validate item fields + /// + private static bool ParseDatItemFilterId(ref string itemName, ref string fieldName) + { + // Special case if the item name is reserved + if (string.Equals(itemName, "datitem", StringComparison.OrdinalIgnoreCase) + || string.Equals(itemName, "item", StringComparison.OrdinalIgnoreCase)) + { + // Get all item types + var itemTypes = TypeHelper.GetDatItemTypeNames(); + + // If we get any matches + string localFieldName = fieldName; + string? matchedType = Array.Find(itemTypes, t => DatItemContainsField(t, localFieldName)); + if (matchedType is not null) + { + // Check for a matching field + string? matchedField = GetMatchingField(matchedType, fieldName); + if (matchedField is null) + return false; + + itemName = "item"; + fieldName = matchedField; + return true; + } + } + else + { + // Check for a matching field + string? matchedField = GetMatchingField(itemName, fieldName); + if (matchedField is null) + return false; + + itemName = itemName.ToLowerInvariant(); + fieldName = matchedField; + return true; + } + + // Nothing was found + return false; + } + + /// + /// Determine if an item type contains a field + /// + private static bool DatItemContainsField(string itemName, string fieldName) + => GetMatchingField(itemName, fieldName) is not null; + + /// + /// Determine if an item type contains a field + /// + private static string? GetMatchingField(string itemName, string fieldName) + { + // Get the correct item type + var itemType = TypeHelper.GetDatItemType(itemName.ToLowerInvariant()); + if (itemType is null) + return null; + + // Get the set of constants + var constants = TypeHelper.GetConstants(itemType); + if (constants is null) + return null; + + // Get if there's a match to the constant + string localFieldName = fieldName; + string? constantMatch = Array.Find(constants, c => string.Equals(c, localFieldName, StringComparison.OrdinalIgnoreCase)); + return constantMatch; + } + } +} diff --git a/SabreTools.Metadata.Filter/FilterObject.cs b/SabreTools.Metadata.Filter/FilterObject.cs new file mode 100644 index 00000000..dfcb6d1f --- /dev/null +++ b/SabreTools.Metadata.Filter/FilterObject.cs @@ -0,0 +1,404 @@ +using System; +using System.Text.RegularExpressions; +using SabreTools.Data.Models.Metadata; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.Filter +{ + /// + /// Represents a single filtering object + /// + /// TODO: Add ability to have a set of values that are accepted + public class FilterObject + { + /// + /// Item key associated with the filter + /// + public readonly FilterKey Key; + + /// + /// Value to match in the filter + /// + public readonly string? Value; + + /// + /// Operation on how to match the filter + /// + public readonly Operation Operation; + + public FilterObject(string? filterString) + { + if (!SplitFilterString(filterString, out var keyItem, out Operation operation, out var value)) + throw new ArgumentException($"{nameof(filterString)} could not be parsed", nameof(filterString)); + + Key = new FilterKey(keyItem); + Value = value; + Operation = operation; + } + + public FilterObject(string itemField, string? value, string? operation) + { + Key = new FilterKey(itemField); + Value = value; + Operation = GetOperation(operation); + } + + public FilterObject(string itemField, string? value, Operation operation) + { + Key = new FilterKey(itemField); + Value = value; + Operation = operation; + } + + #region Matching + + /// + /// Determine if a DictionaryBase object matches the key and value + /// + public bool Matches(DictionaryBase dictionaryBase) + { + return Operation switch + { + Operation.Equals => MatchesEqual(dictionaryBase), + Operation.NotEquals => MatchesNotEqual(dictionaryBase), + Operation.GreaterThan => MatchesGreaterThan(dictionaryBase), + Operation.GreaterThanOrEqual => MatchesGreaterThanOrEqual(dictionaryBase), + Operation.LessThan => MatchesLessThan(dictionaryBase), + Operation.LessThanOrEqual => MatchesLessThanOrEqual(dictionaryBase), + + Operation.NONE => false, + _ => false, + }; + } + + /// + /// Determines if a value matches exactly + /// + private bool MatchesEqual(DictionaryBase dictionaryBase) + { + // If the key doesn't exist, we count it as null + if (!dictionaryBase.ContainsKey(Key.FieldName)) + return string.IsNullOrEmpty(Value); + + // If the value in the dictionary is null + string? checkValue = dictionaryBase.ReadString(Key.FieldName); + if (checkValue is null) + return string.IsNullOrEmpty(Value); + + // If we have both a potentally boolean check and value + bool? checkValueBool = checkValue.AsYesNo(); + bool? matchValueBool = Value.AsYesNo(); + if (checkValueBool is not null && matchValueBool is not null) + return checkValueBool == matchValueBool; + + // If we have both a potentially numeric check and value + if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value)) + { + // Check Int64 values + long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); + long? matchValueLong = NumberHelper.ConvertToInt64(Value); + if (checkValueLong is not null && matchValueLong is not null) + return checkValueLong == matchValueLong; + + // Check Double values + double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); + double? matchValueDouble = NumberHelper.ConvertToDouble(Value); + if (checkValueDouble is not null && matchValueDouble is not null) + return checkValueDouble == matchValueDouble; + } + + // If the value might contain valid Regex + if (Value is not null && ContainsRegex(Value)) + return Regex.IsMatch(checkValue, Value); + + return string.Equals(checkValue, Value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines if a value does not match exactly + /// + private bool MatchesNotEqual(DictionaryBase dictionaryBase) + { + // If the key doesn't exist, we count it as null + if (!dictionaryBase.ContainsKey(Key.FieldName)) + return !string.IsNullOrEmpty(Value); + + // If the value in the dictionary is null + string? checkValue = dictionaryBase.ReadString(Key.FieldName); + if (checkValue is null) + return !string.IsNullOrEmpty(Value); + + // If we have both a potentally boolean check and value + bool? checkValueBool = checkValue.AsYesNo(); + bool? matchValueBool = Value.AsYesNo(); + if (checkValueBool is not null && matchValueBool is not null) + return checkValueBool != matchValueBool; + + // If we have both a potentially numeric check and value + if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value)) + { + // Check Int64 values + long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); + long? matchValueLong = NumberHelper.ConvertToInt64(Value); + if (checkValueLong is not null && matchValueLong is not null) + return checkValueLong != matchValueLong; + + // Check Double values + double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); + double? matchValueDouble = NumberHelper.ConvertToDouble(Value); + if (checkValueDouble is not null && matchValueDouble is not null) + return checkValueDouble != matchValueDouble; + } + + // If the value might contain valid Regex + if (Value is not null && ContainsRegex(Value)) + return !Regex.IsMatch(checkValue, Value); + + return !string.Equals(checkValue, Value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines if a value is strictly greater than + /// + private bool MatchesGreaterThan(DictionaryBase dictionaryBase) + { + // If the key doesn't exist, we count it as null + if (!dictionaryBase.ContainsKey(Key.FieldName)) + return false; + + // If the value in the dictionary is null + string? checkValue = dictionaryBase.ReadString(Key.FieldName); + if (checkValue is null) + return false; + + // If we have both a potentially numeric check and value + if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value)) + { + // Check Int64 values + long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); + long? matchValueLong = NumberHelper.ConvertToInt64(Value); + if (checkValueLong is not null && matchValueLong is not null) + return checkValueLong > matchValueLong; + + // Check Double values + double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); + double? matchValueDouble = NumberHelper.ConvertToDouble(Value); + if (checkValueDouble is not null && matchValueDouble is not null) + return checkValueDouble > matchValueDouble; + } + + return false; + } + + /// + /// Determines if a value is greater than or equal + /// + private bool MatchesGreaterThanOrEqual(DictionaryBase dictionaryBase) + { + // If the key doesn't exist, we count it as null + if (!dictionaryBase.ContainsKey(Key.FieldName)) + return false; + + // If the value in the dictionary is null + string? checkValue = dictionaryBase.ReadString(Key.FieldName); + if (checkValue is null) + return false; + + // If we have both a potentially numeric check and value + if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value)) + { + // Check Int64 values + long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); + long? matchValueLong = NumberHelper.ConvertToInt64(Value); + if (checkValueLong is not null && matchValueLong is not null) + return checkValueLong >= matchValueLong; + + // Check Double values + double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); + double? matchValueDouble = NumberHelper.ConvertToDouble(Value); + if (checkValueDouble is not null && matchValueDouble is not null) + return checkValueDouble >= matchValueDouble; + } + + return false; + } + + /// + /// Determines if a value is strictly less than + /// + private bool MatchesLessThan(DictionaryBase dictionaryBase) + { + // If the key doesn't exist, we count it as null + if (!dictionaryBase.ContainsKey(Key.FieldName)) + return false; + + // If the value in the dictionary is null + string? checkValue = dictionaryBase.ReadString(Key.FieldName); + if (checkValue is null) + return false; + + // If we have both a potentially numeric check and value + if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value)) + { + // Check Int64 values + long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); + long? matchValueLong = NumberHelper.ConvertToInt64(Value); + if (checkValueLong is not null && matchValueLong is not null) + return checkValueLong < matchValueLong; + + // Check Double values + double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); + double? matchValueDouble = NumberHelper.ConvertToDouble(Value); + if (checkValueDouble is not null && matchValueDouble is not null) + return checkValueDouble < matchValueDouble; + } + + return false; + } + + /// + /// Determines if a value is less than or equal + /// + private bool MatchesLessThanOrEqual(DictionaryBase dictionaryBase) + { + // If the key doesn't exist, we count it as null + if (!dictionaryBase.ContainsKey(Key.FieldName)) + return false; + + // If the value in the dictionary is null + string? checkValue = dictionaryBase.ReadString(Key.FieldName); + if (checkValue is null) + return false; + + // If we have both a potentially numeric check and value + if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value)) + { + // Check Int64 values + long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); + long? matchValueLong = NumberHelper.ConvertToInt64(Value); + if (checkValueLong is not null && matchValueLong is not null) + return checkValueLong <= matchValueLong; + + // Check Double values + double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); + double? matchValueDouble = NumberHelper.ConvertToDouble(Value); + if (checkValueDouble is not null && matchValueDouble is not null) + return checkValueDouble <= matchValueDouble; + } + + return false; + } + + #endregion + + #region Helpers + + /// + /// Determine if a value may contain regex for matching + /// + /// + /// If a value contains one of the following characters: + /// ^ $ * ? + + /// Then it will attempt to check if the value is regex or not. + /// If none of those characters exist, then value will assumed + /// not to be regex. + /// + private static bool ContainsRegex(string? value) + { + // If the value is missing, it can't be regex + if (value is null) + return false; + + // If we find a special character, try parsing as regex +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + if (value.Contains('^') + || value.Contains('$') + || value.Contains('*') + || value.Contains('?') + || value.Contains('+')) +#else + if (value.Contains("^") + || value.Contains("$") + || value.Contains("*") + || value.Contains("?") + || value.Contains("+")) +#endif + { + try + { + _ = new Regex(value); + return true; + } + catch + { + return false; + } + } + + return false; + } + + /// + /// Derive an operation from the input string, if possible + /// + private static Operation GetOperation(string? operation) + { + return operation?.ToLowerInvariant() switch + { + "=" => Operation.Equals, + "==" => Operation.Equals, + ":" => Operation.Equals, + "::" => Operation.Equals, + + "!" => Operation.NotEquals, + "!=" => Operation.NotEquals, + "!:" => Operation.NotEquals, + + ">" => Operation.GreaterThan, + ">=" => Operation.GreaterThanOrEqual, + + "<" => Operation.LessThan, + "<=" => Operation.LessThanOrEqual, + + _ => Operation.NONE, + }; + } + + /// + /// Derive a key, operation, and value from the input string, if possible + /// + private static bool SplitFilterString(string? filterString, out string? key, out Operation operation, out string? value) + { + // Set default values + key = null; operation = Operation.NONE; value = null; + + if (string.IsNullOrEmpty(filterString)) + return false; + + // Trim quotations, if necessary +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + if (filterString!.StartsWith('\"')) + filterString = filterString[1..^1]; +#else + if (filterString!.StartsWith("\"")) + filterString = filterString.Substring(1, filterString.Length - 2); +#endif + + // Split the string using regex + var match = Regex.Match(filterString, @"^(?[a-zA-Z._]+)(?[=!:><]{1,2})(?.*)$", RegexOptions.Compiled); + if (!match.Success) + return false; + + key = match.Groups["itemField"].Value; + operation = GetOperation(match.Groups["operation"].Value); + + // Only non-zero length values are counted as non-null + if (match.Groups["value"]?.Value?.Length > 0) + value = match.Groups["value"].Value; + + return true; + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Filter/FilterRunner.cs b/SabreTools.Metadata.Filter/FilterRunner.cs new file mode 100644 index 00000000..3f8c0310 --- /dev/null +++ b/SabreTools.Metadata.Filter/FilterRunner.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using SabreTools.Data.Models.Metadata; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata.Filter +{ + /// + /// Represents a set of filters that can be run against an object + /// + public class FilterRunner + { + /// + /// Set of filters to be run against an object + /// + public readonly Dictionary Filters = []; + + /// + /// Cached item type names for filter selection + /// + private readonly string[] _datItemTypeNames = TypeHelper.GetDatItemTypeNames(); + + public FilterRunner(FilterObject[] filters) + { + Array.ForEach(filters, AddFilter); + } + + public FilterRunner(string[] filterStrings) + { + Array.ForEach(filterStrings, AddFilter); + } + + /// + /// Run filtering on a DictionaryBase item + /// + public bool Run(DictionaryBase dictionaryBase) + { + string? itemName = dictionaryBase switch + { + Header => MetadataFile.HeaderKey, + Machine => MetadataFile.MachineKey, + DatItem => TypeHelper.GetXmlRootAttributeElementName(dictionaryBase.GetType()), + _ => null, + }; + + // Null is invalid + if (itemName is null) + return false; + + // Loop through and run each filter in order + foreach (var filterKey in Filters.Keys) + { + // Skip filters not applicable to the item + if (filterKey.StartsWith("item.") && Array.IndexOf(_datItemTypeNames, itemName) == -1) + continue; + else if (!filterKey.StartsWith("item.") && !filterKey.StartsWith(itemName)) + continue; + + // If we don't get a match, it's a failure + bool matchOne = Filters[filterKey].Matches(dictionaryBase); + if (!matchOne) + return false; + } + + return true; + } + + /// + /// Add a single filter to the runner in a group by key + /// + private void AddFilter(FilterObject filter) + { + // Get the key as a string + string key = filter.Key.ToString(); + + // Special case for machine types + if (filter.Key.ItemName == MetadataFile.MachineKey && filter.Key.FieldName == Machine.IsBiosKey) + key = $"{MetadataFile.MachineKey}.COMBINEDTYPE"; + else if (filter.Key.ItemName == MetadataFile.MachineKey && filter.Key.FieldName == Machine.IsDeviceKey) + key = $"{MetadataFile.MachineKey}.COMBINEDTYPE"; + else if (filter.Key.ItemName == MetadataFile.MachineKey && filter.Key.FieldName == Machine.IsMechanicalKey) + key = $"{MetadataFile.MachineKey}.COMBINEDTYPE"; + + // Ensure the key exists + if (!Filters.ContainsKey(key)) + Filters[key] = new FilterGroup(GroupType.OR); + + // Add the filter to the set + Filters[key].AddFilter(filter); + } + + /// + /// Add a single filter to the runner in a group by key + /// + private void AddFilter(string filterString) + { + try + { + var filter = new FilterObject(filterString); + AddFilter(filter); + } + catch { } + } + } +} diff --git a/SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj b/SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj new file mode 100644 index 00000000..db5f8b40 --- /dev/null +++ b/SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj @@ -0,0 +1,41 @@ + + + + + net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 + true + false + false + true + latest + enable + true + snupkg + true + 2.3.0 + + + Matt Nadareski + Filter functionality for metadata file processing + Copyright (c) Matt Nadareski 2016-2026 + https://github.com/SabreTools/ + README.md + https://github.com/SabreTools/SabreTools.Serialization + git + metadata dat datfile + MIT + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SabreTools.Metadata.Test/DictionaryBaseExtensionsTests.cs b/SabreTools.Metadata.Test/DictionaryBaseExtensionsTests.cs new file mode 100644 index 00000000..c38d2869 --- /dev/null +++ b/SabreTools.Metadata.Test/DictionaryBaseExtensionsTests.cs @@ -0,0 +1,983 @@ +using SabreTools.Data.Models.Metadata; +using Xunit; + +namespace SabreTools.Metadata.Test +{ + public class DictionaryBaseExtensionsTests + { + #region EqualTo + + [Fact] + public void EqualTo_MismatchedTypes_False() + { + DictionaryBase self = new Disk(); + DictionaryBase other = new Rom(); + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Disk_Nodumps_True() + { + DictionaryBase self = new Disk + { + [Disk.StatusKey] = "nodump", + [Disk.NameKey] = "XXXXXX", + [Disk.MD5Key] = string.Empty, + [Disk.SHA1Key] = string.Empty, + }; + DictionaryBase other = new Disk + { + [Disk.StatusKey] = "NODUMP", + [Disk.NameKey] = "XXXXXX", + [Disk.MD5Key] = string.Empty, + [Disk.SHA1Key] = string.Empty, + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Disk_Mismatch_False() + { + DictionaryBase self = new Disk + { + [Disk.NameKey] = "XXXXXX", + [Disk.MD5Key] = "XXXXXX", + [Disk.SHA1Key] = string.Empty, + }; + DictionaryBase other = new Disk + { + [Disk.NameKey] = "XXXXXX", + [Disk.MD5Key] = string.Empty, + [Disk.SHA1Key] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Disk_PartialMD5_True() + { + DictionaryBase self = new Disk + { + [Disk.NameKey] = "XXXXXX1", + [Disk.MD5Key] = "XXXXXX", + [Disk.SHA1Key] = string.Empty, + }; + DictionaryBase other = new Disk + { + [Disk.NameKey] = "XXXXXX2", + [Disk.MD5Key] = "XXXXXX", + [Disk.SHA1Key] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Disk_PartialSHA1_True() + { + DictionaryBase self = new Disk + { + [Disk.NameKey] = "XXXXXX1", + [Disk.MD5Key] = string.Empty, + [Disk.SHA1Key] = "XXXXXX", + }; + DictionaryBase other = new Disk + { + [Disk.NameKey] = "XXXXXX2", + [Disk.MD5Key] = "XXXXXX", + [Disk.SHA1Key] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Disk_FullMatch_True() + { + DictionaryBase self = new Disk + { + [Disk.NameKey] = "XXXXXX1", + [Disk.MD5Key] = "XXXXXX", + [Disk.SHA1Key] = "XXXXXX", + }; + DictionaryBase other = new Disk + { + [Disk.NameKey] = "XXXXXX2", + [Disk.MD5Key] = "XXXXXX", + [Disk.SHA1Key] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Media_Mismatch_False() + { + DictionaryBase self = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = string.Empty, + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = string.Empty, + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = string.Empty, + [Media.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Media_PartialMD5_True() + { + DictionaryBase self = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = string.Empty, + [Media.SHA256Key] = string.Empty, + [Media.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Media_PartialSHA1_True() + { + DictionaryBase self = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = string.Empty, + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = string.Empty, + [Media.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Media_PartialSHA256_True() + { + DictionaryBase self = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = string.Empty, + [Media.SHA1Key] = string.Empty, + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Media_PartialSpamSum_True() + { + DictionaryBase self = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = string.Empty, + [Media.SHA1Key] = string.Empty, + [Media.SHA256Key] = string.Empty, + [Media.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Media_FullMatch_True() + { + DictionaryBase self = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Media + { + [Media.NameKey] = "XXXXXX", + [Media.MD5Key] = "XXXXXX", + [Media.SHA1Key] = "XXXXXX", + [Media.SHA256Key] = "XXXXXX", + [Media.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_Nodumps_True() + { + DictionaryBase self = new Rom + { + [Rom.StatusKey] = "nodump", + [Rom.NameKey] = "XXXXXX", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.StatusKey] = "NODUMP", + [Rom.NameKey] = "XXXXXX", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_Mismatch_False() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = string.Empty, + }; + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Rom_NoSelfSize_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_NoOtherSize_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialCRC_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialMD2_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialMD4_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialMD5_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialRIPEMD128_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialRIPEMD160_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialSHA1_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialSHA256_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialSHA384_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialSHA512_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = string.Empty, + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_PartialSpamSum_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = string.Empty, + [Rom.MD2Key] = string.Empty, + [Rom.MD4Key] = string.Empty, + [Rom.MD5Key] = string.Empty, + [Rom.RIPEMD128Key] = string.Empty, + [Rom.RIPEMD160Key] = string.Empty, + [Rom.SHA1Key] = string.Empty, + [Rom.SHA256Key] = string.Empty, + [Rom.SHA384Key] = string.Empty, + [Rom.SHA512Key] = string.Empty, + [Rom.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Rom_FullMatch_True() + { + DictionaryBase self = new Rom + { + [Rom.NameKey] = "XXXXXX1", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + DictionaryBase other = new Rom + { + [Rom.NameKey] = "XXXXXX2", + [Rom.SizeKey] = 12345, + [Rom.CRCKey] = "XXXXXX", + [Rom.MD2Key] = "XXXXXX", + [Rom.MD4Key] = "XXXXXX", + [Rom.MD5Key] = "XXXXXX", + [Rom.RIPEMD128Key] = "XXXXXX", + [Rom.RIPEMD160Key] = "XXXXXX", + [Rom.SHA1Key] = "XXXXXX", + [Rom.SHA256Key] = "XXXXXX", + [Rom.SHA384Key] = "XXXXXX", + [Rom.SHA512Key] = "XXXXXX", + [Rom.SpamSumKey] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Other_BothEmpty_True() + { + DictionaryBase self = new Sample(); + DictionaryBase other = new Sample(); + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + [Fact] + public void EqualTo_Other_MismatchedCount_False() + { + DictionaryBase self = new Sample + { + ["KEY1"] = "XXXXXX", + }; + DictionaryBase other = new Sample + { + ["KEY1"] = "XXXXXX", + ["KEY2"] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Other_MismatchedKeys_False() + { + DictionaryBase self = new Sample + { + ["KEY1"] = "XXXXXX", + }; + DictionaryBase other = new Sample + { + ["KEY2"] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Other_MismatchedValues_False() + { + DictionaryBase self = new Sample + { + ["KEY1"] = "XXXXXX", + }; + DictionaryBase other = new Sample + { + ["KEY1"] = "ZZZZZZ", + }; + + bool actual = self.EqualTo(other); + Assert.False(actual); + } + + [Fact] + public void EqualTo_Other_Matching_True() + { + DictionaryBase self = new Sample + { + ["KEY1"] = "XXXXXX", + }; + DictionaryBase other = new Sample + { + ["KEY1"] = "XXXXXX", + }; + + bool actual = self.EqualTo(other); + Assert.True(actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Test/ModelBackedItemTests.cs b/SabreTools.Metadata.Test/ModelBackedItemTests.cs new file mode 100644 index 00000000..463c33ea --- /dev/null +++ b/SabreTools.Metadata.Test/ModelBackedItemTests.cs @@ -0,0 +1,317 @@ +using SabreTools.Data.Models.Metadata; +using Xunit; + +namespace SabreTools.Metadata.Test +{ + public class ModelBackedItemTests + { + #region Private Testing Classes + + /// + /// Testing implementation of DictionaryBase + /// + private class TestDictionaryBase : DictionaryBase + { + public const string NameKey = "__NAME__"; + } + + /// + /// Testing implementation of ModelBackedItem + /// + private class TestModelBackedItem : ModelBackedItem + { + #region Comparision Methods + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not TestModelBackedItem otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not TestModelBackedItem otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + #endregion + } + + /// + /// Alternate testing implementation of ModelBackedItem + /// + private class TestModelAltBackedItem : ModelBackedItem + { + #region Comparision Methods + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not TestModelAltBackedItem otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + /// + public override bool Equals(ModelBackedItem? other) + { + // If other is null + if (other is null) + return false; + + // If the type is mismatched + if (other is not TestModelAltBackedItem otherItem) + return false; + + // Compare internal models + return _internal.EqualTo(otherItem._internal); + } + + #endregion + } + + #endregion + + #region Equals + + [Fact] + public void Equals_NullOther_False() + { + ModelBackedItem self = new TestModelBackedItem(); + ModelBackedItem? other = null; + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_MismatchedType_False() + { + ModelBackedItem self = new TestModelBackedItem(); + ModelBackedItem? other = new TestModelAltBackedItem(); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_MismatchedTypeAlt_False() + { + ModelBackedItem self = new TestModelAltBackedItem(); + ModelBackedItem? other = new TestModelBackedItem(); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_DifferentModels_False() + { + ModelBackedItem self = new TestModelBackedItem(); + self.SetFieldValue(TestDictionaryBase.NameKey, "self"); + + ModelBackedItem? other = new TestModelBackedItem(); + other.SetFieldValue(TestDictionaryBase.NameKey, "other"); + + bool actual = self.Equals(other); + Assert.False(actual); + } + + [Fact] + public void Equals_EqualModels_True() + { + ModelBackedItem self = new TestModelBackedItem(); + self.SetFieldValue(TestDictionaryBase.NameKey, "name"); + + ModelBackedItem? other = new TestModelBackedItem(); + other.SetFieldValue(TestDictionaryBase.NameKey, "name"); + + bool actual = self.Equals(other); + Assert.True(actual); + } + + #endregion + + #region RemoveField + + [Fact] + public void RemoveField_NullItem_False() + { + TestModelBackedItem? modelBackedItem = null; + string? fieldName = TestDictionaryBase.NameKey; + bool? actual = modelBackedItem?.RemoveField(fieldName); + Assert.Null(actual); + } + + [Fact] + public void RemoveField_EmptyFieldName_False() + { + var modelBackedItem = new TestModelBackedItem(); + string? fieldName = string.Empty; + bool actual = modelBackedItem.RemoveField(fieldName); + Assert.False(actual); + } + + [Fact] + public void RemoveField_MissingKey_True() + { + var modelBackedItem = new TestModelBackedItem(); + string? fieldName = TestDictionaryBase.NameKey; + bool actual = modelBackedItem.RemoveField(fieldName); + Assert.True(actual); + Assert.Null(modelBackedItem.GetStringFieldValue(fieldName)); + } + + [Fact] + public void RemoveField_ValidKey_True() + { + var modelBackedItem = new TestModelBackedItem(); + modelBackedItem.SetFieldValue(TestDictionaryBase.NameKey, "value"); + string? fieldName = TestDictionaryBase.NameKey; + bool actual = modelBackedItem.RemoveField(fieldName); + Assert.True(actual); + Assert.Null(modelBackedItem.GetStringFieldValue(fieldName)); + } + + #endregion + + #region ReplaceField + + [Fact] + public void ReplaceField_NullFrom_False() + { + TestModelBackedItem? from = null; + var to = new TestModelBackedItem(); + string? fieldName = TestDictionaryBase.NameKey; + bool actual = to.ReplaceField(from, fieldName); + Assert.False(actual); + } + + [Fact] + public void ReplaceField_NullTo_False() + { + TestModelBackedItem? from = null; + TestModelBackedItem? to = new TestModelBackedItem(); + string? fieldName = TestDictionaryBase.NameKey; + bool actual = to.ReplaceField(from, fieldName); + Assert.False(actual); + } + + [Fact] + public void ReplaceField_EmptyFieldName_False() + { + TestModelBackedItem? from = new TestModelBackedItem(); + TestModelBackedItem? to = new TestModelBackedItem(); + string? fieldName = string.Empty; + bool actual = to.ReplaceField(from, fieldName); + Assert.False(actual); + } + + [Fact] + public void ReplaceField_MissingKey_False() + { + TestModelBackedItem? from = new TestModelBackedItem(); + TestModelBackedItem? to = new TestModelBackedItem(); + string? fieldName = TestDictionaryBase.NameKey; + bool actual = to.ReplaceField(from, fieldName); + Assert.False(actual); + } + + [Fact] + public void ReplaceField_ValidKey_True() + { + TestModelBackedItem? from = new TestModelBackedItem(); + from.SetFieldValue(TestDictionaryBase.NameKey, "value"); + TestModelBackedItem? to = new TestModelBackedItem(); + string? fieldName = TestDictionaryBase.NameKey; + bool actual = to.ReplaceField(from, fieldName); + Assert.True(actual); + Assert.Equal("value", to.GetStringFieldValue(TestDictionaryBase.NameKey)); + } + + #endregion + + #region SetField + + [Fact] + public void SetField_NullItem_False() + { + TestModelBackedItem? modelBackedItem = null; + string? fieldName = TestDictionaryBase.NameKey; + object value = "value"; + bool? actual = modelBackedItem?.SetField(fieldName, value); + Assert.Null(actual); + } + + [Fact] + public void SetField_EmptyFieldName_False() + { + TestModelBackedItem? modelBackedItem = new TestModelBackedItem(); + string? fieldName = string.Empty; + object value = "value"; + bool actual = modelBackedItem.SetField(fieldName, value); + Assert.False(actual); + } + + [Fact] + public void SetField_MissingKey_False() + { + TestModelBackedItem? modelBackedItem = new TestModelBackedItem(); + string? fieldName = Rom.SHA1Key; + object value = "value"; + bool actual = modelBackedItem.SetField(fieldName, value); + Assert.False(actual); + } + + [Fact] + public void SetField_InvalidKey_True() + { + TestModelBackedItem? modelBackedItem = new TestModelBackedItem(); + modelBackedItem.SetFieldValue(TestDictionaryBase.NameKey, "old"); + string? fieldName = "INVALID"; + object value = "value"; + bool actual = modelBackedItem.SetField(fieldName, value); + Assert.False(actual); + Assert.Null(modelBackedItem.GetStringFieldValue(fieldName)); + } + + [Fact] + public void SetField_ValidKey_True() + { + TestModelBackedItem? modelBackedItem = new TestModelBackedItem(); + modelBackedItem.SetFieldValue(TestDictionaryBase.NameKey, "old"); + string? fieldName = TestDictionaryBase.NameKey; + object value = "value"; + bool actual = modelBackedItem.SetField(fieldName, value); + Assert.True(actual); + Assert.Equal(value, modelBackedItem.GetStringFieldValue(fieldName)); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Test/SabreTools.Metadata.Test.csproj b/SabreTools.Metadata.Test/SabreTools.Metadata.Test.csproj new file mode 100644 index 00000000..805e3d58 --- /dev/null +++ b/SabreTools.Metadata.Test/SabreTools.Metadata.Test.csproj @@ -0,0 +1,21 @@ + + + + net8.0;net9.0;net10.0 + false + latest + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SabreTools.Metadata.Test/Tools/ConvertersTests.cs b/SabreTools.Metadata.Test/Tools/ConvertersTests.cs new file mode 100644 index 00000000..d840fa55 --- /dev/null +++ b/SabreTools.Metadata.Test/Tools/ConvertersTests.cs @@ -0,0 +1,38 @@ +using Xunit; + +namespace SabreTools.Metadata.Tools.Test +{ + public class ConvertersTests + { + #region String to Enum + + [Theory] + [InlineData(null, null)] + [InlineData("INVALID", null)] + [InlineData("yes", true)] + [InlineData("True", true)] + [InlineData("no", false)] + [InlineData("False", false)] + public void AsYesNoTest(string? field, bool? expected) + { + bool? actual = field.AsYesNo(); + Assert.Equal(expected, actual); + } + + #endregion + + #region Enum to String + + [Theory] + [InlineData(null, null)] + [InlineData(true, "yes")] + [InlineData(false, "no")] + public void FromYesNo(bool? field, string? expected) + { + string? actual = field.FromYesNo(); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata.Test/Tools/UtilitiesTests.cs b/SabreTools.Metadata.Test/Tools/UtilitiesTests.cs new file mode 100644 index 00000000..a2f5111d --- /dev/null +++ b/SabreTools.Metadata.Test/Tools/UtilitiesTests.cs @@ -0,0 +1,70 @@ +using Xunit; + +namespace SabreTools.Metadata.Tools.Test +{ + public class UtilitiesTests + { + #region GetDepotPath + + [Theory] + [InlineData(null, 0, null)] + [InlineData(null, 4, null)] + [InlineData(new byte[] { 0x12, 0x34, 0x56 }, 0, null)] + [InlineData(new byte[] { 0x12, 0x34, 0x56 }, 4, null)] + [InlineData(new byte[] { 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09 }, -1, "da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(new byte[] { 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09 }, 0, "da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData(new byte[] { 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09 }, 1, "da\\da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + public void GetDepotPath_Array(byte[]? hash, int depth, string? expected) + { + string? actual = Utilities.GetDepotPath(hash, depth); + if (System.IO.Path.DirectorySeparatorChar == '/') + expected = expected?.Replace('\\', '/'); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(null, 0, null)] + [InlineData(null, 4, null)] + [InlineData("123456", 0, null)] + [InlineData("123456", 4, null)] + [InlineData("da39a3ee5e6b4b0d3255bfef95601890afd80709", -1, "da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData("da39a3ee5e6b4b0d3255bfef95601890afd80709", 0, "da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + [InlineData("da39a3ee5e6b4b0d3255bfef95601890afd80709", 1, "da\\da39a3ee5e6b4b0d3255bfef95601890afd80709.gz")] + public void GetDepotPath_String(string? hash, int depth, string? expected) + { + string? actual = Utilities.GetDepotPath(hash, depth); + if (System.IO.Path.DirectorySeparatorChar == '/') + expected = expected?.Replace('\\', '/'); + + Assert.Equal(expected, actual); + } + + #endregion + + #region HasValidDatExtension + + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("no-extension", false)] + [InlineData("no-extension.", false)] + [InlineData("invalid.ext", false)] + [InlineData("invalid..ext", false)] + [InlineData("INVALID.EXT", false)] + [InlineData("INVALID..EXT", false)] + [InlineData(".dat", true)] + [InlineData(".DAT", true)] + [InlineData("valid_extension.dat", true)] + [InlineData("valid_extension..dat", true)] + [InlineData("valid_extension.DAT", true)] + [InlineData("valid_extension..DAT", true)] + public void HasValidDatExtensionTest(string? path, bool expected) + { + bool actual = Utilities.HasValidDatExtension(path); + Assert.Equal(expected, actual); + } + + #endregion + } +} diff --git a/SabreTools.Metadata/DictionaryBaseExtensions.cs b/SabreTools.Metadata/DictionaryBaseExtensions.cs new file mode 100644 index 00000000..003c1877 --- /dev/null +++ b/SabreTools.Metadata/DictionaryBaseExtensions.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.Metadata; + +namespace SabreTools.Metadata +{ + public static class DictionaryBaseExtensions + { + #region Cloning + + /// + /// Deep clone a DictionaryBase object + /// + public static DictionaryBase? Clone(this DictionaryBase self) + { + // If construction failed, we can't do anything + if (Activator.CreateInstance(self.GetType()) is not DictionaryBase clone) + return null; + + // Loop through and clone per type + foreach (string key in self.Keys) + { + object? value = self[key]; + clone[key] = value switch + { + // Primative types + bool or long or double or string => value, + + // DictionaryBase types + DictionaryBase db => db.Clone(), + + // Enumerable types + byte[] bytArr => bytArr.Clone(), + string[] strArr => strArr.Clone(), + DictionaryBase[] dbArr => Array.ConvertAll(dbArr, Clone), + ICloneable[] clArr => Array.ConvertAll(clArr, cl => cl.Clone()), + + // Everything else just copies + _ => value, + }; + } + + return clone; + } + + #endregion + + #region Equality Checking + + /// + /// Check equality of two DictionaryBase objects + /// + public static bool EqualTo(this DictionaryBase self, DictionaryBase other) + { + // Check types first + if (self.GetType() != other.GetType()) + return false; + + // Check based on the item type +#if NETCOREAPP || NETSTANDARD2_0_OR_GREATER + return (self, other) switch + { + (Disk diskSelf, Disk diskOther) => EqualsImpl(diskSelf, diskOther), + (Media mediaSelf, Media mediaOther) => EqualsImpl(mediaSelf, mediaOther), + (Rom romSelf, Rom romOther) => EqualsImpl(romSelf, romOther), + _ => EqualsImpl(self, other), + }; +#else + if (self is Disk diskSelf && other is Disk diskOther) + return EqualsImpl(diskSelf, diskOther); + else if (self is Media mediaSelf && other is Media mediaOther) + return EqualsImpl(mediaSelf, mediaOther); + else if (self is Rom romSelf && other is Rom romOther) + return EqualsImpl(romSelf, romOther); + else + return EqualsImpl(self, other); +#endif + } + + /// + /// Check equality of two DictionaryBase objects + /// + private static bool EqualsImpl(this DictionaryBase self, DictionaryBase other) + { + // If the number of key-value pairs doesn't match, they can't match + if (self.Count != other.Count) + return false; + + // If any keys are missing on either side, they can't match + var selfKeys = new HashSet(self.Keys); + var otherKeys = new HashSet(other.Keys); + if (!selfKeys.SetEquals(otherKeys)) + return false; + + // Check all pairs to see if they're equal + foreach (var kvp in self) + { +#if NETCOREAPP || NETSTANDARD2_0_OR_GREATER + switch (kvp.Value, other[kvp.Key]) + { + case (string selfString, string otherString): + if (!string.Equals(selfString, otherString, StringComparison.OrdinalIgnoreCase)) + return false; + break; + + case (ModelBackedItem selfMbi, ModelBackedItem otherMbi): + if (!selfMbi.Equals(otherMbi)) + return false; + break; + + case (DictionaryBase selfDb, DictionaryBase otherDb): + if (!selfDb.Equals(otherDb)) + return false; + break; + + // TODO: Make this case-insensitive + case (string[] selfStrArr, string[] otherStrArr): + if (selfStrArr.Length != otherStrArr.Length) + return false; + if (selfStrArr.Except(otherStrArr).Any()) + return false; + if (otherStrArr.Except(selfStrArr).Any()) + return false; + break; + + // TODO: Fix the logic here for real equality + case (DictionaryBase[] selfDbArr, DictionaryBase[] otherDbArr): + if (selfDbArr.Length != otherDbArr.Length) + return false; + if (selfDbArr.Except(otherDbArr).Any()) + return false; + if (otherDbArr.Except(selfDbArr).Any()) + return false; + break; + + default: + // Handle cases where a null is involved + if (kvp.Value is null && other[kvp.Key] is null) + return true; + else if (kvp.Value is null && other[kvp.Key] is not null) + return false; + else if (kvp.Value is not null && other[kvp.Key] is null) + return false; + + // Try to rely on type hashes + else if (kvp.Value!.GetHashCode() != other[kvp.Key]!.GetHashCode()) + return false; + + break; + } +#else + if (kvp.Value is string selfString && other[kvp.Key] is string otherString) + { + if (!string.Equals(selfString, otherString, StringComparison.OrdinalIgnoreCase)) + return false; + } + else if (kvp.Value is ModelBackedItem selfMbi && other[kvp.Key] is ModelBackedItem otherMbi) + { + if (!selfMbi.Equals(otherMbi)) + return false; + } + else if (kvp.Value is DictionaryBase selfDb && other[kvp.Key] is DictionaryBase otherDb) + { + if (!selfDb.Equals(otherDb)) + return false; + } + else if (kvp.Value is string[] selfStrArr && other[kvp.Key] is string[] otherStrArr) + { + // TODO: Make this case-insensitive + if (selfStrArr.Length != otherStrArr.Length) + return false; + if (selfStrArr.Except(otherStrArr).Any()) + return false; + if (otherStrArr.Except(selfStrArr).Any()) + return false; + } + else if (kvp.Value is DictionaryBase[] selfDbArr && other[kvp.Key] is DictionaryBase[] otherDbArr) + { + // TODO: Fix the logic here for real equality + if (selfDbArr.Length != otherDbArr.Length) + return false; + if (selfDbArr.Except(otherDbArr).Any()) + return false; + if (otherDbArr.Except(selfDbArr).Any()) + return false; + } + else + { + // Handle cases where a null is involved + if (kvp.Value is null && other[kvp.Key] is null) + return true; + else if (kvp.Value is null && other[kvp.Key] is not null) + return false; + else if (kvp.Value is not null && other[kvp.Key] is null) + return false; + + // Try to rely on type hashes + else if (kvp.Value!.GetHashCode() != other[kvp.Key]!.GetHashCode()) + return false; + } +#endif + } + + return true; + } + + /// + /// Check equality of two Disk objects + /// + private static bool EqualsImpl(this Disk self, Disk other) + { + string? selfStatus = self.ReadString(Disk.StatusKey); + string? otherStatus = other.ReadString(Disk.StatusKey); + + string? selfName = self.ReadString(Disk.NameKey); + string? otherName = other.ReadString(Disk.NameKey); + + // If all hashes are empty but they're both nodump and the names match, then they're dupes + if (string.Equals(selfStatus, "nodump", StringComparison.OrdinalIgnoreCase) + && string.Equals(otherStatus, "nodump", StringComparison.OrdinalIgnoreCase) + && string.Equals(selfName, otherName, StringComparison.OrdinalIgnoreCase) + && !self.HasHashes() + && !other.HasHashes()) + { + return true; + } + + // If we get a partial match + if (self.HashMatch(other)) + return true; + + // All other cases fail + return false; + } + + /// + /// Check equality of two Media objects + /// + private static bool EqualsImpl(this Media self, Media other) + { + // If we get a partial match + if (self.HashMatch(other)) + return true; + + // All other cases fail + return false; + } + + /// + /// Check equality of two Rom objects + /// + private static bool EqualsImpl(this Rom self, Rom other) + { + string? selfStatus = self.ReadString(Rom.StatusKey); + string? otherStatus = other.ReadString(Rom.StatusKey); + + string? selfName = self.ReadString(Rom.NameKey); + string? otherName = other.ReadString(Rom.NameKey); + + long? selfSize = self.ReadLong(Rom.SizeKey); + long? otherSize = other.ReadLong(Rom.SizeKey); + + // If all hashes are empty but they're both nodump and the names match, then they're dupes + if (string.Equals(selfStatus, "nodump", StringComparison.OrdinalIgnoreCase) + && string.Equals(otherStatus, "nodump", StringComparison.OrdinalIgnoreCase) + && string.Equals(selfName, otherName, StringComparison.OrdinalIgnoreCase) + && !self.HasHashes() + && !other.HasHashes()) + { + return true; + } + + // If we have a file that has no known size, rely on the hashes only + if (selfSize is null && self.HashMatch(other)) + return true; + else if (otherSize is null && self.HashMatch(other)) + return true; + + // If we get a partial match + if (selfSize == otherSize && self.HashMatch(other)) + return true; + + // All other cases fail + return false; + } + + #endregion + } +} diff --git a/SabreTools.Metadata/MappingAttribute.cs b/SabreTools.Metadata/MappingAttribute.cs new file mode 100644 index 00000000..59f45d93 --- /dev/null +++ b/SabreTools.Metadata/MappingAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace SabreTools.Metadata +{ + /// + /// Maps a set of strings to an enum value + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class MappingAttribute(params string[] mappings) : Attribute + { + /// + /// Set of mapping strings + /// + public string[] Mappings { get; } = mappings; + } +} diff --git a/SabreTools.Metadata/ModelBackedItem.cs b/SabreTools.Metadata/ModelBackedItem.cs new file mode 100644 index 00000000..de5e7cdf --- /dev/null +++ b/SabreTools.Metadata/ModelBackedItem.cs @@ -0,0 +1,17 @@ +using System; + +namespace SabreTools.Metadata +{ + /// + /// Represents an item that's backed by a DictionaryBase item + /// + public abstract class ModelBackedItem : IEquatable + { + #region Comparision Methods + + /// + public abstract bool Equals(ModelBackedItem? other); + + #endregion + } +} diff --git a/SabreTools.Metadata/ModelBackedItemT.cs b/SabreTools.Metadata/ModelBackedItemT.cs new file mode 100644 index 00000000..0887ed1c --- /dev/null +++ b/SabreTools.Metadata/ModelBackedItemT.cs @@ -0,0 +1,228 @@ +using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Metadata.Tools; + +namespace SabreTools.Metadata +{ + /// + /// Represents an item that's backed by a DictionaryBase item + /// + public abstract class ModelBackedItem : ModelBackedItem, IEquatable> where T : Data.Models.Metadata.DictionaryBase + { + /// + /// Internal model wrapped by this DatItem + /// + [JsonIgnore, XmlIgnore] + protected T _internal; + + #region Constructors + + public ModelBackedItem() + { + _internal = Activator.CreateInstance(); + } + + #endregion + + #region Accessors + + /// + /// Get the value from a field based on the type provided + /// + /// Type of the value to get from the internal model + /// Field to retrieve + /// Value from the field, if possible + public U? GetFieldValue(string fieldName) + { + // Invalid field cannot be processed + if (!_internal.ContainsKey(fieldName)) + return default; + + // Get the value based on the type + return _internal.Read(fieldName); + } + + /// + /// Get the value from a field based on the type provided + /// + /// Field to retrieve + /// Value from the field, if possible + public bool? GetBoolFieldValue(string fieldName) + { + // Invalid field cannot be processed + if (!_internal.ContainsKey(fieldName)) + return default; + + // Get the value based on the type + return _internal.ReadBool(fieldName); + } + + /// + /// Get the value from a field based on the type provided + /// + /// Field to retrieve + /// Value from the field, if possible + public double? GetDoubleFieldValue(string fieldName) + { + // Invalid field cannot be processed + if (!_internal.ContainsKey(fieldName)) + return default; + + // Try to parse directly + double? doubleValue = _internal.ReadDouble(fieldName); + if (doubleValue is not null) + return doubleValue; + + // Try to parse from the string + string? stringValue = _internal.ReadString(fieldName); + return NumberHelper.ConvertToDouble(stringValue); + } + + /// + /// Get the value from a field based on the type provided + /// + /// Field to retrieve + /// Value from the field, if possible + public long? GetInt64FieldValue(string fieldName) + { + // Invalid field cannot be processed + if (!_internal.ContainsKey(fieldName)) + return default; + + // Try to parse directly + long? longValue = _internal.ReadLong(fieldName); + if (longValue is not null) + return longValue; + + // Try to parse from the string + string? stringValue = _internal.ReadString(fieldName); + return NumberHelper.ConvertToInt64(stringValue); + } + + /// + /// Get the value from a field based on the type provided + /// + /// Field to retrieve + /// Value from the field, if possible + public string? GetStringFieldValue(string fieldName) + { + // Invalid field cannot be processed + if (!_internal.ContainsKey(fieldName)) + return default; + + // Get the value based on the type + return _internal.ReadString(fieldName); + } + + /// + /// Get the value from a field based on the type provided + /// + /// Field to retrieve + /// Value from the field, if possible + public string[]? GetStringArrayFieldValue(string fieldName) + { + // Invalid field cannot be processed + if (!_internal.ContainsKey(fieldName)) + return default; + + // Get the value based on the type + return _internal.ReadStringArray(fieldName); + } + + /// + /// Set the value from a field based on the type provided + /// + /// Type of the value to set in the internal model + /// Field to set + /// Value to set + /// True if the value was set, false otherwise + public bool SetFieldValue(string fieldName, U? value) + { + // Invalid field cannot be processed + if (fieldName is null) + return false; + + // Set the value based on the type + _internal[fieldName] = value; + return true; + } + + #endregion + + #region Comparision Methods + + /// + public abstract bool Equals(ModelBackedItem? other); + + #endregion + + #region Manipulation + + /// + /// Remove a field from the backing item + /// + public bool RemoveField(string fieldName) + { + // If the item or field name are missing, we can't do anything + if (string.IsNullOrEmpty(fieldName)) + return false; + + // If the key doesn't exist, then it's already removed + if (!_internal.ContainsKey(fieldName!)) + return true; + + // Remove the key + _internal.Remove(fieldName!); + return true; + } + + /// + /// Replace a field from another ModelBackedItem + /// + public bool ReplaceField(ModelBackedItem? from, string fieldName) + { + // If the items or field name are missing, we can't do anything + if (from?._internal is null || string.IsNullOrEmpty(fieldName)) + return false; + + // If the types of the items are not the same, we can't do anything + if (from._internal.GetType() != _internal.GetType()) + return false; + + // If the key doesn't exist in the source, we can't do anything + if (!from._internal.ContainsKey(fieldName!)) + return false; + + // Set the key + _internal[fieldName!] = from._internal[fieldName!]; + return true; + } + + /// + /// Set a field from the backing item + /// + public bool SetField(string fieldName, object value) + { + // If the item or field name are missing, we can't do anything + if (string.IsNullOrEmpty(fieldName)) + return false; + + // Retrieve the list of valid fields for the item + var constants = TypeHelper.GetConstants(_internal.GetType()); + if (constants is null) + return false; + + // Get the value that matches the field name provided + string? realField = Array.Find(constants, c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase)); + if (realField is null) + return false; + + // Set the field with the new value + _internal[realField] = value; + return true; + } + + #endregion + } +} diff --git a/SabreTools.Metadata/SabreTools.Metadata.csproj b/SabreTools.Metadata/SabreTools.Metadata.csproj new file mode 100644 index 00000000..b412ad6a --- /dev/null +++ b/SabreTools.Metadata/SabreTools.Metadata.csproj @@ -0,0 +1,42 @@ + + + + + net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 + true + false + false + true + latest + enable + true + snupkg + true + 2.3.0 + + + Matt Nadareski + Core functionality for metadata file processing + Copyright (c) Matt Nadareski 2016-2026 + https://github.com/SabreTools/ + README.md + https://github.com/SabreTools/SabreTools.Serialization + git + metadata dat datfile + MIT + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SabreTools.Metadata/Tools/AttributeHelper.cs b/SabreTools.Metadata/Tools/AttributeHelper.cs new file mode 100644 index 00000000..52b78e04 --- /dev/null +++ b/SabreTools.Metadata/Tools/AttributeHelper.cs @@ -0,0 +1,47 @@ +using System; + +namespace SabreTools.Metadata.Tools +{ + public static class AttributeHelper + { + /// + /// Get the MappingAttribute from a supported value + /// + /// Value to use + /// MappingAttribute attached to the value + public static MappingAttribute? GetAttribute(T? value) + { + // Null value in, null value out + if (value is null) + return null; + + // Current enumeration type + var enumType = typeof(T); + if (Nullable.GetUnderlyingType(enumType) is not null) + enumType = Nullable.GetUnderlyingType(enumType); + + // If the value returns a null on ToString, just return null + string? valueStr = value.ToString(); + if (string.IsNullOrEmpty(valueStr)) + return null; + + // Get the member info array + var memberInfos = enumType?.GetMember(valueStr); + if (memberInfos is null) + return null; + + // Get the enum value info from the array, if possible + var enumValueMemberInfo = Array.Find(memberInfos, m => m.DeclaringType == enumType); + if (enumValueMemberInfo is null) + return null; + + // Try to get the relevant attribute + var attributes = enumValueMemberInfo.GetCustomAttributes(typeof(MappingAttribute), true); + if (attributes is null || attributes.Length == 0) + return null; + + // Return the first attribute, if possible + return (MappingAttribute?)attributes[0]; + } + } +} diff --git a/SabreTools.Metadata/Tools/Converters.cs b/SabreTools.Metadata/Tools/Converters.cs new file mode 100644 index 00000000..43669dfe --- /dev/null +++ b/SabreTools.Metadata/Tools/Converters.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; + +namespace SabreTools.Metadata.Tools +{ + public static class Converters + { + #region String to Enum + + /// + /// Get bool? value from input string + /// + /// String to get value from + /// bool? corresponding to the string + public static bool? AsYesNo(this string? yesno) + { + return yesno?.ToLowerInvariant() switch + { + "yes" or "true" => true, + "no" or "false" => false, + _ => null, + }; + } + + /// + /// Get a set of mappings from strings to enum values + /// + /// Enum type that is expected + /// Dictionary of string to enum values + public static Dictionary GenerateToEnum() + { + try + { + // Get all of the values for the enum type + var values = Enum.GetValues(typeof(T)); + + // Build the output dictionary + Dictionary mappings = []; + foreach (T? value in values) + { + // If the value is null + if (value is null) + continue; + + // Try to get the mapping attribute + MappingAttribute? attr = AttributeHelper.GetAttribute(value); + if (attr?.Mappings is null || attr.Mappings.Length == 0) + continue; + + // Loop through the mappings and add each + foreach (string mapString in attr.Mappings) + { + if (mapString is not null) + mappings[mapString] = value; + } + } + + // Return the output dictionary + return mappings; + } + catch + { + // This should not happen, only if the type was not an enum + return []; + } + } + + #endregion + + #region Enum to String + + /// + /// Get string value from input bool? + /// + /// bool? to get value from + /// String corresponding to the bool? + public static string? FromYesNo(this bool? yesno) + { + return yesno switch + { + true => "yes", + false => "no", + _ => null, + }; + } + + /// + /// Get a set of mappings from enum values to string + /// + /// True to use the second mapping option, if it exists + /// Enum type that is expected + /// Dictionary of enum to string values + public static Dictionary GenerateToString(bool useSecond) where T : notnull + { + try + { + // Get all of the values for the enum type + var values = Enum.GetValues(typeof(T)); + + // Build the output dictionary + Dictionary mappings = []; + foreach (T? value in values) + { + // If the value is null + if (value is null) + continue; + + // Try to get the mapping attribute + MappingAttribute? attr = AttributeHelper.GetAttribute(value); + if (attr?.Mappings is null || attr.Mappings.Length == 0) + continue; + + // Use either the first or second item in the list + if (attr.Mappings.Length > 1 && useSecond) + mappings[value] = attr.Mappings[1]; + else + mappings[value] = attr.Mappings[0]; + } + + // Return the output dictionary + return mappings; + } + catch + { + // This should not happen, only if the type was not an enum + return []; + } + } + + #endregion + } +} diff --git a/SabreTools.Metadata/Tools/NumberHelper.cs b/SabreTools.Metadata/Tools/NumberHelper.cs new file mode 100644 index 00000000..e83d3826 --- /dev/null +++ b/SabreTools.Metadata/Tools/NumberHelper.cs @@ -0,0 +1,264 @@ +using System; + +namespace SabreTools.Metadata.Tools +{ + // TODO: Replace when IO is updated + public static class NumberHelper + { + #region Constants + + #region Byte (1000-based) size comparisons + + private const long KiloByte = 1000; + private static readonly long MegaByte = (long)Math.Pow(KiloByte, 2); + private static readonly long GigaByte = (long)Math.Pow(KiloByte, 3); + private static readonly long TeraByte = (long)Math.Pow(KiloByte, 4); + private static readonly long PetaByte = (long)Math.Pow(KiloByte, 5); + + // The following are too big to be represented in Int64 + // private readonly static long ExaByte = (long)Math.Pow(KiloByte, 6); + // private readonly static long ZettaByte = (long)Math.Pow(KiloByte, 7); + // private readonly static long YottaByte = (long)Math.Pow(KiloByte, 8); + + #endregion + + #region Byte (1024-based) size comparisons + + private const long KibiByte = 1024; + private static readonly long MibiByte = (long)Math.Pow(KibiByte, 2); + private static readonly long GibiByte = (long)Math.Pow(KibiByte, 3); + private static readonly long TibiByte = (long)Math.Pow(KibiByte, 4); + private static readonly long PibiByte = (long)Math.Pow(KibiByte, 5); + + // The following are too big to be represented in Int64 + // private readonly static long ExiByte = (long)Math.Pow(KibiByte, 6); + // private readonly static long ZittiByte = (long)Math.Pow(KibiByte, 7); + // private readonly static long YittiByte = (long)Math.Pow(KibiByte, 8); + + #endregion + + #endregion + + /// + /// Convert a string to a Double + /// + public static double? ConvertToDouble(string? numeric) + { + // If we don't have a valid string, we can't do anything + if (string.IsNullOrEmpty(numeric)) + return null; + + if (!double.TryParse(numeric, out double doubleValue)) + return null; + + return doubleValue; + } + + /// + /// Convert a string to an Int64 + /// + public static long? ConvertToInt64(string? numeric) + { + // If we don't have a valid string, we can't do anything + if (string.IsNullOrEmpty(numeric)) + return null; + + // Normalize the string for easier comparison + numeric = numeric!.ToLowerInvariant(); + + // Parse the numeric string, if possible + if (numeric.StartsWith("0x")) + { + return Convert.ToInt64(numeric.Substring(2), 16); + } + else + { + // Get the multiplication modifier and trim characters + long multiplier = DetermineMultiplier(numeric); + numeric = numeric.TrimEnd(['k', 'm', 'g', 't', 'p', 'e', 'z', 'y', 'i', 'b', ' ']); + + // Apply the multiplier and return + if (!long.TryParse(numeric, out long longValue)) + return null; + + return longValue * multiplier; + } + } + + /// + /// Determine the multiplier from a numeric string + /// + public static long DetermineMultiplier(string? numeric) + { + if (string.IsNullOrEmpty(numeric)) + return 0; + + long multiplier = 1; + if (numeric!.EndsWith("k") || numeric.EndsWith("kb")) + multiplier = KiloByte; + else if (numeric.EndsWith("ki") || numeric.EndsWith("kib")) + multiplier = KibiByte; + else if (numeric.EndsWith("m") || numeric.EndsWith("mb")) + multiplier = MegaByte; + else if (numeric.EndsWith("mi") || numeric.EndsWith("mib")) + multiplier = MibiByte; + else if (numeric.EndsWith("g") || numeric.EndsWith("gb")) + multiplier = GigaByte; + else if (numeric.EndsWith("gi") || numeric.EndsWith("gib")) + multiplier = GibiByte; + else if (numeric.EndsWith("t") || numeric.EndsWith("tb")) + multiplier = TeraByte; + else if (numeric.EndsWith("ti") || numeric.EndsWith("tib")) + multiplier = TibiByte; + else if (numeric.EndsWith("p") || numeric.EndsWith("pb")) + multiplier = PetaByte; + else if (numeric.EndsWith("pi") || numeric.EndsWith("pib")) + multiplier = PibiByte; + + // The following are too big to be represented in Int64 + // else if (numeric.EndsWith("e") || numeric.EndsWith("eb")) + // multiplier = ExaByte; + // else if (numeric.EndsWith("ei") || numeric.EndsWith("eib")) + // multiplier = ExiByte; + // else if (numeric.EndsWith("z") || numeric.EndsWith("zb")) + // multiplier = ZettaByte; + // else if (numeric.EndsWith("zi") || numeric.EndsWith("zib")) + // multiplier = ZittiByte; + // else if (numeric.EndsWith("y") || numeric.EndsWith("yb")) + // multiplier = YottaByte; + // else if (numeric.EndsWith("yi") || numeric.EndsWith("yib")) + // multiplier = YittiByte; + + return multiplier; + } + + /// + /// Determine if a string is fully numeric or not + /// + public static bool IsNumeric(string? value) + { + // If we have no value, it is not numeric + if (string.IsNullOrEmpty(value)) + return false; + + // If we have a hex value + value = value!.ToLowerInvariant(); + if (value.StartsWith("0x")) + value = value.Substring(2); + + // If we have a negative value + if (value.StartsWith("-")) + value = value.Substring(1); + + // If the value has a multiplier + if (DetermineMultiplier(value) > 1) + value = value.TrimEnd(['k', 'm', 'g', 't', 'p', 'e', 'z', 'y', 'i', 'b', ' ']); + + // If the value is empty after trimming + if (value.Length == 0) + return false; + + // Otherwise, make sure that every character is a proper match + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; +#if NET7_0_OR_GREATER + if (!char.IsAsciiHexDigit(c) && c != '.' && c != ',') +#else + if (!c.IsAsciiHexDigit() && c != '.' && c != ',') +#endif + return false; + } + + return true; + } + + /// + /// Returns the human-readable file size for an arbitrary, 64-bit file size + /// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB". + /// + /// http://www.somacon.com/p576.php + /// This uses 1024-byte partitions, not 1000-byte + public static string GetBytesReadable(long input) + { + // Get absolute value + long absolute_i = input < 0 ? -input : input; + + // Determine the suffix and readable value + string suffix; + double readable; + if (absolute_i >= 0x1000_0000_0000_0000) // Exabyte + { + suffix = "EB"; + readable = input >> 50; + } + else if (absolute_i >= 0x4_0000_0000_0000) // Petabyte + { + suffix = "PB"; + readable = input >> 40; + } + else if (absolute_i >= 0x100_0000_0000) // Terabyte + { + suffix = "TB"; + readable = input >> 30; + } + else if (absolute_i >= 0x4000_0000) // Gigabyte + { + suffix = "GB"; + readable = input >> 20; + } + else if (absolute_i >= 0x10_0000) // Megabyte + { + suffix = "MB"; + readable = input >> 10; + } + else if (absolute_i >= 0x400) // Kilobyte + { + suffix = "KB"; + readable = input; + } + else + { + return input.ToString("0 B"); // Byte + } + + // Divide by 1024 to get fractional value + readable /= 1024; + + // Return formatted number with suffix + return readable.ToString("0.### ") + suffix; + } + +#if NETFRAMEWORK || NETCOREAPP3_1 || NET5_0 || NET6_0 || NETSTANDARD2_0_OR_GREATER + /// + /// Indicates whether a character is categorized as an ASCII hexademical digit. + /// + /// The character to evaluate. + /// true if c is a hexademical digit; otherwise, false. + /// This method determines whether the character is in the range '0' through '9', inclusive, 'A' through 'F', inclusive, or 'a' through 'f', inclusive. + internal static bool IsAsciiHexDigit(this char c) + { + return char.ToLowerInvariant(c) switch + { + '0' => true, + '1' => true, + '2' => true, + '3' => true, + '4' => true, + '5' => true, + '6' => true, + '7' => true, + '8' => true, + '9' => true, + 'a' => true, + 'b' => true, + 'c' => true, + 'd' => true, + 'e' => true, + 'f' => true, + _ => false, + }; + } +#endif + } +} diff --git a/SabreTools.Metadata/Tools/TextHelper.cs b/SabreTools.Metadata/Tools/TextHelper.cs new file mode 100644 index 00000000..2a5e8c65 --- /dev/null +++ b/SabreTools.Metadata/Tools/TextHelper.cs @@ -0,0 +1,302 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using SabreTools.Hashing; + +namespace SabreTools.Metadata.Tools +{ + // TODO: Replace when IO is updated + public static class TextHelper + { + #region Normalization + + /// + /// Normalize a string to the WoD standard + /// + public static string? NormalizeCharacters(string? input) + { + if (string.IsNullOrEmpty(input)) + return input; + + ///Run the name through the filters to make sure that it's correct + input = NormalizeChars(input!); + input = RussianToLatin(input); + input = SearchPattern(input); + + input = new Regex(@"(([[(].*[\)\]] )?([^([]+))", RegexOptions.Compiled).Match(input).Groups[1].Value; + input = input.TrimStart().TrimEnd(); + return input; + } + + /// + /// Normalize a CRC32 string and pad to the correct size + /// + public static string? NormalizeCRC32(string? hash) + => NormalizeHashData(hash, Constants.CRCLength); + + /// + /// Normalize a MD2 string and pad to the correct size + /// + /// MD2 is the same length as MD5 + public static string? NormalizeMD2(string? hash) + => NormalizeHashData(hash, Constants.MD5Length); + + /// + /// Normalize a MD4 string and pad to the correct size + /// + /// MD4 is the same length as MD5 + public static string? NormalizeMD4(string? hash) + => NormalizeHashData(hash, Constants.MD5Length); + + /// + /// Normalize a MD5 string and pad to the correct size + /// + public static string? NormalizeMD5(string? hash) + => NormalizeHashData(hash, Constants.MD5Length); + + /// + /// Normalize a RIPEMD128 string and pad to the correct size + /// + public static string? NormalizeRIPEMD128(string? hash) + => NormalizeHashData(hash, Constants.MD5Length); + + /// + /// Normalize a RIPEMD160 string and pad to the correct size + /// + public static string? NormalizeRIPEMD160(string? hash) + => NormalizeHashData(hash, Constants.SHA1Length); + + /// + /// Normalize a SHA1 string and pad to the correct size + /// + public static string? NormalizeSHA1(string? hash) + => NormalizeHashData(hash, Constants.SHA1Length); + + /// + /// Normalize a SHA256 string and pad to the correct size + /// + public static string? NormalizeSHA256(string? hash) + => NormalizeHashData(hash, Constants.SHA256Length); + + /// + /// Normalize a SHA384 string and pad to the correct size + /// + public static string? NormalizeSHA384(string? hash) + => NormalizeHashData(hash, Constants.SHA384Length); + + /// + /// Normalize a SHA512 string and pad to the correct size + /// + public static string? NormalizeSHA512(string? hash) + => NormalizeHashData(hash, Constants.SHA512Length); + + /// + /// Remove all chars that are considered path unsafe + /// + public static string RemovePathUnsafeCharacters(string? input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + foreach (char invalid in Path.GetInvalidPathChars()) + { + input = input!.Replace(invalid.ToString(), string.Empty); + } + + return input!; + } + + /// + /// Remove all Unicode-specific chars from a string + /// + /// + /// "Unicode characters" here means any characters outside of the + /// Extended ASCII (0x00 to 0xFF) set. This is just a simple + /// way of filtering out characters that won't work on all + /// supported platforms. + /// + public static string RemoveUnicodeCharacters(string? input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + return new string(Array.FindAll(input!.ToCharArray(), c => c <= 255)); + } + + #endregion + + #region Helpers + + /// + /// Replace accented characters + /// + private static string NormalizeChars(string input) + { + string[,] charmap = { + { "Á", "A" }, { "á", "a" }, + { "À", "A" }, { "à", "a" }, + { "Â", "A" }, { "â", "a" }, + { "Ä", "Ae" }, { "ä", "ae" }, + { "Ã", "A" }, { "ã", "a" }, + { "Å", "A" }, { "å", "a" }, + { "Æ", "Ae" }, { "æ", "ae" }, + { "Ç", "C" }, { "ç", "c" }, + { "Ð", "D" }, { "ð", "d" }, + { "É", "E" }, { "é", "e" }, + { "È", "E" }, { "è", "e" }, + { "Ê", "E" }, { "ê", "e" }, + { "Ë", "E" }, { "ë", "e" }, + { "ƒ", "f" }, + { "Í", "I" }, { "í", "i" }, + { "Ì", "I" }, { "ì", "i" }, + { "Î", "I" }, { "î", "i" }, + { "Ï", "I" }, { "ï", "i" }, + { "Ñ", "N" }, { "ñ", "n" }, + { "Ó", "O" }, { "ó", "o" }, + { "Ò", "O" }, { "ò", "o" }, + { "Ô", "O" }, { "ô", "o" }, + { "Ö", "Oe" }, { "ö", "oe" }, + { "Õ", "O" }, { "õ", "o" }, + { "Ø", "O" }, { "ø", "o" }, + { "Š", "S" }, { "š", "s" }, + { "ß", "ss" }, + { "Þ", "B" }, { "þ", "b" }, + { "Ú", "U" }, { "ú", "u" }, + { "Ù", "U" }, { "ù", "u" }, + { "Û", "U" }, { "û", "u" }, + { "Ü", "Ue" }, { "ü", "ue" }, + { "ÿ", "y" }, + { "Ý", "Y" }, { "ý", "y" }, + { "Ž", "Z" }, { "ž", "z" }, + }; + + for (int i = 0; i < charmap.GetLength(0); i++) + { + input = input.Replace(charmap[i, 0], charmap[i, 1]); + } + + return input; + } + + /// + /// Normalize a hash string and pad to the correct size + /// + private static string? NormalizeHashData(string? hash, int expectedLength) + { + // If we have a known blank hash, return blank + if (hash is null) + return null; + else if (hash == string.Empty || hash == "-" || hash == "_") + return string.Empty; + + // Check to see if it's a "hex" hash + hash = hash!.Trim().Replace("0x", string.Empty); + + // If we have a blank hash now, return blank + if (string.IsNullOrEmpty(hash)) + return string.Empty; + + // If the hash shorter than the required length, pad it + if (hash.Length < expectedLength) + hash = hash.PadLeft(expectedLength, '0'); + + // If the hash is longer than the required length, it's invalid + else if (hash.Length > expectedLength) + return string.Empty; + + // Now normalize the hash + hash = hash.ToLowerInvariant(); + + // Otherwise, make sure that every character is a proper match + for (int i = 0; i < hash.Length; i++) + { + char c = hash[i]; +#if NET7_0_OR_GREATER + if (!char.IsAsciiHexDigit(c)) +#else + if (!c.IsAsciiHexDigit()) +#endif + { + hash = string.Empty; + break; + } + } + + return hash; + } + + /// + /// Convert Cyrillic lettering to Latin lettering + /// + private static string RussianToLatin(string input) + { + string[,] charmap = { + { "А", "A" }, { "Б", "B" }, { "В", "V" }, { "Г", "G" }, { "Д", "D" }, + { "Е", "E" }, { "Ё", "Yo" }, { "Ж", "Zh" }, { "З", "Z" }, { "И", "I" }, + { "Й", "J" }, { "К", "K" }, { "Л", "L" }, { "М", "M" }, { "Н", "N" }, + { "О", "O" }, { "П", "P" }, { "Р", "R" }, { "С", "S" }, { "Т", "T" }, + { "У", "U" }, { "Ф", "f" }, { "Х", "Kh" }, { "Ц", "Ts" }, { "Ч", "Ch" }, + { "Ш", "Sh" }, { "Щ", "Sch" }, { "Ъ", string.Empty }, { "Ы", "y" }, { "Ь", string.Empty }, + { "Э", "e" }, { "Ю", "yu" }, { "Я", "ya" }, { "а", "a" }, { "б", "b" }, + { "в", "v" }, { "г", "g" }, { "д", "d" }, { "е", "e" }, { "ё", "yo" }, + { "ж", "zh" }, { "з", "z" }, { "и", "i" }, { "й", "j" }, { "к", "k" }, + { "л", "l" }, { "м", "m" }, { "н", "n" }, { "о", "o" }, { "п", "p" }, + { "р", "r" }, { "с", "s" }, { "т", "t" }, { "у", "u" }, { "ф", "f" }, + { "х", "kh" }, { "ц", "ts" }, { "ч", "ch" }, { "ш", "sh" }, { "щ", "sch" }, + { "ъ", string.Empty }, { "ы", "y" }, { "ь", string.Empty }, { "э", "e" }, { "ю", "yu" }, + { "я", "ya" }, + }; + + for (int i = 0; i < charmap.GetLength(0); i++) + { + input = input.Replace(charmap[i, 0], charmap[i, 1]); + } + + return input; + } + + /// + /// Replace special characters and patterns + /// + private static string SearchPattern(string input) + { + string[,] charmap = { + { @"~", " - " }, + { @"_", " " }, + { @":", " " }, + { @">", ")" }, + { @"<", "(" }, + { @"\|", "-" }, + { "\"", "'" }, + { @"\*", "." }, + { @"\\", "-" }, + { @"/", "-" }, + { @"\?", " " }, + { @"\(([^)(]*)\(([^)]*)\)([^)(]*)\)", " " }, + { @"\(([^)]+)\)", " " }, + { @"\[([^]]+)\]", " " }, + { @"\{([^}]+)\}", " " }, + { @"(ZZZJUNK|ZZZ-UNK-|ZZZ-UNK |zzz unknow |zzz unk |Copy of |[.][a-z]{3}[.][a-z]{3}[.]|[.][a-z]{3}[.])", " " }, + { @" (r|rev|v|ver)\s*[\d\.]+[^\s]*", " " }, + { @"(( )|(\A))(\d{6}|\d{8})(( )|(\Z))", " " }, + { @"(( )|(\A))(\d{1,2})-(\d{1,2})-(\d{4}|\d{2})", " " }, + { @"(( )|(\A))(\d{4}|\d{2})-(\d{1,2})-(\d{1,2})", " " }, + { @"[-]+", "-" }, + { @"\A\s*\)", " " }, + { @"\A\s*(,|-)", " " }, + { @"\s+", " " }, + { @"\s+,", "," }, + { @"\s*(,|-)\s*\Z", " " }, + }; + + for (int i = 0; i < charmap.GetLength(0); i++) + { + input = Regex.Replace(input, charmap[i, 0], charmap[i, 1]); + } + + return input; + } + + #endregion + } +} diff --git a/SabreTools.Metadata/Tools/TypeHelper.cs b/SabreTools.Metadata/Tools/TypeHelper.cs new file mode 100644 index 00000000..b869c4ac --- /dev/null +++ b/SabreTools.Metadata/Tools/TypeHelper.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Xml.Serialization; +using SabreTools.Data.Models; +using SabreTools.Data.Models.Metadata; + +namespace SabreTools.Metadata.Tools +{ + public static class TypeHelper + { + /// + /// Get constant values for the given type, if possible + /// + public static string[]? GetConstants(Type? type) + { + if (type is null) + return null; + + var fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); + if (fields is null) + return null; + + FieldInfo[] noFilterFields = Array.FindAll(fields, + f => f.IsLiteral + && !f.IsInitOnly + && Attribute.GetCustomAttributes(f, typeof(NoFilterAttribute)).Length == 0); + string[] constantValues = Array.ConvertAll(noFilterFields, + f => (f.GetRawConstantValue() as string) ?? string.Empty); + return Array.FindAll(constantValues, s => s.Length > 0); + } + + /// + /// Attempt to get all DatItem types + /// + public static string[] GetDatItemTypeNames() + { + List typeNames = []; + + // Loop through all loaded assemblies + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + // If not all types can be loaded, use the ones that could be + Type?[] assemblyTypes = []; + try + { + assemblyTypes = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException rtle) + { + assemblyTypes = Array.FindAll(rtle.Types ?? [], t => t is not null); + } + + // Loop through all types + foreach (Type? type in assemblyTypes) + { + // If the type is invalid + if (type is null) + continue; + + // If the type isn't a class or doesn't implement the interface + if (!type.IsClass || !typeof(DatItem).IsAssignableFrom(type)) + continue; + + // Get the XML type name + string? elementName = GetXmlRootAttributeElementName(type); + if (elementName is not null) + typeNames.Add(elementName); + } + } + + return [.. typeNames]; + } + + /// + /// Attempt to get the DatItem type from the name + /// + public static Type? GetDatItemType(string? itemType) + { + if (string.IsNullOrEmpty(itemType)) + return null; + + // Loop through all loaded assemblies + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + // If not all types can be loaded, use the ones that could be + Type?[] assemblyTypes = []; + try + { + assemblyTypes = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException rtle) + { + assemblyTypes = Array.FindAll(rtle.Types ?? [], t => t is not null); + } + + // Loop through all types + foreach (Type? type in assemblyTypes) + { + // If the type is invalid + if (type is null) + continue; + + // If the type isn't a class or doesn't implement the interface + if (!type.IsClass || !typeof(DatItem).IsAssignableFrom(type)) + continue; + + // Get the XML type name + string? elementName = GetXmlRootAttributeElementName(type); + if (elementName is null) + continue; + + // If the name matches + if (string.Equals(elementName, itemType, StringComparison.OrdinalIgnoreCase)) + return type; + } + } + + return null; + } + + /// + /// Attempt to get the XmlRootAttribute.ElementName value from a type + /// + public static string? GetXmlRootAttributeElementName(Type? type) + { + if (type is null) + return null; + +#if NET20 || NET35 || NET40 + return (Attribute.GetCustomAttribute(type, typeof(XmlRootAttribute)) as XmlRootAttribute)!.ElementName; +#else + return type.GetCustomAttribute()?.ElementName; +#endif + } + } +} diff --git a/SabreTools.Metadata/Tools/Utilities.cs b/SabreTools.Metadata/Tools/Utilities.cs new file mode 100644 index 00000000..00498be3 --- /dev/null +++ b/SabreTools.Metadata/Tools/Utilities.cs @@ -0,0 +1,97 @@ +using System.IO; +using SabreTools.Hashing; +using SabreTools.IO.Extensions; + +namespace SabreTools.Metadata.Tools +{ + /// + /// Static utility functions used throughout the library + /// + public static class Utilities + { + /// + /// Get a proper romba sub path + /// + /// SHA-1 hash to get the path for + /// Positive value representing the depth of the depot + /// Subfolder path for the given hash + public static string? GetDepotPath(byte[]? hash, int depth) + { + string? sha1 = hash.ToHexString(); + return GetDepotPath(sha1, depth); + } + + /// + /// Get a proper romba sub path + /// + /// SHA-1 hash to get the path for + /// Positive value representing the depth of the depot + /// Subfolder path for the given hash + public static string? GetDepotPath(string? hash, int depth) + { + // If the hash is null or empty, then we return null + if (string.IsNullOrEmpty(hash)) + return null; + + // If the hash isn't the right size, then we return null + if (hash!.Length != Constants.SHA1Length) + return null; + + // Cap the depth between 0 and 20, for now + if (depth < 0) + depth = 0; + else if (depth > ZeroHash.SHA1Arr.Length) + depth = ZeroHash.SHA1Arr.Length; + + // Loop through and generate the subdirectory + string path = string.Empty; + for (int i = 0; i < depth; i++) + { + path += hash.Substring(i * 2, 2) + Path.DirectorySeparatorChar; + } + + // Now append the filename + path += $"{hash}.gz"; + return path; + } + + /// + /// Get if the given path has a valid DAT extension + /// + /// Path to check + /// True if the extension is valid, false otherwise + public static bool HasValidDatExtension(string? path) + { + // If the path is null or empty, then we return false + if (string.IsNullOrEmpty(path)) + return false; + + // Get the extension from the path, if possible + string ext = Path.GetExtension(path).TrimStart('.').ToLowerInvariant(); + + // Check against the list of known DAT extensions + return ext switch + { + "csv" => true, + "dat" => true, + "json" => true, + "md2" => true, + "md4" => true, + "md5" => true, + "ripemd128" => true, + "ripemd160" => true, + "sfv" => true, + "sha1" => true, + "sha256" => true, + "sha384" => true, + "sha512" => true, + "spamsum" => true, + "ssv" => true, + "tsv" => true, + "txt" => true, + "xml" => true, + _ => false, + }; + } + } +} diff --git a/SabreTools.Serialization.sln b/SabreTools.Serialization.sln index aa01ea09..4d6f4e26 100644 --- a/SabreTools.Serialization.sln +++ b/SabreTools.Serialization.sln @@ -35,6 +35,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Serialization.Re EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Serialization.Writers.Test", "SabreTools.Serialization.Writers.Test\SabreTools.Serialization.Writers.Test.csproj", "{99619CA9-6D7D-4653-BD1E-8F586F219607}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata", "SabreTools.Metadata\SabreTools.Metadata.csproj", "{F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.Filter", "SabreTools.Metadata.Filter\SabreTools.Metadata.Filter.csproj", "{76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.Test", "SabreTools.Metadata.Test\SabreTools.Metadata.Test.csproj", "{D817566C-C94F-4ADE-A36A-F67B3F644299}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.Filter.Test", "SabreTools.Metadata.Filter.Test\SabreTools.Metadata.Filter.Test.csproj", "{43D66007-FCAA-4F69-893B-562B0B61B8B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.DatItems", "SabreTools.Metadata.DatItems\SabreTools.Metadata.DatItems.csproj", "{14931F5A-203A-4C22-A162-D38288B6A576}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.DatItems.Test", "SabreTools.Metadata.DatItems.Test\SabreTools.Metadata.DatItems.Test.csproj", "{E973A35B-148E-4E86-A4D1-C88575D1B09C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.DatFiles", "SabreTools.Metadata.DatFiles\SabreTools.Metadata.DatFiles.csproj", "{68B9FD8F-63A8-45C6-98B4-E1E040167587}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.DatFiles.Test", "SabreTools.Metadata.DatFiles.Test\SabreTools.Metadata.DatFiles.Test.csproj", "{EFF85ED6-27D6-4076-A935-9792D2975078}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -237,6 +253,102 @@ Global {99619CA9-6D7D-4653-BD1E-8F586F219607}.Release|x64.Build.0 = Release|Any CPU {99619CA9-6D7D-4653-BD1E-8F586F219607}.Release|x86.ActiveCfg = Release|Any CPU {99619CA9-6D7D-4653-BD1E-8F586F219607}.Release|x86.Build.0 = Release|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Debug|x64.Build.0 = Debug|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Debug|x86.Build.0 = Debug|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Release|Any CPU.Build.0 = Release|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Release|x64.ActiveCfg = Release|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Release|x64.Build.0 = Release|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Release|x86.ActiveCfg = Release|Any CPU + {F4C9FAE7-6777-4C1A-BEE7-DA664CA3485B}.Release|x86.Build.0 = Release|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Debug|x64.Build.0 = Debug|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Debug|x86.Build.0 = Debug|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Release|Any CPU.Build.0 = Release|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Release|x64.ActiveCfg = Release|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Release|x64.Build.0 = Release|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Release|x86.ActiveCfg = Release|Any CPU + {76E096C6-72A9-4A5C-A250-95D2BBBFA5D1}.Release|x86.Build.0 = Release|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Debug|x64.ActiveCfg = Debug|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Debug|x64.Build.0 = Debug|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Debug|x86.ActiveCfg = Debug|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Debug|x86.Build.0 = Debug|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Release|Any CPU.Build.0 = Release|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Release|x64.ActiveCfg = Release|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Release|x64.Build.0 = Release|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Release|x86.ActiveCfg = Release|Any CPU + {D817566C-C94F-4ADE-A36A-F67B3F644299}.Release|x86.Build.0 = Release|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Debug|x64.Build.0 = Debug|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Debug|x86.Build.0 = Debug|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Release|Any CPU.Build.0 = Release|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Release|x64.ActiveCfg = Release|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Release|x64.Build.0 = Release|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Release|x86.ActiveCfg = Release|Any CPU + {43D66007-FCAA-4F69-893B-562B0B61B8B1}.Release|x86.Build.0 = Release|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Debug|x64.ActiveCfg = Debug|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Debug|x64.Build.0 = Debug|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Debug|x86.ActiveCfg = Debug|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Debug|x86.Build.0 = Debug|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Release|Any CPU.Build.0 = Release|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Release|x64.ActiveCfg = Release|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Release|x64.Build.0 = Release|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Release|x86.ActiveCfg = Release|Any CPU + {14931F5A-203A-4C22-A162-D38288B6A576}.Release|x86.Build.0 = Release|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Debug|x64.Build.0 = Debug|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Debug|x86.Build.0 = Debug|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Release|Any CPU.Build.0 = Release|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Release|x64.ActiveCfg = Release|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Release|x64.Build.0 = Release|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Release|x86.ActiveCfg = Release|Any CPU + {E973A35B-148E-4E86-A4D1-C88575D1B09C}.Release|x86.Build.0 = Release|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Debug|x64.ActiveCfg = Debug|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Debug|x64.Build.0 = Debug|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Debug|x86.ActiveCfg = Debug|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Debug|x86.Build.0 = Debug|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Release|Any CPU.Build.0 = Release|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Release|x64.ActiveCfg = Release|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Release|x64.Build.0 = Release|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Release|x86.ActiveCfg = Release|Any CPU + {68B9FD8F-63A8-45C6-98B4-E1E040167587}.Release|x86.Build.0 = Release|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Debug|x64.Build.0 = Debug|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Debug|x86.Build.0 = Debug|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Release|Any CPU.Build.0 = Release|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x64.ActiveCfg = Release|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x64.Build.0 = Release|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x86.ActiveCfg = Release|Any CPU + {EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE