]> git.localhorst.tv Git - alttp.git/blob - resources/js/helpers/bps.js
fix waterfall fairy chest IDs
[alttp.git] / resources / js / helpers / bps.js
1 import CRC32 from 'crc-32';
2
3 const ACTION_SOURCE_READ = 0;
4 const ACTION_TARGET_READ = 1;
5 const ACTION_SOURCE_COPY = 2;
6 const ACTION_TARGET_COPY = 3;
7
8 /**
9  * Class to apply and create BPS's.
10  *
11  * @see https://www.romhacking.net/documents/746/
12  */
13 export default class BPS {
14
15         constructor() {
16                 this.sourceSize = 0;
17                 this.targetSize = 0;
18                 this.metaDataString = "";
19                 this.meta = {};
20                 this.actionsOffset = 0;
21                 this.sourceFile = null;
22                 this.sourceChecksum = 0;
23                 this.targetFile = null;
24                 this.targetChecksum = 0;
25                 this.patchSourceChecksum = 0;
26                 this.patchTargetChecksum = 0;
27                 this.patchChecksum = 0;
28                 this.patchFile = null;
29         }
30
31         /**
32          * Set the patch file to be used.
33          *
34          * @param file BPS formatted file.
35          */
36         setPatch(file) {
37                 this.patchFile = new Uint8Array(file);
38
39                 // Check BPS1 at beginning of patch file
40                 const checkHeader = new Uint32Array(file.slice(0, 4))[0];
41                 if (checkHeader !== 827543618) {
42                         throw new Error("Not a valid patch file");
43                 }
44
45                 let seek = 4; // skip BPS1
46                 const decodedSourceSize = this.decodeBPS(this.patchFile, seek);
47                 this.sourceSize = decodedSourceSize.number;
48                 seek += decodedSourceSize.length;
49                 const decodedTargetSize = this.decodeBPS(this.patchFile, seek);
50                 this.targetSize = decodedTargetSize.number;
51                 seek += decodedTargetSize.length;
52
53                 const decodedMetaDataLength = this.decodeBPS(this.patchFile, seek);
54
55                 seek += decodedMetaDataLength.length;
56                 if (decodedMetaDataLength.number) {
57                         const metaArray = this.patchFile.slice(
58                                 seek,
59                                 seek + decodedMetaDataLength.number
60                         );
61                         for (let i = 0; i < metaArray.byteLength; ++i) {
62                                 this.metaDataString += String.fromCharCode(metaArray[i]);
63                         }
64                         this.meta = JSON.parse(this.metaDataString);
65                         seek += decodedMetaDataLength.number;
66                 }
67
68                 this.actionsOffset = seek;
69
70                 const buf32 = new Int32Array(file.slice(file.byteLength - 12));
71
72                 this.patchSourceChecksum = buf32[0];
73                 this.patchTargetChecksum = buf32[1];
74                 this.patchChecksum = buf32[2];
75
76                 if (
77                         this.patchChecksum !==
78                         CRC32.buf(this.patchFile.slice(0, this.patchFile.byteLength - 4))
79                 ) {
80                         throw new Error("Patch checksum incorrect");
81                 }
82
83                 return this;
84         }
85
86         setSource(file) {
87                 this.sourceFile = new Uint8Array(file);
88                 this.sourceChecksum = CRC32.buf(this.sourceFile);
89
90                 return this;
91         }
92
93         setTarget(file) {
94                 this.targetFile = new Uint8Array(file);
95                 this.targetChecksum = CRC32.buf(this.targetFile);
96
97                 return this;
98         }
99
100         /**
101          * Apply the currently loaded patch to the currently loaded file
102          * and return the patched array buffer.
103          */
104         applyPatch() {
105                 if (this.patchFile === null) {
106                         throw new Error("Patch not set");
107                 }
108
109                 if (this.sourceFile === null) {
110                         throw new Error("Source not set");
111                 }
112
113                 if (this.patchSourceChecksum !== this.sourceChecksum) {
114                         throw new Error("Source checksum incorrect");
115                 }
116
117                 let newFileSize = 0;
118                 let seek = this.actionsOffset;
119
120                 // determine target filesize
121                 while (seek < this.patchFile.byteLength - 12) {
122                         let data = this.decodeBPS(this.patchFile, seek);
123                         let action = {
124                                 type: data.number & 3,
125                                 length: (data.number >> 2) + 1
126                         };
127
128                         seek += data.length;
129
130                         newFileSize += action.length;
131
132                         switch (action.type) {
133                                 case ACTION_TARGET_READ:
134                                         seek += action.length;
135                                         break;
136                                 case ACTION_SOURCE_COPY:
137                                 case ACTION_TARGET_COPY:
138                                         seek += this.decodeBPS(this.patchFile, seek).length;
139                                         break;
140                         }
141                 }
142
143                 const tempFile = new ArrayBuffer(newFileSize);
144                 const tempFileView = new Uint8Array(tempFile);
145
146                 // patch
147                 let outputOffset = 0;
148                 let sourceRelativeOffset = 0;
149                 let targetRelativeOffset = 0;
150
151                 seek = this.actionsOffset;
152
153                 while (seek < this.patchFile.byteLength - 12) {
154                         const data = this.decodeBPS(this.patchFile, seek);
155                         let data2;
156                         const action = {
157                                 type: data.number & 3,
158                                 length: (data.number >> 2) + 1
159                         };
160
161                         seek += data.length;
162
163                         switch (action.type) {
164                                 case ACTION_SOURCE_READ:
165                                         for (let i = 0; i < action.length; ++i) {
166                                                 tempFileView[outputOffset + i] = this.sourceFile[outputOffset + i];
167                                         }
168                                         outputOffset += action.length;
169                                         break;
170                                 case ACTION_TARGET_READ:
171                                         for (let i = 0; i < action.length; ++i) {
172                                                 tempFileView[outputOffset + i] = this.patchFile[seek + i];
173                                         }
174                                         outputOffset += action.length;
175                                         seek += action.length;
176                                         break;
177                                 case ACTION_SOURCE_COPY:
178                                         data2 = this.decodeBPS(this.patchFile, seek);
179                                         seek += data2.length;
180                                         sourceRelativeOffset +=
181                                                 (data2.number & 1 ? -1 : 1) * (data2.number >> 1);
182                                         while (action.length--) {
183                                                 tempFileView[outputOffset] = this.sourceFile[sourceRelativeOffset];
184                                                 outputOffset++;
185                                                 sourceRelativeOffset++;
186                                         }
187                                         break;
188                                 case ACTION_TARGET_COPY:
189                                         data2 = this.decodeBPS(this.patchFile, seek);
190                                         seek += data2.length;
191                                         targetRelativeOffset +=
192                                                 (data2.number & 1 ? -1 : 1) * (data2.number >> 1);
193                                         while (action.length--) {
194                                                 tempFileView[outputOffset] = tempFileView[targetRelativeOffset];
195                                                 outputOffset++;
196                                                 targetRelativeOffset++;
197                                         }
198                                         break;
199                         }
200                 }
201
202                 this.setTarget(tempFile);
203
204                 if (this.patchTargetChecksum !== this.targetChecksum) {
205                         throw new Error("Target checksum incorrect");
206                 }
207
208                 return tempFile;
209         }
210
211         /**
212          * Create a patch from the source and target binaries and return it as an
213          * array buffer.
214          */
215         createPatch() {
216                 throw new Error("Not Currently Implemented");
217         }
218
219         /**
220          * Convert BPS number format into number.
221          *
222          * @todo this is inherrently dangerous with while(true)
223          *
224          * @param dataBytes
225          * @param i
226          */
227         decodeBPS(dataBytes, i) {
228                 let number = 0;
229                 let shift = 1;
230                 let len = 0;
231                 for (let j = 0; j < 16; ++j) {
232                         let x = dataBytes[i];
233                         i++;
234                         len++;
235                         number += (x & 0x7f) * shift;
236                         if (x & 0x80) {
237                                 break;
238                         }
239                         shift <<= 7;
240                         number += shift;
241                 }
242                 return {
243                         number: number,
244                         length: len
245                 };
246         }
247
248         /**
249          * Convert number into BPS number format.
250          *
251          * @todo this is inherrently dangerous with while(true)
252          *
253          * @param toEncode
254          */
255         encodeBPS(toEncode) {
256                 let array = [];
257
258                 for (let i = 0; i < 16; ++i) {
259                         let x = toEncode & 0x7f;
260                         toEncode >>= 7;
261                         if (toEncode === 0) {
262                                 array.push(0x80 | x);
263
264                                 break;
265                         }
266                         array.push(x);
267                         toEncode--;
268                 }
269
270                 return Uint8Array.from(array);
271         }
272 }