1
0
Fork 0
mirror of https://github.com/marcrobledo/RomPatcher.js.git synced 2025-06-27 16:25:54 +00:00

patch creation can now create IPS patches if offsets are below format limit, rearranged files internally

This commit is contained in:
Marc Robledo 2020-05-02 12:10:46 +02:00
parent e5bc46d725
commit 8146f4860a
27 changed files with 190 additions and 171 deletions

198
js/formats/aps.js Normal file
View file

@ -0,0 +1,198 @@
/* APS (N64) module for Rom Patcher JS v20180930 - Marc Robledo 2017-2018 - http://www.marcrobledo.com/license */
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(N64) */
const APS_MAGIC='APS10';
const APS_RECORD_RLE=0x0000;
const APS_RECORD_SIMPLE=0x01;
const APS_N64_MODE=0x01;
function APS(){
this.records=[];
this.headerType=0;
this.encodingMethod=0;
this.description='no description';
this.header={};
}
APS.prototype.addRecord=function(o, d){
this.records.push({offset:o, type:APS_RECORD_SIMPLE, data:d})
}
APS.prototype.addRLERecord=function(o, b, l){
this.records.push({offset:o, type:APS_RECORD_RLE, length:l, byte:b})
}
APS.prototype.toString=function(){
var s='Total records: '+this.records.length;
s+='\nHeader type: '+this.headerType;
if(this.headerType===APS_N64_MODE){
s+=' (N64)';
}
s+='\nEncoding method: '+this.encodingMethod;
s+='\nDescription: '+this.description;
s+='\nHeader: '+JSON.stringify(this.header);
return s
}
APS.prototype.validateSource=function(sourceFile){
if(this.headerType===APS_N64_MODE){
sourceFile.seek(0x3c);
if(sourceFile.readString(3)!==this.header.cartId)
return false;
sourceFile.seek(0x10);
var crc=sourceFile.readBytes(8);
for(var i=0; i<8; i++){
if(crc[i]!==this.header.crc[i])
return false
}
}
return true
}
APS.prototype.export=function(fileName){
var patchFileSize=61;
if(this.headerType===APS_N64_MODE)
patchFileSize+=17;
for(var i=0; i<this.records.length; i++){
if(this.records[i].type===APS_RECORD_RLE)
patchFileSize+=7;
else
patchFileSize+=5+this.records[i].data.length; //offset+length+data
}
tempFile=new MarcFile(patchFileSize);
tempFile.littleEndian=true;
tempFile.fileName=fileName+'.aps';
tempFile.writeString(APS_MAGIC, APS_MAGIC.length);
tempFile.writeU8(this.headerType);
tempFile.writeU8(this.encodingMethod);
tempFile.writeString(this.description, 50);
if(this.headerType===APS_N64_MODE){
tempFile.writeU8(this.header.originalN64Format);
tempFile.writeString(this.header.cartId, 3);
tempFile.writeBytes(this.header.crc);
tempFile.writeBytes(this.header.pad);
}
tempFile.writeU32(this.header.sizeOutput);
for(var i=0; i<this.records.length; i++){
var rec=this.records[i];
tempFile.writeU32(rec.offset);
if(rec.type===APS_RECORD_RLE){
tempFile.writeU8(0x00);
tempFile.writeU8(rec.byte);
tempFile.writeU8(rec.length);
}else{
tempFile.writeU8(rec.data.length);
tempFile.writeBytes(rec.data);
}
}
return tempFile
}
APS.prototype.apply=function(romFile, validate){
if(validate && !this.validateSource(romFile)){
throw new Error('error_crc_input');
}
tempFile=new MarcFile(this.header.sizeOutput);
romFile.copyToFile(tempFile, 0, tempFile.fileSize);
for(var i=0; i<this.records.length; i++){
tempFile.seek(this.records[i].offset);
if(this.records[i].type===APS_RECORD_RLE){
for(var j=0; j<this.records[i].length; j++)
tempFile.writeU8(this.records[i].byte);
}else{
tempFile.writeBytes(this.records[i].data);
}
}
return tempFile
}
function parseAPSFile(patchFile){
var patch=new APS();
patchFile.littleEndian=true;
patchFile.seek(5);
patch.headerType=patchFile.readU8();
patch.encodingMethod=patchFile.readU8();
patch.description=patchFile.readString(50);
var seek;
if(patch.headerType===APS_N64_MODE){
patch.header.originalN64Format=patchFile.readU8();
patch.header.cartId=patchFile.readString(3);
patch.header.crc=patchFile.readBytes(8);
patch.header.pad=patchFile.readBytes(5);
}
patch.header.sizeOutput=patchFile.readU32();
while(!patchFile.isEOF()){
var offset=patchFile.readU32();
var length=patchFile.readU8();
if(length===APS_RECORD_RLE)
patch.addRLERecord(offset, patchFile.readU8(seek), patchFile.readU8(seek+1));
else
patch.addRecord(offset, patchFile.readBytes(length));
}
return patch;
}
function createAPSFromFiles(original, modified){
var patch=new APS();
if(original.readU32()===0x80371240){ //is N64 ROM
patch.headerType=APS_N64_MODE;
patch.header.originalN64Format=/\.v64$/i.test(original.fileName)?0:1;
original.seek(0x3c);
patch.header.cartId=original.readString(3);
original.seek(0x10);
patch.header.crc=original.readBytes(8);
patch.header.pad=[0,0,0,0,0];
}
patch.header.sizeOutput=modified.fileSize;
original.seek(0);
modified.seek(0);
while(!modified.isEOF()){
var b1=original.isEOF()?0x00:original.readU8();
var b2=modified.readU8();
if(b1!==b2){
var RLERecord=true;
var differentBytes=[];
var offset=modified.offset-1;
while(b1!==b2 && differentBytes.length<0xff){
differentBytes.push(b2);
if(b2!==differentBytes[0])
RLERecord=false;
if(modified.isEOF() || differentBytes.length===0xff)
break;
b1=original.isEOF()?0x00:original.readU8();
b2=modified.readU8();
}
if(RLERecord && differentBytes.length>2){
patch.addRLERecord(offset, differentBytes[0], differentBytes.length);
}else{
patch.addRecord(offset, differentBytes);
}
//NO se puede comentar??? why????
}
}
return patch
}

453
js/formats/bps.js Normal file
View file

@ -0,0 +1,453 @@
/* BPS module for Rom Patcher JS v20180930 - Marc Robledo 2016-2018 - 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;
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.validateSource=function(romFile,headerSize){return this.sourceChecksum===crc32(romFile, headerSize)}
BPS.prototype.apply=function(romFile, validate){
if(validate && !this.validateSource(romFile)){
throw new Error('error_crc_input');
}
tempFile=new MarcFile(this.targetSize);
//patch
var sourceRelativeOffset=0;
var targetRelativeOffset=0;
for(var i=0; i<this.actions.length; i++){
var action=this.actions[i];
if(action.type===BPS_ACTION_SOURCE_READ){
romFile.copyToFile(tempFile, tempFile.offset, action.length);
tempFile.skip(action.length);
}else if(action.type===BPS_ACTION_TARGET_READ){
tempFile.writeBytes(action.bytes);
}else if(action.type===BPS_ACTION_SOURCE_COPY){
sourceRelativeOffset+=action.relativeOffset;
var actionLength=action.length;
while(actionLength--){
tempFile.writeU8(romFile._u8array[sourceRelativeOffset]);
sourceRelativeOffset++;
}
}else if(action.type===BPS_ACTION_TARGET_COPY){
targetRelativeOffset+=action.relativeOffset;
var actionLength=action.length;
while(actionLength--) {
tempFile.writeU8(tempFile._u8array[targetRelativeOffset]);
targetRelativeOffset++;
}
}
}
if(validate && this.targetChecksum!==crc32(tempFile)){
throw new Error('error_crc_output');
}
return tempFile
}
function parseBPSFile(file){
file.readVLV=BPS_readVLV;
file.littleEndian=true;
var patch=new BPS();
file.seek(4); //skip BPS1
patch.sourceSize=file.readVLV();
patch.targetSize=file.readVLV();
var metaDataLength=file.readVLV();
if(metaDataLength){
patch.metaData=file.readString(metaDataLength);
}
var endActionsOffset=file.fileSize-12;
while(file.offset<endActionsOffset){
var data=file.readVLV();
var action={type: data & 3, length: (data >> 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!==crc32(file, 0, true)){
throw new Error('error_crc_patch');
}
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<this.actions.length; i++){
var action=this.actions[i];
patchFileSize+=BPS_getVLVLen(((action.length-1)<<2) + action.type);
if(action.type===BPS_ACTION_TARGET_READ){
patchFileSize+=action.length;
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
patchFileSize+=BPS_getVLVLen((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
}
}
patchFileSize+=12;
var patchFile=new MarcFile(patchFileSize);
patchFile.fileName=fileName+'.bps';
patchFile.littleEndian=true;
patchFile.writeVLV=BPS_writeVLV;
patchFile.writeString(BPS_MAGIC);
patchFile.writeVLV(this.sourceSize);
patchFile.writeVLV(this.targetSize);
patchFile.writeVLV(this.metaData.length);
patchFile.writeString(this.metaData, this.metaData.length);
for(var i=0; i<this.actions.length; i++){
var action=this.actions[i];
patchFile.writeVLV(((action.length-1)<<2) + action.type);
if(action.type===BPS_ACTION_TARGET_READ){
patchFile.writeBytes(action.bytes);
}else if(action.type===BPS_ACTION_SOURCE_COPY || action.type===BPS_ACTION_TARGET_COPY){
patchFile.writeVLV((Math.abs(action.relativeOffset)<<1)+(action.relativeOffset<0?1:0));
}
}
patchFile.writeU32(this.sourceChecksum);
patchFile.writeU32(this.targetChecksum);
patchFile.writeU32(this.patchChecksum);
return patchFile;
}
function BPS_Node(){
this.offset=0;
this.next=null;
};
BPS_Node.prototype.delete=function(){
if(this.next)
delete this.next;
}
function createBPSFromFiles(original, modified, deltaMode){
var patch=new BPS();
patch.sourceSize=original.fileSize;
patch.targetSize=modified.fileSize;
if(deltaMode){
patch.actions=createBPSFromFilesDelta(original, modified);
}else{
patch.actions=createBPSFromFilesLinear(original, modified);
}
patch.sourceChecksum=crc32(original);
patch.targetChecksum=crc32(modified);
patch.patchChecksum=crc32(patch.export(), 0, true);
return patch;
}
/* delta implementation from https://github.com/chiya/beat/blob/master/nall/beat/linear.hpp */
function createBPSFromFilesLinear(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 targetRelativeOffset=0;
var outputOffset=0;
var targetReadLength=0;
function targetReadFlush(){
if(targetReadLength){
//encode(TargetRead | ((targetReadLength - 1) << 2));
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
patchActions.push(action);
var offset = outputOffset - targetReadLength;
while(targetReadLength){
//write(targetData[offset++]);
action.bytes.push(targetData[offset++]);
targetReadLength--;
}
}
};
while(outputOffset < targetSize) {
var sourceLength = 0;
for(var n = 0; outputOffset + n < Math.min(sourceSize, targetSize); n++) {
if(sourceData[outputOffset + n] != targetData[outputOffset + n]) break;
sourceLength++;
}
var rleLength = 0;
for(var n = 1; outputOffset + n < targetSize; n++) {
if(targetData[outputOffset] != targetData[outputOffset + n]) break;
rleLength++;
}
if(rleLength >= 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<sourceSize; offset++) {
var symbol = sourceData[offset + 0];
//sourceChecksum = crc32_adjust(sourceChecksum, symbol);
if(offset < sourceSize - 1)
symbol |= sourceData[offset + 1] << 8;
var node=new BPS_Node();
node.offset=offset;
node.next=sourceTree[symbol];
sourceTree[symbol] = node;
}
var targetReadLength=0;
function targetReadFlush(){
if(targetReadLength) {
//encode(TargetRead | ((targetReadLength - 1) << 2));
var action={type:BPS_ACTION_TARGET_READ, length:targetReadLength, bytes:[]};
patchActions.push(action);
var offset = outputOffset - targetReadLength;
while(targetReadLength){
//write(targetData[offset++]);
action.bytes.push(targetData[offset++]);
targetReadLength--;
}
}
};
while(outputOffset<modified.fileSize){
var maxLength = 0, maxOffset = 0, mode = BPS_ACTION_TARGET_READ;
var symbol = targetData[outputOffset + 0];
if(outputOffset < targetSize - 1) symbol |= targetData[outputOffset + 1] << 8;
{ //source read
var length = 0, offset = outputOffset;
while(offset < sourceSize && offset < targetSize && sourceData[offset] == targetData[offset]) {
length++;
offset++;
}
if(length > 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;
}

227
js/formats/ips.js Normal file
View file

@ -0,0 +1,227 @@
/* IPS module for Rom Patcher JS v20200502 - Marc Robledo 2016-2020 - http://www.marcrobledo.com/license */
/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */
const IPS_MAGIC='PATCH';
const IPS_MAX_SIZE=0x1000000; //16 megabytes
const IPS_RECORD_RLE=0x0000;
const IPS_RECORD_SIMPLE=0x01;
function IPS(){
this.records=[];
this.truncate=false;
}
IPS.prototype.addSimpleRecord=function(o, d){
this.records.push({offset:o, type:IPS_RECORD_SIMPLE, length:d.length, data:d})
}
IPS.prototype.addRLERecord=function(o, l, b){
this.records.push({offset:o, type:IPS_RECORD_RLE, length:l, byte:b})
}
IPS.prototype.toString=function(){
nSimpleRecords=0;
nRLERecords=0;
for(var i=0; i<this.records.length; i++){
if(this.records[i].type===IPS_RECORD_RLE)
nRLERecords++;
else
nSimpleRecords++;
}
var s='Simple records: '+nSimpleRecords;
s+='\nRLE records: '+nRLERecords;
s+='\nTotal records: '+this.records.length;
if(this.truncate)
s+='\nTruncate at: 0x'+this.truncate.toString(16);
return s
}
IPS.prototype.export=function(fileName){
var patchFileSize=5; //PATCH string
for(var i=0; i<this.records.length; i++){
if(this.records[i].type===IPS_RECORD_RLE)
patchFileSize+=(3+2+2+1); //offset+0x0000+length+RLE byte to be written
else
patchFileSize+=(3+2+this.records[i].data.length); //offset+length+data
}
patchFileSize+=3; //EOF string
if(this.truncate)
patchFileSize+=3; //truncate
tempFile=new MarcFile(patchFileSize);
tempFile.fileName=fileName+'.ips';
tempFile.writeString(IPS_MAGIC);
for(var i=0; i<this.records.length; i++){
var rec=this.records[i];
tempFile.writeU24(rec.offset);
if(rec.type===IPS_RECORD_RLE){
tempFile.writeU16(0x0000);
tempFile.writeU16(rec.length);
tempFile.writeU8(rec.byte);
}else{
tempFile.writeU16(rec.data.length);
tempFile.writeBytes(rec.data);
}
}
tempFile.writeString('EOF');
if(rec.truncate)
tempFile.writeU24(rec.truncate);
return tempFile
}
IPS.prototype.apply=function(romFile){
if(this.truncate){
tempFile=romFile.slice(0, this.truncate);
}else{
var newFileSize=romFile.fileSize;
for(var i=0; i<this.records.length; i++){
var rec=this.records[i];
if(rec.type===IPS_RECORD_RLE){
if(rec.offset+rec.length>newFileSize){
newFileSize=rec.offset+rec.length;
}
}else{
if(rec.offset+rec.data.length>newFileSize){
newFileSize=rec.offset+rec.data.length;
}
}
}
if(newFileSize===romFile.fileSize){
tempFile=romFile.slice(0, romFile.fileSize);
}else{
tempFile=new MarcFile(newFileSize);
romFile.copyToFile(tempFile,0);
}
}
romFile.seek(0);
for(var i=0; i<this.records.length; i++){
tempFile.seek(this.records[i].offset);
if(this.records[i].type===IPS_RECORD_RLE){
for(var j=0; j<this.records[i].length; j++)
tempFile.writeU8(this.records[i].byte);
}else{
tempFile.writeBytes(this.records[i].data);
}
}
return tempFile
}
function parseIPSFile(file){
var patchFile=new IPS();
file.seek(5);
while(!file.isEOF()){
var offset=file.readU24();
if(offset===0x454f46){ /* EOF */
if(file.isEOF()){
break;
}else if((file.offset+3)===file.fileSize){
patchFile.truncate=file.readU24();
break;
}
}
var length=file.readU16();
if(length===IPS_RECORD_RLE){
patchFile.addRLERecord(offset, file.readU16(), file.readU8());
}else{
patchFile.addSimpleRecord(offset, file.readBytes(length));
}
}
return patchFile;
}
function createIPSFromFiles(original, modified){
var patch=new IPS();
if(modified.fileSize<original.fileSize){
patch.truncate=modified.fileSize;
}
//solucion: guardar startOffset y endOffset (ir mirando de 6 en 6 hacia atrás)
var previousRecord={type:0xdeadbeef,startOffset:0,length:0};
while(!modified.isEOF()){
var b1=original.isEOF()?0x00:original.readU8();
var b2=modified.readU8();
if(b1!==b2){
var RLEmode=true;
var differentData=[];
var startOffset=modified.offset-1;
while(b1!==b2 && differentData.length<0xffff){
differentData.push(b2);
if(b2!==differentData[0])
RLEmode=false;
if(modified.isEOF() || differentData.length===0xffff)
break;
b1=original.isEOF()?0x00:original.readU8();
b2=modified.readU8();
}
//check if this record is near the previous one
var distance=startOffset-(previousRecord.offset+previousRecord.length);
if(
previousRecord.type===IPS_RECORD_SIMPLE &&
distance<6 && (previousRecord.length+distance+differentData.length)<0xffff
){
if(RLEmode && differentData.length>6){
// separate a potential RLE record
original.seek(startOffset);
modified.seek(startOffset);
previousRecord={type:0xdeadbeef,startOffset:0,length:0};
}else{
// merge both records
while(distance--){
previousRecord.data.push(modified._u8array[previousRecord.offset+previousRecord.length]);
previousRecord.length++;
}
previousRecord.data=previousRecord.data.concat(differentData);
previousRecord.length=previousRecord.data.length;
}
}else{
if(startOffset>=IPS_MAX_SIZE){
throw new Error('files are too big for IPS format');
return null;
}
if(RLEmode && differentData.length>2){
patch.addRLERecord(startOffset, differentData.length, differentData[0]);
}else{
patch.addSimpleRecord(startOffset, differentData);
}
previousRecord=patch.records[patch.records.length-1];
}
}
}
if(modified.fileSize>original.fileSize){
var lastRecord=patch.records[patch.records.length-1];
var lastOffset=lastRecord.offset+lastRecord.length;
if(lastOffset<modified.fileSize){
patch.addSimpleRecord(modified.fileSize-1, [0x00]);
}
}
return patch
}

87
js/formats/pmsr.js Normal file
View file

@ -0,0 +1,87 @@
/* PMSR (Paper Mario Star Rod) module for Rom Patcher JS v20200225 - Marc Robledo 2020 - http://www.marcrobledo.com/license */
/* File format specification: http://origami64.net/attachment.php?aid=790 (dead link) */
const PMSR_MAGIC='PMSR';
const YAY0_MAGIC='Yay0';
const PAPER_MARIO_USA10_CRC32=0xa7f5cd7e;
const PAPER_MARIO_USA10_FILE_SIZE=41943040;
function PMSR(){
this.targetSize=0;
this.records=[];
}
PMSR.prototype.addRecord=function(offset, data){
this.records.push({offset:offset, data:data})
}
PMSR.prototype.toString=function(){
var s='Star Rod patch';
s+='\nTarget file size: '+this.targetSize;
s+='\n#Records: '+this.records.length;
return s;
}
PMSR.prototype.validateSource=function(romFile){
return romFile.fileSize===PAPER_MARIO_USA10_FILE_SIZE && crc32(romFile)===PAPER_MARIO_USA10_CRC32;
}
PMSR.prototype.apply=function(romFile, validate){
if(validate && !this.validateSource(romFile)){
throw new Error('error_crc_input');
}
console.log('a');
if(this.targetSize===romFile.fileSize){
tempFile=romFile.slice(0, romFile.fileSize);
}else{
tempFile=new MarcFile(this.targetSize);
romFile.copyToFile(tempFile,0);
}
console.log('b');
for(var i=0; i<this.records.length; i++){
tempFile.seek(this.records[i].offset);
tempFile.writeBytes(this.records[i].data);
}
return tempFile;
}
function parseMODFile(file){
var patch=new PMSR();
/*file.seek(0);
if(file.readString(YAY0_MAGIC.length)===YAY0_MAGIC){
file=PMSR.YAY0_decode(file);
}*/
patch.targetSize=PAPER_MARIO_USA10_FILE_SIZE;
file.seek(4);
var nRecords=file.readU32();
for(var i=0; i<nRecords; i++){
var offset=file.readU32();
var length=file.readU32();
patch.addRecord(offset, file.readBytes(length));
if((offset+length)>patch.targetSize)
patch.targetSize=offset+length;
}
return patch;
}
/* to-do */
//MOD.prototype.export=function(fileName){return null}
//function createMODFromFiles(original, modified){return null}
/* https://github.com/pho/WindViewer/wiki/Yaz0-and-Yay0 */
PMSR.YAY0_decode=function(file){
/* to-do */
}

266
js/formats/ppf.js Normal file
View file

@ -0,0 +1,266 @@
/* PPF module for Rom Patcher JS v20200221 - Marc Robledo 2019-2020 - http://www.marcrobledo.com/license */
/* File format specification: https://www.romhacking.net/utilities/353/ */
const PPF_MAGIC='PPF';
const PPF_IMAGETYPE_BIN=0x00;
const PPF_IMAGETYPE_GI=0x01;
const PPF_BEGIN_FILE_ID_DIZ_MAGIC='@BEG';//@BEGIN_FILE_ID.DIZ
function PPF(){
this.version=3;
this.imageType=PPF_IMAGETYPE_BIN;
this.blockCheck=false;
this.undoData=false;
this.records=[];
}
PPF.prototype.addRecord=function(offset, data, undoData){
if(this.undoData){
this.records.push({offset:offset, data:data, undoData:undoData});
}else{
this.records.push({offset:offset, data:data});
}
}
PPF.prototype.toString=function(){
var s=this.description;
s+='\nPPF version: '+this.version;
s+='\n#Records: '+this.records.length;
s+='\nImage type: '+this.imageType;
s+='\nBlock check: '+!!this.blockCheck;
s+='\nUndo data: '+this.undoData;
if(this.fileIdDiz)
s+='\nFILE_ID.DIZ: '+this.fileIdDiz;
return s
}
PPF.prototype.export=function(fileName){
var patchFileSize=5+1+50; //PPFx0
for(var i=0; i<this.records.length; i++){
patchFileSize+=4+1+this.records[i].data.length;
if(this.version===3)
patchFileSize+=4; //offsets are u64
}
if(this.version===3 || this.version===2){
patchFileSize+=4;
}
if(this.blockCheck){
patchFileSize+=1024;
}
if(this.fileIdDiz){
patchFileSize+=18+this.fileIdDiz.length+16+4;
}
tempFile=new MarcFile(patchFileSize);
tempFile.fileName=fileName+'.ppf';
tempFile.writeString(PPF_MAGIC);
tempFile.writeString((this.version*10).toString());
tempFile.writeU8(parseInt(this.version)-1);
tempFile.writeString(this.description, 50);
if(this.version===3){
tempFile.writeU8(this.imageType);
tempFile.writeU8(this.blockCheck?0x01:0x00);
tempFile.writeU8(this.undoData?0x01:0x00);
tempFile.writeU8(0x00); //dummy
}else if(this.version===2){
tempFile.writeU32(this.inputFileSize);
}
if(this.blockCheck){
tempFile.writeBytes(this.blockCheck);
}
tempFile.littleEndian=true;
for(var i=0; i<this.records.length; i++){
tempFile.writeU32(this.records[i].offset & 0xffffffff);
if(this.version===3){
var offset2=this.records[i].offset;
for(var j=0; j<32; j++)
offset2=parseInt((offset2/2)>>>0);
tempFile.writeU32(offset2);
}
tempFile.writeU8(this.records[i].data.length);
tempFile.writeBytes(this.records[i].data);
if(this.undoData)
tempFile.writeBytes(this.records[i].undoData);
}
if(this.fileIdDiz){
tempFile.writeString('@BEGIN_FILE_ID.DIZ');
tempFile.writeString(this.fileIdDiz);
tempFile.writeString('@END_FILE_ID.DIZ');
tempFile.writeU16(this.fileIdDiz.length);
tempFile.writeU16(0x00);
}
return tempFile
}
PPF.prototype.apply=function(romFile){
var newFileSize=romFile.fileSize;
for(var i=0; i<this.records.length; i++){
if(this.records[i].offset+this.records[i].data.length>newFileSize)
newFileSize=this.records[i].offset+this.records[i].data.length;
}
if(newFileSize===romFile.fileSize){
tempFile=romFile.slice(0, romFile.fileSize);
}else{
tempFile=new MarcFile(newFileSize);
romFile.copyToFile(tempFile,0);
}
//check if undoing
var undoingData=false;
if(this.undoData){
tempFile.seek(this.records[0].offset);
var originalBytes=tempFile.readBytes(this.records[0].data.length);
var foundDifferences=false;
for(var i=0; i<originalBytes.length && !foundDifferences; i++){
if(originalBytes[i]!==this.records[0].data[i]){
foundDifferences=true;
}
}
if(!foundDifferences){
undoingData=true;
}
}
for(var i=0; i<this.records.length; i++){
tempFile.seek(this.records[i].offset);
if(undoingData){
tempFile.writeBytes(this.records[i].undoData);
}else{
tempFile.writeBytes(this.records[i].data);
}
}
return tempFile
}
function parsePPFFile(patchFile){
var patch=new PPF();
patchFile.seek(3);
var version1=parseInt(patchFile.readString(2))/10;
var version2=patchFile.readU8()+1;
if(version1!==version2 || version1>3){
throw new Error('invalid PPF version');
}
patch.version=version1;
patch.description=patchFile.readString(50).replace(/ +$/,'');
if(patch.version===3){
patch.imageType=patchFile.readU8();
if(patchFile.readU8())
patch.blockCheck=true;
if(patchFile.readU8())
patch.undoData=true;
patchFile.skip(1);
}else if(patch.version===2){
patch.blockCheck=true;
patch.inputFileSize=patchFile.readU32();
}
if(patch.blockCheck){
patch.blockCheck=patchFile.readBytes(1024);
}
patchFile.littleEndian=true;
while(!patchFile.isEOF()){
if(patchFile.readString(4)===PPF_BEGIN_FILE_ID_DIZ_MAGIC){
patchFile.skip(14);
//console.log('found file_id.diz begin');
patch.fileIdDiz=patchFile.readString(3072);
patch.fileIdDiz=patch.fileIdDiz.substr(0, patch.fileIdDiz.indexOf('@END_FILE_ID.DIZ'));
break;
}
patchFile.skip(-4);
var offset;
if(patch.version===3){
var u64_1=patchFile.readU32();
var u64_2=patchFile.readU32();
offset=u64_1+(u64_2*0x100000000);
}else
offset=patchFile.readU32();
var len=patchFile.readU8();
var data=patchFile.readBytes(len);
var undoData=false;
if(patch.undoData){
undoData=patchFile.readBytes(len);
}
patch.addRecord(offset, data, undoData);
}
return patch;
}
function createPPFFromFiles(original, modified){
var patch=new PPF();
patch.description='Patch description';
if(original.fileSize>modified.fileSize){
var expandedModified=new MarcFile(original.fileSize);
modified.copyToFile(expandedModified,0);
modified=expandedModified;
}
original.seek(0);
modified.seek(0);
while(!modified.isEOF()){
var b1=original.isEOF()?0x00:original.readU8();
var b2=modified.readU8();
if(b1!==b2){
var differentData=[];
var offset=modified.offset-1;
while(b1!==b2 && differentData.length<0xff){
differentData.push(b2);
if(modified.isEOF() || differentData.length===0xff)
break;
b1=original.isEOF()?0x00:original.readU8();
b2=modified.readU8();
}
patch.addRecord(offset, differentData);
}
}
if(original.fileSize<modified.fileSize){
modified.seek(modified.fileSize-1);
if(modified.readU8()===0x00)
patch.addRecord(modified.fileSize-1, [0x00]);
}
return patch
}

328
js/formats/rup.js Normal file
View file

@ -0,0 +1,328 @@
/* RUP module for Rom Patcher JS v20180930 - Marc Robledo 2018 - http://www.marcrobledo.com/license */
/* File format specification: http://www.romhacking.net/documents/288/ */
const RUP_MAGIC='NINJA2';
const RUP_COMMAND_END=0x00;
const RUP_COMMAND_OPEN_NEW_FILE=0x01;
const RUP_COMMAND_XOR_RECORD=0x02;
const RUP_ROM_TYPES=['raw','nes','fds','snes','n64','gb','sms','mega','pce','lynx'];
function RUP(){
this.author='';
this.version='';
this.title='';
this.genre='';
this.language='';
this.date='';
this.web='';
this.description='';
this.files=[];
}
RUP.prototype.toString=function(){
var s='Author: '+this.author;
s+='\nVersion: '+this.version;
s+='\nTitle: '+this.title;
s+='\nGenre: '+this.genre;
s+='\nLanguage: '+this.language;
s+='\nDate: '+this.date;
s+='\nWeb: '+this.web;
s+='\nDescription: '+this.description;
for(var i=0; i<this.files.length; i++){
var file=this.files[i];
s+='\n---------------';
s+='\nFile '+i+':';
s+='\nFile name: '+file.fileName;
s+='\nRom type: '+RUP_ROM_TYPES[file.romType];
s+='\nSource file size: '+file.sourceFileSize;
s+='\nTarget file size: '+file.targetFileSize;
s+='\nSource MD5: '+file.sourceMD5;
s+='\nTarget MD5: '+file.targetMD5;
s+='\nOverflow text: '+file.overflowText;
s+='\n#records: '+file.records.length;
}
return s
}
RUP.prototype.validateSource=function(romFile,headerSize){
var md5string=md5(romFile, headerSize);
for(var i=0; i<this.files.length; i++){
if(this.files[i].sourceMD5===md5string){
return this.files[i];
}
}
return false;
}
RUP.prototype.apply=function(romFile, validate){
var validFile;
if(validate){
validFile=this.validateSource(romFile);
if(!validFile)
throw new Error('error_crc_input');
}else{
validFile=this.files[0];
}
tempFile=new MarcFile(validFile.targetFileSize);
/* copy original file */
romFile.copyToFile(tempFile, 0);
for(var i=0; i<validFile.records.length; i++){
var offset=validFile.records[i].offset;
romFile.seek(offset);
tempFile.seek(offset);
for(var j=0; j<validFile.records[i].xor.length; j++){
tempFile.writeU8(
(romFile.isEOF()?0x00:romFile.readU8()) ^ validFile.records[i].xor[j]
);
}
}
if(validate && md5(tempFile)!==validFile.targetMD5){
throw new Error('error_crc_output');
}
return tempFile
}
function parseRUPFile(file){
var patch=new RUP();
file.readVLV=RUP_readVLV;
file.seek(RUP_MAGIC.length);
patch.textEncoding=file.readU8();
patch.author=file.readString(84);
patch.version=file.readString(11);
patch.title=file.readString(256);
patch.genre=file.readString(48);
patch.language=file.readString(48);
patch.date=file.readString(8);
patch.web=file.readString(512);
patch.description=file.readString(1074).replace(/\\n/g,'\n');
file.seek(0x800);
var nextFile;
while(!file.isEOF()){
var command=file.readU8();
if(command===RUP_COMMAND_OPEN_NEW_FILE){
if(nextFile)
patch.files.push(nextFile)
nextFile={
records:[]
};
nextFile.fileName=file.readString(file.readVLV());
nextFile.romType=file.readU8();
nextFile.sourceFileSize=file.readVLV();
nextFile.targetFileSize=file.readVLV();
nextFile.sourceMD5='';
for(var i=0; i<16; i++)
nextFile.sourceMD5+=padZeroes(file.readU8(),1);
nextFile.targetMD5='';
for(var i=0; i<16; i++)
nextFile.targetMD5+=padZeroes(file.readU8(),1);
if(nextFile.sourceFileSize!==nextFile.targetFileSize){
file.skip(1); //skip 'M' (source>target) or 'A' (source<target)
nextFile.overflowText=file.readString(file.readVLV());
}
}else if(command===RUP_COMMAND_XOR_RECORD){
nextFile.records.push({
offset:file.readVLV(),
xor:file.readBytes(file.readVLV())
});
}else if(command===RUP_COMMAND_END){
if(nextFile)
patch.files.push(nextFile);
break;
}else{
throw new Error('invalid RUP command');
}
}
return patch;
}
function RUP_readVLV(){
var nBytes=this.readU8();
var data=0;
for(var i=0; i<nBytes; i++){
data+=this.readU8() << i*8;
}
return data;
}
function RUP_writeVLV(data){
var len=RUP_getVLVLen(data)-1;
this.writeU8(len);
while(data){
this.writeU8(data & 0xff);
data>>=8;
}
}
function RUP_getVLVLen(data){
var ret=1;
while(data){
ret++;
data>>=8;
}
return ret;
}
RUP.prototype.export=function(fileName){
var patchFileSize=2048;
for(var i=0; i<this.files.length; i++){
var file=this.files[i];
patchFileSize++; //command 0x01
patchFileSize+=RUP_getVLVLen(file.fileName.length);
patchFileSize+=file.fileName.length;
patchFileSize++; //rom type
patchFileSize+=RUP_getVLVLen(file.sourceFileSize);
patchFileSize+=RUP_getVLVLen(file.targetFileSize);
patchFileSize+=32; //MD5s
if(file.sourceFileSize!==file.targetFileSize){
patchFileSize++; // M or A
patchFileSize+=RUP_getVLVLen(file.overflowText);
patchFileSize+=file.overflowText;
}
for(var j=0; j<file.records.length; j++){
patchFileSize++; //command 0x01
patchFileSize+=RUP_getVLVLen(file.records[j].offset);
patchFileSize+=RUP_getVLVLen(file.records[j].xor.length);
patchFileSize+=file.records[j].xor.length;
}
}
patchFileSize++; //command 0x00
var patchFile=new MarcFile(patchFileSize);
patchFile.fileName=fileName+'.rup';
patchFile.writeVLV=RUP_writeVLV;
patchFile.writeString(RUP_MAGIC);
patchFile.writeU8(this.textEncoding);
patchFile.writeString(this.author, 84);
patchFile.writeString(this.version, 11);
patchFile.writeString(this.title, 256);
patchFile.writeString(this.genre, 48);
patchFile.writeString(this.language, 48);
patchFile.writeString(this.date, 8);
patchFile.writeString(this.web, 512);
patchFile.writeString(this.description.replace(/\n/g,'\\n'), 1074);
for(var i=0; i<this.files.length; i++){
var file=this.files[i];
patchFile.writeU8(RUP_COMMAND_OPEN_NEW_FILE);
patchFile.writeVLV(file.fileName);
patchFile.writeU8(file.romType);
patchFile.writeVLV(file.sourceFileSize);
patchFile.writeVLV(file.targetFileSize);
for(var j=0; j<16; j++)
patchFile.writeU8(parseInt(file.sourceMD5.substr(j*2,2), 16));
for(var j=0; j<16; j++)
patchFile.writeU8(parseInt(file.targetMD5.substr(j*2,2), 16));
if(file.sourceFileSize!==file.targetFileSize){
patchFile.writeString(file.sourceFileSize>file.targetFileSize?'M':'A');
patchFile.writeVLV(file.overflowText.length);
patchFile.writeString(file.overflowText);
}
for(var j=0; j<file.records.length; j++){
patchFile.writeU8(RUP_COMMAND_XOR_RECORD);
patchFile.writeVLV(file.records[j].offset);
patchFile.writeVLV(file.records[j].xor.length);
patchFile.writeBytes(file.records[j].xor);
}
}
patchFile.writeU8(RUP_COMMAND_END);
return patchFile;
}
function createRUPFromFiles(original, modified){
var patch=new RUP();
var today=new Date();
patch.date=(today.getYear()+1900)+padZeroes(today.getMonth()+1, 1)+padZeroes(today.getDate(), 1);
var file={
fileName:'',
romType:0,
sourceFileSize:original.fileSize,
targetFileSize:modified.fileSize,
sourceMD5:md5(original),
targetMD5:md5(modified),
overflowText:'',
records:[]
};
original.seek(0);
modified.seek(0);
while(!modified.isEOF()){
var b1=original.isEOF()?0x00:original.readU8();
var b2=modified.readU8();
if(b1!==b2){
var originalOffset=modified.offset-1;
var xorDifferences=[];
while(b1!==b2){
xorDifferences.push(b1^b2);
if(modified.isEOF())
break;
b1=original.isEOF()?0x00:original.readU8();
b2=modified.readU8();
}
file.records.push({offset:originalOffset, xor:xorDifferences});
}
}
patch.files.push(file);
return patch
}

204
js/formats/ups.js Normal file
View file

@ -0,0 +1,204 @@
/* UPS module for Rom Patcher JS v20180930 - Marc Robledo 2017-2018 - http://www.marcrobledo.com/license */
/* File format specification: http://www.romhacking.net/documents/392/ */
const UPS_MAGIC='UPS1';
function UPS(){
this.records=[];
this.sizeInput=0;
this.sizeOutput=0;
this.checksumInput=0;
this.checksumOutput=0;
}
UPS.prototype.addRecord=function(relativeOffset, d){
this.records.push({offset:relativeOffset, XORdata:d})
}
UPS.prototype.toString=function(){
var s='Records: '+this.records.length;
s+='\nInput file size: '+this.sizeInput;
s+='\nOutput file size: '+this.sizeOutput;
s+='\nInput file checksum: '+padZeroes(this.checksumInput,4);
s+='\nOutput file checksum: '+padZeroes(this.checksumOutput,4);
return s
}
UPS.prototype.export=function(fileName){
var patchFileSize=UPS_MAGIC.length;//UPS1 string
patchFileSize+=UPS_getVLVLength(this.sizeInput); //input file size
patchFileSize+=UPS_getVLVLength(this.sizeOutput); //output file size
for(var i=0; i<this.records.length; i++){
patchFileSize+=UPS_getVLVLength(this.records[i].offset);
patchFileSize+=this.records[i].XORdata.length+1;
}
patchFileSize+=12; //input/output/patch checksums
tempFile=new MarcFile(patchFileSize);
tempFile.writeVLV=UPS_writeVLV;
tempFile.fileName=fileName+'.ups';
tempFile.writeString(UPS_MAGIC);
tempFile.writeVLV(this.sizeInput);
tempFile.writeVLV(this.sizeOutput);
for(var i=0; i<this.records.length; i++){
tempFile.writeVLV(this.records[i].offset);
tempFile.writeBytes(this.records[i].XORdata);
tempFile.writeU8(0x00);
}
tempFile.littleEndian=true;
tempFile.writeU32(this.checksumInput);
tempFile.writeU32(this.checksumOutput);
tempFile.writeU32(crc32(tempFile, 0, true));
return tempFile
}
UPS.prototype.validateSource=function(romFile,headerSize){return crc32(romFile,headerSize)===this.checksumInput}
UPS.prototype.apply=function(romFile, validate){
if(validate && !this.validateSource(romFile)){
throw new Error('error_crc_input');
}
/* copy original file */
tempFile=new MarcFile(this.sizeOutput);
romFile.copyToFile(tempFile, 0, this.sizeInput);
romFile.seek(0);
var nextOffset=0;
for(var i=0; i<this.records.length; i++){
var record=this.records[i];
tempFile.skip(record.offset);
romFile.skip(record.offset);
for(var j=0; j<record.XORdata.length; j++){
tempFile.writeU8((romFile.isEOF()?0x00:romFile.readU8()) ^ record.XORdata[j]);
}
tempFile.skip(1);
romFile.skip(1);
}
if(validate && crc32(tempFile)!==this.checksumOutput){
throw new Error('error_crc_output');
}
return tempFile
}
/* encode/decode variable length values, used by UPS file structure */
function UPS_writeVLV(data){
while(1){
var x=data & 0x7f;
data=data>>7;
if(data===0){
this.writeU8(0x80 | x);
break;
}
this.writeU8(x);
data=data-1;
}
}
function UPS_readVLV(){
var data=0;
var shift=1;
while(1){
var x=this.readU8();
if(x==-1)
throw new Error('Can\'t read UPS VLV at 0x'+(this.offset-1).toString(16));
data+=(x&0x7f)*shift;
if((x&0x80)!==0)
break;
shift=shift<<7;
data+=shift;
}
return data
}
function UPS_getVLVLength(data){
var len=0;
while(1){
var x=data & 0x7f;
data=data>>7;
len++;
if(data===0){
break;
}
data=data-1;
}
return len;
}
function parseUPSFile(file){
var patch=new UPS();
file.readVLV=UPS_readVLV;
file.seek(UPS_MAGIC.length);
patch.sizeInput=file.readVLV();
patch.sizeOutput=file.readVLV();
var nextOffset=0;
while(file.offset<(file.fileSize-12)){
var relativeOffset=file.readVLV();
var XORdifferences=[];
while(file.readU8()){
XORdifferences.push(file._lastRead);
}
patch.addRecord(relativeOffset, XORdifferences);
}
file.littleEndian=true;
patch.checksumInput=file.readU32();
patch.checksumOutput=file.readU32();
if(file.readU32()!==crc32(file, 0, true)){
throw new Error('error_crc_patch');
}
file.littleEndian=false;
return patch;
}
function createUPSFromFiles(original, modified){
var patch=new UPS();
patch.sizeInput=original.fileSize;
patch.sizeOutput=modified.fileSize;
var previousSeek=1;
while(!modified.isEOF()){
var b1=original.isEOF()?0x00:original.readU8();
var b2=modified.readU8();
if(b1!==b2){
var currentSeek=modified.offset;
var XORdata=[];
while(b1!==b2){
XORdata.push(b1 ^ b2);
if(modified.isEOF())
break;
b1=original.isEOF()?0x00:original.readU8();
b2=modified.readU8();
}
patch.addRecord(currentSeek-previousSeek, XORdata);
previousSeek=currentSeek+XORdata.length+1;
}
}
patch.checksumInput=crc32(original);
patch.checksumOutput=crc32(modified);
return patch
}

374
js/formats/vcdiff.js Normal file
View file

@ -0,0 +1,374 @@
/* VCDIFF module for RomPatcher.js v20181021 - Marc Robledo 2018 - http://www.marcrobledo.com/license */
/* File format specification: https://tools.ietf.org/html/rfc3284 */
/*
Mostly based in:
https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
some code and ideas borrowed from:
https://hack64.net/jscripts/libpatch.js?6
*/
//const VCDIFF_MAGIC=0xd6c3c400;
const VCDIFF_MAGIC='\xd6\xc3\xc4';
/*
const XDELTA_014_MAGIC='%XDELTA';
const XDELTA_018_MAGIC='%XDZ000';
const XDELTA_020_MAGIC='%XDZ001';
const XDELTA_100_MAGIC='%XDZ002';
const XDELTA_104_MAGIC='%XDZ003';
const XDELTA_110_MAGIC='%XDZ004';
*/
function VCDIFF(patchFile){
this.file=patchFile;
}
VCDIFF.prototype.toString=function(){
return 'VCDIFF patch'
}
VCDIFF.prototype.apply=function(romFile, validate){
//romFile._u8array=new Uint8Array(romFile._dataView.buffer);
//var t0=performance.now();
var parser=new VCDIFF_Parser(this.file);
//read header
parser.seek(4);
var headerIndicator=parser.readU8();
if(headerIndicator & VCD_DECOMPRESS){
//has secondary decompressor, read its id
var secondaryDecompressorId=parser.readU8();
if(secondaryDecompressorId!==0)
throw new Error('not implemented: secondary decompressor');
}
if(headerIndicator & VCD_CODETABLE){
var codeTableDataLength=parser.read7BitEncodedInt();
if(codeTableDataLength!==0)
throw new Error('not implemented: custom code table'); // custom code table
}
if(headerIndicator & VCD_APPHEADER){
// ignore app header data
var appDataLength=parser.read7BitEncodedInt();
parser.skip(appDataLength);
}
var headerEndOffset=parser.offset;
//calculate target file size
var newFileSize=0;
while(!parser.isEOF()){
var winHeader=parser.decodeWindowHeader();
newFileSize+=winHeader.targetWindowLength;
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
}
tempFile=new MarcFile(newFileSize);
parser.seek(headerEndOffset);
var cache = new VCD_AdressCache(4,3);
var codeTable = VCD_DEFAULT_CODE_TABLE;
var targetWindowPosition = 0; //renombrar
while(!parser.isEOF()){
var winHeader = parser.decodeWindowHeader();
var addRunDataStream = new VCDIFF_Parser(this.file, parser.offset);
var instructionsStream = new VCDIFF_Parser(this.file, addRunDataStream.offset + winHeader.addRunDataLength);
var addressesStream = new VCDIFF_Parser(this.file, instructionsStream.offset + winHeader.instructionsLength);
var addRunDataIndex = 0;
cache.reset(addressesStream);
var addressesStreamEndOffset = addressesStream.offset;
while(instructionsStream.offset<addressesStreamEndOffset){
/*
var instructionIndex=instructionsStream.readS8();
if(instructionIndex===-1){
break;
}
*/
var instructionIndex = instructionsStream.readU8();
for(var i=0; i<2; i++){
var instruction=codeTable[instructionIndex][i];
var size=instruction.size;
if(size===0 && instruction.type!==VCD_NOOP){
size=instructionsStream.read7BitEncodedInt()
}
if(instruction.type===VCD_NOOP){
continue;
}else if(instruction.type===VCD_ADD){
addRunDataStream.copyToFile2(tempFile, addRunDataIndex+targetWindowPosition, size);
addRunDataIndex += size;
}else if(instruction.type===VCD_COPY){
var addr = cache.decodeAddress(addRunDataIndex+winHeader.sourceLength, instruction.mode);
var absAddr = 0;
// source segment and target segment are treated as if they're concatenated
var sourceData = null;
if(addr < winHeader.sourceLength){
absAddr = winHeader.sourcePosition + addr;
if(winHeader.indicator & VCD_SOURCE){
sourceData = romFile;
}else if(winHeader.indicator & VCD_TARGET){
sourceData = tempFile;
}
}else{
absAddr = targetWindowPosition + (addr - winHeader.sourceLength);
sourceData = tempFile;
}
while(size--){
tempFile._u8array[targetWindowPosition + addRunDataIndex++]=sourceData._u8array[absAddr++];
//targetU8[targetWindowPosition + targetWindowOffs++] = copySourceU8[absAddr++];
}
//to-do: test
//sourceData.copyToFile2(tempFile, absAddr, size, targetWindowPosition + addRunDataIndex);
//addRunDataIndex += size;
}else if(instruction.type===VCD_RUN){
var runByte = addRunDataStream.readU8();
var offset = targetWindowPosition + addRunDataIndex;
for(var j=0; j<size; j++){
tempFile._u8array[offset+j]=runByte;
}
addRunDataIndex += size;
}else{
throw new Error('invalid instruction type found');
}
}
}
if(validate && winHeader.adler32 && (winHeader.adler32 !== adler32(tempFile, targetWindowPosition, winHeader.targetWindowLength))){
throw new Error('error_crc_output');
}
parser.skip(winHeader.addRunDataLength + winHeader.addressesLength + winHeader.instructionsLength);
targetWindowPosition += winHeader.targetWindowLength;
}
//console.log((performance.now()-t0)/1000);
return tempFile;
}
function parseVCDIFF(file){
return new VCDIFF(file);
}
function VCDIFF_Parser(marcFile, offset)
{
MarcFile.call(this, 0);
this.fileSize=marcFile.fileSize;
this._u8array=marcFile._u8array;
this._dataView=marcFile._dataView;
this.offset=offset || 0;
}
VCDIFF_Parser.prototype=Object.create(MarcFile.prototype);
VCDIFF_Parser.prototype.read7BitEncodedInt=function(){
var num=0, bits = 0;
do {
bits = this.readU8();
num = (num << 7) + (bits & 0x7f);
} while(bits & 0x80);
return num;
}
VCDIFF_Parser.prototype.decodeWindowHeader=function(){
var windowHeader={
indicator:this.readU8(),
sourceLength:0,
sourcePosition:0,
adler32:false
};
if(windowHeader.indicator & (VCD_SOURCE | VCD_TARGET)){
windowHeader.sourceLength = this.read7BitEncodedInt();
windowHeader.sourcePosition = this.read7BitEncodedInt();
}
windowHeader.deltaLength = this.read7BitEncodedInt();
windowHeader.targetWindowLength = this.read7BitEncodedInt();
windowHeader.deltaIndicator = this.readU8(); // secondary compression: 1=VCD_DATACOMP,2=VCD_INSTCOMP,4=VCD_ADDRCOMP
if(windowHeader.deltaIndicator!==0){
throw new Error('unimplemented windowHeader.deltaIndicator:'+windowHeader.deltaIndicator);
}
windowHeader.addRunDataLength = this.read7BitEncodedInt();
windowHeader.instructionsLength = this.read7BitEncodedInt();
windowHeader.addressesLength = this.read7BitEncodedInt();
if(windowHeader.indicator & VCD_ADLER32){
windowHeader.adler32 = this.readU32();
}
return windowHeader;
}
VCDIFF_Parser.prototype.copyToFile2=function(target, targetOffset, len){
for(var i=0; i<len; i++){
target._u8array[targetOffset+i]=this._u8array[this.offset+i];
}
//this.file.copyToFile(target, this.offset, len, targetOffset);
this.skip(len);
}
//------------------------------------------------------
// hdrIndicator
const VCD_DECOMPRESS = 0x01;
const VCD_CODETABLE = 0x02;
const VCD_APPHEADER = 0x04; // nonstandard?
// winIndicator
const VCD_SOURCE = 0x01;
const VCD_TARGET = 0x02;
const VCD_ADLER32 = 0x04;
function VCD_Instruction(instruction, size, mode){
this.type=instruction;
this.size=size;
this.mode=mode;
}
/*
build the default code table (used to encode/decode instructions) specified in RFC 3284
heavily based on
https://github.com/vic-alexiev/TelerikAcademy/blob/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff/CodeTable.cs
*/
const VCD_NOOP=0;
const VCD_ADD=1;
const VCD_RUN=2;
const VCD_COPY=3;
const VCD_DEFAULT_CODE_TABLE=(function(){
var entries=[];
var empty = {type: VCD_NOOP, size: 0, mode: 0};
// 0
entries.push([{type: VCD_RUN, size: 0, mode: 0}, empty]);
// 1,18
for(var size=0; size<18; size++){
entries.push([{type: VCD_ADD, size: size, mode: 0}, empty]);
}
// 19,162
for(var mode=0; mode<9; mode++){
entries.push([{type: VCD_COPY, size: 0, mode: mode}, empty]);
for(var size=4; size<19; size++){
entries.push([{type: VCD_COPY, size: size, mode: mode}, empty]);
}
}
// 163,234
for(var mode=0; mode<6; mode++){
for(var addSize=1; addSize<5; addSize++){
for(var copySize=4; copySize<7; copySize++){
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
{type: VCD_COPY, size: copySize, mode: mode}]);
}
}
}
// 235,246
for(var mode=6; mode<9; mode++){
for(var addSize=1; addSize<5; addSize++){
entries.push([{type: VCD_ADD, size: addSize, mode: 0},
{type: VCD_COPY, size: 4, mode: mode}]);
}
}
// 247,255
for(var mode=0; mode<9; mode++){
entries.push([{type: VCD_COPY, size: 4, mode: mode},
{type: VCD_ADD, size: 1, mode: 0}]);
}
return entries;
})();
/*
ported from https://github.com/vic-alexiev/TelerikAcademy/tree/master/C%23%20Fundamentals%20II/Homework%20Assignments/3.%20Methods/000.%20MiscUtil/Compression/Vcdiff
by Victor Alexiev (https://github.com/vic-alexiev)
*/
const VCD_MODE_SELF=0;
const VCD_MODE_HERE=1;
function VCD_AdressCache(nearSize, sameSize){
this.nearSize=nearSize;
this.sameSize=sameSize;
this.near=new Array(nearSize);
this.same=new Array(sameSize*256);
}
VCD_AdressCache.prototype.reset=function(addressStream){
this.nextNearSlot=0;
this.near.fill(0);
this.same.fill(0);
this.addressStream=addressStream;
}
VCD_AdressCache.prototype.decodeAddress=function(here, mode){
var address=0;
if(mode===VCD_MODE_SELF){
address=this.addressStream.read7BitEncodedInt();
}else if(mode===VCD_MODE_HERE){
address=here-this.addressStream.read7BitEncodedInt();
}else if(mode-2<this.nearSize){ //near cache
address=this.near[mode-2]+this.addressStream.read7BitEncodedInt();
}else{ //same cache
var m=mode-(2+this.nearSize);
address=this.same[m*256+this.addressStream.readU8()];
}
this.update(address);
return address;
}
VCD_AdressCache.prototype.update=function(address){
if(this.nearSize>0){
this.near[this.nextNearSlot]=address;
this.nextNearSlot=(this.nextNearSlot+1)%this.nearSize;
}
if(this.sameSize>0){
this.same[address%(this.sameSize*256)]=address;
}
}

83
js/formats/zip.js Normal file
View file

@ -0,0 +1,83 @@
/* ZIP module for Rom Patcher JS v20190531 - Marc Robledo 2016-2019 - http://www.marcrobledo.com/license */
const ZIP_MAGIC='\x50\x4b\x03\x04';
function parseZIPFile(zipFile, unzipEntryName){
var regex=(zipFile===patchFile)? /\.(ips|ups|bps|aps|rup|ppf|xdelta)$/i : /\.(\w+)$/i;
setMessage('apply', _('unzipping'), 'loading');
var arrayBuffer=zipFile._u8array.buffer;
zip.createReader(
new zip.BlobReader(new Blob([arrayBuffer])),
function(zipReader){
zipReader.getEntries(function(zipEntries){
var zippedFiles=[];
for(var i=0; i<zipEntries.length; i++){
if(typeof unzipEntryName==='string' && unzipEntryName === zipEntries[i].filename){
zippedFiles=[zipEntries[i]];
break;
}else if(regex.test(zipEntries[i].filename)){
zippedFiles.push(zipEntries[i]);
}
}
if(zippedFiles.length>1){
var zipOverlay=document.createElement('div');
zipOverlay.className='zip-overlay';
var zipDialog=document.createElement('div');
zipDialog.className='zip-dialog';
var zipList=document.createElement('ul');
zipList.className='zipped-files'
for(var i=0; i<zippedFiles.length; i++){
var li=document.createElement('li');
li.zipEntry=zippedFiles[i];
li.zipEntry.originalZipFile=zipFile;
li.innerHTML=zippedFiles[i].filename;
addEvent(li, 'click', _evtClickZipEntry);
zipList.appendChild(li);
}
zipDialog.innerHTML=_('patch_file');
zipDialog.appendChild(zipList);
zipOverlay.appendChild(zipDialog);
document.body.appendChild(zipOverlay);
}else if(zippedFiles.length===1){
zippedFiles[0].originalZipFile=zipFile;
unzipEntry(zippedFiles[0]);
}else{
if(zipFile===romFile){
romFile=null;
}else{
patchFile=null;
}
}
setTabApplyEnabled(true);
});
},
function(zipReader){
setTabApplyEnabled(true);
setMessage('apply', _('error_unzipping'), 'error');
}
);
}
function unzipEntry(zipEntry){
zipEntry.getData(new zip.BlobWriter(), function(blob){
var fileReader=new FileReader();
fileReader.onload=function(){
var unzippedFile=new MarcFile(this.result);
unzippedFile.fileName=zipEntry.filename;
if(zipEntry.originalZipFile===romFile){
romFile=unzippedFile;
_parseROM();
}else if(zipEntry.originalZipFile===patchFile){
patchFile=unzippedFile;
_readPatchFile();
}
};
fileReader.readAsArrayBuffer(blob);
});
}
function _evtClickZipEntry(evt){
document.body.removeChild(this.parentElement.parentElement.parentElement);
unzipEntry(this.zipEntry);
}