mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
Add nullable context to SabreTools.DatItems
This change also starts migrating the internals of the DatItem formats to the new internal models. Right now, it's basically just acting like a wrapper around those models.
This commit is contained in:
479
SabreTools.Core/DictionaryBaseExtensions.cs
Normal file
479
SabreTools.Core/DictionaryBaseExtensions.cs
Normal file
@@ -0,0 +1,479 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SabreTools.Models.Internal;
|
||||
|
||||
namespace SabreTools.Core
|
||||
{
|
||||
public static class DictionaryBaseExtensions
|
||||
{
|
||||
#region Cloning
|
||||
|
||||
/// <summary>
|
||||
/// Deep clone a DictionaryBase object
|
||||
/// </summary>
|
||||
public static DictionaryBase? Clone(this DictionaryBase dictionaryBase)
|
||||
{
|
||||
// Create a new object of the same type
|
||||
var clone = dictionaryBase
|
||||
.GetType()
|
||||
.GetConstructor(System.Reflection.BindingFlags.Public, Array.Empty<Type>())?
|
||||
.Invoke(null) as DictionaryBase;
|
||||
|
||||
// If construction failed, we can't do anything
|
||||
if (clone == null)
|
||||
return null;
|
||||
|
||||
// Loop through and clone per type
|
||||
foreach (string key in dictionaryBase.Keys)
|
||||
{
|
||||
object? value = dictionaryBase[key];
|
||||
switch (value)
|
||||
{
|
||||
// Primative types
|
||||
case bool:
|
||||
case long:
|
||||
case double:
|
||||
case string:
|
||||
clone[key] = value;
|
||||
break;
|
||||
|
||||
// DictionaryBase types
|
||||
case DictionaryBase db:
|
||||
clone[key] = db.Clone();
|
||||
break;
|
||||
|
||||
// Enumerable types
|
||||
case byte[] bytArr:
|
||||
clone[key] = bytArr.Clone();
|
||||
break;
|
||||
case string[] strArr:
|
||||
clone[key] = strArr.Clone();
|
||||
break;
|
||||
case DictionaryBase[] enDb:
|
||||
clone[key] = enDb.Select(Clone).ToArray();
|
||||
break;
|
||||
|
||||
// Everything else just copies
|
||||
default:
|
||||
clone[key] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Equality Checking
|
||||
|
||||
/// <summary>
|
||||
/// Check equality of two DictionaryBase objects
|
||||
/// </summary>
|
||||
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
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check equality of two DictionaryBase objects
|
||||
/// </summary>
|
||||
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
|
||||
if (self.Keys.Except(other.Keys).Any())
|
||||
return false;
|
||||
if (other.Keys.Except(self.Keys).Any())
|
||||
return false;
|
||||
|
||||
// Check all pairs to see if they're equal
|
||||
foreach (var kvp in self)
|
||||
{
|
||||
switch (kvp.Value, other[kvp.Key])
|
||||
{
|
||||
case (string selfString, string otherString):
|
||||
if (!string.Equals(selfString, otherString, StringComparison.OrdinalIgnoreCase))
|
||||
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:
|
||||
if (kvp.Value != other[kvp.Key])
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check equality of two Disk objects
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check equality of two Media objects
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check equality of two Rom objects
|
||||
/// </summary>
|
||||
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 == null && self.HashMatch(other))
|
||||
|
||||
// If we get a partial match
|
||||
if (selfSize == otherSize && self.HashMatch(other))
|
||||
return true;
|
||||
|
||||
// All other cases fail
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hash Checking
|
||||
|
||||
/// <summary>
|
||||
/// Returns if any hashes are common
|
||||
/// </summary>
|
||||
public static bool HashMatch(this Disk self, Disk other)
|
||||
{
|
||||
// If either have no hashes, we return false, otherwise this would be a false positive
|
||||
if (!self.HasHashes() || !other.HasHashes())
|
||||
return false;
|
||||
|
||||
// If neither have hashes in common, we return false, otherwise this would be a false positive
|
||||
if (!self.HasCommonHash(other))
|
||||
return false;
|
||||
|
||||
// Return if all hashes match according to merge rules
|
||||
return ConditionalHashEquals(self.ReadString(Disk.MD5Key), other.ReadString(Disk.MD5Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Disk.SHA1Key), other.ReadString(Disk.SHA1Key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if any hashes are common
|
||||
/// </summary>
|
||||
public static bool HashMatch(this Media self, Media other)
|
||||
{
|
||||
// If either have no hashes, we return false, otherwise this would be a false positive
|
||||
if (!self.HasHashes() || !other.HasHashes())
|
||||
return false;
|
||||
|
||||
// If neither have hashes in common, we return false, otherwise this would be a false positive
|
||||
if (!self.HasCommonHash(other))
|
||||
return false;
|
||||
|
||||
// Return if all hashes match according to merge rules
|
||||
return ConditionalHashEquals(self.ReadString(Media.MD5Key), other.ReadString(Media.MD5Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Media.SHA1Key), other.ReadString(Media.SHA1Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Media.SHA256Key), other.ReadString(Media.SHA256Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Media.SpamSumKey), other.ReadString(Media.SpamSumKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if any hashes are common
|
||||
/// </summary>
|
||||
public static bool HashMatch(this Rom self, Rom other)
|
||||
{
|
||||
// If either have no hashes, we return false, otherwise this would be a false positive
|
||||
if (!self.HasHashes() || !other.HasHashes())
|
||||
return false;
|
||||
|
||||
// If neither have hashes in common, we return false, otherwise this would be a false positive
|
||||
if (!self.HasCommonHash(other))
|
||||
return false;
|
||||
|
||||
// Return if all hashes match according to merge rules
|
||||
return ConditionalHashEquals(self.ReadString(Rom.CRCKey), other.ReadString(Rom.CRCKey))
|
||||
&& ConditionalHashEquals(self.ReadString(Rom.MD5Key), other.ReadString(Rom.MD5Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Rom.SHA1Key), other.ReadString(Rom.SHA1Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Rom.SHA256Key), other.ReadString(Rom.SHA256Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Rom.SHA384Key), other.ReadString(Rom.SHA384Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Rom.SHA512Key), other.ReadString(Rom.SHA512Key))
|
||||
&& ConditionalHashEquals(self.ReadString(Rom.SpamSumKey), other.ReadString(Rom.SpamSumKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if any hashes exist
|
||||
/// </summary>
|
||||
public static bool HasHashes(this Disk disk)
|
||||
{
|
||||
bool md5Null = string.IsNullOrWhiteSpace(disk.ReadString(Disk.MD5Key));
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(disk.ReadString(Disk.SHA1Key));
|
||||
|
||||
return !md5Null || !sha1Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if any hashes exist
|
||||
/// </summary>
|
||||
public static bool HasHashes(this Media media)
|
||||
{
|
||||
bool md5Null = string.IsNullOrWhiteSpace(media.ReadString(Media.MD5Key));
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(media.ReadString(Media.SHA1Key));
|
||||
bool sha256Null = string.IsNullOrWhiteSpace(media.ReadString(Media.SHA256Key));
|
||||
bool spamsumNull = string.IsNullOrWhiteSpace(media.ReadString(Media.SpamSumKey));
|
||||
|
||||
return !md5Null || !sha1Null || !sha256Null || !spamsumNull;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if any hashes exist
|
||||
/// </summary>
|
||||
public static bool HasHashes(this Rom rom)
|
||||
{
|
||||
bool crcNull = string.IsNullOrWhiteSpace(rom.ReadString(Rom.CRCKey));
|
||||
bool md5Null = string.IsNullOrWhiteSpace(rom.ReadString(Rom.MD5Key));
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(rom.ReadString(Rom.SHA1Key));
|
||||
bool sha256Null = string.IsNullOrWhiteSpace(rom.ReadString(Rom.SHA256Key));
|
||||
bool sha384Null = string.IsNullOrWhiteSpace(rom.ReadString(Rom.SHA384Key));
|
||||
bool sha512Null = string.IsNullOrWhiteSpace(rom.ReadString(Rom.SHA512Key));
|
||||
bool spamsumNull = string.IsNullOrWhiteSpace(rom.ReadString(Rom.SpamSumKey));
|
||||
|
||||
return !crcNull || !md5Null || !sha1Null || !sha256Null || !sha384Null || !sha512Null || !spamsumNull;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if all of the hashes are set to their 0-byte values or null
|
||||
/// </summary>
|
||||
public static bool HasZeroHash(this Disk disk)
|
||||
{
|
||||
string? md5 = disk.ReadString(Disk.MD5Key);
|
||||
bool md5Null = string.IsNullOrWhiteSpace(md5) || string.Equals(md5, Constants.MD5Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha1 = disk.ReadString(Disk.SHA1Key);
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(sha1) || string.Equals(sha1, Constants.SHA1Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return md5Null && sha1Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if all of the hashes are set to their 0-byte values or null
|
||||
/// </summary>
|
||||
public static bool HasZeroHash(this Media media)
|
||||
{
|
||||
string? md5 = media.ReadString(Media.MD5Key);
|
||||
bool md5Null = string.IsNullOrWhiteSpace(md5) || string.Equals(md5, Constants.MD5Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha1 = media.ReadString(Media.SHA1Key);
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(sha1) || string.Equals(sha1, Constants.SHA1Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha256 = media.ReadString(Media.SHA256Key);
|
||||
bool sha256Null = string.IsNullOrWhiteSpace(sha256) || string.Equals(sha256, Constants.SHA256Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? spamsum = media.ReadString(Media.SpamSumKey);
|
||||
bool spamsumNull = string.IsNullOrWhiteSpace(spamsum) || string.Equals(spamsum, Constants.SpamSumZero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return md5Null && sha1Null && sha256Null && spamsumNull;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if all of the hashes are set to their 0-byte values or null
|
||||
/// </summary>
|
||||
public static bool HasZeroHash(this Rom rom)
|
||||
{
|
||||
string? crc = rom.ReadString(Rom.CRCKey);
|
||||
bool crcNull = string.IsNullOrWhiteSpace(crc) || string.Equals(crc, Constants.CRCZero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? md5 = rom.ReadString(Rom.MD5Key);
|
||||
bool md5Null = string.IsNullOrWhiteSpace(md5) || string.Equals(md5, Constants.MD5Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha1 = rom.ReadString(Rom.SHA1Key);
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(sha1) || string.Equals(sha1, Constants.SHA1Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha256 = rom.ReadString(Rom.SHA256Key);
|
||||
bool sha256Null = string.IsNullOrWhiteSpace(sha256) || string.Equals(sha256, Constants.SHA256Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha384 = rom.ReadString(Rom.SHA384Key);
|
||||
bool sha384Null = string.IsNullOrWhiteSpace(sha384) || string.Equals(sha384, Constants.SHA384Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? sha512 = rom.ReadString(Rom.SHA512Key);
|
||||
bool sha512Null = string.IsNullOrWhiteSpace(sha512) || string.Equals(sha512, Constants.SHA512Zero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string? spamsum = rom.ReadString(Rom.SpamSumKey);
|
||||
bool spamsumNull = string.IsNullOrWhiteSpace(spamsum) || string.Equals(spamsum, Constants.SpamSumZero, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return crcNull && md5Null && sha1Null && sha256Null && sha384Null && sha512Null && spamsumNull;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if two hashes are equal for the purposes of merging
|
||||
/// </summary>
|
||||
private static bool ConditionalHashEquals(string? firstHash, string? secondHash)
|
||||
{
|
||||
// If either hash is empty, we say they're equal for merging
|
||||
if (string.IsNullOrWhiteSpace(firstHash) || string.IsNullOrWhiteSpace(secondHash))
|
||||
return true;
|
||||
|
||||
// If they're different sizes, they can't match
|
||||
if (firstHash!.Length != secondHash!.Length)
|
||||
return false;
|
||||
|
||||
// Otherwise, they need to match exactly
|
||||
return string.Equals(firstHash, secondHash, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there are no, non-empty hashes in common
|
||||
/// </summary>
|
||||
private static bool HasCommonHash(this Disk self, Disk other)
|
||||
{
|
||||
bool md5Null = string.IsNullOrWhiteSpace(self.ReadString(Disk.MD5Key));
|
||||
md5Null ^= string.IsNullOrWhiteSpace(other.ReadString(Disk.MD5Key));
|
||||
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(self.ReadString(Disk.SHA1Key));
|
||||
sha1Null ^= string.IsNullOrWhiteSpace(other.ReadString(Disk.SHA1Key));
|
||||
|
||||
return !md5Null || !sha1Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there are no, non-empty hashes in common
|
||||
/// </summary>
|
||||
private static bool HasCommonHash(this Media self, Media other)
|
||||
{
|
||||
bool md5Null = string.IsNullOrWhiteSpace(self.ReadString(Media.MD5Key));
|
||||
md5Null ^= string.IsNullOrWhiteSpace(other.ReadString(Media.MD5Key));
|
||||
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(self.ReadString(Media.SHA1Key));
|
||||
sha1Null ^= string.IsNullOrWhiteSpace(other.ReadString(Media.SHA1Key));
|
||||
|
||||
bool sha256Null = string.IsNullOrWhiteSpace(self.ReadString(Media.SHA256Key));
|
||||
sha256Null ^= string.IsNullOrWhiteSpace(other.ReadString(Media.SHA256Key));
|
||||
|
||||
bool spamsumNull = string.IsNullOrWhiteSpace(self.ReadString(Media.SpamSumKey));
|
||||
spamsumNull ^= string.IsNullOrWhiteSpace(other.ReadString(Media.SpamSumKey));
|
||||
|
||||
return !md5Null || !sha1Null || !sha256Null || !spamsumNull;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there are no, non-empty hashes in common
|
||||
/// </summary>
|
||||
private static bool HasCommonHash(this Rom self, Rom other)
|
||||
{
|
||||
bool crcNull = string.IsNullOrWhiteSpace(self.ReadString(Rom.CRCKey));
|
||||
crcNull ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.CRCKey));
|
||||
|
||||
bool md5Null = string.IsNullOrWhiteSpace(self.ReadString(Rom.MD5Key));
|
||||
md5Null ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.MD5Key));
|
||||
|
||||
bool sha1Null = string.IsNullOrWhiteSpace(self.ReadString(Rom.SHA1Key));
|
||||
sha1Null ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.SHA1Key));
|
||||
|
||||
bool sha256Null = string.IsNullOrWhiteSpace(self.ReadString(Rom.SHA256Key));
|
||||
sha256Null ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.SHA256Key));
|
||||
|
||||
bool sha384Null = string.IsNullOrWhiteSpace(self.ReadString(Rom.SHA384Key));
|
||||
sha384Null ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.SHA384Key));
|
||||
|
||||
bool sha512Null = string.IsNullOrWhiteSpace(self.ReadString(Rom.SHA512Key));
|
||||
sha512Null ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.SHA512Key));
|
||||
|
||||
bool spamsumNull = string.IsNullOrWhiteSpace(self.ReadString(Rom.SpamSumKey));
|
||||
spamsumNull ^= string.IsNullOrWhiteSpace(other.ReadString(Rom.SpamSumKey));
|
||||
|
||||
return !crcNull || !md5Null || !sha1Null || !sha256Null || !sha384Null || !sha512Null || !spamsumNull;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// TODO: Add DatItem conversion extensions here
|
||||
// TODO: Once done with the above, replace innards on each current model
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user