Add `.bps` patch applier
currently unused, like `.ips` patch applier; will return to hook up to loading (currently flagged as good dump if base was), write unit tests, and add a way to save patched rom to disk
This commit is contained in:
parent
eb13e46f45
commit
a8f26ccf08
|
@ -114,18 +114,29 @@ namespace BizHawk.Client.Common
|
|||
if (patch is null) return;
|
||||
using var patchFile = new HawkFile(patch);
|
||||
patchFile.BindFirstOf(".ips");
|
||||
if (!patchFile.IsBound) patchFile.BindFirstOf(".bps");
|
||||
if (!patchFile.IsBound) return;
|
||||
var patchBytes = patchFile.GetStream().ReadAllBytes();
|
||||
if (BPSPatcher.IsIPSFile(patchBytes))
|
||||
{
|
||||
RomData = BPSPatcher.Patch(RomData, new BPSPatcher.IPSPayload(patchBytes));
|
||||
}
|
||||
else if (BPSPatcher.IsBPSFile(patchBytes, out var patchStruct))
|
||||
{
|
||||
var ignoreBaseChecksum = true; //TODO check base checksum and ask user before continuing
|
||||
RomData = BPSPatcher.Patch(StripSNESDumpHeader(RomData), patchStruct, out var checksumsMatch);
|
||||
if (!checksumsMatch && !ignoreBaseChecksum) throw new Exception("BPS patch didn't produce the expected output");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("doesn't appear to be a BPS or IPS patch");
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>https://snes.nesdev.org/wiki/ROM_file_formats#Detecting_Headered_ROM</remarks>
|
||||
private static ReadOnlySpan<byte> StripSNESDumpHeader(ReadOnlySpan<byte> rom)
|
||||
=> rom.Length % 512 is 0 ? rom : rom.Slice(start: 512);
|
||||
|
||||
private static byte[] DeInterleaveSMD(byte[] source)
|
||||
{
|
||||
// SMD files are interleaved in pages of 16k, with the first 8k containing all
|
||||
|
|
|
@ -3,10 +3,117 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
using BizHawk.Common.BufferExtensions;
|
||||
using BizHawk.Common.CollectionExtensions;
|
||||
|
||||
namespace BizHawk.Common
|
||||
{
|
||||
public static class BPSPatcher
|
||||
{
|
||||
/// <remarks>
|
||||
/// constructor assumes valid header/footer<br/>
|
||||
/// https://github.com/Alcaro/Flips/blob/master/bps_spec.md
|
||||
/// </remarks>
|
||||
/// <seealso cref="IsBPSFile"/>
|
||||
public readonly ref struct BPSPayload
|
||||
{
|
||||
internal static readonly byte[] HEADER = Encoding.ASCII.GetBytes("BPS1");
|
||||
|
||||
public static int DecodeVarInt(ReadOnlySpan<byte> data, ref int i)
|
||||
{
|
||||
// no idea how this works or what it will do if a varint is too large, I just copied from the spec --yoshi
|
||||
// also casting to int (for use with Span) so the max is even smaller
|
||||
ulong num = 0, mul = 1;
|
||||
while (true)
|
||||
{
|
||||
var x = data[i++];
|
||||
num += (x & 0b0111_1111UL) * mul;
|
||||
if ((x & 0b1000_0000UL) is not 0UL) break;
|
||||
mul <<= 7;
|
||||
num += mul;
|
||||
}
|
||||
return (int) num;
|
||||
}
|
||||
|
||||
private readonly ReadOnlySpan<byte> _data;
|
||||
|
||||
private readonly bool _isValid;
|
||||
|
||||
private readonly ReadOnlySpan<byte> _sourceChecksum;
|
||||
|
||||
private readonly int _sourceSize;
|
||||
|
||||
private readonly ReadOnlySpan<byte> _targetChecksum;
|
||||
|
||||
public readonly int TargetSize;
|
||||
|
||||
internal readonly ReadOnlySpan<byte> PatchChecksum;
|
||||
|
||||
/// <remarks>assumes valid header/footer</remarks>
|
||||
/// <seealso cref="IsBPSFile"/>
|
||||
public BPSPayload(ReadOnlySpan<byte> dataWithHeader)
|
||||
{
|
||||
_isValid = true;
|
||||
var i = 4;
|
||||
_sourceSize = DecodeVarInt(dataWithHeader, ref i);
|
||||
TargetSize = DecodeVarInt(dataWithHeader, ref i);
|
||||
var metadataSize = DecodeVarInt(dataWithHeader, ref i);
|
||||
if (metadataSize is not 0)
|
||||
{
|
||||
Console.WriteLine($"ignoring {metadataSize} bytes of .bps metadata");
|
||||
i += metadataSize;
|
||||
}
|
||||
_data = dataWithHeader.Slice(start: i, length: dataWithHeader.Length - 12 - i);
|
||||
_sourceChecksum = dataWithHeader.Slice(start: dataWithHeader.Length - 12, length: 4);
|
||||
_targetChecksum = dataWithHeader.Slice(start: dataWithHeader.Length - 8, length: 4);
|
||||
PatchChecksum = dataWithHeader.Slice(start: dataWithHeader.Length - 4, length: 4);
|
||||
}
|
||||
|
||||
/// <returns><see langword="true"/> iff checksums of base rom and result both matched</returns>
|
||||
public bool DoPatch(ReadOnlySpan<byte> baseRom, Span<byte> target)
|
||||
{
|
||||
if (!_isValid) throw new InvalidOperationException(ERR_MSG_UNINIT);
|
||||
if (target.Length != TargetSize) throw new ArgumentException(message: $"target buffer too {(target.Length < TargetSize ? "small" : "large")}", paramName: nameof(target));
|
||||
if (baseRom.Length != _sourceSize) throw new ArgumentException(message: $"target buffer too {(baseRom.Length < _sourceSize ? "small" : "large")}", paramName: nameof(baseRom));
|
||||
var checksumsMatch = CheckCRC(data: baseRom, reversedChecksum: _sourceChecksum);
|
||||
var outputOffset = 0;
|
||||
var sourceRelOffset = 0;
|
||||
var targetRelOffset = 0;
|
||||
var i = 0;
|
||||
while (i < _data.Length)
|
||||
{
|
||||
var actionAndLength = DecodeVarInt(_data, ref i);
|
||||
var length = (actionAndLength >> 2) + 1;
|
||||
switch (actionAndLength & 0b11)
|
||||
{
|
||||
case 0b00: // SourceRead
|
||||
while (length-- is not 0)
|
||||
{
|
||||
target[outputOffset] = baseRom[outputOffset];
|
||||
outputOffset++;
|
||||
}
|
||||
break;
|
||||
case 0b01: // TargetRead
|
||||
while (length-- is not 0) target[outputOffset++] = _data[i++];
|
||||
break;
|
||||
case 0b10: // SourceCopy
|
||||
var offset = DecodeVarInt(_data, ref i);
|
||||
if ((offset & 1) is 0) sourceRelOffset += offset >> 1;
|
||||
else sourceRelOffset -= offset >> 1;
|
||||
while (length-- is not 0) target[outputOffset++] = baseRom[sourceRelOffset++];
|
||||
break;
|
||||
case 0b11: // TargetCopy
|
||||
var offset1 = DecodeVarInt(_data, ref i);
|
||||
if ((offset1 & 1) is 0) targetRelOffset += offset1 >> 1;
|
||||
else targetRelOffset -= offset1 >> 1;
|
||||
while (length-- is not 0) target[outputOffset++] = target[targetRelOffset++];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return checksumsMatch && CheckCRC(data: target, reversedChecksum: _targetChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// constructor assumes valid header/footer<br/>
|
||||
/// https://zerosoft.zophar.net/ips.php
|
||||
|
@ -100,6 +207,22 @@ namespace BizHawk.Common
|
|||
|
||||
private const string ERR_MSG_UNINIT = "uninitialised struct";
|
||||
|
||||
private static bool CheckCRC(ReadOnlySpan<byte> data, ReadOnlySpan<byte> reversedChecksum)
|
||||
=> ((ReadOnlySpan<byte>) CRC32Checksum.Compute(data)).ReversedSequenceEqual(reversedChecksum);
|
||||
|
||||
public static bool IsBPSFile(ReadOnlySpan<byte> dataWithHeader, out BPSPayload patchStruct)
|
||||
{
|
||||
patchStruct = default;
|
||||
const int MIN_VALID_BPS_SIZE = 20;
|
||||
if (dataWithHeader.Length < MIN_VALID_BPS_SIZE
|
||||
|| !dataWithHeader.Slice(start: 0, length: 4).SequenceEqual(BPSPayload.HEADER))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
patchStruct = new(dataWithHeader);
|
||||
return CheckCRC(data: dataWithHeader.Slice(start: 0, length: dataWithHeader.Length - 4), reversedChecksum: patchStruct.PatchChecksum);
|
||||
}
|
||||
|
||||
public static bool IsIPSFile(ReadOnlySpan<byte> dataWithHeader)
|
||||
{
|
||||
const int MIN_VALID_IPS_SIZE = 8;
|
||||
|
@ -108,6 +231,14 @@ namespace BizHawk.Common
|
|||
&& dataWithHeader.Slice(start: dataWithHeader.Length - 3, length: 3).SequenceEqual(IPSPayload.FOOTER);
|
||||
}
|
||||
|
||||
/// <remarks>always allocates a new array</remarks>
|
||||
public static byte[] Patch(ReadOnlySpan<byte> baseRom, BPSPayload patch, out bool checksumsMatch)
|
||||
{
|
||||
var target = new byte[patch.TargetSize];
|
||||
checksumsMatch = patch.DoPatch(baseRom: baseRom, target: target);
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <remarks>may patch in place, returning <paramref name="baseRom"/>, or allocate a new array</remarks>
|
||||
public static byte[] Patch(byte[] baseRom, IPSPayload patch)
|
||||
{
|
||||
|
|
|
@ -223,6 +223,21 @@ namespace BizHawk.Common.CollectionExtensions
|
|||
private static T ReturnSelf<T>(this T self)
|
||||
=> self;
|
||||
|
||||
public static bool ReversedSequenceEqual<T>(this ReadOnlySpan<T> a, ReadOnlySpan<T> b)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
var len = a.Length;
|
||||
if (len != b.Length) return false;
|
||||
if (len is 0) return true;
|
||||
var i = 0;
|
||||
while (i < len)
|
||||
{
|
||||
if (!a[i].Equals(b[len - 1 - i])) return false;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsSortedAsc<T>(this IReadOnlyList<T> list)
|
||||
where T : IComparable<T>
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue