1
0
Fork 0
mirror of https://github.com/marcrobledo/RomPatcher.js.git synced 2025-06-27 16:25:54 +00:00
RomPatcher.js/rom-patcher-js/modules/BinFile.js
2024-08-09 19:30:49 +02:00

475 lines
14 KiB
JavaScript

/*
* BinFile.js (last update: 2024-02-27)
* 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
offset = Math.floor(offset);
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.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');
}