233 lines
6.8 KiB
C#
233 lines
6.8 KiB
C#
#nullable enable
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using BizHawk.Common;
|
|
using BizHawk.Common.BufferExtensions;
|
|
using BizHawk.Common.CollectionExtensions;
|
|
|
|
namespace BizHawk.Emulation.DiscSystem
|
|
{
|
|
public class DiscHasher
|
|
{
|
|
public DiscHasher(Disc disc)
|
|
{
|
|
this.disc = disc;
|
|
}
|
|
|
|
private readonly Disc disc;
|
|
|
|
public string CalculateBizHash(DiscType discType)
|
|
{
|
|
return discType switch
|
|
{
|
|
DiscType.SonyPSX => Calculate_PSX_BizIDHash(),
|
|
DiscType.JaguarCD => CalculateRAJaguarHash() ?? "",
|
|
_ => OldHash(),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// calculates the hash for quick PSX Disc identification
|
|
/// </summary>
|
|
public string Calculate_PSX_BizIDHash()
|
|
{
|
|
//notes about the hash:
|
|
//"Arc the Lad II (J) 1.0 and 1.1 conflict up to 25 sectors (so use 26)
|
|
//Tekken 3 (Europe) (Alt) and Tekken 3 (Europe) conflict in track 2 and 3 unfortunately, not sure what to do about this yet
|
|
//the TOC isn't needed!
|
|
//but it will help detect dumps with mangled TOCs which are all too common
|
|
|
|
CRC32 crc = new();
|
|
var buffer2352 = new byte[2352];
|
|
|
|
var dsr = new DiscSectorReader(disc)
|
|
{
|
|
Policy = { DeterministicClearBuffer = false }, // live dangerously
|
|
};
|
|
|
|
//hash the TOC
|
|
static void AddAsBytesTo(CRC32 crc32, int i)
|
|
=> crc32.Add(BitConverter.GetBytes(i));
|
|
|
|
AddAsBytesTo(crc, (int)disc.TOC.SessionFormat);
|
|
AddAsBytesTo(crc, disc.TOC.FirstRecordedTrackNumber);
|
|
AddAsBytesTo(crc, disc.TOC.LastRecordedTrackNumber);
|
|
for (var i = 1; i <= 100; i++)
|
|
{
|
|
//if (disc.TOC.TOCItems[i].Exists) Console.WriteLine("{0:X8} {1:X2} {2:X2} {3:X8}", crc.Current, (int)disc.TOC.TOCItems[i].Control, disc.TOC.TOCItems[i].Exists ? 1 : 0, disc.TOC.TOCItems[i].LBATimestamp.Sector); //a little debugging
|
|
AddAsBytesTo(crc, (int)disc.TOC.TOCItems[i].Control);
|
|
AddAsBytesTo(crc, disc.TOC.TOCItems[i].Exists ? 1 : 0);
|
|
AddAsBytesTo(crc, disc.TOC.TOCItems[i].LBA);
|
|
}
|
|
|
|
//hash first 26 sectors
|
|
for (var i = 0; i < 26; i++)
|
|
{
|
|
dsr.ReadLBA_2352(i, buffer2352, 0);
|
|
crc.Add(buffer2352);
|
|
}
|
|
|
|
return CRC32Checksum.BytesAsDigest(crc.Result).BytesToHexString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// calculates the complete disc hash for matching to a redump
|
|
/// </summary>
|
|
public uint Calculate_PSX_RedumpHash()
|
|
{
|
|
CRC32 crc = new();
|
|
var buffer2352 = new byte[2352];
|
|
|
|
var dsr = new DiscSectorReader(disc)
|
|
{
|
|
Policy = { DeterministicClearBuffer = false }, // live dangerously
|
|
};
|
|
|
|
|
|
//read all sectors for redump hash
|
|
for (var i = 0; i < disc.Session1.LeadoutLBA; i++)
|
|
{
|
|
dsr.ReadLBA_2352(i, buffer2352, 0);
|
|
crc.Add(buffer2352);
|
|
}
|
|
|
|
return crc.Result;
|
|
}
|
|
|
|
// gets an identifying hash. hashes the first 512 sectors of
|
|
// the first data track on the disc.
|
|
//TODO - this is a very platform-specific thing. hashing the TOC may be faster and be just as effective. so, rename it appropriately
|
|
public string OldHash()
|
|
{
|
|
var buffer = new byte[512 * 2352];
|
|
var dsr = new DiscSectorReader(disc);
|
|
// don't hash generated lead-in and lead-out tracks
|
|
for (int i = 1; i <= disc.Session1.InformationTrackCount; i++)
|
|
{
|
|
var track = disc.Session1.Tracks[i];
|
|
|
|
if (track.IsAudio)
|
|
continue;
|
|
|
|
var lba_len = Math.Min(track.NextTrack.LBA - track.LBA, 512);
|
|
for (var s = 0; s < 512 && s < lba_len; s++)
|
|
dsr.ReadLBA_2352(track.LBA + s, buffer, s * 2352);
|
|
|
|
return MD5Checksum.ComputeDigestHex(buffer.AsSpan(start: 0, length: lba_len * 2352));
|
|
}
|
|
return "no data track found";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate Jaguar CD hash according to RetroAchievements logic
|
|
/// </summary>
|
|
public string? CalculateRAJaguarHash()
|
|
{
|
|
if (disc.Sessions.Count <= 2)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var dsr = new DiscSectorReader(disc)
|
|
{
|
|
Policy = { DeterministicClearBuffer = false }, // let's make this a little faster
|
|
};
|
|
|
|
static string? HashJaguar(DiscTrack bootTrack, DiscSectorReader dsr, bool commonHomebrewHash)
|
|
{
|
|
const string _jaguarHeader = "ATARI APPROVED DATA HEADER ATRI";
|
|
const string _jaguarBSHeader = "TARA IPARPVODED TA AEHDAREA RT";
|
|
List<ArraySegment<byte>> bitsToHash = new();
|
|
var buf2352 = new byte[2352];
|
|
|
|
// find the boot track header
|
|
// see https://github.com/TASEmulators/BizHawk/blob/f29113287e88c6a644dbff30f92a9833307aad20/waterbox/virtualjaguar/src/cdhle.cpp#L109-L145
|
|
var startLba = bootTrack.LBA;
|
|
var numLbas = bootTrack.NextTrack.LBA - bootTrack.LBA;
|
|
int bootLen = 0, bootLba = 0, bootOff = 0;
|
|
bool byteswapped = false, foundHeader = false;
|
|
var bootLenOffset = (commonHomebrewHash ? 0x40 : 0) + 32 + 4;
|
|
for (var i = 0; i < numLbas; i++)
|
|
{
|
|
dsr.ReadLBA_2352(startLba + i, buf2352, 0);
|
|
|
|
for (var j = 0; j < 2352 - bootLenOffset - 4; j++)
|
|
{
|
|
if (buf2352[j] == _jaguarHeader[0])
|
|
{
|
|
if (_jaguarHeader == Encoding.ASCII.GetString(buf2352, j, 32 - 1))
|
|
{
|
|
bootLen = BinaryPrimitives.ReadInt32BigEndian(buf2352.AsSpan(start: bootLenOffset + j));
|
|
bootLba = startLba + i;
|
|
bootOff = j + bootLenOffset + 4;
|
|
// byteswapped = false;
|
|
foundHeader = true;
|
|
break;
|
|
}
|
|
}
|
|
else if (buf2352[j] == _jaguarBSHeader[0])
|
|
{
|
|
if (_jaguarBSHeader == Encoding.ASCII.GetString(buf2352, j, 32 - 2))
|
|
{
|
|
var slice = buf2352.AsSpan(start: bootLenOffset + j, length: sizeof(int)).ToArray();
|
|
EndiannessUtils.MutatingByteSwap16(slice);
|
|
bootLen = BinaryPrimitives.ReadInt32BigEndian(slice);
|
|
bootLba = startLba + i;
|
|
bootOff = j + bootLenOffset + 4;
|
|
byteswapped = true;
|
|
foundHeader = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundHeader)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundHeader)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
dsr.ReadLBA_2352(bootLba++, buf2352, 0);
|
|
|
|
if (byteswapped)
|
|
{
|
|
EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan());
|
|
}
|
|
|
|
bitsToHash.Add(new(buf2352, offset: bootOff, count: Math.Min(2352 - bootOff, bootLen)));
|
|
bootLen -= 2352 - bootOff;
|
|
|
|
while (bootLen > 0)
|
|
{
|
|
dsr.ReadLBA_2352(bootLba++, buf2352, 0);
|
|
|
|
if (byteswapped)
|
|
{
|
|
EndiannessUtils.MutatingByteSwap16(buf2352.AsSpan());
|
|
}
|
|
|
|
bitsToHash.Add(new(buf2352, offset: 0, count: Math.Min(2352, bootLen)));
|
|
bootLen -= 2352;
|
|
}
|
|
|
|
return MD5Checksum.ComputeDigestHex(CollectionExtensions.ConcatArrays(bitsToHash));
|
|
}
|
|
|
|
var jaguarHash = HashJaguar(disc.Sessions[2].Tracks[1], dsr, false);
|
|
|
|
if (jaguarHash is "254487B59AB21BC005338E85CBF9FD2F") // see https://github.com/RetroAchievements/rcheevos/pull/234
|
|
{
|
|
jaguarHash = HashJaguar(disc.Sessions[1].Tracks[2], dsr, true);
|
|
}
|
|
|
|
return jaguarHash;
|
|
}
|
|
}
|
|
} |