mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-14 02:02:57 +00:00
This is both big and not big. The not big part of this is that I essentially just moved some stuff up to the typed type that used to live in the untyped type. The big part is that this allows every single DatItem implementation to be significantly cleaner with their implementations of the methods and internal model.
345 lines
11 KiB
C#
345 lines
11 KiB
C#
using System;
|
|
using System.Xml.Serialization;
|
|
using Newtonsoft.Json;
|
|
using SabreTools.Data.Extensions;
|
|
using SabreTools.Hashing;
|
|
using SabreTools.IO.Extensions;
|
|
using SabreTools.Metadata.Filter;
|
|
using SabreTools.Text.Extensions;
|
|
|
|
// TODO: Add item mappings for all fields
|
|
// TODO: Investigate whether this is the "File" from NIDB
|
|
namespace SabreTools.Metadata.DatItems.Formats
|
|
{
|
|
/// <summary>
|
|
/// Represents a single file item
|
|
/// </summary>
|
|
[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 Properties
|
|
|
|
/// <inheritdoc>/>
|
|
public override Data.Models.Metadata.ItemType ItemType => Data.Models.Metadata.ItemType.File;
|
|
|
|
/// <summary>
|
|
/// ID value
|
|
/// </summary>
|
|
[JsonProperty("id", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("id")]
|
|
public string? Id { get; set; }
|
|
|
|
/// <summary>
|
|
/// Extension value
|
|
/// </summary>
|
|
[JsonProperty("extension", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("extension")]
|
|
public string? Extension { get; set; }
|
|
|
|
/// <summary>
|
|
/// Byte size of the rom
|
|
/// </summary>
|
|
[JsonProperty("size", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("size")]
|
|
public long? Size { get; set; } = null;
|
|
|
|
/// <summary>
|
|
/// File CRC32 hash
|
|
/// </summary>
|
|
[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(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// File MD5 hash
|
|
/// </summary>
|
|
[JsonProperty("md5", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("md5")]
|
|
public string? MD5
|
|
{
|
|
get { return _md5.ToHexString(); }
|
|
set { _md5 = TextHelper.NormalizeMD5(value).FromHexString(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// File SHA-1 hash
|
|
/// </summary>
|
|
[JsonProperty("sha1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha1")]
|
|
public string? SHA1
|
|
{
|
|
get { return _sha1.ToHexString(); }
|
|
set { _sha1 = TextHelper.NormalizeSHA1(value).FromHexString(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// File SHA-256 hash
|
|
/// </summary>
|
|
[JsonProperty("sha256", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha256")]
|
|
public string? SHA256
|
|
{
|
|
get { return _sha256.ToHexString(); }
|
|
set { _sha256 = TextHelper.NormalizeSHA256(value).FromHexString(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format value
|
|
/// </summary>
|
|
[JsonProperty("format", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("format")]
|
|
public string? Format { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Create a default, empty File object
|
|
/// </summary>
|
|
public File() { }
|
|
|
|
#endregion
|
|
|
|
#region Accessors
|
|
|
|
/// <inheritdoc/>
|
|
public override string? GetName() => null;
|
|
|
|
/// <inheritdoc/>
|
|
public override void SetName(string? name) { }
|
|
|
|
#endregion
|
|
|
|
#region Cloning Methods
|
|
|
|
/// <inheritdoc/>
|
|
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.DupeType = DupeType;
|
|
file.Machine = Machine!.Clone() as Machine ?? new Machine();
|
|
file.RemoveFlag = RemoveFlag;
|
|
file.Source = Source?.Clone() as Source;
|
|
|
|
return file;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert a disk to the closest Rom approximation
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public Rom ConvertToRom()
|
|
{
|
|
var rom = new Rom();
|
|
|
|
rom.Name = $"{Id}.{Extension}";
|
|
rom.Size = Size;
|
|
rom.CRC = CRC;
|
|
rom.MD5 = MD5;
|
|
rom.SHA1 = SHA1;
|
|
rom.SHA256 = SHA256;
|
|
|
|
rom.DupeType = DupeType;
|
|
rom.Machine = Machine?.Clone() as Machine;
|
|
rom.RemoveFlag = RemoveFlag;
|
|
rom.Source = Source?.Clone() as Source;
|
|
|
|
return rom;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Comparision Methods
|
|
|
|
/// <inheritdoc/>
|
|
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 is null && HashMatch(newOther!))
|
|
{
|
|
dupefound = true;
|
|
}
|
|
|
|
// Otherwise if we get a partial match
|
|
else if (Size == newOther!.Size && HashMatch(newOther))
|
|
{
|
|
dupefound = true;
|
|
}
|
|
|
|
return dupefound;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fill any missing size and hash information from another Rom
|
|
/// </summary>
|
|
/// <param name="other">File to fill information from</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if the File contains any hashes
|
|
/// </summary>
|
|
/// <returns>True if any hash exists, false otherwise</returns>
|
|
public bool HasHashes()
|
|
{
|
|
return !_crc.IsNullOrEmpty()
|
|
|| !_md5.IsNullOrEmpty()
|
|
|| !_sha1.IsNullOrEmpty()
|
|
|| !_sha256.IsNullOrEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if all of the hashes are set to their 0-byte values
|
|
/// </summary>
|
|
/// <returns>True if any hash matches the 0-byte value, false otherwise</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if there are no, non-empty hashes in common with another File
|
|
/// </summary>
|
|
/// <param name="other">File to compare against</param>
|
|
/// <returns>True if at least one hash is not mutually exclusive, false otherwise</returns>
|
|
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());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if any hashes are common with another File
|
|
/// </summary>
|
|
/// <param name="other">File to compare against</param>
|
|
/// <returns>True if any hashes are in common, false otherwise</returns>
|
|
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
|
|
|
|
/// <inheritdoc/>
|
|
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
|
|
|
|
#region Manipulation
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>Not implemented</remarks>
|
|
public override bool PassesFilter(FilterRunner filterRunner) => false;
|
|
|
|
/// <inheritdoc/>
|
|
/// <remarks>Not implemented</remarks>
|
|
public override bool PassesFilterDB(FilterRunner filterRunner) => false;
|
|
|
|
#endregion
|
|
}
|
|
}
|