diff --git a/MassEffectModManagerCore/modmanager/me3tweaks/JPatch.cs b/MassEffectModManagerCore/modmanager/me3tweaks/JPatch.cs index a99524cf3..37aba98a8 100644 --- a/MassEffectModManagerCore/modmanager/me3tweaks/JPatch.cs +++ b/MassEffectModManagerCore/modmanager/me3tweaks/JPatch.cs @@ -1,124 +1,247 @@ -using MassEffectModManagerCore.modmanager.helpers; +//#define JPATCH_DEBUG +// Uncomment the above to print debug output, same as -v on the jdiff.exe program (0.8.4) + +/* + A C# Implementation of JoJoDiff's JPatch functionality. + Supports 0.8.4 and below diff files. + Copyright (C) 2019-2020 Mgamerz + + Ported from the original implementation by Joris Heirbaut: + http://jojodiff.sourceforge.net/ + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; namespace MassEffectModManagerCore.modmanager.me3tweaks { - [Localizable(false)] - /// - /// A C# Implemention of JoJoDiff's JPatch functionality + /// + [Localizable(false)] public class JPatch { +#if JPATCH_DEBUG public static void DebugTest() { - string sourcefile = @"C:\Users\Mgamerz\Desktop\jdiff-cs\Patch_SFXPawn_Banshee.pcc"; - string patchfile = @"C:\Users\Mgamerz\Desktop\jdiff-cs\bansheeattackspets.jsf"; + // For debugging + + var rootpath = @"C:\Users\Mgamerz\source\repos\Mgamerz\BuildTools\ME3Tweaks\Generic\PatchBuilder"; + var sourcefile = Path.Combine(rootpath, "BioP_MPBrdg-orig.pcc"); + //string oldpatch = Path.Combine(rootpath, "oldpatch"); + string newpatch = Path.Combine(rootpath, "patch"); + var destFile = Path.Combine(rootpath, "BioP_MPBrdg.pcc"); + var destMd5 = Utilities.CalculateMD5(destFile); + var destSize = new FileInfo(destFile).Length; using FileStream sourceStream = new FileStream(sourcefile, FileMode.Open); - using FileStream patchStream = new FileStream(patchfile, FileMode.Open); + //using FileStream oldPatchStream = new FileStream(oldpatch, FileMode.Open); + using FileStream newPatchStream = new FileStream(newpatch, FileMode.Open); MemoryStream outStream = new MemoryStream(); - ApplyJPatch(sourceStream, patchStream, outStream); - File.WriteAllBytes(@"C:\Users\Mgamerz\Desktop\jdiff-cs\Patch_SFXPawn_Banshee-patched-cs.pcc", outStream.ToArray()); + //ApplyJPatch(sourceStream, oldPatchStream, outStream); + //var oldCalcedMd5 = Utilities.CalculateMD5(outStream); + //if (destMd5 != oldCalcedMd5) + //{ + // Debug.WriteLine("Old jpatch failed! Wrong MD5"); + //} + + sourceStream.Position = 0; + outStream = new MemoryStream(); + var result = ApplyJPatch(sourceStream, newPatchStream, outStream); + + if (outStream.Length != destSize) + { + Debug.WriteLine($"Wrong new patch size! Should be {destSize} however we got {outStream.Length}"); + } + + var newCalcedMd5 = Utilities.CalculateMD5(outStream); + if (destMd5 != newCalcedMd5) + { + Debug.WriteLine("New jpatch failed! Wrong MD5"); + } } +#endif + + /// + /// Opcodes used by JoJoDiff + /// private enum JojoOpcode { - OPERATION_ESC = 0xA7, //Opening opcoede - OPERATION_MOD = 0xA6, //Overwrite bytes - OPERATION_INS = 0xA5, //Insert bytes - OPERATION_DEL = 0xA4, //Delete bytes - OPERATION_EQL = 0xA3, //copy data (equal) - OPERATION_BKT = 0xA2 //backtrack from current position + /// + /// Opening opcode that precedes other opcodes (unless followed by another ESC, in which case it deduces to a single ESC) + /// + OPERATION_ESC = 0xA7, + /// + /// Overwrite bytes (by writing from patch and advancing source position with it) + /// + OPERATION_MOD = 0xA6, + /// + /// Insert new byte (by writing new bytes without advancing the source) + /// + OPERATION_INS = 0xA5, + /// + /// Delete bytes (by skipping it in the source) + /// + OPERATION_DEL = 0xA4, + /// + /// Copy bytes (by copying directly from source to output) + /// + OPERATION_EQL = 0xA3, + /// + /// Backtrace source (move the source position backwards) + /// + OPERATION_BKT = 0xA2 } - public static void ApplyJPatch(Stream sourceData, Stream patchData, Stream outData) + + /// + /// End of file (-1) + /// + private const int EOF = -1; + + /// + /// Apply a JPatch to a stream and put the result in another stream. + /// + /// The source stream to patch against + /// The patch file stream + /// The resulting patch output + /// True if successful, false otherwise + public static bool ApplyJPatch(Stream sourceData, Stream patchData, Stream outData) { //all positions should be at 0. sourceData.Position = 0; patchData.Position = 0; outData.Position = 0; - int readChar; + int readByte = 0; + int peekChar1; + int peekChar2; //Read patch data. - while ((readChar = patchData.ReadByte()) != -1) + while (readByte != EOF) // I don't think this condition will occur unless patch is malformed { - if (Enum.TryParse(readChar.ToString(), out var opcode)) + // Read operator from input, unless this has already been done + if (readByte == 0) { - //if (previousItemWasEsc && opcode == JojoOpcode.OPERATION_ESC) - //{ - // //it's esc esc, essentially escaping itself. - // isEScEsc = true; - //} - //else - //{ - //is opcode - //isEScEsc = false; - //currentOpcode = opcode; - //hasCurrentOpcode = true; - //} - switch (opcode) + peekChar1 = patchData.ReadByte(); + if (peekChar1 == EOF) + break; // We have hit the end of the file + + if (peekChar1 == (int)JojoOpcode.OPERATION_ESC) + { + peekChar2 = patchData.ReadByte(); + switch (peekChar2) + { + case (int)JojoOpcode.OPERATION_EQL: + case (int)JojoOpcode.OPERATION_DEL: + case (int)JojoOpcode.OPERATION_BKT: + case (int)JojoOpcode.OPERATION_MOD: + case (int)JojoOpcode.OPERATION_INS: + readByte = peekChar2; + peekChar2 = EOF; + peekChar1 = EOF; + break; + case EOF: + // FAILED, EOF + Debug.WriteLine("Unexpected end of patch file!"); + return false; + default: + // ESC xxx or ESC ESC at the start of a sequence + // Resolve by double pending bytes: peekChar1 and peekChar2 + readByte = (int)JojoOpcode.OPERATION_MOD; + break; + } + } + else { - case JojoOpcode.OPERATION_MOD: - //Overwrite data - //Debug.WriteLine("Opcode MOD at 0x" + (patchData.Position - 1).ToString("X6")); - processModInsOpcode(sourceData, patchData, outData, true); - break; - case JojoOpcode.OPERATION_INS: - //Insert new data - //Debug.WriteLine("Opcode INS at 0x" + (patchData.Position - 1).ToString("X6")); - processModInsOpcode(sourceData, patchData, outData, false); - break; - case JojoOpcode.OPERATION_EQL: - //Equal, move pointers forward - //Debug.WriteLine("Opcode EQL at 0x" + (patchData.Position - 1).ToString("X6")); - processEqlBktOpcode(sourceData, patchData, outData, false); - break; - case JojoOpcode.OPERATION_BKT: - //Backtrace, move backwards - //Debug.WriteLine("Opcode BKT at 0x" + (patchData.Position - 1).ToString("X6")); - processEqlBktOpcode(sourceData, patchData, outData, true); - break; - case JojoOpcode.OPERATION_DEL: - //Backtrace, move backwards - //Debug.WriteLine("Opcode DEL at 0x" + (patchData.Position - 1).ToString("X6")); - processDel(sourceData, patchData, outData); - break; - case JojoOpcode.OPERATION_ESC: - continue; //this is not actually an opcode. Skip it. - default: - Debug.WriteLine("Unsupported opcode currently: " + opcode.ToString()); - break; + // If an ESC is missing, set default operator (gaining two bytes) + readByte = (int)JojoOpcode.OPERATION_MOD; + peekChar2 = EOF; } } else { - Debug.WriteLine("Invalid patch data. Found unexpected byte that is not an opcode: " + readChar.ToString("X2")); + peekChar1 = EOF; // only needed when switching between MOD and INS + peekChar2 = EOF; } - } - if (sourceData.Position != sourceData.Length) - { - Debug.WriteLine($"Not at end of source data. Len: 0x{sourceData.Length:X6} Pos: 0x{sourceData.Position:X6}"); + //Perform operations + switch (readByte) + { + case (int)JojoOpcode.OPERATION_MOD: + //Overwrite data + readByte = processModInsOpcode(sourceData, patchData, outData, true, peekChar1, peekChar2); + break; + case (int)JojoOpcode.OPERATION_INS: + //Insert new data + readByte = processModInsOpcode(sourceData, patchData, outData, false, peekChar1, peekChar2); + break; + case (int)JojoOpcode.OPERATION_EQL: + //Equal, move pointers forward + processEqlBktOpcode(sourceData, patchData, outData, false); + readByte = 0; + break; + case (int)JojoOpcode.OPERATION_BKT: + //Backtrace, move backwards + processEqlBktOpcode(sourceData, patchData, outData, true); + readByte = 0; + break; + case (int)JojoOpcode.OPERATION_DEL: + //Delete, move the source data position forward without writing anything out + processDel(sourceData, patchData); + readByte = 0; // Makes next op read fully (ESC 0xA7) + break; + default: + Debug.WriteLine("Unsupported opcode: " + readByte.ToString("X2")); + break; + } } - else +#if JPATCH_DEBUG + Debug.WriteLine($"\t{sourceData.Position}\t{outData.Position}\tEOF"); +#endif + if (patchData.Position != patchData.Length) { - //Debug.WriteLine("At end of source data. OK"); + Debug.WriteLine("didn't read until end of patch file!"); } + + return true; //OK } - private static void processDel(Stream sourceData, Stream patchData, Stream outData) + /// + /// Deletes data from the source stream by advancing the source data position without writing it to the output stream. + /// + /// The sourcedata stream + /// The patchdata stream + private static void processDel(Stream sourceData, Stream patchData) { //Delete data from source. Essentially, skip the data. var len = getJLength(patchData); +#if JPATCH_DEBUG + Debug.WriteLine($"\t{sourceData.Position}\t{outData.Position}\tDEL\t{len}"); +#endif sourceData.Position += len; } + /// + /// Reads a length value from the patch stream + /// + /// The patch stream/param> + /// A length value, up to 2GiB private static int getJLength(Stream patchData) { /*From Jojo diff source documentation: @@ -129,7 +252,9 @@ private static int getJLength(Stream patchData) * 508 <= x < 0x10000 3 bytes: 253, xx * 0x10000 <= x < 0x100000000 5 bytes: 254, xxxx * 9 bytes: 255, xxxxxxxx - * + * + * Length will never be zero as that makes no sense, you would never advance + * positions by 0 or read 0 bytes */ var byte1 = patchData.ReadByte(); if (byte1 <= 251) @@ -150,7 +275,9 @@ private static int getJLength(Stream patchData) } else { - Debug.WriteLine("Error! Unexpected byte when fetching length"); + // LARGE FILE SUPPORT (0xFF) IS NOT SUPPORTED + // IN THIS IMPLEMENTATION OF JPATCH + Debug.WriteLine("64-bit length numbers are not supported by this implementation of JPatch"); return -1; } } @@ -158,13 +285,36 @@ private static int getJLength(Stream patchData) /// /// Modifies or Inserts a series of bytes. Specify MOD using modifyMode = true, so the source data pointer moves as each new byte is written in. /// - /// - /// - /// - private static void processModInsOpcode(Stream sourceData, Stream patchData, Stream outData, bool modifyMode) + /// The sourcedata stream + /// The patchdata stream + /// The output data stream + /// If this is a MOD opcode operation. Indicate false to operate in INS mode + /// The first peeked byte ahead of the opcode + /// The second peeked byte ahead of the opcode + /// + private static int processModInsOpcode(Stream sourceData, Stream patchData, Stream outData, bool modifyMode, int peekChar1, int peekChar2) { + // First write pending bytes + if (peekChar1 != EOF) + { + outData.WriteByte((byte)peekChar1); + if (modifyMode) + { + sourceData.Position++; + } + + if (peekChar1 == (int)JojoOpcode.OPERATION_ESC && peekChar2 != (int)JojoOpcode.OPERATION_ESC) + { + outData.WriteByte((byte)peekChar2); + if (modifyMode) + { + sourceData.Position++; + } + } + } + int readChar; - while ((readChar = patchData.ReadByte()) != -1) + while ((readChar = patchData.ReadByte()) != EOF) { if (readChar != (int)JojoOpcode.OPERATION_ESC) { @@ -190,14 +340,14 @@ private static void processModInsOpcode(Stream sourceData, Stream patchData, Str else if (isOpcode(nextChar)) { //its - //Roll back 2 bytes we read - patchData.Position -= 2; - return; +#if JPATCH_DEBUG + Debug.WriteLine($"\t{sourceStartPos}\t{outStartPos}\t{(modifyMode ? "MOD" : "INS")}\t{outData.Position - outStartPos}"); +#endif + return nextChar; // loop func will process this as the next opcode } else { //it's ... nothing... this shouldn't be possible but maybe some sort of edge case. - //Debug.WriteLine($"Encountered unexpected esc sequence value: {nextChar:X2} at {(patchData.Position - 1):X2}"); outData.WriteByte((byte)readChar); outData.WriteByte((byte)nextChar); @@ -207,20 +357,26 @@ private static void processModInsOpcode(Stream sourceData, Stream patchData, Str } } } +#if JPATCH_DEBUG + Debug.WriteLine($"\t{sourceData.Position}\t{outData.Position}\t{(modifyMode ? "MOD" : "INS")}\t{outData.Position - outStartPos}"); +#endif + return readChar; } /// /// Advances the pointers forward or backwards as the source and destination file data is assumed to be the same. /// Data is copied from source data if backwards is false. /// - /// - /// - /// - /// For BCT Backtrace + /// The source file stream + /// The patch data stream + /// The output file stream + /// If this is a backtrace (BKT) opcode. If false, this is an EQL opcode and sourcedata will be copied to the outstream for the read length private static void processEqlBktOpcode(Stream sourceData, Stream patchData, Stream outData, bool backwards) { var length = getJLength(patchData); - //Debug.WriteLine(" Length: " + length + " bytes"); +#if JPATCH_DEBUG + Debug.WriteLine($"\t{sourceData.Position}\t{outData.Position}\t{(backwards ? "BKT" : "EQL")}\t{length}"); +#endif if (backwards) { sourceData.Position -= length; @@ -228,7 +384,7 @@ private static void processEqlBktOpcode(Stream sourceData, Stream patchData, Str else { //copy - sourceData.CopyToEx(outData, length); + CopyToEx(sourceData, outData, length); } } @@ -236,5 +392,23 @@ public static bool isOpcode(int code) { return code >= 0xA2 && code <= 0xA6; } + + /// + /// Copies the inputstream to the outputstream, for the specified amount of bytes + /// + /// Stream to copy from + /// Stream to copy to + /// The number of bytes to copy + public static void CopyToEx(Stream input, Stream output, int bytes) + { + var buffer = new byte[32768]; + int read; + while (bytes > 0 && + (read = input.Read(buffer, 0, Math.Min(buffer.Length, bytes))) > 0) + { + output.Write(buffer, 0, read); + bytes -= read; + } + } } }