From a4c9fa7e9ca763996f4a84fdf46752f068da81f2 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Mon, 9 May 2022 17:08:30 +0200 Subject: [PATCH] js beat decoder --- package-lock.json | 17 +++ package.json | 2 + resources/js/helpers/bps.js | 272 ++++++++++++++++++++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 resources/js/helpers/bps.js diff --git a/package-lock.json b/package-lock.json index 0ae79c2..181e184 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "@fortawesome/free-brands-svg-icons": "^6.0.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.1.17", + "crc-32": "^1.2.2", "formik": "^2.2.9", "i18next": "^21.6.13", "i18next-browser-languagedetector": "^6.1.3", @@ -3905,6 +3906,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -15031,6 +15043,11 @@ "yaml": "^1.10.0" } }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, "create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", diff --git a/package.json b/package.json index 2732cb7..d494074 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "eslintConfig": { "env": { "browser": true, + "es6": true, "node": true }, "extends": [ @@ -75,6 +76,7 @@ "@fortawesome/free-brands-svg-icons": "^6.0.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/react-fontawesome": "^0.1.17", + "crc-32": "^1.2.2", "formik": "^2.2.9", "i18next": "^21.6.13", "i18next-browser-languagedetector": "^6.1.3", diff --git a/resources/js/helpers/bps.js b/resources/js/helpers/bps.js new file mode 100644 index 0000000..9409309 --- /dev/null +++ b/resources/js/helpers/bps.js @@ -0,0 +1,272 @@ +import CRC32 from 'crc-32'; + +const ACTION_SOURCE_READ = 0; +const ACTION_TARGET_READ = 1; +const ACTION_SOURCE_COPY = 2; +const ACTION_TARGET_COPY = 3; + +/** + * Class to apply and create BPS's. + * + * @see https://www.romhacking.net/documents/746/ + */ +export default class BPS { + + constructor() { + this.sourceSize = 0; + this.targetSize = 0; + this.metaDataString = ""; + this.meta = {}; + this.actionsOffset = 0; + this.sourceFile = null; + this.sourceChecksum = 0; + this.targetFile = null; + this.targetChecksum = 0; + this.patchSourceChecksum = 0; + this.patchTargetChecksum = 0; + this.patchChecksum = 0; + this.patchFile = null; + } + + /** + * Set the patch file to be used. + * + * @param file BPS formatted file. + */ + setPatch(file) { + this.patchFile = new Uint8Array(file); + + // Check BPS1 at beginning of patch file + const checkHeader = new Uint32Array(file.slice(0, 4))[0]; + if (checkHeader !== 827543618) { + throw new Error("Not a valid patch file"); + } + + let seek = 4; // skip BPS1 + const decodedSourceSize = this.decodeBPS(this.patchFile, seek); + this.sourceSize = decodedSourceSize.number; + seek += decodedSourceSize.length; + const decodedTargetSize = this.decodeBPS(this.patchFile, seek); + this.targetSize = decodedTargetSize.number; + seek += decodedTargetSize.length; + + const decodedMetaDataLength = this.decodeBPS(this.patchFile, seek); + + seek += decodedMetaDataLength.length; + if (decodedMetaDataLength.number) { + const metaArray = this.patchFile.slice( + seek, + seek + decodedMetaDataLength.number + ); + for (let i = 0; i < metaArray.byteLength; ++i) { + this.metaDataString += String.fromCharCode(metaArray[i]); + } + this.meta = JSON.parse(this.metaDataString); + seek += decodedMetaDataLength.number; + } + + this.actionsOffset = seek; + + const buf32 = new Int32Array(file.slice(file.byteLength - 12)); + + this.patchSourceChecksum = buf32[0]; + this.patchTargetChecksum = buf32[1]; + this.patchChecksum = buf32[2]; + + if ( + this.patchChecksum !== + CRC32.buf(this.patchFile.slice(0, this.patchFile.byteLength - 4)) + ) { + throw new Error("Patch checksum incorrect"); + } + + return this; + } + + setSource(file) { + this.sourceFile = new Uint8Array(file); + this.sourceChecksum = CRC32.buf(file); + + return this; + } + + setTarget(file) { + this.targetFile = new Uint8Array(file); + this.targetChecksum = CRC32.buf(file); + + return this; + } + + /** + * Apply the currently loaded patch to the currently loaded file + * and return the patched array buffer. + */ + applyPatch() { + if (this.patchFile === null) { + throw new Error("Patch not set"); + } + + if (this.sourceFile === null) { + throw new Error("Source not set"); + } + + if (this.patchSourceChecksum !== this.sourceChecksum) { + throw new Error("Source checksum incorrect"); + } + + let newFileSize = 0; + let seek = this.actionsOffset; + + // determine target filesize + while (seek < this.patchFile.byteLength - 12) { + let data = this.decodeBPS(this.patchFile, seek); + let action = { + type: data.number & 3, + length: (data.number >> 2) + 1 + }; + + seek += data.length; + + newFileSize += action.length; + + switch (action.type) { + case ACTION_TARGET_READ: + seek += action.length; + break; + case ACTION_SOURCE_COPY: + case ACTION_TARGET_COPY: + seek += this.decodeBPS(this.patchFile, seek).length; + break; + } + } + + const tempFile = new ArrayBuffer(newFileSize); + const tempFileView = new Uint8Array(tempFile); + + // patch + let outputOffset = 0; + let sourceRelativeOffset = 0; + let targetRelativeOffset = 0; + + seek = this.actionsOffset; + + while (seek < this.patchFile.byteLength - 12) { + const data = this.decodeBPS(this.patchFile, seek); + let data2; + const action = { + type: data.number & 3, + length: (data.number >> 2) + 1 + }; + + seek += data.length; + + switch (action.type) { + case ACTION_SOURCE_READ: + for (let i = 0; i < action.length; ++i) { + tempFileView[outputOffset + i] = this.sourceFile[outputOffset + i]; + } + outputOffset += action.length; + break; + case ACTION_TARGET_READ: + for (let i = 0; i < action.length; ++i) { + tempFileView[outputOffset + i] = this.patchFile[seek + i]; + } + outputOffset += action.length; + seek += action.length; + break; + case ACTION_SOURCE_COPY: + data2 = this.decodeBPS(this.patchFile, seek); + seek += data2.length; + sourceRelativeOffset += + (data2.number & 1 ? -1 : 1) * (data2.number >> 1); + while (action.length--) { + tempFileView[outputOffset] = this.sourceFile[sourceRelativeOffset]; + outputOffset++; + sourceRelativeOffset++; + } + break; + case ACTION_TARGET_COPY: + data2 = this.decodeBPS(this.patchFile, seek); + seek += data2.length; + targetRelativeOffset += + (data2.number & 1 ? -1 : 1) * (data2.number >> 1); + while (action.length--) { + tempFileView[outputOffset] = tempFileView[targetRelativeOffset]; + outputOffset++; + targetRelativeOffset++; + } + break; + } + } + + this.setTarget(tempFile); + + if (this.patchTargetChecksum !== this.targetChecksum) { + throw new Error("Target checksum incorrect"); + } + + return tempFile; + } + + /** + * Create a patch from the source and target binaries and return it as an + * array buffer. + */ + createPatch() { + throw new Error("Not Currently Implemented"); + } + + /** + * Convert BPS number format into number. + * + * @todo this is inherrently dangerous with while(true) + * + * @param dataBytes + * @param i + */ + decodeBPS(dataBytes, i) { + let number = 0; + let shift = 1; + let len = 0; + for (let j = 0; j < 16; ++j) { + let x = dataBytes[i]; + i++; + len++; + number += (x & 0x7f) * shift; + if (x & 0x80) { + break; + } + shift <<= 7; + number += shift; + } + return { + number: number, + length: len + }; + } + + /** + * Convert number into BPS number format. + * + * @todo this is inherrently dangerous with while(true) + * + * @param toEncode + */ + encodeBPS(toEncode) { + let array = []; + + for (let i = 0; i < 16; ++i) { + let x = toEncode & 0x7f; + toEncode >>= 7; + if (toEncode === 0) { + array.push(0x80 | x); + + break; + } + array.push(x); + toEncode--; + } + + return Uint8Array.from(array); + } +} -- 2.39.2