PageSourceSearch

https://projects.jbritain.net/patchbin/diff.js

js jbritain.net collected 2026-09-25 20:03:45 UTC 7,754 bytes, 249 lines download raw bytes

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.