]> git.localhorst.tv Git - alttp.git/commitdiff
js beat decoder
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 9 May 2022 15:08:30 +0000 (17:08 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 9 May 2022 15:08:30 +0000 (17:08 +0200)
package-lock.json
package.json
resources/js/helpers/bps.js [new file with mode: 0644]

index 0ae79c206e83eb61a8ce421e7399f822561af4d3..181e184cef89646d262c387fd25b71eb8f7b324c 100644 (file)
@@ -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",
                 "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",
                 "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",
index 2732cb74166ec33085c5d0d5d3e9d877ab03060e..d494074a58886ffe2ea22519c0290b0cb3a53bd8 100644 (file)
@@ -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 (file)
index 0000000..9409309
--- /dev/null
@@ -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);
+       }
+}