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

266
js/MarcFile.js Normal file
View file

@ -0,0 +1,266 @@
/* MODDED VERSION OF MarcFile.js v20181020 - Marc Robledo 2014-2018 - http://www.marcrobledo.com/license */
function MarcFile(source, onLoad){
if(typeof source==='object' && source.files) /* get first file only if source is input with multiple files */
source=source.files[0];
this.littleEndian=false;
this.offset=0;
this._lastRead=null;
if(typeof source==='object' && source.name && source.size){ /* source is file */
if(typeof window.FileReader!=='function')
throw new Error('Incompatible Browser');
this.fileName=source.name;
this.fileType=source.type;
this.fileSize=source.size;
this._fileReader=new FileReader();
this._fileReader.marcFile=this;
this._fileReader.addEventListener('load',function(){
this.marcFile._u8array=new Uint8Array(this.result);
this.marcFile._dataView=new DataView(this.result);
if(onLoad)
onLoad.call();
},false);
this._fileReader.readAsArrayBuffer(source);
}else if(typeof source==='object' && typeof source.fileName==='string' && typeof source.littleEndian==='boolean'){ /* source is MarcFile */
this.fileName=source.fileName;
this.fileType=source.fileType;
this.fileSize=source.fileSize;
var ab=new ArrayBuffer(source);
this._u8array=new Uint8Array(this.fileType);
this._dataView=new DataView(this.fileType);
source.copyToFile(this, 0);
if(onLoad)
onLoad.call();
}else if(typeof source==='object' && typeof source.byteLength==='number'){ /* source is ArrayBuffer or TypedArray */
this.fileName='file.bin';
this.fileType='application/octet-stream';
this.fileSize=source.byteLength;
if(typeof source.buffer !== 'undefined')
source=source.buffer;
this._u8array=new Uint8Array(source);
this._dataView=new DataView(source);
if(onLoad)
onLoad.call();
}else if(typeof source==='number'){ /* source is integer (new empty file) */
this.fileName='file.bin';
this.fileType='application/octet-stream';
this.fileSize=source;
var ab=new ArrayBuffer(source);
this._u8array=new Uint8Array(ab);
this._dataView=new DataView(ab);
if(onLoad)
onLoad.call();
}else{
throw new Error('Invalid source');
}
}
MarcFile.IS_MACHINE_LITTLE_ENDIAN=(function(){ /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness */
var buffer=new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
// Int16Array uses the platform's endianness.
return new Int16Array(buffer)[0] === 256;
})();
MarcFile.prototype.seek=function(offset){
this.offset=offset;
}
MarcFile.prototype.skip=function(nBytes){
this.offset+=nBytes;
}
MarcFile.prototype.isEOF=function(){
return !(this.offset<this.fileSize)
}
MarcFile.prototype.slice=function(offset, len){
len=len || (this.fileSize-offset);
var newFile;
if(typeof this._u8array.buffer.slice!=='undefined'){
newFile=new MarcFile(0);
newFile.fileSize=len;
newFile._u8array=new Uint8Array(this._u8array.buffer.slice(offset, offset+len));
}else{
newFile=new MarcFile(len);
this.copyToFile(newFile, offset, len, 0);
}
newFile.fileName=this.fileName;
newFile.fileType=this.fileType;
newFile.littleEndian=this.littleEndian;
return newFile;
}
MarcFile.prototype.copyToFile=function(target, offsetSource, len, offsetTarget){
if(typeof offsetTarget==='undefined')
offsetTarget=offsetSource;
len=len || (this.fileSize-offsetSource);
for(var i=0; i<len; i++){
target._u8array[offsetTarget+i]=this._u8array[offsetSource+i];
}
}
MarcFile.prototype.save=function(){
var blob;
try{
blob=new Blob([this._u8array],{type:this.fileType});
}catch(e){
//old browser, use BlobBuilder
window.BlobBuilder=window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
if(e.name==='InvalidStateError' && window.BlobBuilder){
var bb=new BlobBuilder();
bb.append(this._u8array.buffer);
blob=bb.getBlob(this.fileType);
}else{
throw new Error('Incompatible Browser');
return false;
}
}
saveAs(blob,this.fileName);
}
MarcFile.prototype.readU8=function(){
this._lastRead=this._u8array[this.offset];
this.offset++;
return this._lastRead
}
MarcFile.prototype.readU16=function(){
if(this.littleEndian)
this._lastRead=this._u8array[this.offset] + (this._u8array[this.offset+1] << 8);
else
this._lastRead=(this._u8array[this.offset] << 8) + this._u8array[this.offset+1];
this.offset+=2;
return this._lastRead >>> 0
}
MarcFile.prototype.readU24=function(){
if(this.littleEndian)
this._lastRead=this._u8array[this.offset] + (this._u8array[this.offset+1] << 8) + (this._u8array[this.offset+2] << 16);
else
this._lastRead=(this._u8array[this.offset] << 16) + (this._u8array[this.offset+1] << 8) + this._u8array[this.offset+2];
this.offset+=3;
return this._lastRead >>> 0
}
MarcFile.prototype.readU32=function(){
if(this.littleEndian)
this._lastRead=this._u8array[this.offset] + (this._u8array[this.offset+1] << 8) + (this._u8array[this.offset+2] << 16) + (this._u8array[this.offset+3] << 24);
else
this._lastRead=(this._u8array[this.offset] << 24) + (this._u8array[this.offset+1] << 16) + (this._u8array[this.offset+2] << 8) + this._u8array[this.offset+3];
this.offset+=4;
return this._lastRead >>> 0
}
MarcFile.prototype.readBytes=function(len){
this._lastRead=new Array(len);
for(var i=0; i<len; i++){
this._lastRead[i]=this._u8array[this.offset+i];
}
this.offset+=len;
return this._lastRead
}
MarcFile.prototype.readString=function(len){
this._lastRead='';
for(var i=0;i<len && (this.offset+i)<this.fileSize && this._u8array[this.offset+i]>0;i++)
this._lastRead=this._lastRead+String.fromCharCode(this._u8array[this.offset+i]);
this.offset+=len;
return this._lastRead
}
MarcFile.prototype.writeU8=function(u8){
this._u8array[this.offset]=u8;
this.offset++;
}
MarcFile.prototype.writeU16=function(u16){
if(this.littleEndian){
this._u8array[this.offset]=u16 & 0xff;
this._u8array[this.offset+1]=u16 >> 8;
}else{
this._u8array[this.offset]=u16 >> 8;
this._u8array[this.offset+1]=u16 & 0xff;
}
this.offset+=2;
}
MarcFile.prototype.writeU24=function(u24){
if(this.littleEndian){
this._u8array[this.offset]=u24 & 0x0000ff;
this._u8array[this.offset+1]=(u24 & 0x00ff00) >> 8;
this._u8array[this.offset+2]=(u24 & 0xff0000) >> 16;
}else{
this._u8array[this.offset]=(u24 & 0xff0000) >> 16;
this._u8array[this.offset+1]=(u24 & 0x00ff00) >> 8;
this._u8array[this.offset+2]=u24 & 0x0000ff;
}
this.offset+=3;
}
MarcFile.prototype.writeU32=function(u32){
if(this.littleEndian){
this._u8array[this.offset]=u32 & 0x000000ff;
this._u8array[this.offset+1]=(u32 & 0x0000ff00) >> 8;
this._u8array[this.offset+2]=(u32 & 0x00ff0000) >> 16;
this._u8array[this.offset+3]=(u32 & 0xff000000) >> 24;
}else{
this._u8array[this.offset]=(u32 & 0xff000000) >> 24;
this._u8array[this.offset+1]=(u32 & 0x00ff0000) >> 16;
this._u8array[this.offset+2]=(u32 & 0x0000ff00) >> 8;
this._u8array[this.offset+3]=u32 & 0x000000ff;
}
this.offset+=4;
}
MarcFile.prototype.writeBytes=function(a){
for(var i=0;i<a.length;i++)
this._u8array[this.offset+i]=a[i]
this.offset+=a.length;
}
MarcFile.prototype.writeString=function(str,len){
len=len || str.length;
for(var i=0;i<str.length && i<len;i++)
this._u8array[this.offset+i]=str.charCodeAt(i);
for(;i<len;i++)
this._u8array[this.offset+i]=0x00;
this.offset+=len;
}

658
js/RomPatcher.js Normal file
View file

@ -0,0 +1,658 @@
/* Rom Patcher JS v20200502 - Marc Robledo 2016-2020 - http://www.marcrobledo.com/license */
const TOO_BIG_ROM_SIZE=67108863;
const HEADERS_INFO=[
[/\.nes$/, 16, 1024], //interNES
[/\.fds$/, 16, 65500], //fwNES
[/\.lnx$/, 64, 1024],
//[/\.rom$/, 8192, 1024], //jaguar
[/\.(pce|nes|gbc?|smc|sfc|fig|swc)$/, 512, 1024]
];
/* service worker */
const FORCE_HTTPS=true;
if(FORCE_HTTPS && location.protocol==='http:')
location.href=window.location.href.replace('http:','https:');
else if(location.protocol==='https:' && 'serviceWorker' in navigator)
navigator.serviceWorker.register('/RomPatcher.js/_cache_service_worker.js', {scope: '/RomPatcher.js/'});
var romFile, patchFile, patch, romFile1, romFile2, tempFile, headerSize, oldHeader;
var fetchedPatches;
var userLanguage;
var CAN_USE_WEB_WORKERS=true;
var webWorkerApply,webWorkerCreate,webWorkerCrc;
try{
webWorkerApply=new Worker('./js/worker_apply.js');
webWorkerApply.onmessage = event => { // listen for events from the worker
//retrieve arraybuffers back from webworker
if(!el('checkbox-removeheader').checked && !el('checkbox-addheader').checked){ //when adding/removing header we don't need the arraybuffer back since we made a copy previously
romFile._u8array=event.data.romFileU8Array;
romFile._dataView=new DataView(romFile._u8array.buffer);
}
patchFile._u8array=event.data.patchFileU8Array;
patchFile._dataView=new DataView(patchFile._u8array.buffer);
if(event.data.patchedRomU8Array)
preparePatchedRom(romFile, new MarcFile(event.data.patchedRomU8Array.buffer), headerSize);
setTabApplyEnabled(true);
if(event.data.errorMessage)
setMessage('apply', _(event.data.errorMessage.replace('Error: ','')), 'error');
else
setMessage('apply');
};
webWorkerApply.onerror = event => { // listen for events from the worker
setTabApplyEnabled(true);
setMessage('apply', _(event.message.replace('Error: ','')), 'error');
};
webWorkerCreate=new Worker('./js/worker_create.js');
webWorkerCreate.onmessage = event => { // listen for events from the worker
var newPatchFile=new MarcFile(event.data.patchFileU8Array);
newPatchFile.fileName=romFile2.fileName.replace(/\.[^\.]+$/,'')+'.'+el('select-patch-type').value;
newPatchFile.save();
setMessage('create');
setTabCreateEnabled(true);
};
webWorkerCreate.onerror = event => { // listen for events from the worker
setTabCreateEnabled(true);
setMessage('create', _(event.message.replace('Error: ','')), 'error');
};
webWorkerCrc=new Worker('./js/worker_crc.js');
webWorkerCrc.onmessage = event => { // listen for events from the worker
//console.log('received_crc');
el('crc32').innerHTML=padZeroes(event.data.crc32, 4);
el('md5').innerHTML=padZeroes(event.data.md5, 16);
romFile._u8array=event.data.u8array;
romFile._dataView=new DataView(event.data.u8array.buffer);
if(window.crypto&&window.crypto.subtle&&window.crypto.subtle.digest){
sha1(romFile);
}
validateSource();
setTabApplyEnabled(true);
};
webWorkerCrc.onerror = event => { // listen for events from the worker
setMessage('apply', event.message.replace('Error: ',''), 'error');
};
}catch(e){
CAN_USE_WEB_WORKERS=false;
}
/* Shortcuts */
function addEvent(e,ev,f){e.addEventListener(ev,f,false)}
function el(e){return document.getElementById(e)}
function _(str){return userLanguage[str] || str}
function fetchPatch(uri){
setTabApplyEnabled(false);
setMessage('apply', _('downloading'), 'loading');
var isCompressed=/\#/.test(uri);
var patchURI=decodeURI(uri.replace(/\#.*?$/, ''));
//console.log(patchURI);
var compressedName=uri.replace(/^.*?\#/,'');
//console.log(compressedName);
if(typeof window.fetch==='function'){
fetch(patchURI)
.then(result => result.arrayBuffer()) // Gets the response and returns it as a blob
.then(arrayBuffer => {
fetchedPatches[patchURI]=patchFile=new MarcFile(arrayBuffer);
fetchedPatches[patchURI].fileName=patchURI.replace(/^(.*?\/)+/g, '');
_readPatchFile();
})
.catch(function(evt){
setMessage('apply', _('error_downloading'), 'error');
//setMessage('apply', evt.message, 'error');
});
}else{
var xhr=new XMLHttpRequest();
xhr.open('GET', patchURI, true);
xhr.responseType='arraybuffer';
xhr.onload=function(evt){
if(this.status===200){
fetchedPatches[patchURI]=patchFile=new MarcFile(xhr.response);
fetchedPatches[patchURI].fileName=patchURI.replace(/^(.*?\/)+/g, '');
_readPatchFile();
}else{
setMessage('apply', _('error_downloading')+' ('+this.status+')', 'error');
}
};
xhr.onerror=function(evt){
setMessage('apply', _('error_downloading'), 'error');
};
xhr.send(null);
}
}
function _parseROM(){
el('checkbox-addheader').checked=false;
el('checkbox-removeheader').checked=false;
if(romFile.readString(4).startsWith(ZIP_MAGIC)){
parseZIPFile(romFile);
setTabApplyEnabled(false);
}else{
if(headerSize=canHaveFakeHeader(romFile)){
el('row-addheader').style.display='flex';
if(headerSize<1024){
el('headersize').innerHTML=headerSize+'b';
}else{
el('headersize').innerHTML=parseInt(headerSize/1024)+'kb';
}
el('row-removeheader').style.display='none';
}else if(headerSize=hasHeader(romFile)){
el('row-addheader').style.display='none';
el('row-removeheader').style.display='flex';
}else{
el('row-addheader').style.display='none';
el('row-removeheader').style.display='none';
}
updateChecksums(romFile, 0);
}
}
/* initialize app */
addEvent(window,'load',function(){
/* zip-js web worker */
if(CAN_USE_WEB_WORKERS){
zip.useWebWorkers=true;
zip.workerScriptsPath='./js/zip.js/';
}else{
zip.useWebWorkers=false;
var script=document.createElement('script');
script.src='./js/zip.js/inflate.js';
document.getElementsByTagName('head')[0].appendChild(script);
}
/* language */
var langCode=(navigator.language || navigator.userLanguage).substr(0,2);
if(typeof LOCALIZATION[langCode]==='object'){
userLanguage=LOCALIZATION[langCode];
}else{
userLanguage=LOCALIZATION.en;
}
var translatableElements=document.querySelectorAll('*[data-localize]');
for(var i=0; i<translatableElements.length; i++){
translatableElements[i].innerHTML=_(translatableElements[i].dataset.localize);
}
el('row-file-patch').title=_('compatible_formats')+' IPS, UPS, APS, BPS, RUP, PPF, MOD (Paper Mario Star Rod), xdelta';
el('input-file-rom').value='';
el('input-file-patch').value='';
setTabApplyEnabled(true);
addEvent(el('input-file-rom'), 'change', function(){
setTabApplyEnabled(false);
romFile=new MarcFile(this, _parseROM);
});
/* predefined patches */
if(typeof PREDEFINED_PATCHES!=='undefined'){
fetchedPatches={};
var container=el('input-file-patch').parentElement;
container.removeChild(el('input-file-patch'));
var select=document.createElement('select');
select.id='input-file-patch';
for(var i=0; i<PREDEFINED_PATCHES.length; i++){
var option=document.createElement('option');
option.value=PREDEFINED_PATCHES[i].patch;
option.innerHTML=PREDEFINED_PATCHES[i].name;
select.appendChild(option);
}
container.appendChild(select)
container.parentElement.title='';
addEvent(select,'change',function(){
if(fetchedPatches[this.value.replace(/\#.*?$/, '')]){
patchFile=fetchedPatches[this.value.replace(/\#.*?$/, '')];
patchFile.seek(0);
_readPatchFile();
}else{
patch=null;
patchFile=null;
fetchPatch(this.value);
}
});
fetchPatch(select.value);
}else{
setTabCreateEnabled(true);
el('input-file-rom1').value='';
el('input-file-rom2').value='';
el('switch-container').style.visibility='visible';
addEvent(el('input-file-patch'), 'change', function(){
setTabApplyEnabled(false);
patchFile=new MarcFile(this, _readPatchFile)
});
addEvent(el('input-file-rom1'), 'change', function(){
setTabCreateEnabled(false);
romFile1=new MarcFile(this, function(){setTabCreateEnabled(true)});
});
addEvent(el('input-file-rom2'), 'change', function(){
setTabCreateEnabled(false);
romFile2=new MarcFile(this, function(){setTabCreateEnabled(true)});
});
}
addEvent(el('checkbox-removeheader'), 'change', function(){
if(this.checked)
updateChecksums(romFile, headerSize);
else
updateChecksums(romFile, 0);
});
//setCreatorMode(true);
});
function canHaveFakeHeader(romFile){
if(romFile.fileSize<=0x600000){
for(var i=0; i<HEADERS_INFO.length; i++){
if(HEADERS_INFO[i][0].test(romFile.fileName) && (romFile.fileSize%HEADERS_INFO[i][2]===0)){
return HEADERS_INFO[i][1];
}
}
}
return 0;
}
function hasHeader(romFile){
if(romFile.fileSize<=0x600200){
if(romFile.fileSize%1024===0)
return 0;
for(var i=0; i<HEADERS_INFO.length; i++){
if(HEADERS_INFO[i][0].test(romFile.fileName) && (romFile.fileSize-HEADERS_INFO[i][1])%HEADERS_INFO[i][1]===0){
return HEADERS_INFO[i][1];
}
}
}
return 0;
}
function updateChecksums(file, startOffset, force){
if(file===romFile && file.fileSize>33554432 && !force){
el('crc32').innerHTML='File is too big. <span onclick=\"updateChecksums(romFile,'+startOffset+',true)\">Force calculate checksum</span>';
el('md5').innerHTML='';
el('sha1').innerHTML='';
setTabApplyEnabled(true);
return false;
}
el('crc32').innerHTML='Calculating...';
el('md5').innerHTML='Calculating...';
if(CAN_USE_WEB_WORKERS){
setTabApplyEnabled(false);
webWorkerCrc.postMessage({u8array:file._u8array, startOffset:startOffset}, [file._u8array.buffer]);
if(window.crypto&&window.crypto.subtle&&window.crypto.subtle.digest){
el('sha1').innerHTML='Calculating...';
}
}else{
window.setTimeout(function(){
el('crc32').innerHTML=padZeroes(crc32(file, startOffset), 4);
el('md5').innerHTML=padZeroes(md5(file, startOffset), 16);
validateSource();
setTabApplyEnabled(true);
}, 30);
if(window.crypto&&window.crypto.subtle&&window.crypto.subtle.digest){
el('sha1').innerHTML='Calculating...';
sha1(file);
}
}
}
function validateSource(){
if(patch && romFile && typeof patch.validateSource !== 'undefined'){
if(patch.validateSource(romFile, el('checkbox-removeheader').checked && hasHeader(romFile))){
el('crc32').className='valid';
setMessage('apply');
}else{
el('crc32').className='invalid';
setMessage('apply', _('error_crc_input'), 'warning');
}
}else{
el('crc32').className='';
setMessage('apply');
}
}
function _readPatchFile(){
setTabApplyEnabled(false);
patchFile.littleEndian=false;
var header=patchFile.readString(6);
if(header.startsWith(ZIP_MAGIC)){
if(typeof PREDEFINED_PATCHES !== 'undefined' && /\#/.test(el('input-file-patch').value)){
parseZIPFile(patchFile, el('input-file-patch').value.replace(/^.*?\#/, ''));
}else{
parseZIPFile(patchFile);
}
patch=false;
validateSource();
setTabApplyEnabled(false);
}else{
if(header.startsWith(IPS_MAGIC)){
patch=parseIPSFile(patchFile);
}else if(header.startsWith(UPS_MAGIC)){
patch=parseUPSFile(patchFile);
}else if(header.startsWith(APS_MAGIC)){
patch=parseAPSFile(patchFile);
}else if(header.startsWith(BPS_MAGIC)){
patch=parseBPSFile(patchFile);
}else if(header.startsWith(RUP_MAGIC)){
patch=parseRUPFile(patchFile);
}else if(header.startsWith(PPF_MAGIC)){
patch=parsePPFFile(patchFile);
}else if(header.startsWith(PMSR_MAGIC)){
patch=parseMODFile(patchFile);
}else if(header.startsWith(VCDIFF_MAGIC)){
patch=parseVCDIFF(patchFile);
}else{
patch=null;
setMessage('apply', _('error_invalid_patch'), 'error');
}
if(patch && typeof PREDEFINED_PATCHES!=='undefined' && PREDEFINED_PATCHES[el('input-file-patch').selectedIndex].crc){
patch.validateSource=function(romFile, headerSize){
return PREDEFINED_PATCHES[el('input-file-patch').selectedIndex].crc===crc32(romFile, headerSize)
}
}
validateSource();
setTabApplyEnabled(true);
}
}
function preparePatchedRom(originalRom, patchedRom, headerSize){
patchedRom.fileName=originalRom.fileName.replace(/\.([^\.]*?)$/, ' (patched).$1');
patchedRom.fileType=originalRom.fileType;
if(headerSize){
if(el('checkbox-removeheader').checked){
var patchedRomWithOldHeader=new MarcFile(headerSize+patchedRom.fileSize);
oldHeader.copyToFile(patchedRomWithOldHeader, 0);
patchedRom.copyToFile(patchedRomWithOldHeader, 0, patchedRom.fileSize, headerSize);
patchedRomWithOldHeader.fileName=patchedRom.fileName;
patchedRomWithOldHeader.fileType=patchedRom.fileType;
patchedRom=patchedRomWithOldHeader;
}else if(el('checkbox-addheader').checked){
patchedRom=patchedRom.slice(headerSize);
}
}
setMessage('apply');
patchedRom.save();
//debug: create unheadered patch
/*if(headerSize && el('checkbox-addheader').checked){
createPatch(romFile, patchedRom);
}*/
}
/*function removeHeader(romFile){
//r._dataView=new DataView(r._dataView.buffer, headerSize);
oldHeader=romFile.slice(0,headerSize);
r=r.slice(headerSize);
}*/
function applyPatch(p,r,validateChecksums){
if(p && r){
if(headerSize){
if(el('checkbox-removeheader').checked){
//r._dataView=new DataView(r._dataView.buffer, headerSize);
oldHeader=r.slice(0,headerSize);
r=r.slice(headerSize);
}else if(el('checkbox-addheader').checked){
var romWithFakeHeader=new MarcFile(headerSize+r.fileSize);
romWithFakeHeader.fileName=r.fileName;
romWithFakeHeader.fileType=r.fileType;
r.copyToFile(romWithFakeHeader, 0, r.fileSize, headerSize);
//add FDS header
if(/\.fds$/.test(r.FileName) && r.fileSize%65500===0){
//romWithFakeHeader.seek(0);
romWithFakeHeader.writeBytes([0x46, 0x44, 0x53, 0x1a, r.fileSize/65500]);
}
r=romWithFakeHeader;
}
}
if(CAN_USE_WEB_WORKERS){
setMessage('apply', _('applying_patch'), 'loading');
setTabApplyEnabled(false);
webWorkerApply.postMessage(
{
romFileU8Array:r._u8array,
patchFileU8Array:patchFile._u8array,
validateChecksums:validateChecksums
},[
r._u8array.buffer,
patchFile._u8array.buffer
]
);
}else{
setMessage('apply', _('applying_patch'), 'loading');
try{
p.apply(r, validateChecksums);
preparePatchedRom(r, p.apply(r, validateChecksums), headerSize);
}catch(e){
setMessage('apply', 'Error: '+_(e.message), 'error');
}
}
}else{
setMessage('apply', 'No ROM/patch selected', 'error');
}
}
function createPatch(sourceFile, modifiedFile, mode){
if(!sourceFile){
setMessage('create', 'No source ROM file specified.', 'error');
return false;
}else if(!modifiedFile){
setMessage('create', 'No modified ROM file specified.', 'error');
return false;
}
if(CAN_USE_WEB_WORKERS){
setTabCreateEnabled(false);
setMessage('create', _('creating_patch'), 'loading');
webWorkerCreate.postMessage(
{
sourceFileU8Array:sourceFile._u8array,
modifiedFileU8Array:modifiedFile._u8array,
modifiedFileName:modifiedFile.fileName,
patchMode:mode
},[
sourceFile._u8array.buffer,
modifiedFile._u8array.buffer
]
);
romFile1=new MarcFile(el('input-file-rom1'));
romFile2=new MarcFile(el('input-file-rom2'));
}else{
try{
sourceFile.seek(0);
modifiedFile.seek(0);
var newPatch;
if(mode==='ips'){
newPatch=createIPSFromFiles(sourceFile, modifiedFile);
}else if(mode==='bps'){
newPatch=createBPSFromFiles(sourceFile, modifiedFile, (sourceFile.fileSize<=4194304));
}else if(mode==='ups'){
newPatch=createUPSFromFiles(sourceFile, modifiedFile);
}else if(mode==='aps'){
newPatch=createAPSFromFiles(sourceFile, modifiedFile);
}else if(mode==='rup'){
newPatch=createRUPFromFiles(sourceFile, modifiedFile);
}else{
setMessage('create', _('error_invalid_patch'), 'error');
}
if(crc32(modifiedFile)===crc32(newPatch.apply(sourceFile))){
newPatch.export(modifiedFile.fileName.replace(/\.[^\.]+$/,'')).save();
}else{
setMessage('create', 'Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.', 'error');
}
}catch(e){
setMessage('create', 'Error: '+_(e.message), 'error');
}
}
}
/* GUI functions */
function setMessage(tab, msg, className){
var messageBox=el('message-'+tab);
if(msg){
if(className==='loading'){
messageBox.className='message';
messageBox.innerHTML='<span class="loading"></span> '+msg;
}else{
messageBox.className='message '+className;
if(className==='warning')
messageBox.innerHTML='&#9888; '+msg;
else if(className==='error')
messageBox.innerHTML='&#10007; '+msg;
else
messageBox.innerHTML=msg;
}
messageBox.style.display='inline';
}else{
messageBox.style.display='none';
}
}
function setElementEnabled(element,status){
if(status){
el(element).className='enabled';
}else{
el(element).className='disabled';
}
el(element).disabled=!status;
}
function setTabCreateEnabled(status){
if(
(romFile1 && romFile1.fileSize>TOO_BIG_ROM_SIZE) ||
(romFile2 && romFile2.fileSize>TOO_BIG_ROM_SIZE)
){
setMessage('create',_('warning_too_big'),'warning');
}
setElementEnabled('input-file-rom1', status);
setElementEnabled('input-file-rom2', status);
setElementEnabled('select-patch-type', status);
if(romFile1 && romFile2 && status){
setElementEnabled('button-create', status);
}else{
setElementEnabled('button-create', false);
}
}
function setTabApplyEnabled(status){
setElementEnabled('input-file-rom', status);
setElementEnabled('input-file-patch', status);
if(romFile && status && (patch || typeof PREDEFINED_PATCHES!=='undefined')){
setElementEnabled('button-apply', status);
}else{
setElementEnabled('button-apply', false);
}
}
function setCreatorMode(creatorMode){
if(creatorMode){
el('tab0').style.display='none';
el('tab1').style.display='block';
el('switch-create').className='switch enabled'
}else{
el('tab0').style.display='block';
el('tab1').style.display='none';
el('switch-create').className='switch disabled'
}
}
/* FileSaver.js (source: http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js)
* A saveAs() FileSaver implementation.
* 1.3.8
* 2018-03-22 14:03:47
*
* By Eli Grey, https://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
var saveAs=saveAs||function(c){"use strict";if(!(void 0===c||"undefined"!=typeof navigator&&/MSIE [1-9]\./.test(navigator.userAgent))){var t=c.document,f=function(){return c.URL||c.webkitURL||c},s=t.createElementNS("http://www.w3.org/1999/xhtml","a"),d="download"in s,u=/constructor/i.test(c.HTMLElement)||c.safari,l=/CriOS\/[\d]+/.test(navigator.userAgent),p=c.setImmediate||c.setTimeout,v=function(t){p(function(){throw t},0)},w=function(t){setTimeout(function(){"string"==typeof t?f().revokeObjectURL(t):t.remove()},4e4)},m=function(t){return/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(t.type)?new Blob([String.fromCharCode(65279),t],{type:t.type}):t},r=function(t,n,e){e||(t=m(t));var r,o=this,a="application/octet-stream"===t.type,i=function(){!function(t,e,n){for(var r=(e=[].concat(e)).length;r--;){var o=t["on"+e[r]];if("function"==typeof o)try{o.call(t,n||t)}catch(t){v(t)}}}(o,"writestart progress write writeend".split(" "))};if(o.readyState=o.INIT,d)return r=f().createObjectURL(t),void p(function(){var t,e;s.href=r,s.download=n,t=s,e=new MouseEvent("click"),t.dispatchEvent(e),i(),w(r),o.readyState=o.DONE},0);!function(){if((l||a&&u)&&c.FileReader){var e=new FileReader;return e.onloadend=function(){var t=l?e.result:e.result.replace(/^data:[^;]*;/,"data:attachment/file;");c.open(t,"_blank")||(c.location.href=t),t=void 0,o.readyState=o.DONE,i()},e.readAsDataURL(t),o.readyState=o.INIT}r||(r=f().createObjectURL(t)),a?c.location.href=r:c.open(r,"_blank")||(c.location.href=r);o.readyState=o.DONE,i(),w(r)}()},e=r.prototype;return"undefined"!=typeof navigator&&navigator.msSaveOrOpenBlob?function(t,e,n){return e=e||t.name||"download",n||(t=m(t)),navigator.msSaveOrOpenBlob(t,e)}:(e.abort=function(){},e.readyState=e.INIT=0,e.WRITING=1,e.DONE=2,e.error=e.onwritestart=e.onprogress=e.onwrite=e.onabort=e.onerror=e.onwriteend=null,function(t,e,n){return new r(t,e||t.name||"download",n)})}}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||this);

105
js/crc.js Normal file
View file

@ -0,0 +1,105 @@
/* Rom Patcher JS - CRC32/MD5/SHA-1 calculators v20181017 - Marc Robledo 2016-2018 - http://www.marcrobledo.com/license */
function padZeroes(intVal, nBytes){
var hexString=intVal.toString(16);
while(hexString.length<nBytes*2)
hexString='0'+hexString;
return hexString
}
/* SHA-1 using WebCryptoAPI */
function _sha1_promise(hash){
var bytes=new Uint8Array(hash);
var hexString='';
for(var i=0;i<bytes.length;i++)
hexString+=padZeroes(bytes[i], 1);
el('sha1').innerHTML=hexString;
}
function sha1(marcFile){
window.crypto.subtle.digest('SHA-1', marcFile._u8array.buffer)
.then(_sha1_promise)
.catch(function(error){
el('sha1').innerHTML='Error';
})
;
}
/* MD5 - from Joseph's Myers - http://www.myersdaily.org/joseph/javascript/md5.js */
const HEX_CHR='0123456789abcdef'.split('');
function _add32(a,b){return (a+b)&0xffffffff}
function _md5cycle(x,k){var a=x[0],b=x[1],c=x[2],d=x[3];a=ff(a,b,c,d,k[0],7,-680876936);d=ff(d,a,b,c,k[1],12,-389564586);c=ff(c,d,a,b,k[2],17,606105819);b=ff(b,c,d,a,k[3],22,-1044525330);a=ff(a,b,c,d,k[4],7,-176418897);d=ff(d,a,b,c,k[5],12,1200080426);c=ff(c,d,a,b,k[6],17,-1473231341);b=ff(b,c,d,a,k[7],22,-45705983);a=ff(a,b,c,d,k[8],7,1770035416);d=ff(d,a,b,c,k[9],12,-1958414417);c=ff(c,d,a,b,k[10],17,-42063);b=ff(b,c,d,a,k[11],22,-1990404162);a=ff(a,b,c,d,k[12],7,1804603682);d=ff(d,a,b,c,k[13],12,-40341101);c=ff(c,d,a,b,k[14],17,-1502002290);b=ff(b,c,d,a,k[15],22,1236535329);a=gg(a,b,c,d,k[1],5,-165796510);d=gg(d,a,b,c,k[6],9,-1069501632);c=gg(c,d,a,b,k[11],14,643717713);b=gg(b,c,d,a,k[0],20,-373897302);a=gg(a,b,c,d,k[5],5,-701558691);d=gg(d,a,b,c,k[10],9,38016083);c=gg(c,d,a,b,k[15],14,-660478335);b=gg(b,c,d,a,k[4],20,-405537848);a=gg(a,b,c,d,k[9],5,568446438);d=gg(d,a,b,c,k[14],9,-1019803690);c=gg(c,d,a,b,k[3],14,-187363961);b=gg(b,c,d,a,k[8],20,1163531501);a=gg(a,b,c,d,k[13],5,-1444681467);d=gg(d,a,b,c,k[2],9,-51403784);c=gg(c,d,a,b,k[7],14,1735328473);b=gg(b,c,d,a,k[12],20,-1926607734);a=hh(a,b,c,d,k[5],4,-378558);d=hh(d,a,b,c,k[8],11,-2022574463);c=hh(c,d,a,b,k[11],16,1839030562);b=hh(b,c,d,a,k[14],23,-35309556);a=hh(a,b,c,d,k[1],4,-1530992060);d=hh(d,a,b,c,k[4],11,1272893353);c=hh(c,d,a,b,k[7],16,-155497632);b=hh(b,c,d,a,k[10],23,-1094730640);a=hh(a,b,c,d,k[13],4,681279174);d=hh(d,a,b,c,k[0],11,-358537222);c=hh(c,d,a,b,k[3],16,-722521979);b=hh(b,c,d,a,k[6],23,76029189);a=hh(a,b,c,d,k[9],4,-640364487);d=hh(d,a,b,c,k[12],11,-421815835);c=hh(c,d,a,b,k[15],16,530742520);b=hh(b,c,d,a,k[2],23,-995338651);a=ii(a,b,c,d,k[0],6,-198630844);d=ii(d,a,b,c,k[7],10,1126891415);c=ii(c,d,a,b,k[14],15,-1416354905);b=ii(b,c,d,a,k[5],21,-57434055);a=ii(a,b,c,d,k[12],6,1700485571);d=ii(d,a,b,c,k[3],10,-1894986606);c=ii(c,d,a,b,k[10],15,-1051523);b=ii(b,c,d,a,k[1],21,-2054922799);a=ii(a,b,c,d,k[8],6,1873313359);d=ii(d,a,b,c,k[15],10,-30611744);c=ii(c,d,a,b,k[6],15,-1560198380);b=ii(b,c,d,a,k[13],21,1309151649);a=ii(a,b,c,d,k[4],6,-145523070);d=ii(d,a,b,c,k[11],10,-1120210379);c=ii(c,d,a,b,k[2],15,718787259);b=ii(b,c,d,a,k[9],21,-343485551);x[0]=_add32(a,x[0]);x[1]=_add32(b,x[1]);x[2]=_add32(c,x[2]);x[3]=_add32(d,x[3])}
function _md5blk(d){var md5blks=[],i;for(i=0;i<64;i+=4)md5blks[i>>2]=d[i]+(d[i+1]<<8)+(d[i+2]<<16)+(d[i+3]<<24);return md5blks}
function _cmn(q,a,b,x,s,t){a=_add32(_add32(a,q),_add32(x,t));return _add32((a<<s)|(a>>>(32-s)),b)}
function ff(a,b,c,d,x,s,t){return _cmn((b&c)|((~b)&d),a,b,x,s,t)}
function gg(a,b,c,d,x,s,t){return _cmn((b&d)|(c&(~d)),a,b,x,s,t)}
function hh(a,b,c,d,x,s,t){return _cmn(b^c^d,a,b,x,s,t)}
function ii(a,b,c,d,x,s,t){return _cmn(c^(b|(~d)),a,b,x,s,t)}
function md5(marcFile, headerSize){
var data=headerSize? new Uint8Array(marcFile._u8array.buffer, headerSize):marcFile._u8array;
var n=data.length,state=[1732584193,-271733879,-1732584194,271733878],i;
for(i=64;i<=data.length;i+=64)
_md5cycle(state,_md5blk(data.slice(i-64,i)));
data=data.slice(i-64);
var tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
for(i=0;i<data.length;i++)
tail[i>>2]|=data[i]<<((i%4)<<3);
tail[i>>2]|=0x80<<((i%4)<<3);
if(i>55){
_md5cycle(state,tail);
for(i=0;i<16;i++)tail[i]=0;
}
tail[14]=n*8;
_md5cycle(state,tail);
for(var i=0;i<state.length;i++){
var s='',j=0;
for(;j<4;j++)
s+=HEX_CHR[(state[i]>>(j*8+4))&0x0f]+HEX_CHR[(state[i]>>(j*8))&0x0f];
state[i]=s;
}
return state.join('')
}
/* CRC32 - from Alex - https://stackoverflow.com/a/18639999 */
const CRC32_TABLE=(function(){
var c,crcTable=[];
for(var n=0;n<256;n++){
c=n;
for(var k=0;k<8;k++)
c=((c&1)?(0xedb88320^(c>>>1)):(c>>>1));
crcTable[n]=c;
}
return crcTable;
}());
function crc32(marcFile, headerSize, ignoreLast4Bytes){
var data=headerSize? new Uint8Array(marcFile._u8array.buffer, headerSize):marcFile._u8array;
var crc=0^(-1);
var len=ignoreLast4Bytes?data.length-4:data.length;
for(var i=0;i<len;i++)
crc=(crc>>>8)^CRC32_TABLE[(crc^data[i])&0xff];
return ((crc^(-1))>>>0);
}
/* Adler-32 - https://en.wikipedia.org/wiki/Adler-32#Example_implementation */
const ADLER32_MOD=0xfff1;
function adler32(marcFile, offset, len){
var a=1, b=0;
for(var i=0; i<len; i++){
a=(a+marcFile._u8array[i+offset])%ADLER32_MOD;
b=(b+a)%ADLER32_MOD;
}
return ((b<<16) | a)>>>0;
}

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);
}

110
js/locale.js Normal file
View file

@ -0,0 +1,110 @@
const LOCALIZATION={
'en':{
'creator_mode': 'Creator mode',
'apply_patch': 'Apply patch',
'rom_file': 'ROM file:',
'patch_file': 'Patch file:',
'remove_header': 'Remove header',
'add_header': 'Add temporary header',
'compatible_formats': 'Compatible formats:',
'applying_patch': 'Applying patch...',
'downloading': 'Downloading...',
'unzipping': 'Unzipping...',
'create_patch': 'Create patch:',
'original_rom': 'Original ROM:',
'modified_rom': 'Modified ROM:',
'patch_type': 'Patch type:',
'creating_patch': 'Creating patch...',
'error_crc_input': 'Source ROM checksum mismatch',
'error_crc_output': 'Target ROM checksum mismatch',
'error_crc_patch': 'Patch checksum mismatch',
'error_downloading': 'Error downloading patch',
'error_unzipping': 'Error unzipping file',
'error_invalid_patch': 'Invalid patch file',
'warning_too_big': 'Using big files is not recommended.'
},
'es':{
'creator_mode': 'Modo creador',
'apply_patch': 'Aplicar parche',
'rom_file': 'Archivo ROM:',
'patch_file': 'Archivo parche:',
'remove_header': 'Quitar cabecera',
'add_header': 'Añadir cabecera temporal',
'compatible_formats': 'Formatos compatibles:',
'applying_patch': 'Aplicando parche...',
'downloading': 'Descargando...',
'unzipping': 'Descomprimiendo...',
'create_patch': 'Crear parche',
'original_rom': 'ROM original:',
'modified_rom': 'ROM modificada:',
'patch_type': 'Tipo de parche:',
'creating_patch': 'Creando parche...',
'error_crc_input': 'Checksum de ROM original no válida',
'error_crc_output': 'Checksum de ROM creada no válida',
'error_crc_patch': 'Checksum de parche no válida',
'error_downloading': 'Error descargando parche',
'error_unzipping': 'Error descomprimiendo archivo',
'error_invalid_patch': 'Archivo de parche no válido',
'warning_too_big': 'No se recomienda usar archivos muy grandes.'
},
'ca':{
'creator_mode': 'Mode creador',
'apply_patch': 'Aplicar pedaç',
'rom_file': 'Arxiu ROM:',
'patch_file': 'Arxiu pedaç:',
'remove_header': 'Treure capçalera',
'add_header': 'Afegir capçalera temporal',
'compatible_formats': 'Formats compatibles:',
'applying_patch': 'Aplicant pedaç...',
'downloading': 'Descarregant...',
'unzipping': 'Descomprimint...',
'create_patch': 'Crear pedaç',
'original_rom': 'ROM original:',
'modified_rom': 'ROM modificada:',
'patch_type': 'Tipus de pedaç:',
'creating_patch': 'Creant pedaç...',
'error_crc_input': 'Checksum de ROM original no vàlida',
'error_crc_output': 'Checksum de ROM creada no vàlida',
'error_crc_patch': 'Checksum de pedaç no vàlida',
'error_downloading': 'Error descarregant pedaç',
'error_unzipping': 'Error descomprimint arxiu',
'error_invalid_patch': 'Arxiu de pedaç no vàlid',
'warning_too_big': 'No es recomana usar arxius molt grans.'
},
'ru':{
'creator_mode': 'Режим создания',
'apply_patch': 'Применить патч',
'rom_file': 'Файл ROM:',
'patch_file': 'Файл патча:',
'remove_header': 'Удалить заголовок перед применением',
'add_header': 'Добавить временный заголовок',
'compatible_formats': 'Совместимые форматы:',
'applying_patch': 'Применяется патч...',
'downloading': 'Загрузка...',
'unzipping': 'Unzipping...',
'create_patch': 'Создать патч:',
'original_rom': 'Оригинальный ROM:',
'modified_rom': 'Изменённый ROM:',
'patch_type': 'Тип патча:',
'creating_patch': 'Патч создаётся...',
'error_crc_input': 'Неправильная контрольная сумма входного ROM',
'error_crc_output': 'Неправильная контрольная сумма выходного ROM',
'error_crc_patch': 'Неправильная контрольная сумма патча',
'error_downloading': 'Ошибка при скачивании патча',
'error_unzipping': 'Error unzipping file',
'error_invalid_patch': 'Неправильный файл патча',
'warning_too_big': 'Не рекомендуется использовать большие файлы.'
}
};

83
js/worker_apply.js Normal file
View file

@ -0,0 +1,83 @@
/* Rom Patcher JS v20200502 - Marc Robledo 2016-2020 - http://www.marcrobledo.com/license */
self.importScripts(
'./MarcFile.js',
'./crc.js',
'./formats/ips.js',
'./formats/aps.js',
'./formats/ups.js',
'./formats/bps.js',
'./formats/rup.js',
'./formats/ppf.js',
'./formats/pmsr.js',
'./formats/vcdiff.js'
);
self.onmessage = event => { // listen for messages from the main thread
var romFile=new MarcFile(event.data.romFileU8Array);
var patchFile=new MarcFile(event.data.patchFileU8Array);
var errorMessage=false;
var patch;
var header=patchFile.readString(6);
if(header.startsWith(IPS_MAGIC)){
patch=parseIPSFile(patchFile);
}else if(header.startsWith(UPS_MAGIC)){
patch=parseUPSFile(patchFile);
}else if(header.startsWith(APS_MAGIC)){
patch=parseAPSFile(patchFile);
}else if(header.startsWith(BPS_MAGIC)){
patch=parseBPSFile(patchFile);
}else if(header.startsWith(RUP_MAGIC)){
patch=parseRUPFile(patchFile);
}else if(header.startsWith(PPF_MAGIC)){
patch=parsePPFFile(patchFile);
}else if(header.startsWith(PMSR_MAGIC)){
patch=parseMODFile(patchFile);
}else if(header.startsWith(VCDIFF_MAGIC)){
patch=parseVCDIFF(patchFile);
}else{
errorMessage='error_invalid_patch';
}
//console.log('apply');
var patchedRom;
if(patch){
try{
patchedRom=patch.apply(romFile, event.data.validateChecksums);
}catch(evt){
errorMessage=evt.message;
}
}
//console.log('postMessage');
if(patchedRom){
self.postMessage(
{
romFileU8Array:event.data.romFileU8Array,
patchFileU8Array:event.data.patchFileU8Array,
patchedRomU8Array:patchedRom._u8array,
errorMessage:errorMessage
},
[
event.data.romFileU8Array.buffer,
event.data.patchFileU8Array.buffer,
patchedRom._u8array.buffer
]
);
}else{
self.postMessage(
{
romFileU8Array:event.data.romFileU8Array,
patchFileU8Array:event.data.patchFileU8Array,
errorMessage:errorMessage
},
[
event.data.romFileU8Array.buffer,
event.data.patchFileU8Array.buffer
]
);
}
};

23
js/worker_crc.js Normal file
View file

@ -0,0 +1,23 @@
/* Rom Patcher JS v20200502 - Marc Robledo 2016-2020 - http://www.marcrobledo.com/license */
self.importScripts(
'./MarcFile.js',
'./crc.js'
);
self.onmessage = event => { // listen for messages from the main thread
var sourceFile=new MarcFile(event.data.u8array);
self.postMessage(
{
crc32:crc32(sourceFile, event.data.startOffset),
md5:md5(sourceFile, event.data.startOffset),
u8array:event.data.u8array
},
[
event.data.u8array.buffer
]
);
};

63
js/worker_create.js Normal file
View file

@ -0,0 +1,63 @@
/* Rom Patcher JS v20200502 - Marc Robledo 2016-2020 - http://www.marcrobledo.com/license */
self.importScripts(
'./MarcFile.js',
'./crc.js',
'./formats/ips.js',
'./formats/aps.js',
'./formats/ups.js',
'./formats/bps.js',
'./formats/ppf.js',
'./formats/rup.js'
);
self.onmessage = event => { // listen for messages from the main thread
var sourceFile=new MarcFile(event.data.sourceFileU8Array);
var modifiedFile=new MarcFile(event.data.modifiedFileU8Array);
var mode=event.data.patchMode;
sourceFile.seek(0);
modifiedFile.seek(0);
var patch;
if(mode==='ips'){
patch=createIPSFromFiles(sourceFile, modifiedFile);
}else if(mode==='bps'){
//use delta mode (slower, but smaller patch size) only with <4mb files
patch=createBPSFromFiles(sourceFile, modifiedFile, (sourceFile.fileSize<=4194304));
}else if(mode==='ups'){
patch=createUPSFromFiles(sourceFile, modifiedFile);
}else if(mode==='aps'){
patch=createAPSFromFiles(sourceFile, modifiedFile);
}else if(mode==='rup'){
patch=createRUPFromFiles(sourceFile, modifiedFile);
}else if(mode==='ppf'){
patch=createPPFFromFiles(sourceFile, modifiedFile);
}else{
throw new Error('error_invalid_patch');
}
//special case: PPF+modified size>original size, skip verification
if(!(mode==='ppf' && sourceFile.fileSize>modifiedFile.fileSize) && crc32(modifiedFile)!==crc32(patch.apply(sourceFile))){
throw new Error('Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.');
}
var newPatchFile=patch.export('file');
//console.log('postMessage');
self.postMessage(
{
//sourceFileU8Array:event.data.sourceFileU8Array,
//modifiedFileU8Array:event.data.modifiedFileU8Array,
patchFileU8Array:newPatchFile._u8array
},
[
//event.data.sourceFileU8Array.buffer,
//event.data.modifiedFileU8Array.buffer,
newPatchFile._u8array.buffer
]
);
};

36
js/zip.js/inflate.js Normal file

File diff suppressed because one or more lines are too long

2
js/zip.js/z-worker.js Normal file
View file

@ -0,0 +1,2 @@
/* jshint worker:true */
!function(c){"use strict";if(c.zWorkerInitialized)throw new Error("z-worker.js should be run only once");c.zWorkerInitialized=!0,addEventListener("message",function(t){var e,r,c=t.data,n=c.type,s=c.sn,p=o[n];if(p)try{p(c)}catch(t){e={type:n,sn:s,error:(r=t,{message:r.message,stack:r.stack})},postMessage(e)}});var o={importScripts:function(t){t.scripts&&0<t.scripts.length&&importScripts.apply(void 0,t.scripts);postMessage({type:"importScripts"})},newTask:h,append:t,flush:t},f={};function h(t){var e=c[t.codecClass],r=t.sn;if(f[r])throw Error("duplicated sn");f[r]={codec:new e(t.options),crcInput:"input"===t.crcType,crcOutput:"output"===t.crcType,crc:new n},postMessage({type:"newTask",sn:r})}var l=c.performance?c.performance.now.bind(c.performance):Date.now;function t(t){var e=t.sn,r=t.type,c=t.data,n=f[e];!n&&t.codecClass&&(h(t),n=f[e]);var s,p="append"===r,o=l();if(p)try{s=n.codec.append(c,function(t){postMessage({type:"progress",sn:e,loaded:t})})}catch(t){throw delete f[e],t}else delete f[e],s=n.codec.flush();var a=l()-o;o=l(),c&&n.crcInput&&n.crc.append(c),s&&n.crcOutput&&n.crc.append(s);var i=l()-o,u={type:r,sn:e,codecTime:a,crcTime:i},d=[];s&&(u.data=s,d.push(s.buffer)),p||!n.crcInput&&!n.crcOutput||(u.crc=n.crc.get());try{postMessage(u,d)}catch(t){postMessage(u)}}function n(){this.crc=-1}function e(){}n.prototype.append=function(t){for(var e=0|this.crc,r=this.table,c=0,n=0|t.length;c<n;c++)e=e>>>8^r[255&(e^t[c])];this.crc=e},n.prototype.get=function(){return~this.crc},n.prototype.table=function(){var t,e,r,c=[];for(t=0;t<256;t++){for(r=t,e=0;e<8;e++)1&r?r=r>>>1^3988292384:r>>>=1;c[t]=r}return c}(),(c.NOOP=e).prototype.append=function(t,e){return t},e.prototype.flush=function(){}}(this);

28
js/zip.js/zip.js Normal file

File diff suppressed because one or more lines are too long