feat: add ROM patcher functionality with UI integration

- Implemented z-worker.js and zip.min.js for handling zip operations.
- Added PatcherBtn component to the main navigation for easy access to the patcher.
- Created Patcher.vue view for applying patches to ROM files.
- Updated router to include a new route for the patcher.
- Enhanced navigation store with a method to navigate to the patcher.
- Integrated file input handling for ROM and patch files in the Patcher component.
- Added error handling and loading states during patch application.
This commit is contained in:
zurdi
2025-12-20 00:56:40 +00:00
parent be3615b51a
commit 4c8cd3261d
25 changed files with 6841 additions and 0 deletions

View File

@@ -0,0 +1,465 @@
/*
* Rom Patcher JS core
* A ROM patcher/builder made in JavaScript, can be implemented as a webapp or a Node.JS CLI tool
* By Marc Robledo https://www.marcrobledo.com
* Sourcecode: https://github.com/marcrobledo/RomPatcher.js
* License:
*
* MIT License
*
* Copyright (c) 2016-2025 Marc Robledo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const RomPatcher = (function () {
const TOO_BIG_ROM_SIZE = 67108863;
const HEADERS_INFO = [
{
extensions: ["nes"],
size: 16,
romSizeMultiple: 1024,
name: "iNES",
} /* https://www.nesdev.org/wiki/INES */,
{
extensions: ["fds"],
size: 16,
romSizeMultiple: 65500,
name: "fwNES",
} /* https://www.nesdev.org/wiki/FDS_file_format */,
{ extensions: ["lnx"], size: 64, romSizeMultiple: 1024, name: "LNX" },
{
extensions: ["sfc", "smc", "swc", "fig"],
size: 512,
romSizeMultiple: 262144,
name: "SNES copier",
},
];
const GAME_BOY_NINTENDO_LOGO = [
0xce, 0xed, 0x66, 0x66, 0xcc, 0x0d, 0x00, 0x0b, 0x03, 0x73, 0x00, 0x83,
0x00, 0x0c, 0x00, 0x0d, 0x00, 0x08, 0x11, 0x1f, 0x88, 0x89, 0x00, 0x0e,
0xdc, 0xcc, 0x6e, 0xe6, 0xdd, 0xdd, 0xd9, 0x99,
];
const _getRomSystem = function (binFile) {
/* to-do: add more systems */
const extension = binFile.getExtension().trim();
if (binFile.fileSize > 0x0200 && binFile.fileSize % 4 === 0) {
if (
(extension === "gb" || extension === "gbc") &&
binFile.fileSize % 0x4000 === 0
) {
binFile.seek(0x0104);
var valid = true;
for (var i = 0; i < GAME_BOY_NINTENDO_LOGO.length && valid; i++) {
if (GAME_BOY_NINTENDO_LOGO[i] !== binFile.readU8()) valid = false;
}
if (valid) return "gb";
} else if (extension === "md" || extension === "bin") {
binFile.seek(0x0100);
if (/SEGA (GENESIS|MEGA DR)/.test(binFile.readString(12))) return "smd";
} else if (extension === "z64" && binFile.fileSize >= 0x400000) {
return "n64";
}
} else if (extension === "fds" && binFile.fileSize % 65500 === 0) {
return "fds";
}
return null;
};
const _getRomAdditionalChecksum = function (binFile) {
/* to-do: add more systems */
const romSystem = _getRomSystem(binFile);
if (romSystem === "n64") {
binFile.seek(0x3c);
const cartId = binFile.readString(3);
binFile.seek(0x10);
const crc = binFile.readBytes(8).reduce(function (hex, b) {
if (b < 16) return hex + "0" + b.toString(16);
else return hex + b.toString(16);
}, "");
return cartId + " (" + crc + ")";
}
return null;
};
return {
parsePatchFile: function (patchFile) {
if (!(patchFile instanceof BinFile))
throw new Error("Patch file is not an instance of BinFile");
patchFile.littleEndian = false;
patchFile.seek(0);
var header = patchFile.readString(8);
var patch = null;
if (header.startsWith(IPS.MAGIC)) {
patch = IPS.fromFile(patchFile);
} else if (header.startsWith(UPS.MAGIC)) {
patch = UPS.fromFile(patchFile);
} else if (header.startsWith(APS.MAGIC)) {
patch = APS.fromFile(patchFile);
} else if (header.startsWith(APSGBA.MAGIC)) {
patch = APSGBA.fromFile(patchFile);
} else if (header.startsWith(BPS.MAGIC)) {
patch = BPS.fromFile(patchFile);
} else if (header.startsWith(RUP.MAGIC)) {
patch = RUP.fromFile(patchFile);
} else if (header.startsWith(PPF.MAGIC)) {
patch = PPF.fromFile(patchFile);
} else if (header.startsWith(BDF.MAGIC)) {
patch = BDF.fromFile(patchFile);
} else if (header.startsWith(PMSR.MAGIC)) {
patch = PMSR.fromFile(patchFile);
} else if (header.startsWith(VCDIFF.MAGIC)) {
patch = VCDIFF.fromFile(patchFile);
}
if (patch) patch._originalPatchFile = patchFile;
return patch;
},
validateRom: function (romFile, patch, skipHeaderSize) {
if (!(romFile instanceof BinFile))
throw new Error("ROM file is not an instance of BinFile");
else if (typeof patch !== "object")
throw new Error("Unknown patch format");
if (typeof skipHeaderSize !== "number" || skipHeaderSize < 0)
skipHeaderSize = 0;
if (
typeof patch.validateSource === "function" &&
!patch.validateSource(romFile, skipHeaderSize)
) {
return false;
}
return true;
},
applyPatch: function (romFile, patch, optionsParam) {
if (!(romFile instanceof BinFile))
throw new Error("ROM file is not an instance of BinFile");
else if (typeof patch !== "object")
throw new Error("Unknown patch format");
const options = {
requireValidation: false,
removeHeader: false,
addHeader: false,
fixChecksum: false,
outputSuffix: true,
};
if (typeof optionsParam === "object") {
if (typeof optionsParam.requireValidation !== "undefined")
options.requireValidation = !!optionsParam.requireValidation;
if (typeof optionsParam.removeHeader !== "undefined")
options.removeHeader = !!optionsParam.removeHeader;
if (typeof optionsParam.addHeader !== "undefined")
options.addHeader = !!optionsParam.addHeader;
if (typeof optionsParam.fixChecksum !== "undefined")
options.fixChecksum = !!optionsParam.fixChecksum;
if (typeof optionsParam.outputSuffix !== "undefined")
options.outputSuffix = !!optionsParam.outputSuffix;
}
var extractedHeader = false;
var fakeHeaderSize = 0;
if (options.removeHeader) {
const headerInfo = RomPatcher.isRomHeadered(romFile);
if (headerInfo) {
const splitData = RomPatcher.removeHeader(romFile);
extractedHeader = splitData.header;
romFile = splitData.rom;
}
} else if (options.addHeader) {
const headerInfo = RomPatcher.canRomGetHeader(romFile);
if (headerInfo) {
fakeHeaderSize = headerInfo.fileSize;
romFile = RomPatcher.addFakeHeader(romFile);
}
}
if (
options.requireValidation &&
!RomPatcher.validateRom(romFile, patch)
) {
throw new Error("Invalid input ROM checksum");
}
var patchedRom = patch.apply(romFile);
if (extractedHeader) {
/* reinsert header */
if (options.fixChecksum) RomPatcher.fixRomHeaderChecksum(patchedRom);
const patchedRomWithHeader = new BinFile(
extractedHeader.fileSize + patchedRom.fileSize,
);
patchedRomWithHeader.fileName = patchedRom.fileName;
patchedRomWithHeader.fileType = patchedRom.fileType;
extractedHeader.copyTo(
patchedRomWithHeader,
0,
extractedHeader.fileSize,
);
patchedRom.copyTo(
patchedRomWithHeader,
0,
patchedRom.fileSize,
extractedHeader.fileSize,
);
patchedRom = patchedRomWithHeader;
} else if (fakeHeaderSize) {
/* remove fake header */
const patchedRomWithoutFakeHeader = patchedRom.slice(fakeHeaderSize);
if (options.fixChecksum)
RomPatcher.fixRomHeaderChecksum(patchedRomWithoutFakeHeader);
patchedRom = patchedRomWithoutFakeHeader;
} else if (options.fixChecksum) {
RomPatcher.fixRomHeaderChecksum(patchedRom);
}
if (options.outputSuffix) {
patchedRom.fileName = romFile.fileName.replace(
/\.([^\.]*?)$/,
" (patched).$1",
);
if (patchedRom.unpatched)
patchedRom.fileName = patchedRom.fileName.replace(
" (patched)",
" (unpatched)",
);
} else if (patch._originalPatchFile) {
patchedRom.fileName = patch._originalPatchFile.fileName.replace(
/\.\w+$/i,
/\.\w+$/i.test(romFile.fileName)
? romFile.fileName.match(/\.\w+$/i)[0]
: "",
);
} else {
patchedRom.fileName = romFile.fileName;
}
return patchedRom;
},
createPatch: function (originalFile, modifiedFile, format, metadata) {
if (!(originalFile instanceof BinFile))
throw new Error("Original ROM file is not an instance of BinFile");
else if (!(modifiedFile instanceof BinFile))
throw new Error("Modified ROM file is not an instance of BinFile");
if (typeof format === "string") format = format.trim().toLowerCase();
else if (typeof format === "undefined") format = "ips";
var patch;
if (format === "ips") {
patch = IPS.buildFromRoms(originalFile, modifiedFile);
} else if (format === "bps") {
patch = BPS.buildFromRoms(
originalFile,
modifiedFile,
originalFile.fileSize <= 4194304,
);
} else if (format === "ppf") {
patch = PPF.buildFromRoms(originalFile, modifiedFile);
} else if (format === "ups") {
patch = UPS.buildFromRoms(originalFile, modifiedFile);
} else if (format === "aps") {
patch = APS.buildFromRoms(originalFile, modifiedFile);
} else if (format === "rup") {
patch = RUP.buildFromRoms(
originalFile,
modifiedFile,
metadata && metadata.Description ? metadata.Description : null,
);
} else if (format === "ebp") {
patch = IPS.buildFromRoms(originalFile, modifiedFile, metadata);
} else {
throw new Error("Invalid patch format");
}
if (
!(format === "ppf" && originalFile.fileSize > modifiedFile.fileSize) && //skip verification if PPF and PPF+modified size>original size
modifiedFile.hashCRC32() !== patch.apply(originalFile).hashCRC32()
) {
//throw new Error('Unexpected error: verification failed. Patched file and modified file mismatch. Please report this bug.');
}
return patch;
},
/* check if ROM can inject a fake header (for patches that require a headered ROM) */
canRomGetHeader: function (romFile) {
if (romFile.fileSize <= 0x600000) {
const compatibleHeader = HEADERS_INFO.find(
(headerInfo) =>
headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 &&
romFile.fileSize % headerInfo.romSizeMultiple === 0,
);
if (compatibleHeader) {
return {
name: compatibleHeader.name,
size: compatibleHeader.size,
};
}
}
return null;
},
/* check if ROM has a known header */
isRomHeadered: function (romFile) {
if (romFile.fileSize <= 0x600200 && romFile.fileSize % 1024 !== 0) {
const compatibleHeader = HEADERS_INFO.find(
(headerInfo) =>
headerInfo.extensions.indexOf(romFile.getExtension()) !== -1 &&
(romFile.fileSize - headerInfo.size) %
headerInfo.romSizeMultiple ===
0,
);
if (compatibleHeader) {
return {
name: compatibleHeader.name,
size: compatibleHeader.size,
};
}
}
return null;
},
/* remove ROM header */
removeHeader: function (romFile) {
const headerInfo = RomPatcher.isRomHeadered(romFile);
if (headerInfo) {
return {
header: romFile.slice(0, headerInfo.size),
rom: romFile.slice(headerInfo.size),
};
}
return null;
},
/* add fake ROM header */
addFakeHeader: function (romFile) {
const headerInfo = RomPatcher.canRomGetHeader(romFile);
if (headerInfo) {
const romWithFakeHeader = new BinFile(
headerInfo.size + romFile.fileSize,
);
romWithFakeHeader.fileName = romFile.fileName;
romWithFakeHeader.fileType = romFile.fileType;
romFile.copyTo(romWithFakeHeader, 0, romFile.fileSize, headerInfo.size);
//add a correct FDS header
if (_getRomSystem(romWithFakeHeader) === "fds") {
romWithFakeHeader.seek(0);
romWithFakeHeader.writeBytes([
0x46,
0x44,
0x53,
0x1a,
romFile.fileSize / 65500,
]);
}
romWithFakeHeader.fakeHeader = true;
return romWithFakeHeader;
}
return null;
},
/* get ROM internal checksum, if possible */
fixRomHeaderChecksum: function (romFile) {
const romSystem = _getRomSystem(romFile);
if (romSystem === "gb") {
/* get current checksum */
romFile.seek(0x014d);
const currentChecksum = romFile.readU8();
/* calculate checksum */
var newChecksum = 0x00;
romFile.seek(0x0134);
for (var i = 0; i <= 0x18; i++) {
newChecksum = ((newChecksum - romFile.readU8() - 1) >>> 0) & 0xff;
}
/* fix checksum */
if (currentChecksum !== newChecksum) {
console.log("fixed Game Boy checksum");
romFile.seek(0x014d);
romFile.writeU8(newChecksum);
return true;
}
} else if (romSystem === "smd") {
/* get current checksum */
romFile.seek(0x018e);
const currentChecksum = romFile.readU16();
/* calculate checksum */
var newChecksum = 0x0000;
romFile.seek(0x0200);
while (!romFile.isEOF()) {
newChecksum = ((newChecksum + romFile.readU16()) >>> 0) & 0xffff;
}
/* fix checksum */
if (currentChecksum !== newChecksum) {
console.log("fixed Megadrive/Genesis checksum");
romFile.seek(0x018e);
romFile.writeU16(newChecksum);
return true;
}
}
return false;
},
/* get ROM additional checksum info, if possible */
getRomAdditionalChecksum: function (romFile) {
return _getRomAdditionalChecksum(romFile);
},
/* check if ROM is too big */
isRomTooBig: function (romFile) {
return romFile && romFile.fileSize > TOO_BIG_ROM_SIZE;
},
};
})();
if (typeof module !== "undefined" && module.exports) {
module.exports = RomPatcher;
IPS = require("./modules/RomPatcher.format.ips");
UPS = require("./modules/RomPatcher.format.ups");
APS = require("./modules/RomPatcher.format.aps_n64");
APSGBA = require("./modules/RomPatcher.format.aps_gba");
BPS = require("./modules/RomPatcher.format.bps");
RUP = require("./modules/RomPatcher.format.rup");
PPF = require("./modules/RomPatcher.format.ppf");
BDF = require("./modules/RomPatcher.format.bdf");
PMSR = require("./modules/RomPatcher.format.pmsr");
VCDIFF = require("./modules/RomPatcher.format.vcdiff");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

View File

@@ -0,0 +1,495 @@
/*
* BinFile.js (last update: 2024-08-21)
* by Marc Robledo, https://www.marcrobledo.com
*
* a JS class for reading/writing sequentially binary data from/to a file
* that allows much more manipulation than simple DataView
* compatible with both browsers and Node.js
*
* MIT License
*
* Copyright (c) 2014-2024 Marc Robledo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
function BinFile(source, onLoad) {
this.littleEndian = false;
this.offset = 0;
this._lastRead = null;
this._offsetsStack = [];
if (
BinFile.RUNTIME_ENVIROMENT === "browser" &&
(source instanceof File ||
source instanceof FileList ||
(source instanceof HTMLElement &&
source.tagName === "INPUT" &&
source.type === "file"))
) {
if (source instanceof HTMLElement) source = source.files;
if (source instanceof FileList) source = source[0];
this.fileName = source.name;
this.fileType = source.type;
this.fileSize = source.size;
if (typeof window.FileReader !== "function")
throw new Error("Incompatible browser");
this._fileReader = new FileReader();
this._fileReader.addEventListener(
"load",
function () {
this.binFile._u8array = new Uint8Array(this.result);
if (typeof onLoad === "function") onLoad(this.binFile);
},
false,
);
this._fileReader.binFile = this;
this._fileReader.readAsArrayBuffer(source);
} else if (
BinFile.RUNTIME_ENVIROMENT === "node" &&
typeof source === "string"
) {
if (!nodeFs.existsSync(source)) throw new Error(source + " does not exist");
const arrayBuffer = nodeFs.readFileSync(source);
this.fileName = nodePath.basename(source);
this.fileType = nodeFs.statSync(source).type;
this.fileSize = arrayBuffer.byteLength;
this._u8array = new Uint8Array(arrayBuffer);
if (typeof onLoad === "function") onLoad(this);
} else if (source instanceof BinFile) {
/* if source is another BinFile, clone it */
this.fileName = source.fileName;
this.fileType = source.fileType;
this.fileSize = source.fileSize;
this._u8array = new Uint8Array(source._u8array.buffer.slice());
if (typeof onLoad === "function") onLoad(this);
} else if (source instanceof ArrayBuffer) {
this.fileName = "file.bin";
this.fileType = "application/octet-stream";
this.fileSize = source.byteLength;
this._u8array = new Uint8Array(source);
if (typeof onLoad === "function") onLoad(this);
} else if (ArrayBuffer.isView(source)) {
/* source is TypedArray */
this.fileName = "file.bin";
this.fileType = "application/octet-stream";
this.fileSize = source.buffer.byteLength;
this._u8array = new Uint8Array(source.buffer);
if (typeof onLoad === "function") onLoad(this);
} else if (typeof source === "number") {
/* source is integer, create new empty file */
this.fileName = "file.bin";
this.fileType = "application/octet-stream";
this.fileSize = source;
this._u8array = new Uint8Array(new ArrayBuffer(source));
if (typeof onLoad === "function") onLoad(this);
} else {
throw new Error("invalid BinFile source");
}
}
BinFile.RUNTIME_ENVIROMENT = (function () {
if (typeof window === "object" && typeof window.document === "object")
return "browser";
else if (
typeof WorkerGlobalScope === "function" &&
self instanceof WorkerGlobalScope
)
return "webworker";
else if (
typeof require === "function" &&
typeof process === "object" &&
typeof process.versions === "object" &&
typeof process.versions.node === "string"
)
return "node";
else return null;
})();
BinFile.DEVICE_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;
})();
BinFile.prototype.push = function () {
this._offsetsStack.push(this.offset);
};
BinFile.prototype.pop = function () {
this.seek(this._offsetsStack.pop());
};
BinFile.prototype.seek = function (offset) {
this.offset = offset;
};
BinFile.prototype.skip = function (nBytes) {
this.offset += nBytes;
};
BinFile.prototype.isEOF = function () {
return !(this.offset < this.fileSize);
};
BinFile.prototype.slice = function (offset, len, doNotClone) {
if (typeof offset !== "number" || offset < 0) offset = 0;
else if (offset >= this.fileSize) throw new Error("out of bounds slicing");
else offset = Math.floor(offset);
if (
typeof len !== "number" ||
offset < 0 ||
offset + len >= this.fileSize.length
)
len = this.fileSize - offset;
else if (len === 0) throw new Error("zero length provided for slicing");
else len = Math.floor(len);
if (offset === 0 && len === this.fileSize && doNotClone) return this;
var newFile = new BinFile(this._u8array.buffer.slice(offset, offset + len));
newFile.fileName = this.fileName;
newFile.fileType = this.fileType;
newFile.littleEndian = this.littleEndian;
return newFile;
};
BinFile.prototype.prependBytes = function (bytes) {
var newFile = new BinFile(this.fileSize + bytes.length);
newFile.seek(0);
newFile.writeBytes(bytes);
this.copyTo(newFile, 0, this.fileSize, bytes.length);
this.fileSize = newFile.fileSize;
this._u8array = newFile._u8array;
return this;
};
BinFile.prototype.removeLeadingBytes = function (nBytes) {
this.seek(0);
var oldData = this.readBytes(nBytes);
var newFile = this.slice(nBytes.length);
this.fileSize = newFile.fileSize;
this._u8array = newFile._u8array;
return oldData;
};
BinFile.prototype.copyTo = function (target, offsetSource, len, offsetTarget) {
if (!(target instanceof BinFile))
throw new Error("target is not a BinFile object");
if (typeof offsetTarget !== "number") offsetTarget = offsetSource;
len = len || this.fileSize - offsetSource;
for (var i = 0; i < len; i++) {
target._u8array[offsetTarget + i] = this._u8array[offsetSource + i];
}
};
BinFile.prototype.save = function () {
if (BinFile.RUNTIME_ENVIROMENT === "browser") {
var fileBlob = new Blob([this._u8array], { type: this.fileType });
var blobUrl = URL.createObjectURL(fileBlob);
var a = document.createElement("a");
a.href = blobUrl;
a.download = this.fileName;
document.body.appendChild(a);
a.dispatchEvent(new MouseEvent("click"));
URL.revokeObjectURL(blobUrl);
document.body.removeChild(a);
} else if (BinFile.RUNTIME_ENVIROMENT === "node") {
nodeFs.writeFileSync(this.fileName, Buffer.from(this._u8array.buffer));
} else {
throw new Error("invalid runtime environment, can't save file");
}
};
BinFile.prototype.getExtension = function () {
var ext = this.fileName ? this.fileName.toLowerCase().match(/\.(\w+)$/) : "";
return ext ? ext[1] : "";
};
BinFile.prototype.getName = function () {
return this.fileName.replace(
new RegExp("\\." + this.getExtension() + "$", "i"),
"",
);
};
BinFile.prototype.setExtension = function (newExtension) {
return (this.fileName = this.getName() + "." + newExtension);
};
BinFile.prototype.setName = function (newName) {
return (this.fileName = newName + "." + this.getExtension());
};
BinFile.prototype.readU8 = function () {
this._lastRead = this._u8array[this.offset++];
return this._lastRead;
};
BinFile.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;
};
BinFile.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;
};
BinFile.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;
};
BinFile.prototype.readU64 = 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) +
(this._u8array[this.offset + 4] << 32) +
(this._u8array[this.offset + 5] << 40) +
(this._u8array[this.offset + 6] << 48) +
(this._u8array[this.offset + 7] << 56);
else
this._lastRead =
(this._u8array[this.offset] << 56) +
(this._u8array[this.offset + 1] << 48) +
(this._u8array[this.offset + 2] << 40) +
(this._u8array[this.offset + 3] << 32) +
(this._u8array[this.offset + 4] << 24) +
(this._u8array[this.offset + 5] << 16) +
(this._u8array[this.offset + 6] << 8) +
this._u8array[this.offset + 7];
this.offset += 8;
return this._lastRead >>> 0;
};
BinFile.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;
};
BinFile.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;
};
BinFile.prototype.writeU8 = function (u8) {
this._u8array[this.offset++] = u8;
};
BinFile.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;
};
BinFile.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;
};
BinFile.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;
};
BinFile.prototype.writeBytes = function (a) {
for (var i = 0; i < a.length; i++) this._u8array[this.offset + i] = a[i];
this.offset += a.length;
};
BinFile.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;
};
BinFile.prototype.swapBytes = function (swapSize, newFile) {
if (typeof swapSize !== "number") {
swapSize = 4;
}
if (this.fileSize % swapSize !== 0) {
throw new Error("file size is not divisible by " + swapSize);
}
var swappedFile = new BinFile(this.fileSize);
this.seek(0);
while (!this.isEOF()) {
swappedFile.writeBytes(this.readBytes(swapSize).reverse());
}
if (newFile) {
swappedFile.fileName = this.fileName;
swappedFile.fileType = this.fileType;
return swappedFile;
} else {
this._u8array = swappedFile._u8array;
return this;
}
};
BinFile.prototype.hashSHA1 = async function (start, len) {
if (
typeof HashCalculator !== "object" ||
typeof HashCalculator.sha1 !== "function"
)
throw new Error("no Hash object found or missing sha1 function");
return HashCalculator.sha1(this.slice(start, len, true)._u8array.buffer);
};
BinFile.prototype.hashMD5 = function (start, len) {
if (
typeof HashCalculator !== "object" ||
typeof HashCalculator.md5 !== "function"
)
throw new Error("no Hash object found or missing md5 function");
return HashCalculator.md5(this.slice(start, len, true)._u8array.buffer);
};
BinFile.prototype.hashCRC32 = function (start, len) {
if (
typeof HashCalculator !== "object" ||
typeof HashCalculator.crc32 !== "function"
)
throw new Error("no Hash object found or missing crc32 function");
return HashCalculator.crc32(this.slice(start, len, true)._u8array.buffer);
};
BinFile.prototype.hashAdler32 = function (start, len) {
if (
typeof HashCalculator !== "object" ||
typeof HashCalculator.adler32 !== "function"
)
throw new Error("no Hash object found or missing adler32 function");
return HashCalculator.adler32(this.slice(start, len, true)._u8array.buffer);
};
BinFile.prototype.hashCRC16 = function (start, len) {
if (
typeof HashCalculator !== "object" ||
typeof HashCalculator.crc16 !== "function"
)
throw new Error("no Hash object found or missing crc16 function");
return HashCalculator.crc16(this.slice(start, len, true)._u8array.buffer);
};
if (
BinFile.RUNTIME_ENVIROMENT === "node" &&
typeof module !== "undefined" &&
module.exports
) {
module.exports = BinFile;
HashCalculator = require("./HashCalculator");
nodePath = require("path");
nodeFs = require("fs");
}

View File

@@ -0,0 +1,275 @@
/*
* HashCalculator.js (last update: 2021-08-15)
* by Marc Robledo, https://www.marcrobledo.com
*
* data hash calculator (CRC32, MD5, SHA1, ADLER-32, CRC16)
*
* MIT License
*
* Copyright (c) 2016-2021 Marc Robledo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const HashCalculator = (function () {
const HEX_CHR = "0123456789abcdef".split("");
/* MD5 helpers */
const _add32 = function (a, b) {
return (a + b) & 0xffffffff;
};
const _md5cycle = function (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]);
};
const _md5blk = function (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;
};
const _cmn = function (q, a, b, x, s, t) {
a = _add32(_add32(a, q), _add32(x, t));
return _add32((a << s) | (a >>> (32 - s)), b);
};
const ff = function (a, b, c, d, x, s, t) {
return _cmn((b & c) | (~b & d), a, b, x, s, t);
};
const gg = function (a, b, c, d, x, s, t) {
return _cmn((b & d) | (c & ~d), a, b, x, s, t);
};
const hh = function (a, b, c, d, x, s, t) {
return _cmn(b ^ c ^ d, a, b, x, s, t);
};
const ii = function (a, b, c, d, x, s, t) {
return _cmn(c ^ (b | ~d), a, b, x, s, t);
};
/* CRC32 helpers */
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;
})();
/* Adler-32 helpers */
const ADLER32_MOD = 0xfff1;
const generateUint8Array = function (arrayBuffer, offset, len) {
if (typeof offset !== "number" || offset < 0) offset = 0;
else if (offset < arrayBuffer.byteLength) offset = Math.floor(offset);
else throw new Error("out of bounds slicing");
if (
typeof len !== "number" ||
len < 0 ||
offset + len >= arrayBuffer.byteLength.length
)
len = arrayBuffer.byteLength - offset;
else if (len > 0) len = Math.floor(len);
else throw new Error("zero length provided for slicing");
return new Uint8Array(arrayBuffer, offset, len);
};
return {
/* SHA-1 using WebCryptoAPI */
sha1: async function sha1(arrayBuffer, offset, len) {
if (typeof window === "undefined" || typeof window.crypto === "undefined")
throw new Error("Web Crypto API is not available");
const u8array = generateUint8Array(arrayBuffer, offset, len);
if (u8array.byteLength !== arrayBuffer.byteLength) {
arrayBuffer = arrayBuffer.slice(
u8array.byteOffset,
u8array.byteOffset + u8array.byteLength,
);
}
const hash = await window.crypto.subtle.digest("SHA-1", arrayBuffer);
const bytes = new Uint8Array(hash);
let hexString = "";
for (let i = 0; i < bytes.length; i++)
hexString +=
bytes[i] < 16 ? "0" + bytes[i].toString(16) : bytes[i].toString(16);
return hexString;
},
/* MD5 - from Joseph's Myers - http://www.myersdaily.org/joseph/javascript/md5.js */
md5: function (arrayBuffer, offset, len) {
let u8array = generateUint8Array(arrayBuffer, offset, len);
var n = u8array.byteLength,
state = [1732584193, -271733879, -1732584194, 271733878],
i;
for (i = 64; i <= u8array.byteLength; i += 64)
_md5cycle(state, _md5blk(u8array.slice(i - 64, i)));
u8array = u8array.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 < u8array.byteLength; i++)
tail[i >> 2] |= u8array[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;
tail[15] = Math.floor(n / 536870912) >>> 0; //if file is bigger than 512Mb*8, value is bigger than 32 bits, so it needs two words to store its length
_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 */
crc32: function (arrayBuffer, offset, len) {
const u8array = generateUint8Array(arrayBuffer, offset, len);
var crc = 0 ^ -1;
for (var i = 0; i < u8array.byteLength; i++)
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ u8array[i]) & 0xff];
return (crc ^ -1) >>> 0;
},
/* Adler-32 - https://en.wikipedia.org/wiki/Adler-32#Example_implementation */
adler32: function (arrayBuffer, offset, len) {
const u8array = generateUint8Array(arrayBuffer, offset, len);
var a = 1,
b = 0;
for (var i = 0; i < u8array.byteLength; i++) {
a = (a + u8array[i]) % ADLER32_MOD;
b = (b + a) % ADLER32_MOD;
}
return ((b << 16) | a) >>> 0;
},
/* CRC16/CCITT-FALSE */
crc16: function (arrayBuffer, offset, len) {
const u8array = generateUint8Array(arrayBuffer, offset, len);
var crc = 0xffff;
var offset = 0;
for (var i = 0; i < u8array.byteLength; i++) {
crc ^= u8array[offset++] << 8;
for (j = 0; j < 8; ++j) {
crc = (crc & 0x8000) >>> 0 ? (crc << 1) ^ 0x1021 : crc << 1;
}
}
return crc & 0xffff;
},
};
})();
if (typeof module !== "undefined" && module.exports) {
module.exports = HashCalculator;
}

View File

@@ -0,0 +1,123 @@
/* APS (GBA) module for Rom Patcher JS v20230331 - Marc Robledo 2017-2023 - http://www.marcrobledo.com/license */
/* File format specification: https://github.com/btimofeev/UniPatcher/wiki/APS-(GBA) */
const APS_GBA_MAGIC = "APS1";
const APS_GBA_BLOCK_SIZE = 0x010000; //64Kb
const APS_GBA_RECORD_SIZE = 4 + 2 + 2 + APS_GBA_BLOCK_SIZE;
if (typeof module !== "undefined" && module.exports) {
module.exports = APSGBA;
}
function APSGBA() {
this.sourceSize = 0;
this.targetSize = 0;
this.records = [];
}
APSGBA.prototype.addRecord = function (
offset,
sourceCrc16,
targetCrc16,
xorBytes,
) {
this.records.push({
offset: offset,
sourceCrc16: sourceCrc16,
targetCrc16: targetCrc16,
xorBytes: xorBytes,
});
};
APSGBA.prototype.toString = function () {
var s = "Total records: " + this.records.length;
s += "\nInput file size: " + this.sourceSize;
s += "\nOutput file size: " + this.targetSize;
return s;
};
APSGBA.prototype.validateSource = function (sourceFile) {
if (sourceFile.fileSize !== this.sourceSize) return false;
for (var i = 0; i < this.records.length; i++) {
sourceFile.seek(this.records[i].offset);
var bytes = sourceFile.readBytes(APS_GBA_BLOCK_SIZE);
if (
sourceFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE) !==
this.records[i].sourceCrc16
)
return false;
}
return true;
};
APSGBA.prototype.export = function (fileName) {
var patchFileSize = 12 + this.records.length * APS_GBA_RECORD_SIZE;
tempFile = new BinFile(patchFileSize);
tempFile.littleEndian = true;
tempFile.fileName = fileName + ".aps";
tempFile.writeString(APS_GBA_MAGIC, APS_GBA_MAGIC.length);
tempFile.writeU32(this.sourceSize);
tempFile.writeU32(this.targetSize);
for (var i = 0; i < this.records.length; i++) {
tempFile.writeU32(this.records[i].offset);
tempFile.writeU16(this.records[i].sourceCrc16);
tempFile.writeU16(this.records[i].targetCrc16);
tempFile.writeBytes(this.records[i].xorBytes);
}
return tempFile;
};
APSGBA.prototype.apply = function (romFile, validate) {
if (validate && !this.validateSource(romFile)) {
throw new Error("Source ROM checksum mismatch");
}
tempFile = new BinFile(this.targetSize);
romFile.copyTo(tempFile, 0, romFile.fileSize);
for (var i = 0; i < this.records.length; i++) {
romFile.seek(this.records[i].offset);
tempFile.seek(this.records[i].offset);
for (var j = 0; j < APS_GBA_BLOCK_SIZE; j++) {
tempFile.writeU8(romFile.readU8() ^ this.records[i].xorBytes[j]);
}
if (
validate &&
tempFile.hashCRC16(this.records[i].offset, APS_GBA_BLOCK_SIZE) !==
this.records[i].targetCrc16
) {
throw new Error("Target ROM checksum mismatch");
}
}
return tempFile;
};
APSGBA.MAGIC = APS_GBA_MAGIC;
APSGBA.fromFile = function (patchFile) {
patchFile.seek(0);
patchFile.littleEndian = true;
if (
patchFile.readString(APS_GBA_MAGIC.length) !== APS_GBA_MAGIC ||
patchFile.fileSize < 12 + APS_GBA_RECORD_SIZE ||
(patchFile.fileSize - 12) % APS_GBA_RECORD_SIZE !== 0
)
return null;
var patch = new APSGBA();
patch.sourceSize = patchFile.readU32();
patch.targetSize = patchFile.readU32();
while (!patchFile.isEOF()) {
var offset = patchFile.readU32();
var sourceCrc16 = patchFile.readU16();
var targetCrc16 = patchFile.readU16();
var xorBytes = patchFile.readBytes(APS_GBA_BLOCK_SIZE);
patch.addRecord(offset, sourceCrc16, targetCrc16, xorBytes);
}
return patch;
};

View File

@@ -0,0 +1,207 @@
/* 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_N64_MAGIC = "APS10";
const APS_RECORD_RLE = 0x0000;
const APS_RECORD_SIMPLE = 0x01;
const APS_N64_MODE = 0x01;
if (typeof module !== "undefined" && module.exports) {
module.exports = APS;
}
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.getValidationInfo = function () {
if (this.headerType === APS_N64_MODE) {
return (
this.header.cartId +
" (" +
this.header.crc.reduce(function (hex, b) {
if (b < 16) return hex + "0" + b.toString(16);
else return hex + b.toString(16);
}, "") +
")"
);
}
return null;
};
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 BinFile(patchFileSize);
tempFile.littleEndian = true;
tempFile.fileName = fileName + ".aps";
tempFile.writeString(APS_N64_MAGIC, APS_N64_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("Source ROM checksum mismatch");
}
tempFile = new BinFile(this.header.sizeOutput);
romFile.copyTo(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;
};
APS.MAGIC = APS_N64_MAGIC;
APS.fromFile = function (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;
};
APS.buildFromRoms = function (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);
}
}
}
return patch;
};

View File

@@ -0,0 +1,323 @@
/* BDF module for Rom Patcher JS v20250922 - Marc Robledo 2025 - http://www.marcrobledo.com/license */
/* File format specification: https://www.daemonology.net/bsdiff/ */
const BDF_MAGIC = "BSDIFF40";
if (typeof module !== "undefined" && module.exports) {
module.exports = BDF;
}
function BDF() {
this.records = [];
this.patchedSize = 0;
}
BDF.prototype.apply = function (file) {
var tempFile = new BinFile(this.patchedSize);
for (const record of this.records) {
for (const b of record.diff) {
tempFile.writeU8(file.readU8() + b);
}
tempFile.writeBytes(record.extra);
file.seek(file.offset + record.skip);
}
return tempFile;
};
BDF.MAGIC = BDF_MAGIC;
BDF.fromFile = function (file) {
var patch = new BDF();
file.seek(8);
file.littleEndian = true;
var controlSize = file.readU64();
var diffSize = file.readU64();
patch.patchedSize = file.readU64();
var controlCompressed = file.readBytes(controlSize);
var diffCompressed = file.readBytes(diffSize);
var extraCompressed = file.readBytes(file.fileSize - file.offset);
var controlFile = new BinFile(bz2.decompress(controlCompressed));
controlFile.littleEndian = true;
var diffFile = new BinFile(bz2.decompress(diffCompressed));
var extraFile = new BinFile(bz2.decompress(extraCompressed));
while (!controlFile.isEOF()) {
var diffLen = controlFile.readU64();
var extraLen = controlFile.readU64();
var skip = controlFile.readU64();
if (skip & (1 << 63)) skip = -(skip & ~(1 << 63));
var diff = diffFile.readBytes(diffLen);
var extra = extraFile.readBytes(extraLen);
patch.records.push({ diff, extra, skip });
}
return patch;
};
/*
bz2 (C) 2019-present SheetJS LLC
Copyright 2019 SheetJS LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
("use strict");
const bz2 = (function $() {
let x = [
0, 79764919, 159529838, 222504665, 319059676, 398814059, 445009330,
507990021, 638119352, 583659535, 797628118, 726387553, 890018660,
835552979, 1015980042, 944750013, 1276238704, 1221641927, 1167319070,
1095957929, 1595256236, 1540665371, 1452775106, 1381403509, 1780037320,
1859660671, 1671105958, 1733955601, 2031960084, 2111593891, 1889500026,
1952343757, 2552477408, 2632100695, 2443283854, 2506133561, 2334638140,
2414271883, 2191915858, 2254759653, 3190512472, 3135915759, 3081330742,
3009969537, 2905550212, 2850959411, 2762807018, 2691435357, 3560074640,
3505614887, 3719321342, 3648080713, 3342211916, 3287746299, 3467911202,
3396681109, 4063920168, 4143685023, 4223187782, 4286162673, 3779000052,
3858754371, 3904687514, 3967668269, 881225847, 809987520, 1023691545,
969234094, 662832811, 591600412, 771767749, 717299826, 311336399,
374308984, 453813921, 533576470, 25881363, 88864420, 134795389, 214552010,
2023205639, 2086057648, 1897238633, 1976864222, 1804852699, 1867694188,
1645340341, 1724971778, 1587496639, 1516133128, 1461550545, 1406951526,
1302016099, 1230646740, 1142491917, 1087903418, 2896545431, 2825181984,
2770861561, 2716262478, 3215044683, 3143675388, 3055782693, 3001194130,
2326604591, 2389456536, 2200899649, 2280525302, 2578013683, 2640855108,
2418763421, 2498394922, 3769900519, 3832873040, 3912640137, 3992402750,
4088425275, 4151408268, 4197601365, 4277358050, 3334271071, 3263032808,
3476998961, 3422541446, 3585640067, 3514407732, 3694837229, 3640369242,
1762451694, 1842216281, 1619975040, 1682949687, 2047383090, 2127137669,
1938468188, 2001449195, 1325665622, 1271206113, 1183200824, 1111960463,
1543535498, 1489069629, 1434599652, 1363369299, 622672798, 568075817,
748617968, 677256519, 907627842, 853037301, 1067152940, 995781531,
51762726, 131386257, 177728840, 240578815, 269590778, 349224269,
429104020, 491947555, 4046411278, 4126034873, 4172115296, 4234965207,
3794477266, 3874110821, 3953728444, 4016571915, 3609705398, 3555108353,
3735388376, 3664026991, 3290680682, 3236090077, 3449943556, 3378572211,
3174993278, 3120533705, 3032266256, 2961025959, 2923101090, 2868635157,
2813903052, 2742672763, 2604032198, 2683796849, 2461293480, 2524268063,
2284983834, 2364738477, 2175806836, 2238787779, 1569362073, 1498123566,
1409854455, 1355396672, 1317987909, 1246755826, 1192025387, 1137557660,
2072149281, 2135122070, 1912620623, 1992383480, 1753615357, 1816598090,
1627664531, 1707420964, 295390185, 358241886, 404320391, 483945776,
43990325, 106832002, 186451547, 266083308, 932423249, 861060070,
1041341759, 986742920, 613929101, 542559546, 756411363, 701822548,
3316196985, 3244833742, 3425377559, 3370778784, 3601682597, 3530312978,
3744426955, 3689838204, 3819031489, 3881883254, 3928223919, 4007849240,
4037393693, 4100235434, 4180117107, 4259748804, 2310601993, 2373574846,
2151335527, 2231098320, 2596047829, 2659030626, 2470359227, 2550115596,
2947551409, 2876312838, 2788305887, 2733848168, 3165939309, 3094707162,
3040238851, 2985771188,
],
f = [
0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383,
32767, 65535, 131071, 262143, 524287, 1048575, 2097151, 4194303, 8388607,
16777215, 33554431, 67108863, 134217727, 268435455, 536870911, 1073741823,
-2147483648,
];
function e($) {
let x = [];
for (let f = 0; f < $.length; f += 1) x.push([f, $[f]]);
x.push([$.length, -1]);
let e = [],
d = x[0][0],
b = x[0][1];
for (let _ = 0; _ < x.length; _ += 1) {
let a = x[_][0],
c = x[_][1];
if (b)
for (let t = d; t < a; t += 1)
e.push({ code: t, bits: b, symbol: void 0 });
if (((d = a), (b = c), -1 === c)) break;
}
e.sort(($, x) => $.bits - x.bits || $.code - x.code);
let l = 0,
o = -1,
r = [],
n;
for (let s = 0; s < e.length; s += 1) {
let i = e[s];
((o += 1),
i.bits !== l && ((o <<= i.bits - l), (n = r[(l = i.bits)] = {})),
(i.symbol = o),
(n[o] = i));
}
return { table: e, fastAccess: r };
}
function d($, x) {
if (x < 0 || x >= $.length) throw RangeError("Out of bound");
let f = $.slice();
$.sort(($, x) => $ - x);
let e = {};
for (let d = $.length - 1; d >= 0; d -= 1) e[$[d]] = d;
let b = [];
for (let _ = 0; _ < $.length; _ += 1) b.push(e[f[_]]++);
let a,
c = $[(a = x)],
t = [];
for (let l = 1; l < $.length; l += 1) {
let o = $[(a = b[a])];
void 0 === o ? t.push(255) : t.push(o);
}
return (t.push(c), t.reverse(), t);
}
let b = {
decompress: function $(b, _ = !1) {
let a = 0,
c = 0,
t = 0,
l = ($) => {
if ($ >= 32) {
let x = $ >> 1;
return l(x) * (1 << x) + l($ - x);
}
for (; t < $; ) ((c = (c << 8) + b[a]), (a += 1), (t += 8));
let e = f[$],
d = (c >> (t - $)) & e;
return ((t -= $), (c &= ~(e << t)), d);
},
o = l(16);
if (16986 !== o) throw Error("Invalid magic");
let r = l(8);
if (104 !== r) throw Error("Invalid method");
let n = l(8);
if (n >= 49 && n <= 57) n -= 48;
else throw Error("Invalid blocksize");
let s = new Uint8Array(1.5 * b.length),
i = 0,
h = -1;
for (;;) {
let u = l(48),
p = 0 | l(32);
if (54156738319193 === u) {
if (l(1)) throw Error("do not support randomised");
let g = l(24),
w = [],
m = l(16);
for (let v = 32768; v > 0; v >>= 1) {
if (!(m & v)) {
for (let y = 0; y < 16; y += 1) w.push(!1);
continue;
}
let k = l(16);
for (let I = 32768; I > 0; I >>= 1) w.push(!!(k & I));
}
let A = l(3);
if (A < 2 || A > 6) throw Error("Invalid number of huffman groups");
let z = l(15),
C = [],
O = Array.from({ length: A }, ($, x) => x);
for (let F = 0; F < z; F += 1) {
let H = 0;
for (; l(1); )
if ((H += 1) >= A) throw Error("MTF table out of range");
let M = O[H];
for (let P = H; P > 0; O[P] = O[--P]);
(C.push(M), (O[0] = M));
}
let R = w.reduce(($, x) => $ + x, 0) + 2,
T = [];
for (let j = 0; j < A; j += 1) {
let q = l(5),
B = [];
for (let D = 0; D < R; D += 1) {
if (q < 0 || q > 20)
throw Error("Huffman group length outside range");
for (; l(1); ) q -= 2 * l(1) - 1;
B.push(q);
}
T.push(e(B));
}
let E = [];
for (let G = 0; G < w.length - 1; G += 1) w[G] && E.push(G);
let J = 0,
K = 0,
L,
N,
Q = 0,
S = 0,
U = [];
for (;;) {
for (let V in ((J -= 1) <= 0 &&
((J = 50), K <= C.length && ((L = T[C[K]]), (K += 1))),
L.fastAccess))
if (
Object.prototype.hasOwnProperty.call(L.fastAccess, V) &&
(t < V && ((c = (c << 8) + b[a]), (a += 1), (t += 8)),
(N = L.fastAccess[V][c >> (t - V)]))
) {
((c &= f[(t -= V)]), (N = N.code));
break;
}
if (N >= 0 && N <= 1) {
(0 === Q && (S = 1), (Q += S << N), (S <<= 1));
continue;
}
{
let W = E[0];
for (; Q > 0; Q -= 1) U.push(W);
}
if (N === R - 1) break;
{
let X = E[N - 1];
for (let Y = N - 1; Y > 0; E[Y] = E[--Y]);
((E[0] = X), U.push(X));
}
}
let Z = d(U, g),
$$ = 0;
for (; $$ < Z.length; ) {
let $x = Z[$$],
$f = 1;
if (
($$ < Z.length - 4 &&
Z[$$ + 1] === $x &&
Z[$$ + 2] === $x &&
Z[$$ + 3] === $x
? (($f = Z[$$ + 4] + 4), ($$ += 5))
: ($$ += 1),
i + $f >= s.length)
) {
let $e = s;
(s = new Uint8Array(2 * $e.length)).set($e);
}
for (let $d = 0; $d < $f; $d += 1)
(_ && (h = (h << 8) ^ x[((h >> 24) ^ $x) & 255]),
(s[i] = $x),
(i += 1));
}
if (_) {
let $b = -1 ^ h;
if ($b !== p) throw Error(`CRC mismatch: ${$b} !== ${p}`);
h = -1;
}
} else if (25779555029136 === u) {
l(7 & t);
break;
} else throw Error("Invalid bz2 blocktype");
}
return s.subarray(0, i);
},
};
return b;
})();

View File

@@ -0,0 +1,501 @@
/* BPS module for Rom Patcher JS v20240821 - Marc Robledo 2016-2024 - http://www.marcrobledo.com/license */
/* File format specification: https://www.romhacking.net/documents/746/ */
const BPS_MAGIC = "BPS1";
const BPS_ACTION_SOURCE_READ = 0;
const BPS_ACTION_TARGET_READ = 1;
const BPS_ACTION_SOURCE_COPY = 2;
const BPS_ACTION_TARGET_COPY = 3;
if (typeof module !== "undefined" && module.exports) {
module.exports = BPS;
}
function BPS() {
this.sourceSize = 0;
this.targetSize = 0;
this.metaData = "";
this.actions = [];
this.sourceChecksum = 0;
this.targetChecksum = 0;
this.patchChecksum = 0;
}
BPS.prototype.toString = function () {
var s = "Source size: " + this.sourceSize;
s += "\nTarget size: " + this.targetSize;
s += "\nMetadata: " + this.metaData;
s += "\n#Actions: " + this.actions.length;
return s;
};
BPS.prototype.calculateFileChecksum = function () {
var patchFile = this.export();
return patchFile.hashCRC32(0, patchFile.fileSize - 4);
};
BPS.prototype.validateSource = function (romFile, headerSize) {
return this.sourceChecksum === romFile.hashCRC32(headerSize);
};
BPS.prototype.getValidationInfo = function () {
return {
type: "CRC32",
value: this.sourceChecksum,
};
};
BPS.prototype.apply = function (romFile, validate) {
if (validate && !this.validateSource(romFile)) {
throw new Error("Source ROM checksum mismatch");
}
tempFile = new BinFile(this.targetSize);
//patch
var sourceRelativeOffset = 0;
var targetRelativeOffset = 0;
for (var i = 0; i < this.actions.length; i++) {
var action = this.actions[i];
if (action.type === BPS_ACTION_SOURCE_READ) {
romFile.copyTo(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 !== tempFile.hashCRC32()) {
throw new Error("Target ROM checksum mismatch");
}
return tempFile;
};
BPS.MAGIC = BPS_MAGIC;
BPS.fromFile = function (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 !== patch.calculateFileChecksum()) {
throw new Error("Patch checksum mismatch");
}
return patch;
};
function BPS_readVLV() {
var data = 0,
shift = 1;
while (true) {
var x = this.readU8();
data += (x & 0x7f) * shift;
if (x & 0x80) break;
shift <<= 7;
data += shift;
}
this._lastRead = data;
return data;
}
function BPS_writeVLV(data) {
while (true) {
var x = data & 0x7f;
data >>= 7;
if (data === 0) {
this.writeU8(0x80 | x);
break;
}
this.writeU8(x);
data--;
}
}
function BPS_getVLVLen(data) {
var len = 0;
while (true) {
var x = data & 0x7f;
data >>= 7;
if (data === 0) {
len++;
break;
}
len++;
data--;
}
return len;
}
BPS.prototype.export = function (fileName) {
var patchFileSize = BPS_MAGIC.length;
patchFileSize += BPS_getVLVLen(this.sourceSize);
patchFileSize += BPS_getVLVLen(this.targetSize);
patchFileSize += BPS_getVLVLen(this.metaData.length);
patchFileSize += this.metaData.length;
for (var i = 0; i < 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 BinFile(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;
};
BPS.buildFromRoms = function (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 = original.hashCRC32();
patch.targetChecksum = modified.hashCRC32();
patch.patchChecksum = patch.calculateFileChecksum();
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;
}

View File

@@ -0,0 +1,289 @@
/* IPS module for Rom Patcher JS v20250430 - Marc Robledo 2016-2025 - http://www.marcrobledo.com/license */
/* File format specification: http://www.smwiki.net/wiki/IPS_file_format */
/* This file also acts as EBP (EarthBound Patch) module */
/* EBP is actually just IPS with some JSON metadata stuck on the end (implementation: https://github.com/Lyrositor/EBPatcher) */
const IPS_MAGIC = "PATCH";
const IPS_MAX_ROM_SIZE = 0x1000000; //16 megabytes
const IPS_RECORD_RLE = 0x0000;
const IPS_RECORD_SIMPLE = 0x01;
if (typeof module !== "undefined" && module.exports) {
module.exports = IPS;
}
function IPS() {
this.records = [];
this.truncate = false;
this.EBPmetadata = null;
}
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.setEBPMetadata = function (metadataObject) {
if (typeof metadataObject !== "object")
throw new TypeError("metadataObject must be an object");
for (var key in metadataObject) {
if (typeof metadataObject[key] !== "string")
throw new TypeError("metadataObject values must be strings");
}
/* EBPatcher (linked above) expects the "patcher" field to be EBPatcher to read the metadata */
/* CoilSnake (EB modding tool) inserts this manually too */
/* So we also add it here for compatibility purposes */
this.EBPmetadata = { patcher: "EBPatcher", ...metadataObject };
};
IPS.prototype.getDescription = function () {
if (this.EBPmetadata) {
var description = "";
for (var key in this.EBPmetadata) {
if (key === "patcher") continue;
const keyPretty = key.charAt(0).toUpperCase() + key.slice(1);
description += keyPretty + ": " + this.EBPmetadata[key] + "\n";
}
return description.trim();
}
return null;
};
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 && !this.EBPmetadata)
s += "\nTruncate at: 0x" + this.truncate.toString(16);
else if (this.EBPmetadata)
s += "\nEBP Metadata: " + JSON.stringify(this.EBPmetadata);
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 && !this.EBPmetadata)
patchFileSize += 3; //truncate
else if (this.EBPmetadata)
patchFileSize += JSON.stringify(this.EBPmetadata).length;
tempFile = new BinFile(patchFileSize);
tempFile.fileName = fileName + (this.EBPmetadata ? ".ebp" : ".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 (this.truncate && !this.EBPmetadata) tempFile.writeU24(this.truncate);
else if (this.EBPmetadata)
tempFile.writeString(JSON.stringify(this.EBPmetadata));
return tempFile;
};
IPS.prototype.apply = function (romFile) {
if (this.truncate && !this.EBPmetadata) {
if (this.truncate > romFile.fileSize) {
//expand (discussed here: https://github.com/marcrobledo/RomPatcher.js/pull/46)
tempFile = new BinFile(this.truncate);
romFile.copyTo(tempFile, 0, romFile.fileSize, 0);
} else {
//truncate
tempFile = romFile.slice(0, this.truncate);
}
} else {
//calculate target ROM size, expanding it if any record offset is beyond target ROM size
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 BinFile(newFileSize);
romFile.copyTo(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;
};
IPS.MAGIC = IPS_MAGIC;
IPS.fromFile = function (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;
} else if (file.readU8() === "{".charCodeAt(0)) {
file.skip(-1);
patchFile.setEBPMetadata(
JSON.parse(file.readString(file.fileSize - file.offset)),
);
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;
};
IPS.buildFromRoms = function (original, modified, asEBP = false) {
var patch = new IPS();
if (!asEBP && modified.fileSize < original.fileSize) {
patch.truncate = modified.fileSize;
} else if (asEBP) {
patch.setEBPMetadata(
typeof asEBP === "object"
? asEBP
: {
Author: "Unknown",
Title: "Untitled",
Description: "No description",
},
);
}
//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_ROM_SIZE) {
throw new Error(
`Files are too big for ${patch.EBPmetadata ? "EBP" : "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;
};

View File

@@ -0,0 +1,93 @@
/* PMSR (Paper Mario Star Rod) module for Rom Patcher JS v20240721 - Marc Robledo 2020-2024 - 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;
if (typeof module !== "undefined" && module.exports) {
module.exports = PMSR;
}
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 &&
romFile.hashCRC32() === PAPER_MARIO_USA10_CRC32
);
};
PMSR.prototype.getValidationInfo = function () {
return {
type: "CRC32",
value: PAPER_MARIO_USA10_CRC32,
};
};
PMSR.prototype.apply = function (romFile, validate) {
if (validate && !this.validateSource(romFile)) {
throw new Error("Source ROM checksum mismatch");
}
console.log("a");
if (this.targetSize === romFile.fileSize) {
tempFile = romFile.slice(0, romFile.fileSize);
} else {
tempFile = new BinFile(this.targetSize);
romFile.copyTo(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;
};
PMSR.MAGIC = PMSR_MAGIC;
PMSR.fromFile = function (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 */
//PMSR.prototype.export=function(fileName){return null}
//PMSR.buildFromRoms=function(original, modified){return null}
/* https://github.com/pho/WindViewer/wiki/Yaz0-and-Yay0 */
PMSR.YAY0_decode = function (file) {
/* to-do */
};

View File

@@ -0,0 +1,245 @@
/* 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
if (typeof module !== "undefined" && module.exports) {
module.exports = PPF;
}
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 BinFile(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 BinFile(newFileSize);
romFile.copyTo(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;
};
PPF.MAGIC = PPF_MAGIC;
PPF.fromFile = function (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;
};
PPF.buildFromRoms = function (original, modified) {
var patch = new PPF();
patch.description = "Patch description";
if (original.fileSize > modified.fileSize) {
var expandedModified = new BinFile(original.fileSize);
modified.copyTo(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;
};

View File

@@ -0,0 +1,387 @@
/* RUP module for Rom Patcher JS v20250430 - Marc Robledo 2018-2025 - 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",
];
if (typeof module !== "undefined" && module.exports) {
module.exports = RUP;
}
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;
if (file.overflowMode === "A") {
s += "\nOverflow mode: Append " + file.overflowData.length + " bytes";
} else if (file.overflowMode === "M") {
s += "\nOverflow mode: Minify " + file.overflowData.length + " bytes";
}
s += "\n#records: " + file.records.length;
}
return s;
};
RUP.prototype.validateSource = function (romFile, headerSize) {
var md5string = romFile.hashMD5(headerSize);
for (var i = 0; i < this.files.length; i++) {
if (
this.files[i].sourceMD5 === md5string ||
this.files[i].targetMD5 === md5string
) {
return {
file: this.files[i],
undo: this.files[i].targetMD5 === md5string,
};
}
}
return false;
};
RUP.prototype.getValidationInfo = function () {
var values = [];
for (var i = 0; i < this.files.length; i++) {
values.push(this.files[i].sourceMD5);
}
return {
type: "MD5",
value: values,
};
};
RUP.prototype.getDescription = function () {
return this.description ? this.description : null;
};
RUP.prototype.apply = function (romFile, validate) {
var validFile;
if (validate) {
validFile = this.validateSource(romFile);
if (!validFile) throw new Error("Source ROM checksum mismatch");
} else {
validFile = {
file: this.files[0],
undo: this.files[0].targetMD5 === romFile.hashMD5(),
};
}
var undo = validFile.undo;
var patch = validFile.file;
tempFile = new BinFile(!undo ? patch.targetFileSize : patch.sourceFileSize);
/* copy original file */
romFile.copyTo(tempFile, 0);
for (var i = 0; i < patch.records.length; i++) {
var offset = patch.records[i].offset;
romFile.seek(offset);
tempFile.seek(offset);
for (var j = 0; j < patch.records[i].xor.length; j++) {
tempFile.writeU8(
(romFile.isEOF() ? 0x00 : romFile.readU8()) ^ patch.records[i].xor[j],
);
}
}
/* add overflow data if needed */
if (patch.overflowMode === "A" && !undo) {
/* append */
tempFile.seek(patch.sourceFileSize);
tempFile.writeBytes(patch.overflowData.map((byte) => byte ^ 0xff));
} else if (patch.overflowMode === "M" && undo) {
/* minify */
tempFile.seek(patch.targetFileSize);
tempFile.writeBytes(patch.overflowData.map((byte) => byte ^ 0xff));
}
if (
validate &&
((!undo && tempFile.hashMD5() !== patch.targetMD5) ||
(undo && tempFile.hashMD5() !== patch.sourceMD5))
) {
throw new Error("Target ROM checksum mismatch");
}
if (undo) tempFile.unpatched = true;
return tempFile;
};
RUP.MAGIC = RUP_MAGIC;
RUP.padZeroes = function (intVal, nBytes) {
var hexString = intVal.toString(16);
while (hexString.length < nBytes * 2) hexString = "0" + hexString;
return hexString;
};
RUP.fromFile = function (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 += RUP.padZeroes(file.readU8(), 1);
nextFile.targetMD5 = "";
for (var i = 0; i < 16; i++)
nextFile.targetMD5 += RUP.padZeroes(file.readU8(), 1);
if (nextFile.sourceFileSize !== nextFile.targetFileSize) {
nextFile.overflowMode = file.readString(1); // 'M' (source>target) or 'A' (source<target)
if (nextFile.overflowMode !== "M" && nextFile.overflowMode !== "A")
throw new Error("RUP: invalid overflow mode");
nextFile.overflowData = file.readBytes(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.overflowData.length);
patchFileSize += file.overflowData.length;
}
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 BinFile(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.overflowData.length);
patchFile.writeBytes(file.overflowData);
}
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;
};
RUP.buildFromRoms = function (original, modified, description) {
var patch = new RUP();
var today = new Date();
patch.date =
today.getYear() +
1900 +
RUP.padZeroes(today.getMonth() + 1, 1) +
RUP.padZeroes(today.getDate(), 1);
if (description) patch.description = description;
var file = {
fileName: "",
romType: 0,
sourceFileSize: original.fileSize,
targetFileSize: modified.fileSize,
sourceMD5: original.hashMD5(),
targetMD5: modified.hashMD5(),
overflowMode: null,
overflowData: [],
records: [],
};
if (file.sourceFileSize < file.targetFileSize) {
modified.seek(file.sourceFileSize);
file.overflowMode = "A";
file.overflowData = modified
.readBytes(file.targetFileSize - file.sourceFileSize)
.map((byte) => byte ^ 0xff);
modified = modified.slice(0, file.sourceFileSize);
} else if (file.sourceFileSize > file.targetFileSize) {
original.seek(file.targetFileSize);
file.overflowMode = "M";
file.overflowData = original
.readBytes(file.sourceFileSize - file.targetFileSize)
.map((byte) => byte ^ 0xff);
original = original.slice(0, file.targetFileSize);
}
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;
};

View File

@@ -0,0 +1,219 @@
/* UPS module for Rom Patcher JS v20240721 - Marc Robledo 2017-2024 - http://www.marcrobledo.com/license */
/* File format specification: http://www.romhacking.net/documents/392/ */
const UPS_MAGIC = "UPS1";
if (typeof module !== "undefined" && module.exports) {
module.exports = UPS;
}
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: " + this.checksumInput.toString(16);
s += "\nOutput file checksum: " + this.checksumOutput.toString(16);
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 BinFile(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(tempFile.hashCRC32(0, tempFile.fileSize - 4));
return tempFile;
};
UPS.prototype.validateSource = function (romFile, headerSize) {
return romFile.hashCRC32(headerSize) === this.checksumInput;
};
UPS.prototype.getValidationInfo = function () {
return {
type: "CRC32",
value: this.checksumInput,
};
};
UPS.prototype.apply = function (romFile, validate) {
if (validate && !this.validateSource(romFile)) {
throw new Error("Source ROM checksum mismatch");
}
/* fix the glitch that cut the end of the file if it's larger than the changed file patch was originally created with */
/* more info: https://github.com/marcrobledo/RomPatcher.js/pull/40#issuecomment-1069087423 */
sizeOutput = this.sizeOutput;
sizeInput = this.sizeInput;
if (!validate && sizeInput < romFile.fileSize) {
sizeInput = romFile.fileSize;
if (sizeOutput < sizeInput) {
sizeOutput = sizeInput;
}
}
/* copy original file */
tempFile = new BinFile(sizeOutput);
romFile.copyTo(tempFile, 0, 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 && tempFile.hashCRC32() !== this.checksumOutput) {
throw new Error("Target ROM checksum mismatch");
}
return tempFile;
};
UPS.MAGIC = UPS_MAGIC;
/* 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;
}
UPS.fromFile = function (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() !== file.hashCRC32(0, file.fileSize - 4)) {
throw new Error("Patch checksum mismatch");
}
file.littleEndian = false;
return patch;
};
UPS.buildFromRoms = function (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 = original.hashCRC32();
patch.checksumOutput = modified.hashCRC32();
return patch;
};

View File

@@ -0,0 +1,394 @@
/* 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';
*/
if (typeof module !== "undefined" && module.exports) {
module.exports = VCDIFF;
BinFile = require("./BinFile");
}
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 BinFile(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("Target ROM checksum mismatch");
}
parser.skip(
winHeader.addRunDataLength +
winHeader.addressesLength +
winHeader.instructionsLength,
);
targetWindowPosition += winHeader.targetWindowLength;
}
//console.log((performance.now()-t0)/1000);
return tempFile;
};
VCDIFF.MAGIC = VCDIFF_MAGIC;
VCDIFF.fromFile = function (file) {
return new VCDIFF(file);
};
function VCDIFF_Parser(binFile, offset) {
this.fileSize = binFile.fileSize;
this._u8array = binFile._u8array;
this.offset = offset || 0;
/* reimplement readU8, readU32 and skip from BinFile */
/* in web implementation, there are no guarantees BinFile will be dynamically loaded before this one */
/* so we cannot rely on cloning BinFile.prototype */
this.readU8 = binFile.readU8;
this.readU32 = binFile.readU32;
this.skip = binFile.skip;
this.isEOF = binFile.isEOF;
this.seek = binFile.seek;
}
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;
}
};

View File

@@ -0,0 +1,19 @@
Copyright 2019 SheetJS LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
/* code has been moved to RomPatcher.format.bdf.js because of incompatibility between webapp, web worker and CLI */

View File

@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2013, Gildas Lormeau
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
/* 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);

View File

@@ -0,0 +1,973 @@
/*
Copyright (c) 2013 Gildas Lormeau. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
3. The names of the authors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
!(function (b) {
"use strict";
var o,
k = "File format is not recognized.",
a = "File contains encrypted entry.",
s = "File is using Zip64 (4gb+ file size).",
w = "Error while reading zip file.",
n = "Error while reading file data.",
y = 524288,
c = "text/plain";
try {
o = 0 === new Blob([new DataView(new ArrayBuffer(0))]).size;
} catch (e) {}
function r() {
this.crc = -1;
}
function l() {}
function A(e, t) {
var r, n;
return (
(r = new ArrayBuffer(e)),
(n = new Uint8Array(r)),
t && n.set(t, 0),
{ buffer: r, array: n, view: new DataView(r) }
);
}
function e() {}
function t(n) {
var i,
o = this;
((o.size = 0),
(o.init = function (e, t) {
var r = new Blob([n], { type: c });
(i = new f(r)).init(function () {
((o.size = i.size), e());
}, t);
}),
(o.readUint8Array = function (e, t, r, n) {
i.readUint8Array(e, t, r, n);
}));
}
function i(f) {
var u,
r = this;
((r.size = 0),
(r.init = function (e) {
for (var t = f.length; "=" == f.charAt(t - 1); ) t--;
((u = f.indexOf(",") + 1), (r.size = Math.floor(0.75 * (t - u))), e());
}),
(r.readUint8Array = function (e, t, r) {
var n,
i = A(t),
o = 4 * Math.floor(e / 3),
a = 4 * Math.ceil((e + t) / 3),
s = b.atob(f.substring(o + u, a + u)),
c = e - 3 * Math.floor(o / 4);
for (n = c; n < c + t; n++) i.array[n - c] = s.charCodeAt(n);
r(i.array);
}));
}
function f(o) {
var t = this;
((t.size = 0),
(t.init = function (e) {
((t.size = o.size), e());
}),
(t.readUint8Array = function (e, t, r, n) {
var i = new FileReader();
((i.onload = function (e) {
r(new Uint8Array(e.target.result));
}),
(i.onerror = n));
try {
i.readAsArrayBuffer(
(function (e, t, r) {
if (t < 0 || r < 0 || t + r > e.size)
throw new RangeError(
"offset:" + t + ", length:" + r + ", size:" + e.size,
);
return e.slice
? e.slice(t, t + r)
: e.webkitSlice
? e.webkitSlice(t, t + r)
: e.mozSlice
? e.mozSlice(t, t + r)
: e.msSlice
? e.msSlice(t, t + r)
: void 0;
})(o, e, t),
);
} catch (e) {
n(e);
}
}));
}
function u() {}
function h(n) {
var i;
((this.init = function (e) {
((i = new Blob([], { type: c })), e());
}),
(this.writeUint8Array = function (e, t) {
((i = new Blob([i, o ? e : e.buffer], { type: c })), t());
}),
(this.getData = function (t, e) {
var r = new FileReader();
((r.onload = function (e) {
t(e.target.result);
}),
(r.onerror = e),
r.readAsText(i, n));
}));
}
function p(t) {
var o = "",
a = "";
((this.init = function (e) {
((o += "data:" + (t || "") + ";base64,"), e());
}),
(this.writeUint8Array = function (e, t) {
var r,
n = a.length,
i = a;
for (a = "", r = 0; r < 3 * Math.floor((n + e.length) / 3) - n; r++)
i += String.fromCharCode(e[r]);
for (; r < e.length; r++) a += String.fromCharCode(e[r]);
(2 < i.length ? (o += b.btoa(i)) : (a = i), t());
}),
(this.getData = function (e) {
e(o + b.btoa(a));
}));
}
function v(r) {
var n;
((this.init = function (e) {
((n = new Blob([], { type: r })), e());
}),
(this.writeUint8Array = function (e, t) {
((n = new Blob([n, o ? e : e.buffer], { type: r })), t());
}),
(this.getData = function (e) {
e(n);
}));
}
function S(i, r, e, o, t, a, s, n, c, f) {
var u,
l,
w,
h = 0,
p = r.sn;
function v() {
(i.removeEventListener("message", d, !1), n(l, w));
}
function d(e) {
var t = e.data,
r = t.data,
n = t.error;
if (n)
return (
(n.toString = function () {
return "Error: " + this.message;
}),
void c(n)
);
if (t.sn === p)
switch (
("number" == typeof t.codecTime && (i.codecTime += t.codecTime),
"number" == typeof t.crcTime && (i.crcTime += t.crcTime),
t.type)
) {
case "append":
r
? ((l += r.length),
o.writeUint8Array(
r,
function () {
g();
},
f,
))
: g();
break;
case "flush":
((w = t.crc),
r
? ((l += r.length),
o.writeUint8Array(
r,
function () {
v();
},
f,
))
: v());
break;
case "progress":
s && s(u + t.loaded, a);
break;
case "importScripts":
case "newTask":
case "echo":
break;
default:
console.warn("zip.js:launchWorkerProcess: unknown message: ", t);
}
}
function g() {
(u = h * y) <= a
? e.readUint8Array(
t + u,
Math.min(y, a - u),
function (e) {
s && s(u, a);
var t = 0 === u ? r : { sn: p };
((t.type = "append"), (t.data = e));
try {
i.postMessage(t, [e.buffer]);
} catch (e) {
i.postMessage(t);
}
h++;
},
c,
)
: i.postMessage({ sn: p, type: "flush" });
}
((l = 0), i.addEventListener("message", d, !1), g());
}
function _(n, t, i, o, a, e, s, c, f, u) {
var l,
w = 0,
h = 0,
p = "input" === e,
v = "output" === e,
d = new r();
!(function r() {
var e;
if ((l = w * y) < a)
t.readUint8Array(
o + l,
Math.min(y, a - l),
function (e) {
var t;
try {
t = n.append(e, function (e) {
s && s(l + e, a);
});
} catch (e) {
return void f(e);
}
(t
? ((h += t.length),
i.writeUint8Array(
t,
function () {
(w++, setTimeout(r, 1));
},
u,
),
v && d.append(t))
: (w++, setTimeout(r, 1)),
p && d.append(e),
s && s(l, a));
},
f,
);
else {
try {
e = n.flush();
} catch (e) {
return void f(e);
}
e
? (v && d.append(e),
(h += e.length),
i.writeUint8Array(
e,
function () {
c(h, d.get());
},
u,
))
: c(h, d.get());
}
})();
}
function D(e, t, r, n, i, o, a, s, c, f, u) {
b.zip.useWebWorkers && a
? S(
e,
{ sn: t, codecClass: "NOOP", crcType: "input" },
r,
n,
i,
o,
c,
s,
f,
u,
)
: _(new l(), r, n, i, o, "input", c, s, f, u);
}
function d(e) {
var t,
r,
n = "",
i = [
"Ç",
"ü",
"é",
"â",
"ä",
"à",
"å",
"ç",
"ê",
"ë",
"è",
"ï",
"î",
"ì",
"Ä",
"Å",
"É",
"æ",
"Æ",
"ô",
"ö",
"ò",
"û",
"ù",
"ÿ",
"Ö",
"Ü",
"ø",
"£",
"Ø",
"×",
"ƒ",
"á",
"í",
"ó",
"ú",
"ñ",
"Ñ",
"ª",
"º",
"¿",
"®",
"¬",
"½",
"¼",
"¡",
"«",
"»",
"_",
"_",
"_",
"¦",
"¦",
"Á",
"Â",
"À",
"©",
"¦",
"¦",
"+",
"+",
"¢",
"¥",
"+",
"+",
"-",
"-",
"+",
"-",
"+",
"ã",
"Ã",
"+",
"+",
"-",
"-",
"¦",
"-",
"+",
"¤",
"ð",
"Ð",
"Ê",
"Ë",
"È",
"i",
"Í",
"Î",
"Ï",
"+",
"+",
"_",
"_",
"¦",
"Ì",
"_",
"Ó",
"ß",
"Ô",
"Ò",
"õ",
"Õ",
"µ",
"þ",
"Þ",
"Ú",
"Û",
"Ù",
"ý",
"Ý",
"¯",
"´",
"­",
"±",
"_",
"¾",
"¶",
"§",
"÷",
"¸",
"°",
"¨",
"·",
"¹",
"³",
"²",
"_",
" ",
];
for (t = 0; t < e.length; t++)
n +=
127 < (r = 255 & e.charCodeAt(t)) ? i[r - 128] : String.fromCharCode(r);
return n;
}
function g(e) {
return decodeURIComponent(escape(e));
}
function L(e) {
var t,
r = "";
for (t = 0; t < e.length; t++) r += String.fromCharCode(e[t]);
return r;
}
function M(e, t, r, n, i) {
((e.version = t.view.getUint16(r, !0)),
(e.bitFlag = t.view.getUint16(r + 2, !0)),
(e.compressionMethod = t.view.getUint16(r + 4, !0)),
(e.lastModDateRaw = t.view.getUint32(r + 6, !0)),
(e.lastModDate = (function (e) {
var t = (4294901760 & e) >> 16,
r = 65535 & e;
try {
return new Date(
1980 + ((65024 & t) >> 9),
((480 & t) >> 5) - 1,
31 & t,
(63488 & r) >> 11,
(2016 & r) >> 5,
2 * (31 & r),
0,
);
} catch (e) {}
})(e.lastModDateRaw)),
1 != (1 & e.bitFlag)
? ((n || 8 != (8 & e.bitFlag)) &&
((e.crc32 = t.view.getUint32(r + 10, !0)),
(e.compressedSize = t.view.getUint32(r + 14, !0)),
(e.uncompressedSize = t.view.getUint32(r + 18, !0))),
4294967295 !== e.compressedSize && 4294967295 !== e.uncompressedSize
? ((e.filenameLength = t.view.getUint16(r + 22, !0)),
(e.extraFieldLength = t.view.getUint16(r + 24, !0)))
: i(s))
: i(a));
}
function m(m, t, U) {
var z = 0;
function l() {}
l.prototype.getData = function (w, i, h, p) {
var v = this;
function d(e, t) {
var r, n;
p &&
((r = t),
(n = A(4)).view.setUint32(0, r),
v.crc32 != n.view.getUint32(0))
? U("CRC failed.")
: w.getData(function (e) {
i(e);
});
}
function g(e) {
U(e || n);
}
function y(e) {
U(e || "Error while writing file data.");
}
m.readUint8Array(
v.offset,
30,
function (e) {
var l,
t = A(e.length, e);
1347093252 == t.view.getUint32(0)
? (M(v, t, 4, !1, U),
(l = v.offset + 30 + v.filenameLength + v.extraFieldLength),
w.init(function () {
var e, t, r, n, i, o, a, s, c, f, u;
0 === v.compressionMethod
? D(v._worker, z++, m, w, l, v.compressedSize, p, d, h, g, y)
: ((e = v._worker),
(t = z++),
(r = m),
(n = w),
(i = l),
(o = v.compressedSize),
(a = d),
(s = h),
(c = g),
(f = y),
(u = p ? "output" : "none"),
b.zip.useWebWorkers
? S(
e,
{ sn: t, codecClass: "Inflater", crcType: u },
r,
n,
i,
o,
s,
a,
c,
f,
)
: _(new b.zip.Inflater(), r, n, i, o, u, s, a, c, f));
}, y))
: U(k);
},
g,
);
};
var r = {
getEntries: function (f) {
var u = this._worker;
!(function (n) {
var i = 22;
if (m.size < i) U(k);
else {
var e = i + 65536;
t(i, function () {
t(Math.min(e, m.size), function () {
U(k);
});
});
}
function t(e, r) {
m.readUint8Array(
m.size - e,
e,
function (e) {
for (var t = e.length - i; 0 <= t; t--)
if (
80 === e[t] &&
75 === e[t + 1] &&
5 === e[t + 2] &&
6 === e[t + 3]
)
return void n(new DataView(e.buffer, t, i));
r();
},
function () {
U(w);
},
);
}
})(function (e) {
var t, c;
((t = e.getUint32(16, !0)),
(c = e.getUint16(8, !0)),
t < 0 || t >= m.size
? U(k)
: m.readUint8Array(
t,
m.size - t,
function (e) {
var t,
r,
n,
i,
o = 0,
a = [],
s = A(e.length, e);
for (t = 0; t < c; t++) {
if (
(((r = new l())._worker = u),
1347092738 != s.view.getUint32(o))
)
return void U(k);
(M(r, s, o + 6, !0, U),
(r.commentLength = s.view.getUint16(o + 32, !0)),
(r.directory = 16 == (16 & s.view.getUint8(o + 38))),
(r.offset = s.view.getUint32(o + 42, !0)),
(n = L(
s.array.subarray(o + 46, o + 46 + r.filenameLength),
)),
(r.filename = 2048 == (2048 & r.bitFlag) ? g(n) : d(n)),
r.directory ||
"/" != r.filename.charAt(r.filename.length - 1) ||
(r.directory = !0),
(i = L(
s.array.subarray(
o + 46 + r.filenameLength + r.extraFieldLength,
o +
46 +
r.filenameLength +
r.extraFieldLength +
r.commentLength,
),
)),
(r.comment = 2048 == (2048 & r.bitFlag) ? g(i) : d(i)),
a.push(r),
(o +=
46 +
r.filenameLength +
r.extraFieldLength +
r.commentLength));
}
f(a);
},
function () {
U(w);
},
));
});
},
close: function (e) {
(this._worker && (this._worker.terminate(), (this._worker = null)),
e && e());
},
_worker: null,
};
b.zip.useWebWorkers
? E(
"inflater",
function (e) {
((r._worker = e), t(r));
},
function (e) {
U(e);
},
)
: t(r);
}
function z(e) {
return unescape(encodeURIComponent(e));
}
function W(e) {
var t,
r = [];
for (t = 0; t < e.length; t++) r.push(e.charCodeAt(t));
return r;
}
function U(p, t, s, v) {
var c = {},
d = [],
g = 0,
y = 0;
function m(e) {
s(e || "Error while writing zip file.");
}
function U(e) {
s(e || n);
}
var r = {
add: function (r, f, n, u, l) {
var i,
o,
a,
w = this._worker;
function h(e, t) {
var r = A(16);
((g += e || 0),
r.view.setUint32(0, 1347094280),
void 0 !== t &&
(i.view.setUint32(10, t, !0), r.view.setUint32(4, t, !0)),
f &&
(r.view.setUint32(8, e, !0),
i.view.setUint32(14, e, !0),
r.view.setUint32(12, f.size, !0),
i.view.setUint32(18, f.size, !0)),
p.writeUint8Array(
r.array,
function () {
((g += 16), n());
},
m,
));
}
function e() {
var e, t;
((l = l || {}),
(r = r.trim()),
l.directory && "/" != r.charAt(r.length - 1) && (r += "/"),
c.hasOwnProperty(r))
? s("File already exists.")
: ((o = W(z(r))),
d.push(r),
(e = function () {
var e, t, r, n, i, o, a, s, c;
f
? v || 0 === l.level
? D(w, y++, f, p, 0, f.size, !0, h, u, U, m)
: ((e = w),
(t = y++),
(r = f),
(n = p),
(i = l.level),
(o = h),
(a = u),
(s = U),
(c = m),
b.zip.useWebWorkers
? S(
e,
{
sn: t,
options: { level: i },
codecClass: "Deflater",
crcType: "input",
},
r,
n,
0,
r.size,
a,
o,
s,
c,
)
: _(
new b.zip.Deflater(),
r,
n,
0,
r.size,
"input",
a,
o,
s,
c,
))
: h();
}),
(a = l.lastModDate || new Date()),
(i = A(26)),
(c[r] = {
headerArray: i.array,
directory: l.directory,
filename: o,
offset: g,
comment: W(z(l.comment || "")),
}),
i.view.setUint32(0, 335546376),
l.version && i.view.setUint8(0, l.version),
v || 0 === l.level || l.directory || i.view.setUint16(4, 2048),
i.view.setUint16(
6,
(((a.getHours() << 6) | a.getMinutes()) << 5) |
(a.getSeconds() / 2),
!0,
),
i.view.setUint16(
8,
((((a.getFullYear() - 1980) << 4) | (a.getMonth() + 1)) << 5) |
a.getDate(),
!0,
),
i.view.setUint16(22, o.length, !0),
(t = A(30 + o.length)).view.setUint32(0, 1347093252),
t.array.set(i.array, 4),
t.array.set(o, 30),
(g += t.array.length),
p.writeUint8Array(t.array, e, m));
}
f ? f.init(e, U) : e();
},
close: function (e) {
this._worker && (this._worker.terminate(), (this._worker = null));
var t,
r,
n,
i = 0,
o = 0;
for (r = 0; r < d.length; r++)
i += 46 + (n = c[d[r]]).filename.length + n.comment.length;
for (t = A(i + 22), r = 0; r < d.length; r++)
((n = c[d[r]]),
t.view.setUint32(o, 1347092738),
t.view.setUint16(o + 4, 5120),
t.array.set(n.headerArray, o + 6),
t.view.setUint16(o + 32, n.comment.length, !0),
n.directory && t.view.setUint8(o + 38, 16),
t.view.setUint32(o + 42, n.offset, !0),
t.array.set(n.filename, o + 46),
t.array.set(n.comment, o + 46 + n.filename.length),
(o += 46 + n.filename.length + n.comment.length));
(t.view.setUint32(o, 1347093766),
t.view.setUint16(o + 8, d.length, !0),
t.view.setUint16(o + 10, d.length, !0),
t.view.setUint32(o + 12, i, !0),
t.view.setUint32(o + 16, g, !0),
p.writeUint8Array(
t.array,
function () {
p.getData(e);
},
m,
));
},
_worker: null,
};
b.zip.useWebWorkers
? E(
"deflater",
function (e) {
((r._worker = e), t(r));
},
function (e) {
s(e);
},
)
: t(r);
}
((r.prototype.append = function (e) {
for (
var t = 0 | this.crc, r = this.table, n = 0, i = 0 | e.length;
n < i;
n++
)
t = (t >>> 8) ^ r[255 & (t ^ e[n])];
this.crc = t;
}),
(r.prototype.get = function () {
return ~this.crc;
}),
(r.prototype.table = (function () {
var e,
t,
r,
n = [];
for (e = 0; e < 256; e++) {
for (r = e, t = 0; t < 8; t++)
1 & r ? (r = (r >>> 1) ^ 3988292384) : (r >>>= 1);
n[e] = r;
}
return n;
})()),
(l.prototype.append = function (e, t) {
return e;
}),
(l.prototype.flush = function () {}),
((t.prototype = new e()).constructor = t),
((i.prototype = new e()).constructor = i),
((f.prototype = new e()).constructor = f),
(u.prototype.getData = function (e) {
e(this.data);
}),
((h.prototype = new u()).constructor = h),
((p.prototype = new u()).constructor = p),
((v.prototype = new u()).constructor = v));
var C = {
deflater: ["z-worker.js", "deflate.js"],
inflater: ["z-worker.js", "inflate.js"],
};
function E(e, n, i) {
if (null === b.zip.workerScripts || null === b.zip.workerScriptsPath) {
var t, r, o;
if (b.zip.workerScripts) {
if (((t = b.zip.workerScripts[e]), !Array.isArray(t)))
return void i(
new Error("zip.workerScripts." + e + " is not an array!"),
);
((r = t),
(o = document.createElement("a")),
(t = r.map(function (e) {
return ((o.href = e), o.href);
})));
} else (t = C[e].slice(0))[0] = (b.zip.workerScriptsPath || "") + t[0];
var a = new Worker(t[0]);
((a.codecTime = a.crcTime = 0),
a.postMessage({ type: "importScripts", scripts: t.slice(1) }),
a.addEventListener("message", function e(t) {
var r = t.data;
if (r.error) return (a.terminate(), void i(r.error));
"importScripts" === r.type &&
(a.removeEventListener("message", e),
a.removeEventListener("error", s),
n(a));
}),
a.addEventListener("error", s));
} else
i(
new Error(
"Either zip.workerScripts or zip.workerScriptsPath may be set, not both.",
),
);
function s(e) {
(a.terminate(), i(e));
}
}
function F(e) {
console.error(e);
}
b.zip = {
Reader: e,
Writer: u,
BlobReader: f,
Data64URIReader: i,
TextReader: t,
BlobWriter: v,
Data64URIWriter: p,
TextWriter: h,
createReader: function (e, t, r) {
((r = r || F),
e.init(function () {
m(e, t, r);
}, r));
},
createWriter: function (e, t, r, n) {
((r = r || F),
(n = !!n),
e.init(function () {
U(e, t, r, n);
}, r));
},
useWebWorkers: !0,
workerScriptsPath: null,
workerScripts: null,
};
})(this);

View File

@@ -8,6 +8,7 @@ import CollectionsBtn from "@/components/common/Navigation/CollectionsBtn.vue";
import CollectionsDrawer from "@/components/common/Navigation/CollectionsDrawer.vue";
import ConsoleModeBtn from "@/components/common/Navigation/ConsoleModeBtn.vue";
import HomeBtn from "@/components/common/Navigation/HomeBtn.vue";
import PatcherBtn from "@/components/common/Navigation/PatcherBtn.vue";
import PlatformsBtn from "@/components/common/Navigation/PlatformsBtn.vue";
import PlatformsDrawer from "@/components/common/Navigation/PlatformsDrawer.vue";
import ScanBtn from "@/components/common/Navigation/ScanBtn.vue";
@@ -107,6 +108,7 @@ function collapse() {
<CollectionsBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<ScanBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<ConsoleModeBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<PatcherBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<template #append>
<RandomBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import storeNavigation from "@/stores/navigation";
withDefaults(
defineProps<{
block?: boolean;
height?: string;
rounded?: boolean;
withTag?: boolean;
}>(),
{
block: false,
height: "",
rounded: false,
withTag: false,
},
);
const navigationStore = storeNavigation();
</script>
<template>
<v-btn
icon
:block="block"
variant="flat"
color="background"
:height="height"
:class="{ rounded: rounded }"
class="py-4 bg-background d-flex align-center justify-center"
@click="navigationStore.goPatcher"
>
<div class="d-flex flex-column align-center">
<v-icon :color="$route.path.startsWith('/patcher') ? 'primary' : ''">
mdi-memory-arrow-down
</v-icon>
<v-expand-transition>
<span
v-if="withTag"
class="text-caption text-center"
:class="{ 'text-primary': $route.path.startsWith('/patcher') }"
>
Patcher
</span>
</v-expand-transition>
</div>
</v-btn>
</template>

View File

@@ -28,6 +28,7 @@ export const ROUTES = {
EMULATORJS: "emulatorjs",
RUFFLE: "ruffle",
SCAN: "scan",
PATCHER: "patcher",
USER_PROFILE: "user-profile",
USER_INTERFACE: "user-interface",
LIBRARY_MANAGEMENT: "library-management",
@@ -188,6 +189,14 @@ const routes = [
},
component: () => import("@/views/Scan.vue"),
},
{
path: "patcher",
name: ROUTES.PATCHER,
meta: {
title: i18n.global.t("common.patcher"),
},
component: () => import("@/views/Patcher.vue"),
},
{
path: "user/:user",
name: ROUTES.USER_PROFILE,

View File

@@ -45,6 +45,10 @@ export default defineStore("navigation", {
this.reset();
this.$router.push({ name: ROUTES.SCAN });
},
goPatcher() {
this.reset();
this.$router.push({ name: ROUTES.PATCHER });
},
goSearch() {
this.reset();
this.$router.push({ name: ROUTES.SEARCH });

View File

@@ -0,0 +1,207 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
// Declare globals provided by local scripts
declare const BinFile: any;
declare const RomPatcher: any;
const loadError = ref<string | null>(null);
const coreLoaded = ref(false);
const romFile = ref<File | null>(null);
const patchFile = ref<File | null>(null);
const romBin = ref<any | null>(null);
const patchBin = ref<any | null>(null);
const applying = ref(false);
// Local asset URLs for patcher core (Vite will copy these assets)
const CORE_SCRIPTS = [
new URL("../../assets/patcherjs/modules/HashCalculator.js", import.meta.url)
.href,
new URL("../../assets/patcherjs/modules/BinFile.js", import.meta.url).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.ips.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.ups.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.aps_n64.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.aps_gba.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.bps.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.rup.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.ppf.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.bdf.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.pmsr.js",
import.meta.url,
).href,
new URL(
"../../assets/patcherjs/modules/RomPatcher.format.vcdiff.js",
import.meta.url,
).href,
new URL("../../assets/patcherjs/RomPatcher.js", import.meta.url).href,
];
function loadScriptSequentially(urls: string[]): Promise<void> {
return new Promise((resolve, reject) => {
let i = 0;
const next = () => {
if (i >= urls.length) {
resolve();
return;
}
const s = document.createElement("script");
s.src = urls[i++];
s.type = "text/javascript";
s.onload = () => next();
s.onerror = () => reject(new Error("Failed to load script: " + s.src));
document.head.appendChild(s);
};
next();
});
}
async function ensureCoreLoaded() {
if (coreLoaded.value) return;
try {
await loadScriptSequentially(CORE_SCRIPTS);
coreLoaded.value = true;
} catch (e: any) {
loadError.value = e?.message || String(e);
}
}
function onRomChange(e: Event) {
const input = e.target as HTMLInputElement;
const f = input.files?.[0] || null;
romFile.value = f;
romBin.value = null;
}
function onPatchChange(e: Event) {
const input = e.target as HTMLInputElement;
const f = input.files?.[0] || null;
patchFile.value = f;
patchBin.value = null;
}
async function patchRom() {
loadError.value = null;
if (!coreLoaded.value) await ensureCoreLoaded();
if (!coreLoaded.value) return; // bail on error
if (!romFile.value) {
loadError.value = "Please select a ROM file.";
return;
}
if (!patchFile.value) {
loadError.value = "Please select a patch file.";
return;
}
applying.value = true;
try {
// Build BinFile objects asynchronously
await new Promise<void>((resolve, reject) => {
try {
new BinFile(romFile.value, (bf: any) => {
romBin.value = bf;
resolve();
});
} catch (err) {
reject(err);
}
});
await new Promise<void>((resolve, reject) => {
try {
new BinFile(patchFile.value, (bf: any) => {
patchBin.value = bf;
resolve();
});
} catch (err) {
reject(err);
}
});
if (!romBin.value || !patchBin.value) {
throw new Error("Failed to read selected files.");
}
const patch = RomPatcher.parsePatchFile(patchBin.value);
if (!patch) {
throw new Error("Unsupported or invalid patch format.");
}
const patched = RomPatcher.applyPatch(romBin.value, patch, {
requireValidation: false,
fixChecksum: false,
outputSuffix: true,
});
patched.save();
} catch (err: any) {
loadError.value = err?.message || String(err);
} finally {
applying.value = false;
}
}
onMounted(async () => {
// Preload core for faster interaction
await ensureCoreLoaded();
});
</script>
<template>
<v-row class="align-center justify-center scroll h-100" no-gutters>
<v-col cols="12" sm="10" md="8" xl="6">
<v-card class="pa-4" elevation="2">
<v-card-title class="pb-2">ROM Patcher</v-card-title>
<v-card-text>
<v-alert
v-if="loadError"
type="error"
class="mb-4"
density="compact"
>{{ loadError }}</v-alert
>
<div class="d-flex align-center mb-3">
<div class="mr-3">ROM file:</div>
<input type="file" @change="onRomChange" />
</div>
<div class="d-flex align-center mb-4">
<div class="mr-3">Patch file:</div>
<input type="file" @change="onPatchChange" />
</div>
<v-btn :loading="applying" color="success" @click="patchRom"
>Apply patch</v-btn
>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>