1 import CRC32 from 'crc-32';
3 const ACTION_SOURCE_READ = 0;
4 const ACTION_TARGET_READ = 1;
5 const ACTION_SOURCE_COPY = 2;
6 const ACTION_TARGET_COPY = 3;
9 * Class to apply and create BPS's.
11 * @see https://www.romhacking.net/documents/746/
13 export default class BPS {
18 this.metaDataString = "";
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;
32 * Set the patch file to be used.
34 * @param file BPS formatted file.
37 this.patchFile = new Uint8Array(file);
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");
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;
53 const decodedMetaDataLength = this.decodeBPS(this.patchFile, seek);
55 seek += decodedMetaDataLength.length;
56 if (decodedMetaDataLength.number) {
57 const metaArray = this.patchFile.slice(
59 seek + decodedMetaDataLength.number
61 for (let i = 0; i < metaArray.byteLength; ++i) {
62 this.metaDataString += String.fromCharCode(metaArray[i]);
64 this.meta = JSON.parse(this.metaDataString);
65 seek += decodedMetaDataLength.number;
68 this.actionsOffset = seek;
70 const buf32 = new Int32Array(file.slice(file.byteLength - 12));
72 this.patchSourceChecksum = buf32[0];
73 this.patchTargetChecksum = buf32[1];
74 this.patchChecksum = buf32[2];
77 this.patchChecksum !==
78 CRC32.buf(this.patchFile.slice(0, this.patchFile.byteLength - 4))
80 throw new Error("Patch checksum incorrect");
87 this.sourceFile = new Uint8Array(file);
88 this.sourceChecksum = CRC32.buf(file);
94 this.targetFile = new Uint8Array(file);
95 this.targetChecksum = CRC32.buf(file);
101 * Apply the currently loaded patch to the currently loaded file
102 * and return the patched array buffer.
105 if (this.patchFile === null) {
106 throw new Error("Patch not set");
109 if (this.sourceFile === null) {
110 throw new Error("Source not set");
113 if (this.patchSourceChecksum !== this.sourceChecksum) {
114 throw new Error("Source checksum incorrect");
118 let seek = this.actionsOffset;
120 // determine target filesize
121 while (seek < this.patchFile.byteLength - 12) {
122 let data = this.decodeBPS(this.patchFile, seek);
124 type: data.number & 3,
125 length: (data.number >> 2) + 1
130 newFileSize += action.length;
132 switch (action.type) {
133 case ACTION_TARGET_READ:
134 seek += action.length;
136 case ACTION_SOURCE_COPY:
137 case ACTION_TARGET_COPY:
138 seek += this.decodeBPS(this.patchFile, seek).length;
143 const tempFile = new ArrayBuffer(newFileSize);
144 const tempFileView = new Uint8Array(tempFile);
147 let outputOffset = 0;
148 let sourceRelativeOffset = 0;
149 let targetRelativeOffset = 0;
151 seek = this.actionsOffset;
153 while (seek < this.patchFile.byteLength - 12) {
154 const data = this.decodeBPS(this.patchFile, seek);
157 type: data.number & 3,
158 length: (data.number >> 2) + 1
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];
168 outputOffset += action.length;
170 case ACTION_TARGET_READ:
171 for (let i = 0; i < action.length; ++i) {
172 tempFileView[outputOffset + i] = this.patchFile[seek + i];
174 outputOffset += action.length;
175 seek += action.length;
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];
185 sourceRelativeOffset++;
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];
196 targetRelativeOffset++;
202 this.setTarget(tempFile);
204 if (this.patchTargetChecksum !== this.targetChecksum) {
205 throw new Error("Target checksum incorrect");
212 * Create a patch from the source and target binaries and return it as an
216 throw new Error("Not Currently Implemented");
220 * Convert BPS number format into number.
222 * @todo this is inherrently dangerous with while(true)
227 decodeBPS(dataBytes, i) {
231 for (let j = 0; j < 16; ++j) {
232 let x = dataBytes[i];
235 number += (x & 0x7f) * shift;
249 * Convert number into BPS number format.
251 * @todo this is inherrently dangerous with while(true)
255 encodeBPS(toEncode) {
258 for (let i = 0; i < 16; ++i) {
259 let x = toEncode & 0x7f;
261 if (toEncode === 0) {
262 array.push(0x80 | x);
270 return Uint8Array.from(array);