From eb13e46f457579b7e718858ee64c0cb9c31ff0ae Mon Sep 17 00:00:00 2001 From: YoshiRulz Date: Wed, 15 Mar 2023 10:17:00 +1000 Subject: [PATCH] Refactor `.ips` patch applier `RomGame` is never initialised with a non-null `patch` so this was and is unused --- src/BizHawk.Client.Common/IPS.cs | 75 --------------- src/BizHawk.Client.Common/RomGame.cs | 19 ++-- src/BizHawk.Common/BPSPatcher.cs | 137 +++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 82 deletions(-) delete mode 100644 src/BizHawk.Client.Common/IPS.cs create mode 100644 src/BizHawk.Common/BPSPatcher.cs diff --git a/src/BizHawk.Client.Common/IPS.cs b/src/BizHawk.Client.Common/IPS.cs deleted file mode 100644 index b1a8e76491..0000000000 --- a/src/BizHawk.Client.Common/IPS.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.IO; - -namespace BizHawk.Client.Common -{ - public static class IPS - { - public static byte[] Patch(byte[] rom, Stream patch) - { - var ipsHeader = new byte[5]; - patch.Read(ipsHeader, 0, 5); - - const string header = "PATCH"; - for (int i = 0; i < 5; i++) - { - if (ipsHeader[i] != header[i]) - { - Console.WriteLine("Patch file specified is invalid."); - return null; - } - } - - // header verified, loop over patch entries - const uint eof = ('E' * 0x10000) + ('O' * 0x100) + 'F'; - - var ret = new MemoryStream(rom.Length); - ret.Write(rom, 0, rom.Length); - - while (true) - { - uint offset = Read24(patch); - if (offset == eof) - { - return ret.ToArray(); - } - - ushort size = Read16(patch); - - ret.Seek(offset, SeekOrigin.Begin); - - if (size != 0) // non-RLE patch - { - var patchData = new byte[size]; - patch.Read(patchData, 0, size); - - ret.Write(patchData, 0, patchData.Length); - } - else // RLE patch - { - size = Read16(patch); - byte value = (byte)patch.ReadByte(); - for (int i = 0; i < size; i++) - { - ret.WriteByte(value); - } - } - } - } - - private static ushort Read16(Stream patch) - { - int upper = patch.ReadByte(); - int lower = patch.ReadByte(); - return (ushort)((upper * 0x100) + lower); - } - - private static uint Read24(Stream patch) - { - int upper = patch.ReadByte(); - int middle = patch.ReadByte(); - int lower = patch.ReadByte(); - return (uint)((upper * 0x10000) + (middle * 0x100) + lower); - } - } -} \ No newline at end of file diff --git a/src/BizHawk.Client.Common/RomGame.cs b/src/BizHawk.Client.Common/RomGame.cs index 4a52eadc68..48b4dbee24 100644 --- a/src/BizHawk.Client.Common/RomGame.cs +++ b/src/BizHawk.Client.Common/RomGame.cs @@ -2,6 +2,7 @@ using System.Globalization; using BizHawk.Common; +using BizHawk.Common.IOExtensions; using BizHawk.Common.NumberExtensions; using BizHawk.Emulation.Common; @@ -110,14 +111,18 @@ namespace BizHawk.Client.Common CheckForPatchOptions(); - if (patch != null) + if (patch is null) return; + using var patchFile = new HawkFile(patch); + patchFile.BindFirstOf(".ips"); + if (!patchFile.IsBound) return; + var patchBytes = patchFile.GetStream().ReadAllBytes(); + if (BPSPatcher.IsIPSFile(patchBytes)) { - using var patchFile = new HawkFile(patch); - patchFile.BindFirstOf(".ips"); - if (patchFile.IsBound) - { - RomData = IPS.Patch(RomData, patchFile.GetStream()); - } + RomData = BPSPatcher.Patch(RomData, new BPSPatcher.IPSPayload(patchBytes)); + } + else + { + throw new Exception("doesn't appear to be a BPS or IPS patch"); } } diff --git a/src/BizHawk.Common/BPSPatcher.cs b/src/BizHawk.Common/BPSPatcher.cs new file mode 100644 index 0000000000..5486029abc --- /dev/null +++ b/src/BizHawk.Common/BPSPatcher.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace BizHawk.Common +{ + public static class BPSPatcher + { + /// + /// constructor assumes valid header/footer
+ /// https://zerosoft.zophar.net/ips.php + ///
+ /// + public ref struct IPSPayload + { + internal static readonly byte[] FOOTER = Encoding.ASCII.GetBytes("EOF"); + + internal static readonly byte[] HEADER = Encoding.ASCII.GetBytes("PATCH"); + + internal static void CheckRomSize(ReadOnlySpan rom) + { + const int MAX_BASE_ROM_LENGTH = 0x1000000; // linked spec says 0xFFFFFF bits [sic] but that makes no sense + if (MAX_BASE_ROM_LENGTH < rom.Length) + { +#if true // it can patch the start of the file just fine, no need to throw here + Console.WriteLine("warning: IPS uses 24-bit offsets, so it can only index the first 0x1000000 octets of this rom"); +#else + throw new ArgumentException(message: "IPS can't patch files this big", paramName: nameof(rom)); +#endif + } + } + + private static IReadOnlyList<(int PatchOffset, int TargetOffset, int Size, bool IsRLE)> ParseRecords(ReadOnlySpan data) + { + List<(int PatchOffset, int TargetOffset, int Size, bool IsRLE)> records = new(); + try + { + var i = 0; + while (i != data.Length) + { + var targetOffset = (data[i++] * 0x10000) | (data[i++] * 0x100) | data[i++]; + var size = (data[i++] * 0x100) | data[i++]; + if (size is 0) + { + var rleSize = (data[i++] * 0x100) | data[i++]; + Debug.Assert(rleSize is not 0); + records.Add((i, targetOffset, rleSize, true)); + i++; + } + else + { + records.Add((i, targetOffset, size, false)); + i += size; + } + } + } + catch (ArgumentOutOfRangeException e) + { + throw new Exception("unexpected EOF in IPS patch", e); + } + records.Sort((a, b) => (a.TargetOffset + a.Size).CompareTo(b.TargetOffset + b.Size)); + return records; + } + + private readonly ReadOnlySpan _data; + + private readonly bool _isValid; + + private IReadOnlyList<(int PatchOffset, int TargetOffset, int Size, bool IsRLE)>? _records; + + internal IReadOnlyList<(int PatchOffset, int TargetOffset, int Size, bool IsRLE)> Records + => _isValid ? (_records ??= ParseRecords(_data)) : throw new InvalidOperationException(ERR_MSG_UNINIT); + + /// assumes valid header/footer + /// + public IPSPayload(ReadOnlySpan dataWithHeader) + { + _data = dataWithHeader.Slice(start: 5, length: dataWithHeader.Length - 8); + _isValid = true; + _records = null; + } + + internal void DoPatch(Span rom) + { + foreach (var (patchOffset, targetOffset, size, isRLE) in Records) + { + if (isRLE) + { + var value = _data[patchOffset]; + for (int j = targetOffset, endExclusive = j + size; j < endExclusive; j++) rom[j] = value; + } + else + { + for (var j = 0; j < size; j++) rom[targetOffset + j] = _data[patchOffset + j]; + } + } + } + } + + private const string ERR_MSG_UNINIT = "uninitialised struct"; + + public static bool IsIPSFile(ReadOnlySpan dataWithHeader) + { + const int MIN_VALID_IPS_SIZE = 8; + return MIN_VALID_IPS_SIZE <= dataWithHeader.Length + && dataWithHeader.Slice(start: 0, length: 5).SequenceEqual(IPSPayload.HEADER) + && dataWithHeader.Slice(start: dataWithHeader.Length - 3, length: 3).SequenceEqual(IPSPayload.FOOTER); + } + + /// may patch in place, returning , or allocate a new array + public static byte[] Patch(byte[] baseRom, IPSPayload patch) + { + var rom = baseRom; + var last = patch.Records[patch.Records.Count - 1]; + var reqSize = last.TargetOffset + last.Size; + if (baseRom.Length < reqSize) + { + rom = new byte[reqSize]; + Array.Copy(sourceArray: baseRom, destinationArray: rom, length: baseRom.Length); + } + IPSPayload.CheckRomSize(rom); + patch.DoPatch(rom); + return rom; + } + + /// is this even useful? + public static bool TryPatchInPlace(Span rom, IPSPayload patch) + { + IPSPayload.CheckRomSize(rom); + var last = patch.Records[patch.Records.Count - 1]; + if (rom.Length < last.TargetOffset + last.Size) return false; + patch.DoPatch(rom); + return true; + } + } +}