using System.Xml.Serialization; using Newtonsoft.Json; using SabreTools.Data.Extensions; using SabreTools.Data.Models.Metadata; using SabreTools.Text.Extensions; 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 = ReadString(Data.Models.Metadata.Rom.StatusKey).AsItemStatus(); return status is not null && status != ItemStatus.None; } } [JsonIgnore] public bool OriginalSpecified { get { var original = Read("ORIGINAL"); return original is not null && original != default; } } [JsonIgnore] public bool DataAreaSpecified { get { var dataArea = Read(DataAreaKey); return dataArea is not null && (!string.IsNullOrEmpty(dataArea.GetName()) || dataArea.ReadLong(Data.Models.Metadata.DataArea.SizeKey) is not null || dataArea.ReadLong(Data.Models.Metadata.DataArea.WidthKey) is not null || dataArea.ReadString(Data.Models.Metadata.DataArea.EndiannessKey).AsEndianness() is not null); } } [JsonIgnore] public bool PartSpecified { get { var part = Read(PartKey); return part is not null && (!string.IsNullOrEmpty(part.GetName()) || !string.IsNullOrEmpty(part.ReadString(Data.Models.Metadata.Part.InterfaceKey))); } } #endregion #region Constructors public Rom() : base() { Write(DupeTypeKey, 0x00); Write(Data.Models.Metadata.Rom.StatusKey, ItemStatus.None.AsStringValue()); } public Rom(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 = null; if (item.Read(Dump.RomKey) is not null) { rom = item.Read(Dump.RomKey); subType = OpenMSXSubType.Rom; } else if (item.Read(Dump.MegaRomKey) is not null) { rom = item.Read(Dump.MegaRomKey); subType = OpenMSXSubType.MegaRom; } else if (item.Read(Dump.SCCPlusCartKey) is not null) { rom = item.Read(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); Write(Data.Models.Metadata.Rom.OffsetKey, rom.ReadString(Data.Models.Metadata.Rom.StartKey)); Write(Data.Models.Metadata.Rom.OpenMSXMediaType, subType?.AsStringValue()); Write(Data.Models.Metadata.Rom.OpenMSXType, rom.ReadString(Data.Models.Metadata.Rom.OpenMSXType) ?? rom.ReadString(Data.Models.Metadata.DatItem.TypeKey)); Write(Data.Models.Metadata.Rom.RemarkKey, rom.ReadString(Data.Models.Metadata.Rom.RemarkKey)); Write(Data.Models.Metadata.Rom.SHA1Key, rom.ReadString(Data.Models.Metadata.Rom.SHA1Key)); Write(Data.Models.Metadata.Rom.StartKey, rom.ReadString(Data.Models.Metadata.Rom.StartKey)); Write(SourceKey, source); var original = item.Read(Dump.OriginalKey); if (original is not null) { Write("ORIGINAL", new Original { Value = original.ReadBool(Data.Models.Metadata.Original.ValueKey), Content = original.ReadString(Data.Models.Metadata.Original.ContentKey), }); } CopyMachineInformation(machine); // Process hash values long? size = ReadLong(Data.Models.Metadata.Rom.SizeKey); if (size is not null) Write(Data.Models.Metadata.Rom.SizeKey, size.ToString()); // TODO: This should be normalized to CRC-16 string? crc16 = ReadString(Data.Models.Metadata.Rom.CRC16Key); if (crc16 is not null) Write(Data.Models.Metadata.Rom.CRC16Key, NormalizeHashData(crc16, 4)); string? crc = ReadString(Data.Models.Metadata.Rom.CRCKey); if (crc is not null) Write(Data.Models.Metadata.Rom.CRCKey, TextHelper.NormalizeCRC32(crc)); // TODO: This should be normalized to CRC-64 string? crc64 = ReadString(Data.Models.Metadata.Rom.CRC64Key); if (crc64 is not null) Write(Data.Models.Metadata.Rom.CRC64Key, NormalizeHashData(crc64, 16)); string? md2 = ReadString(Data.Models.Metadata.Rom.MD2Key); if (md2 is not null) Write(Data.Models.Metadata.Rom.MD2Key, TextHelper.NormalizeMD2(md2)); string? md4 = ReadString(Data.Models.Metadata.Rom.MD4Key); if (md4 is not null) Write(Data.Models.Metadata.Rom.MD4Key, TextHelper.NormalizeMD5(md4)); string? md5 = ReadString(Data.Models.Metadata.Rom.MD5Key); if (md5 is not null) Write(Data.Models.Metadata.Rom.MD5Key, TextHelper.NormalizeMD5(md5)); string? ripemd128 = ReadString(Data.Models.Metadata.Rom.RIPEMD128Key); if (ripemd128 is not null) Write(Data.Models.Metadata.Rom.RIPEMD128Key, TextHelper.NormalizeRIPEMD128(ripemd128)); string? ripemd160 = ReadString(Data.Models.Metadata.Rom.RIPEMD160Key); if (ripemd160 is not null) Write(Data.Models.Metadata.Rom.RIPEMD160Key, TextHelper.NormalizeRIPEMD160(ripemd160)); string? sha1 = ReadString(Data.Models.Metadata.Rom.SHA1Key); if (sha1 is not null) Write(Data.Models.Metadata.Rom.SHA1Key, TextHelper.NormalizeSHA1(sha1)); string? sha256 = ReadString(Data.Models.Metadata.Rom.SHA256Key); if (sha256 is not null) Write(Data.Models.Metadata.Rom.SHA256Key, TextHelper.NormalizeSHA256(sha256)); string? sha384 = ReadString(Data.Models.Metadata.Rom.SHA384Key); if (sha384 is not null) Write(Data.Models.Metadata.Rom.SHA384Key, TextHelper.NormalizeSHA384(sha384)); string? sha512 = ReadString(Data.Models.Metadata.Rom.SHA512Key); if (sha512 is not null) Write(Data.Models.Metadata.Rom.SHA512Key, TextHelper.NormalizeSHA512(sha512)); } public Rom(Data.Models.Metadata.Rom item) : base(item) { Write(DupeTypeKey, 0x00); // Process flag values bool? dispose = ReadBool(Data.Models.Metadata.Rom.DisposeKey); if (dispose is not null) Write(Data.Models.Metadata.Rom.DisposeKey, dispose.FromYesNo()); bool? inverted = ReadBool(Data.Models.Metadata.Rom.InvertedKey); if (inverted is not null) Write(Data.Models.Metadata.Rom.InvertedKey, inverted.FromYesNo()); string? loadFlag = ReadString(Data.Models.Metadata.Rom.LoadFlagKey); if (loadFlag is not null) Write(Data.Models.Metadata.Rom.LoadFlagKey, loadFlag.AsLoadFlag()?.AsStringValue()); string? openMSXMediaType = ReadString(Data.Models.Metadata.Rom.OpenMSXMediaType); if (openMSXMediaType is not null) Write(Data.Models.Metadata.Rom.OpenMSXMediaType, openMSXMediaType.AsOpenMSXSubType()?.AsStringValue()); bool? mia = ReadBool(Data.Models.Metadata.Rom.MIAKey); if (mia is not null) Write(Data.Models.Metadata.Rom.MIAKey, mia.FromYesNo()); bool? optional = ReadBool(Data.Models.Metadata.Rom.OptionalKey); if (optional is not null) Write(Data.Models.Metadata.Rom.OptionalKey, optional.FromYesNo()); bool? soundOnly = ReadBool(Data.Models.Metadata.Rom.SoundOnlyKey); if (soundOnly is not null) Write(Data.Models.Metadata.Rom.SoundOnlyKey, soundOnly.FromYesNo()); string? status = ReadString(Data.Models.Metadata.Rom.StatusKey); if (status is not null) Write(Data.Models.Metadata.Rom.StatusKey, status.AsItemStatus()?.AsStringValue()); // Process hash values long? size = ReadLong(Data.Models.Metadata.Rom.SizeKey); if (size is not null) Write(Data.Models.Metadata.Rom.SizeKey, size.ToString()); // TODO: This should be normalized to CRC-16 string? crc16 = ReadString(Data.Models.Metadata.Rom.CRC16Key); if (crc16 is not null) Write(Data.Models.Metadata.Rom.CRC16Key, NormalizeHashData(crc16, 4)); string? crc = ReadString(Data.Models.Metadata.Rom.CRCKey); if (crc is not null) Write(Data.Models.Metadata.Rom.CRCKey, TextHelper.NormalizeCRC32(crc)); // TODO: This should be normalized to CRC-64 string? crc64 = ReadString(Data.Models.Metadata.Rom.CRC64Key); if (crc64 is not null) Write(Data.Models.Metadata.Rom.CRC64Key, NormalizeHashData(crc64, 16)); string? md2 = ReadString(Data.Models.Metadata.Rom.MD2Key); if (md2 is not null) Write(Data.Models.Metadata.Rom.MD2Key, TextHelper.NormalizeMD2(md2)); string? md4 = ReadString(Data.Models.Metadata.Rom.MD4Key); if (md4 is not null) Write(Data.Models.Metadata.Rom.MD4Key, TextHelper.NormalizeMD5(md4)); string? md5 = ReadString(Data.Models.Metadata.Rom.MD5Key); if (md5 is not null) Write(Data.Models.Metadata.Rom.MD5Key, TextHelper.NormalizeMD5(md5)); string? ripemd128 = ReadString(Data.Models.Metadata.Rom.RIPEMD128Key); if (ripemd128 is not null) Write(Data.Models.Metadata.Rom.RIPEMD128Key, TextHelper.NormalizeRIPEMD128(ripemd128)); string? ripemd160 = ReadString(Data.Models.Metadata.Rom.RIPEMD160Key); if (ripemd160 is not null) Write(Data.Models.Metadata.Rom.RIPEMD160Key, TextHelper.NormalizeRIPEMD160(ripemd160)); string? sha1 = ReadString(Data.Models.Metadata.Rom.SHA1Key); if (sha1 is not null) Write(Data.Models.Metadata.Rom.SHA1Key, TextHelper.NormalizeSHA1(sha1)); string? sha256 = ReadString(Data.Models.Metadata.Rom.SHA256Key); if (sha256 is not null) Write(Data.Models.Metadata.Rom.SHA256Key, TextHelper.NormalizeSHA256(sha256)); string? sha384 = ReadString(Data.Models.Metadata.Rom.SHA384Key); if (sha384 is not null) Write(Data.Models.Metadata.Rom.SHA384Key, TextHelper.NormalizeSHA384(sha384)); string? sha512 = ReadString(Data.Models.Metadata.Rom.SHA512Key); if (sha512 is not null) Write(Data.Models.Metadata.Rom.SHA512Key, TextHelper.NormalizeSHA512(sha512)); } public Rom(Data.Models.Metadata.Rom item, Machine machine, Source source) : this(item) { Write(SourceKey, source); CopyMachineInformation(machine); } /// /// Normalize a hash string and pad to the correct size /// /// TODO: Remove when IO is updated 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 (!IsAsciiHexDigit(c)) #endif { hash = string.Empty; break; } } return hash; } #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. /// TODO: Remove when IO is updated internal static bool IsAsciiHexDigit(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 #endregion #region Cloning Methods /// public override object Clone() => new Rom(_internal.Clone() as Data.Models.Metadata.Rom ?? []); #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.CRC16: key = ReadString(Data.Models.Metadata.Rom.CRC16Key); break; case ItemKey.CRC: key = ReadString(Data.Models.Metadata.Rom.CRCKey); break; case ItemKey.CRC64: key = ReadString(Data.Models.Metadata.Rom.CRC64Key); break; case ItemKey.MD2: key = ReadString(Data.Models.Metadata.Rom.MD2Key); break; case ItemKey.MD4: key = ReadString(Data.Models.Metadata.Rom.MD4Key); break; case ItemKey.MD5: key = ReadString(Data.Models.Metadata.Rom.MD5Key); break; case ItemKey.RIPEMD128: key = ReadString(Data.Models.Metadata.Rom.RIPEMD128Key); break; case ItemKey.RIPEMD160: key = ReadString(Data.Models.Metadata.Rom.RIPEMD160Key); break; case ItemKey.SHA1: key = ReadString(Data.Models.Metadata.Rom.SHA1Key); break; case ItemKey.SHA256: key = ReadString(Data.Models.Metadata.Rom.SHA256Key); break; case ItemKey.SHA384: key = ReadString(Data.Models.Metadata.Rom.SHA384Key); break; case ItemKey.SHA512: key = ReadString(Data.Models.Metadata.Rom.SHA512Key); break; case ItemKey.SpamSum: key = ReadString(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 } }