diff --git a/SabreTools.Core/Enums.cs b/SabreTools.Core/Enums.cs index b6f791e4..5fb75c31 100644 --- a/SabreTools.Core/Enums.cs +++ b/SabreTools.Core/Enums.cs @@ -283,6 +283,7 @@ namespace SabreTools.Core Driver, Extension, Feature, + File, Info, Input, Instance, diff --git a/SabreTools.DatItems/Formats/File.cs b/SabreTools.DatItems/Formats/File.cs new file mode 100644 index 00000000..2eb25c20 --- /dev/null +++ b/SabreTools.DatItems/Formats/File.cs @@ -0,0 +1,313 @@ +using System; +using System.Linq; +using System.Xml.Serialization; +using Newtonsoft.Json; +using SabreTools.Core; +using SabreTools.Core.Tools; + +// TODO: Add item mappings for all fields +namespace SabreTools.DatItems.Formats +{ + /// + /// Represents a single file item + /// + [JsonObject("file"), XmlRoot("file")] + public 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 + + /// + /// 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; + + [JsonIgnore] + public bool SizeSpecified { get { return Size != null; } } + + /// + /// File CRC32 hash + /// + [JsonProperty("crc", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("crc")] + public string CRC + { + get { return _crc.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_crc); } + set { _crc = (value == "null" ? Constants.CRCZeroBytes : Utilities.StringToByteArray(CleanCRC32(value))); } + } + + /// + /// File MD5 hash + /// + [JsonProperty("md5", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("md5")] + public string MD5 + { + get { return _md5.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_md5); } + set { _md5 = Utilities.StringToByteArray(CleanMD5(value)); } + } + + /// + /// File SHA-1 hash + /// + [JsonProperty("sha1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha1")] + public string SHA1 + { + get { return _sha1.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha1); } + set { _sha1 = Utilities.StringToByteArray(CleanSHA1(value)); } + } + + /// + /// File SHA-256 hash + /// + [JsonProperty("sha256", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha256")] + public string SHA256 + { + get { return _sha256.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha256); } + set { _sha256 = Utilities.StringToByteArray(CleanSHA256(value)); } + } + + /// + /// Format value + /// + [JsonProperty("format", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("format")] + public string Format { get; set; } + + #endregion // Fields + + #region Constructors + + /// + /// Create a default, empty File object + /// + public File() + { + ItemType = ItemType.File; + } + + #endregion + + #region Cloning Methods + + /// + public override object Clone() + { + return new File() + { + ItemType = this.ItemType, + DupeType = this.DupeType, + + Machine = this.Machine.Clone() as Machine, + Source = this.Source.Clone() as Source, + Remove = this.Remove, + + Id = this.Id, + Extension = this.Extension, + Size = this.Size, + _crc = this._crc, + _md5 = this._md5, + _sha1 = this._sha1, + _sha256 = this._sha256, + Format = this.Format, + }; + } + + #endregion + + #region Comparision Methods + + /// + public override bool Equals(DatItem other) + { + bool dupefound = false; + + // If we don't have a file, return false + if (ItemType != other.ItemType) + 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 == 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 == null && other.Size != 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; + } + + /// + /// Get unique duplicate suffix on name collision + /// + /// String representing the suffix + public string GetDuplicateSuffix() + { + if (!_crc.IsNullOrEmpty()) + return $"_{CRC}"; + else if (!_md5.IsNullOrEmpty()) + return $"_{MD5}"; + else if (!_sha1.IsNullOrEmpty()) + return $"_{SHA1}"; + else if (!_sha256.IsNullOrEmpty()) + return $"_{SHA256}"; + else + return "_1"; + } + + /// + /// 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() + { + return (_crc != null && _crc.SequenceEqual(Constants.CRCZeroBytes)) + || (_md5 != null && _md5.SequenceEqual(Constants.MD5ZeroBytes)) + || (_sha1 != null && _sha1.SequenceEqual(Constants.SHA1ZeroBytes)) + || (_sha256 != null && _sha256.SequenceEqual(Constants.SHA256ZeroBytes)); + } + + /// + /// 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 ConditionalHashEquals(_crc, other._crc) + && ConditionalHashEquals(_md5, other._md5) + && ConditionalHashEquals(_sha1, other._sha1) + && ConditionalHashEquals(_sha256, other._sha256); + } + + #endregion + + #region Sorting and Merging + + /// + public override string GetKey(ItemKey bucketedBy, bool lower = true, bool norename = true) + { + // Set the output key as the default blank string + string key; + + // 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, lower, norename); + } + + // Double and triple check the key for corner cases + if (key == null) + key = string.Empty; + + return key; + } + + #endregion + } +}