/* BPS module for Rom Patcher JS v20240821 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */ /* File format specification: https://www.romhacking.net/documents/746/ */ const BPS_MAGIC='BPS1'; const BPS_ACTION_SOURCE_READ=0; const BPS_ACTION_TARGET_READ=1; const BPS_ACTION_SOURCE_COPY=2; const BPS_ACTION_TARGET_COPY=3; if(typeof module !== "undefined" && module.exports){ module.exports = BPS; } function BPS(){ this.sourceSize=0; this.targetSize=0; this.metaData=''; this.actions=[]; this.sourceChecksum=0; this.targetChecksum=0; this.patchChecksum=0; } BPS.prototype.toString=function(){ var s='Source size: '+this.sourceSize; s+='\nTarget size: '+this.targetSize; s+='\nMetadata: '+this.metaData; s+='\n#Actions: '+this.actions.length; return s } BPS.prototype.calculateFileChecksum = function () { var patchFile = this.export(); return patchFile.hashCRC32(0, patchFile.fileSize - 4); } BPS.prototype.validateSource=function(romFile,headerSize){return this.sourceChecksum===romFile.hashCRC32(headerSize)} BPS.prototype.getValidationInfo=function(){ return { 'type':'CRC32', 'value':this.sourceChecksum } } BPS.prototype.apply=function(romFile, validate){ if(validate && !this.validateSource(romFile)){ throw new Error('Source ROM checksum mismatch'); } tempFile=new BinFile(this.targetSize); //patch var sourceRelativeOffset=0; var targetRelativeOffset=0; for(var i=0; i> 2)+1}; if(action.type===BPS_ACTION_TARGET_READ){ action.bytes=file.readBytes(action.length); }else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){ var relativeOffset=file.readVLV(); action.relativeOffset=(relativeOffset & 1? -1 : +1) * (relativeOffset >> 1) } patch.actions.push(action); } //file.seek(endActionsOffset); patch.sourceChecksum=file.readU32(); patch.targetChecksum=file.readU32(); patch.patchChecksum=file.readU32(); if (patch.patchChecksum !== patch.calculateFileChecksum()) { throw new Error('Patch checksum mismatch'); } return patch; } function BPS_readVLV(){ var data=0, shift=1; while(true){ var x = this.readU8(); data += (x & 0x7f) * shift; if(x & 0x80) break; shift <<= 7; data += shift; } this._lastRead=data; return data; } function BPS_writeVLV(data){ while(true){ var x = data & 0x7f; data >>= 7; if(data === 0){ this.writeU8(0x80 | x); break; } this.writeU8(x); data--; } } function BPS_getVLVLen(data){ var len=0; while(true){ var x = data & 0x7f; data >>= 7; if(data === 0){ len++; break; } len++; data--; } return len; } BPS.prototype.export=function(fileName){ var patchFileSize=BPS_MAGIC.length; patchFileSize+=BPS_getVLVLen(this.sourceSize); patchFileSize+=BPS_getVLVLen(this.targetSize); patchFileSize+=BPS_getVLVLen(this.metaData.length); patchFileSize+=this.metaData.length; for(var i=0; i= 4) { //write byte to repeat targetReadLength++; outputOffset++; targetReadFlush(); //copy starting from repetition byte //encode(TargetCopy | ((rleLength - 1) << 2)); var relativeOffset = (outputOffset - 1) - targetRelativeOffset; //encode(relativeOffset << 1); patchActions.push({type:BPS_ACTION_TARGET_COPY, length:rleLength, relativeOffset:relativeOffset}); outputOffset += rleLength; targetRelativeOffset = outputOffset - 1; } else if(sourceLength >= 4) { targetReadFlush(); //encode(SourceRead | ((sourceLength - 1) << 2)); patchActions.push({type:BPS_ACTION_SOURCE_READ, length:sourceLength}); outputOffset += sourceLength; } else { targetReadLength += Granularity; outputOffset += Granularity; } } targetReadFlush(); return patchActions; } /* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/delta.hpp */ function createBPSFromFilesDelta(original, modified){ var patchActions=[]; /* references to match original beat code */ var sourceData=original._u8array; var targetData=modified._u8array; var sourceSize=original.fileSize; var targetSize=modified.fileSize; var Granularity=1; var sourceRelativeOffset=0; var targetRelativeOffset=0; var outputOffset=0; var sourceTree=new Array(65536); var targetTree=new Array(65536); for(var n=0; n<65536; n++){ sourceTree[n]=null; targetTree[n]=null; } //source tree creation for(var offset=0; offset maxLength) maxLength = length, mode = BPS_ACTION_SOURCE_READ; } { //source copy var node = sourceTree[symbol]; while(node) { var length = 0, x = node.offset, y = outputOffset; while(x < sourceSize && y < targetSize && sourceData[x++] == targetData[y++]) length++; if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_SOURCE_COPY; node = node.next; } } { //target copy var node = targetTree[symbol]; while(node) { var length = 0, x = node.offset, y = outputOffset; while(y < targetSize && targetData[x++] == targetData[y++]) length++; if(length > maxLength) maxLength = length, maxOffset = node.offset, mode = BPS_ACTION_TARGET_COPY; node = node.next; } //target tree append node = new BPS_Node(); node.offset = outputOffset; node.next = targetTree[symbol]; targetTree[symbol] = node; } { //target read if(maxLength < 4) { maxLength = Math.min(Granularity, targetSize - outputOffset); mode = BPS_ACTION_TARGET_READ; } } if(mode != BPS_ACTION_TARGET_READ) targetReadFlush(); switch(mode) { case BPS_ACTION_SOURCE_READ: //encode(BPS_ACTION_SOURCE_READ | ((maxLength - 1) << 2)); patchActions.push({type:BPS_ACTION_SOURCE_READ, length:maxLength}); break; case BPS_ACTION_TARGET_READ: //delay write to group sequential TargetRead commands into one targetReadLength += maxLength; break; case BPS_ACTION_SOURCE_COPY: case BPS_ACTION_TARGET_COPY: //encode(mode | ((maxLength - 1) << 2)); var relativeOffset; if(mode == BPS_ACTION_SOURCE_COPY) { relativeOffset = maxOffset - sourceRelativeOffset; sourceRelativeOffset = maxOffset + maxLength; } else { relativeOffset = maxOffset - targetRelativeOffset; targetRelativeOffset = maxOffset + maxLength; } //encode((relativeOffset < 0) | (abs(relativeOffset) << 1)); patchActions.push({type:mode, length:maxLength, relativeOffset:relativeOffset}); break; } outputOffset += maxLength; } targetReadFlush(); return patchActions; }