1// https://stackoverflow.com/a/48968694/12646131 2function saveFile(blob, filename) { 3 if (window.navigator.msSaveOrOpenBlob) { 4 window.navigator.msSaveOrOpenBlob(blob, filename); 5 } else { 6 const a = document.createElement("a"); 7 document.body.appendChild(a); 8 const url = window.URL.createObjectURL(blob); 9 a.href = url; 10 a.download = filename; 11 a.click(); 12 setTimeout(() => { 13 window.URL.revokeObjectURL(url); 14 document.body.removeChild(a); 15 }, 0); 16 } 17} 18 19// https://stackoverflow.com/a/50868276/12646131 20const fromHexString = (hexString) => 21 Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); 22const toHexString = (bytes) => 23 bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); 24 25function packBits(num, size) { 26 let buffer = []; 27 while (num != 0) { 28 buffer.push(num & 255); 29 num = num >> 8; 30 } 31 // pad with zeros 32 buffer = Array(size - buffer.length) 33 .fill(0) 34 .concat(buffer); 35 return buffer; 36} 37 38function unpackBits(buffer) { 39 let num = 0; 40 while (buffer.length > 0) { 41 num = (num << 8) | buffer.pop(); 42 } 43 44 return num; 45} 46 47const originalReader = new FileReader(); 48const modifiedReader = new FileReader(); 49const patchReader = new FileReader(); 50 51let originalLoaded = false; 52let modifiedLoaded = false; 53let patchLoaded = false; 54 55function attemptGeneratePatch() { 56 if (originalLoaded && modifiedLoaded) { 57 originalLoaded = false; 58 modifiedLoaded = false; 59 generatePatch(); 60 } 61} 62 63function attemptApplyPatch() { 64 if (originalLoaded && patchLoaded) { 65 originalLoaded = false; 66 patchLoaded = false; 67 applyPatch(); 68 } 69} 70 71function tryGeneratePatch() { 72 document.querySelector("#error-text").innerText = ""; 73 originalLoaded = false; 74 modifiedLoaded = false; 75 76 if (document.querySelector("#original-file").files[0] == null) { 77 document.querySelector("#error-text").innerText = 78 "You have not uploaded the original file!"; 79 return; 80 } 81 if (document.querySelector("#modified-file").files[0] == null) { 82 document.querySelector("#error-text").innerText = 83 "You have not uploaded the modified file!"; 84 return; 85 } 86 87 originalReader.readAsArrayBuffer( 88 document.querySelector("#original-file").files[0], 89 ); 90 modifiedReader.readAsArrayBuffer( 91 document.querySelector("#modified-file").files[0], 92 ); 93 94 originalReader.onload = () => { 95 originalLoaded = true; 96 attemptGeneratePatch(); 97 }; 98 99 modifiedReader.onload = () => { 100 modifiedLoaded = true; 101 attemptGeneratePatch(); 102 }; 103} 104 105function tryApplyPatch() { 106 document.querySelector("#error-text").innerText = ""; 107 originalLoaded = false; 108 modifiedLoaded = false; 109 110 if (document.querySelector("#original-file").files[0] == null) { 111 document.querySelector("#error-text").innerText = 112 "You have not uploaded the original file!"; 113 return; 114 } 115 if (document.querySelector("#patch-file").files[0] == null) { 116 document.querySelector("#error-text").innerText = 117 "You have not uploaded the patch file!"; 118 return; 119 } 120 121 originalReader.readAsArrayBuffer( 122 document.querySelector("#original-file").files[0], 123 ); 124 patchReader.readAsArrayBuffer(document.querySelector("#patch-file").files[0]); 125 126 originalReader.onload = () => { 127 originalLoaded = true; 128 attemptApplyPatch(); 129 }; 130 131 patchReader.onload = () => { 132 patchLoaded = true; 133 attemptApplyPatch(); 134 }; 135} 136 137function generatePatch() { 138 document.querySelector("#error-text").innerText = ""; 139 const originalBuffer = new Uint8Array(originalReader.result); 140 const modifiedBuffer = new Uint8Array(modifiedReader.result); 141 142 // we store the size of file B in the last four bytes of the diff 143 // and an md5 of the original file in the 16 bytes before that 144 // I am aware that usually header information like this is stored at the start of the file 145 // but on the other hand, who gives a fuck 146 let patchBuffer = new Uint8Array(new ArrayBuffer(modifiedBuffer.length + 20)); 147 148 for (let i = 0; i < modifiedReader.result.byteLength; i++) { 149 const originalByte = originalBuffer.length > i ? originalBuffer[i] : 0; // pad a with zeros if b is larger since the patch is always the size of b 150 const modifiedByte = modifiedBuffer[i]; 151 patchBuffer[i] = originalByte ^ modifiedByte; 152 } 153 154 console.log(`Patched file size is ${modifiedBuffer.length}`); 155 156 let checksum = SparkMD5.ArrayBuffer.hash(originalReader.result); 157 console.log(`Original file checksum is ${checksum}`); 158 159 let patchArray = Array.from(patchBuffer); 160 patchArray.splice( 161 patchBuffer.length - 20, 162 16, 163 ...Array.from(fromHexString(checksum)), 164 ); 165 patchArray.splice( 166 patchBuffer.length - 4, 167 4, 168 ...packBits(modifiedBuffer.length, 4), 169 ); 170 patchBuffer = Uint8Array.from(patchArray); 171 172 const blob = new Blob([patchBuffer], { type: "application/octet-stream" }); 173 saveFile( 174 blob, 175 document.querySelector("#modified-file").files[0].name + "_patch.bin", 176 ); 177} 178 179function applyPatch() { 180 document.querySelector("#error-text").innerText = "";
181 const originalBuffer = new Uint8Array(originalReader.result); 182 const patchBuffer = new Uint8Array(patchReader.result); 183 184 const patchArray = Array.from(patchBuffer); 185 const patchSize = unpackBits(patchArray.slice(patchBuffer.length - 3)); 186 console.log(`Patched file size is ${patchSize} bytes`); 187 188 if (patchSize == 0) { 189 console.log("Patched file size cannot be zero"); 190 document.querySelector("#error-text").innerText = 191 "Patched file size was zero! This usually indicates a corrupt or invalid patch file."; 192 return; 193 } 194 195 const checksumFromPatch = toHexString( 196 Uint8Array.from( 197 patchArray.slice(patchArray.length - 20, patchArray.length - 4), 198 ), 199 ); 200 console.log( 201 `Patch file expected original file checksum to be ${checksumFromPatch}`, 202 ); 203 const checksumFromUpload = SparkMD5.ArrayBuffer.hash(originalReader.result); 204 console.log(`Original file checksum is ${checksumFromUpload}`); 205 206 if (checksumFromPatch != checksumFromUpload) { 207 console.log("Checksums do not match!"); 208 document.querySelector("#error-text").innerHTML = 209 `File checksums do not match! Make sure you have the correct original and patch files.<br>Original checksum: <code>${checksumFromUpload}</code><br>Patch file expected checksum: <code>${checksumFromPatch}</code>`; 210 return; 211 } 212 213 console.log("Checksums match!"); 214 215 const modifiedBuffer = new Uint8Array(new ArrayBuffer(patchSize)); 216 217 for (let i = 0; i < patchSize; i++) { 218 const originalByte = originalBuffer.length > i ? originalBuffer[i] : 0; 219 const patchByte = patchBuffer[i]; 220 modifiedBuffer[i] = originalByte ^ patchByte; 221 } 222 223 const blob = new Blob([modifiedBuffer], { type: "application/octet-stream" }); 224 // remove anything from the filename after the last occurrence of "_patch" 225 let filename = document.querySelector("#patch-file").files[0].name; 226 saveFile(blob, filename.slice(0, filename.lastIndexOf("_patch"))); 227} 228 229function setTab(t) { 230 const applyTab = document.querySelector("#apply-tab"); 231 const genTab = document.querySelector("#gen-tab"); 232 const bcLabel = document.querySelector("#modified-patch-file-label"); 233 const goButton = document.querySelector("#go-button"); 234 const goButtonText = document.querySelector("#go-button-text"); 235 236 if (t == genTab) { 237 genTab.classList.add("tab-active"); 238 applyTab.classList.remove("tab-active"); 239 bcLabel.innerText = "Modified File"; 240 goButton.onclick = tryGeneratePatch; 241 goButtonText.innerText = "Generate Patch"; 242 } else { 243 genTab.classList.remove("tab-active"); 244 applyTab.classList.add("tab-active"); 245 bcLabel.innerText = "Patch File"; 246 goButton.onclick = tryApplyPatch; 247 goButtonText.innerText = "Patch"; 248 } 249}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.