-
Notifications
You must be signed in to change notification settings - Fork 0
/
generate-patch.html
154 lines (125 loc) · 4.79 KB
/
generate-patch.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<!doctype html><meta charset="utf-8">
<title>Generate patch - Patcher</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* This is just some basic styling. */
body {
max-width: 650px;
margin: 40px auto;
padding: 0 10px;
font: 18px/1.5 sans-serif;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
color: #444
}
h1, h2, h3 {
line-height: 1.2
}
label {
display: block
}
input, button {
width: 100%;
margin: 0.5em 0;
}
</style>
<h1>Patcher</h1>
<h2>Generate patch</h2>
<form>
<label>
<legend>Original file</legend>
<input type="file" name="original">
</label>
<label>
<legend>Target file</legend>
<input type="file" name="target">
</label>
<button>Generate patch</button><br>
<small>After generating the patch upload it to the server where you're hosting this tool.</small><br>
<span id="status"></span>
</form>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"></script>
<script src="https://unpkg.com/[email protected]/blake2s.min.js"></script>
<script src="https://unpkg.com/[email protected]/nacl-fast.min.js"></script>
<script src="https://unpkg.com/[email protected]/nacl-util.min.js"></script>
<script src="https://unpkg.com/[email protected]/fossil-delta.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.10/pako_deflate.min.js"></script>
<script src="https://unpkg.com/@ygoe/[email protected]/msgpack.min.js"></script>
<script type="module">
import loadBlake3 from 'https://unpkg.com/[email protected]/browser-async.js'
const form = document.querySelector('form')
const status = document.querySelector('#status')
form.addEventListener('submit', async evt => {
evt.preventDefault()
const originalFile = form.original.files[0]
const targetFile = form.target.files[0]
if (!originalFile || !targetFile) return
const blake3 = await loadBlake3()
const blake = blake3.createHash()
// Prepend the key creating a sort of HMAC
const key = new TextEncoder().encode('PATCHER')
blake.update(key)
status.textContent = 'Hashing file'
const reader = new Response(originalFile).body.getReader()
let readBytes = 0
while (1) {
const data = await reader.read()
if (data.done) break
blake.update(data.value)
readBytes += data.value.length
status.textContent = 'Hashing file: ' + Math.round(readBytes * 100 / originalFile.size) + '%'
}
const digest = blake.digest()
const serverHash = nacl.util.encodeBase64(digest.slice(0, 6))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
const patchName = serverHash + '.bin'
const chunkSize = 1024 * 1024 * 8
const patch = [targetFile.name, targetFile.size, chunkSize]
const chunkAlignPadding = 1024 * 1024
const chunkAlignVerifierSize = 128
const readFile = async (file, start, size) => {
return new Uint8Array(await new Response(file.slice(start, start + size)).arrayBuffer())
}
let oldChunkStart = 0
let newChunkStart = 0
while (oldChunkStart < originalFile.size) {
status.textContent = `Generating deltas: ${Math.round(oldChunkStart * 100 / originalFile.size)}%`
let startCheckPos = chunkSize + chunkAlignPadding - chunkAlignVerifierSize
let endCheckPos = chunkSize - chunkAlignPadding - chunkAlignVerifierSize
let newChunkSize
const paddedOldChunk = await readFile(originalFile, oldChunkStart, chunkSize + chunkAlignVerifierSize)
const paddedNewChunk = await readFile(targetFile, newChunkStart, chunkSize + chunkAlignPadding)
const chunkAligmentVector = paddedOldChunk.slice(chunkSize)
if (chunkAligmentVector.length) {
for (let i = startCheckPos; i >= endCheckPos; i--) {
let found = true
for (let j = 0; j < chunkAlignVerifierSize; j++) {
if (paddedNewChunk[i + j] !== chunkAligmentVector[j]) {
found = false
break
}
}
if (found) {
newChunkSize = i
break
}
}
}
if (!newChunkSize) newChunkSize = chunkSize
oldChunkStart += chunkSize
newChunkStart += newChunkSize
const oldChunk = paddedOldChunk.slice(0, chunkSize)
const newChunk = paddedNewChunk.slice(0, newChunkSize)
const delta = new Uint8Array(fossilDelta.create(oldChunk, newChunk))
patch.push(delta)
}
const compressedData = pako.deflateRaw(msgpack.serialize(patch))
// Comment the below lines two and replace ciphertext with
// compressedData in the saveAs call to disable encryption
// Remember to also disable it in main.js
const salt = new Uint8Array(24)
const ciphertext = nacl.secretbox(compressedData, salt, digest)
status.textContent = 'Patch generated'
// You can replace the line below with a fetch() to some API
saveAs(new Blob([ciphertext]), patchName)
})
</script>