├── .gitignore
├── License
├── Readme.md
├── bzip2.go
├── common_test.go
├── diff.go
├── diff_test.go
├── doc.go
├── encoding.go
├── go.mod
├── patch.go
├── patch_test.go
├── seek.go
├── sort_test.go
└── testdata
├── sample.new
├── sample.old
└── sample.patch
/.gitignore:
--------------------------------------------------------------------------------
1 | test.*
2 |
--------------------------------------------------------------------------------
/License:
--------------------------------------------------------------------------------
1 | Copyright 2012 Keith Rarick
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # binarydist
2 |
3 | Package binarydist implements binary diff and patch as described on
4 | . It reads and writes files
5 | compatible with the tools there.
6 |
7 | Documentation at .
8 |
--------------------------------------------------------------------------------
/bzip2.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "io"
5 | "os/exec"
6 | )
7 |
8 | type bzip2Writer struct {
9 | c *exec.Cmd
10 | w io.WriteCloser
11 | }
12 |
13 | func (w bzip2Writer) Write(b []byte) (int, error) {
14 | return w.w.Write(b)
15 | }
16 |
17 | func (w bzip2Writer) Close() error {
18 | if err := w.w.Close(); err != nil {
19 | return err
20 | }
21 | return w.c.Wait()
22 | }
23 |
24 | // Package compress/bzip2 implements only decompression,
25 | // so we'll fake it by running bzip2 in another process.
26 | func newBzip2Writer(w io.Writer) (wc io.WriteCloser, err error) {
27 | var bw bzip2Writer
28 | bw.c = exec.Command("bzip2", "-c")
29 | bw.c.Stdout = w
30 |
31 | if bw.w, err = bw.c.StdinPipe(); err != nil {
32 | return nil, err
33 | }
34 |
35 | if err = bw.c.Start(); err != nil {
36 | return nil, err
37 | }
38 |
39 | return bw, nil
40 | }
41 |
--------------------------------------------------------------------------------
/common_test.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "io"
5 | "io/ioutil"
6 | "math/rand"
7 | "os"
8 | )
9 |
10 | func mustOpen(path string) *os.File {
11 | f, err := os.Open(path)
12 | if err != nil {
13 | panic(err)
14 | }
15 |
16 | return f
17 | }
18 |
19 | func mustReadAll(r io.Reader) []byte {
20 | b, err := ioutil.ReadAll(r)
21 | if err != nil {
22 | panic(err)
23 | }
24 | return b
25 | }
26 |
27 | func fileCmp(a, b *os.File) int64 {
28 | sa, err := a.Seek(0, 2)
29 | if err != nil {
30 | panic(err)
31 | }
32 |
33 | sb, err := b.Seek(0, 2)
34 | if err != nil {
35 | panic(err)
36 | }
37 |
38 | if sa != sb {
39 | return sa
40 | }
41 |
42 | _, err = a.Seek(0, 0)
43 | if err != nil {
44 | panic(err)
45 | }
46 |
47 | _, err = b.Seek(0, 0)
48 | if err != nil {
49 | panic(err)
50 | }
51 |
52 | pa, err := ioutil.ReadAll(a)
53 | if err != nil {
54 | panic(err)
55 | }
56 |
57 | pb, err := ioutil.ReadAll(b)
58 | if err != nil {
59 | panic(err)
60 | }
61 |
62 | for i := range pa {
63 | if pa[i] != pb[i] {
64 | return int64(i)
65 | }
66 | }
67 | return -1
68 | }
69 |
70 | func mustWriteRandFile(path string, size int, seed int64) *os.File {
71 | p := make([]byte, size)
72 | rand.Seed(seed)
73 | _, err := rand.Read(p)
74 | if err != nil {
75 | panic(err)
76 | }
77 |
78 | f, err := os.Create(path)
79 | if err != nil {
80 | panic(err)
81 | }
82 |
83 | _, err = f.Write(p)
84 | if err != nil {
85 | panic(err)
86 | }
87 |
88 | _, err = f.Seek(0, 0)
89 | if err != nil {
90 | panic(err)
91 | }
92 |
93 | return f
94 | }
95 |
--------------------------------------------------------------------------------
/diff.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "io"
7 | "io/ioutil"
8 | )
9 |
10 | func swap(a []int, i, j int) { a[i], a[j] = a[j], a[i] }
11 |
12 | func split(I, V []int, start, length, h int) {
13 | var i, j, k, x, jj, kk int
14 |
15 | if length < 16 {
16 | for k = start; k < start+length; k += j {
17 | j = 1
18 | x = V[I[k]+h]
19 | for i = 1; k+i < start+length; i++ {
20 | if V[I[k+i]+h] < x {
21 | x = V[I[k+i]+h]
22 | j = 0
23 | }
24 | if V[I[k+i]+h] == x {
25 | swap(I, k+i, k+j)
26 | j++
27 | }
28 | }
29 | for i = 0; i < j; i++ {
30 | V[I[k+i]] = k + j - 1
31 | }
32 | if j == 1 {
33 | I[k] = -1
34 | }
35 | }
36 | return
37 | }
38 |
39 | x = V[I[start+length/2]+h]
40 | jj = 0
41 | kk = 0
42 | for i = start; i < start+length; i++ {
43 | if V[I[i]+h] < x {
44 | jj++
45 | }
46 | if V[I[i]+h] == x {
47 | kk++
48 | }
49 | }
50 | jj += start
51 | kk += jj
52 |
53 | i = start
54 | j = 0
55 | k = 0
56 | for i < jj {
57 | if V[I[i]+h] < x {
58 | i++
59 | } else if V[I[i]+h] == x {
60 | swap(I, i, jj+j)
61 | j++
62 | } else {
63 | swap(I, i, kk+k)
64 | k++
65 | }
66 | }
67 |
68 | for jj+j < kk {
69 | if V[I[jj+j]+h] == x {
70 | j++
71 | } else {
72 | swap(I, jj+j, kk+k)
73 | k++
74 | }
75 | }
76 |
77 | if jj > start {
78 | split(I, V, start, jj-start, h)
79 | }
80 |
81 | for i = 0; i < kk-jj; i++ {
82 | V[I[jj+i]] = kk - 1
83 | }
84 | if jj == kk-1 {
85 | I[jj] = -1
86 | }
87 |
88 | if start+length > kk {
89 | split(I, V, kk, start+length-kk, h)
90 | }
91 | }
92 |
93 | func qsufsort(obuf []byte) []int {
94 | var buckets [256]int
95 | var i, h int
96 | I := make([]int, len(obuf)+1)
97 | V := make([]int, len(obuf)+1)
98 |
99 | for _, c := range obuf {
100 | buckets[c]++
101 | }
102 | for i = 1; i < 256; i++ {
103 | buckets[i] += buckets[i-1]
104 | }
105 | copy(buckets[1:], buckets[:])
106 | buckets[0] = 0
107 |
108 | for i, c := range obuf {
109 | buckets[c]++
110 | I[buckets[c]] = i
111 | }
112 |
113 | I[0] = len(obuf)
114 | for i, c := range obuf {
115 | V[i] = buckets[c]
116 | }
117 |
118 | V[len(obuf)] = 0
119 | for i = 1; i < 256; i++ {
120 | if buckets[i] == buckets[i-1]+1 {
121 | I[buckets[i]] = -1
122 | }
123 | }
124 | I[0] = -1
125 |
126 | for h = 1; I[0] != -(len(obuf) + 1); h += h {
127 | var n int
128 | for i = 0; i < len(obuf)+1; {
129 | if I[i] < 0 {
130 | n -= I[i]
131 | i -= I[i]
132 | } else {
133 | if n != 0 {
134 | I[i-n] = -n
135 | }
136 | n = V[I[i]] + 1 - i
137 | split(I, V, i, n, h)
138 | i += n
139 | n = 0
140 | }
141 | }
142 | if n != 0 {
143 | I[i-n] = -n
144 | }
145 | }
146 |
147 | for i = 0; i < len(obuf)+1; i++ {
148 | I[V[i]] = i
149 | }
150 | return I
151 | }
152 |
153 | func matchlen(a, b []byte) (i int) {
154 | for i < len(a) && i < len(b) && a[i] == b[i] {
155 | i++
156 | }
157 | return i
158 | }
159 |
160 | func search(I []int, obuf, nbuf []byte, st, en int) (pos, n int) {
161 | if en-st < 2 {
162 | x := matchlen(obuf[I[st]:], nbuf)
163 | y := matchlen(obuf[I[en]:], nbuf)
164 |
165 | if x > y {
166 | return I[st], x
167 | } else {
168 | return I[en], y
169 | }
170 | }
171 |
172 | x := st + (en-st)/2
173 | if bytes.Compare(obuf[I[x]:], nbuf) < 0 {
174 | return search(I, obuf, nbuf, x, en)
175 | } else {
176 | return search(I, obuf, nbuf, st, x)
177 | }
178 | panic("unreached")
179 | }
180 |
181 | // Diff computes the difference between old and new, according to the bsdiff
182 | // algorithm, and writes the result to patch.
183 | func Diff(old, new io.Reader, patch io.Writer) error {
184 | obuf, err := ioutil.ReadAll(old)
185 | if err != nil {
186 | return err
187 | }
188 |
189 | nbuf, err := ioutil.ReadAll(new)
190 | if err != nil {
191 | return err
192 | }
193 |
194 | pbuf, err := diffBytes(obuf, nbuf)
195 | if err != nil {
196 | return err
197 | }
198 |
199 | _, err = patch.Write(pbuf)
200 | return err
201 | }
202 |
203 | func diffBytes(obuf, nbuf []byte) ([]byte, error) {
204 | var patch seekBuffer
205 | err := diff(obuf, nbuf, &patch)
206 | if err != nil {
207 | return nil, err
208 | }
209 | return patch.buf, nil
210 | }
211 |
212 | func diff(obuf, nbuf []byte, patch io.WriteSeeker) error {
213 | var lenf int
214 | I := qsufsort(obuf)
215 | db := make([]byte, len(nbuf))
216 | eb := make([]byte, len(nbuf))
217 | var dblen, eblen int
218 |
219 | var hdr header
220 | hdr.Magic = magic
221 | hdr.NewSize = int64(len(nbuf))
222 | err := binary.Write(patch, signMagLittleEndian{}, &hdr)
223 | if err != nil {
224 | return err
225 | }
226 |
227 | // Compute the differences, writing ctrl as we go
228 | pfbz2, err := newBzip2Writer(patch)
229 | if err != nil {
230 | return err
231 | }
232 | var scan, pos, length int
233 | var lastscan, lastpos, lastoffset int
234 | for scan < len(nbuf) {
235 | var oldscore int
236 | scan += length
237 | for scsc := scan; scan < len(nbuf); scan++ {
238 | pos, length = search(I, obuf, nbuf[scan:], 0, len(obuf))
239 |
240 | for ; scsc < scan+length; scsc++ {
241 | if scsc+lastoffset < len(obuf) &&
242 | obuf[scsc+lastoffset] == nbuf[scsc] {
243 | oldscore++
244 | }
245 | }
246 |
247 | if (length == oldscore && length != 0) || length > oldscore+8 {
248 | break
249 | }
250 |
251 | if scan+lastoffset < len(obuf) && obuf[scan+lastoffset] == nbuf[scan] {
252 | oldscore--
253 | }
254 | }
255 |
256 | if length != oldscore || scan == len(nbuf) {
257 | var s, Sf int
258 | lenf = 0
259 | for i := 0; lastscan+i < scan && lastpos+i < len(obuf); {
260 | if obuf[lastpos+i] == nbuf[lastscan+i] {
261 | s++
262 | }
263 | i++
264 | if s*2-i > Sf*2-lenf {
265 | Sf = s
266 | lenf = i
267 | }
268 | }
269 |
270 | lenb := 0
271 | if scan < len(nbuf) {
272 | var s, Sb int
273 | for i := 1; (scan >= lastscan+i) && (pos >= i); i++ {
274 | if obuf[pos-i] == nbuf[scan-i] {
275 | s++
276 | }
277 | if s*2-i > Sb*2-lenb {
278 | Sb = s
279 | lenb = i
280 | }
281 | }
282 | }
283 |
284 | if lastscan+lenf > scan-lenb {
285 | overlap := (lastscan + lenf) - (scan - lenb)
286 | s := 0
287 | Ss := 0
288 | lens := 0
289 | for i := 0; i < overlap; i++ {
290 | if nbuf[lastscan+lenf-overlap+i] == obuf[lastpos+lenf-overlap+i] {
291 | s++
292 | }
293 | if nbuf[scan-lenb+i] == obuf[pos-lenb+i] {
294 | s--
295 | }
296 | if s > Ss {
297 | Ss = s
298 | lens = i + 1
299 | }
300 | }
301 |
302 | lenf += lens - overlap
303 | lenb -= lens
304 | }
305 |
306 | for i := 0; i < lenf; i++ {
307 | db[dblen+i] = nbuf[lastscan+i] - obuf[lastpos+i]
308 | }
309 | for i := 0; i < (scan-lenb)-(lastscan+lenf); i++ {
310 | eb[eblen+i] = nbuf[lastscan+lenf+i]
311 | }
312 |
313 | dblen += lenf
314 | eblen += (scan - lenb) - (lastscan + lenf)
315 |
316 | err = binary.Write(pfbz2, signMagLittleEndian{}, int64(lenf))
317 | if err != nil {
318 | pfbz2.Close()
319 | return err
320 | }
321 |
322 | val := (scan - lenb) - (lastscan + lenf)
323 | err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val))
324 | if err != nil {
325 | pfbz2.Close()
326 | return err
327 | }
328 |
329 | val = (pos - lenb) - (lastpos + lenf)
330 | err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val))
331 | if err != nil {
332 | pfbz2.Close()
333 | return err
334 | }
335 |
336 | lastscan = scan - lenb
337 | lastpos = pos - lenb
338 | lastoffset = pos - scan
339 | }
340 | }
341 | err = pfbz2.Close()
342 | if err != nil {
343 | return err
344 | }
345 |
346 | // Compute size of compressed ctrl data
347 | l64, err := patch.Seek(0, 1)
348 | if err != nil {
349 | return err
350 | }
351 | hdr.CtrlLen = int64(l64 - 32)
352 |
353 | // Write compressed diff data
354 | pfbz2, err = newBzip2Writer(patch)
355 | if err != nil {
356 | return err
357 | }
358 | n, err := pfbz2.Write(db[:dblen])
359 | if err != nil {
360 | pfbz2.Close()
361 | return err
362 | }
363 | if n != dblen {
364 | pfbz2.Close()
365 | return io.ErrShortWrite
366 | }
367 | err = pfbz2.Close()
368 | if err != nil {
369 | return err
370 | }
371 |
372 | // Compute size of compressed diff data
373 | n64, err := patch.Seek(0, 1)
374 | if err != nil {
375 | return err
376 | }
377 | hdr.DiffLen = n64 - l64
378 |
379 | // Write compressed extra data
380 | pfbz2, err = newBzip2Writer(patch)
381 | if err != nil {
382 | return err
383 | }
384 | n, err = pfbz2.Write(eb[:eblen])
385 | if err != nil {
386 | pfbz2.Close()
387 | return err
388 | }
389 | if n != eblen {
390 | pfbz2.Close()
391 | return io.ErrShortWrite
392 | }
393 | err = pfbz2.Close()
394 | if err != nil {
395 | return err
396 | }
397 |
398 | // Seek to the beginning, write the header, and close the file
399 | _, err = patch.Seek(0, 0)
400 | if err != nil {
401 | return err
402 | }
403 | err = binary.Write(patch, signMagLittleEndian{}, &hdr)
404 | if err != nil {
405 | return err
406 | }
407 | return nil
408 | }
409 |
--------------------------------------------------------------------------------
/diff_test.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os"
7 | "os/exec"
8 | "testing"
9 | )
10 |
11 | var diffT = []struct {
12 | old *os.File
13 | new *os.File
14 | }{
15 | {
16 | old: mustWriteRandFile("test.old", 1e3, 1),
17 | new: mustWriteRandFile("test.new", 1e3, 2),
18 | },
19 | {
20 | old: mustOpen("testdata/sample.old"),
21 | new: mustOpen("testdata/sample.new"),
22 | },
23 | }
24 |
25 | func TestDiff(t *testing.T) {
26 | for _, s := range diffT {
27 | got, err := ioutil.TempFile("/tmp", "bspatch.")
28 | if err != nil {
29 | panic(err)
30 | }
31 | os.Remove(got.Name())
32 |
33 | exp, err := ioutil.TempFile("/tmp", "bspatch.")
34 | if err != nil {
35 | panic(err)
36 | }
37 |
38 | cmd := exec.Command("bsdiff", s.old.Name(), s.new.Name(), exp.Name())
39 | cmd.Stdout = os.Stdout
40 | err = cmd.Run()
41 | os.Remove(exp.Name())
42 | if err != nil {
43 | panic(err)
44 | }
45 |
46 | err = Diff(s.old, s.new, got)
47 | if err != nil {
48 | t.Fatal("err", err)
49 | }
50 |
51 | _, err = got.Seek(0, 0)
52 | if err != nil {
53 | panic(err)
54 | }
55 | gotBuf := mustReadAll(got)
56 | expBuf := mustReadAll(exp)
57 |
58 | if !bytes.Equal(gotBuf, expBuf) {
59 | t.Fail()
60 | t.Logf("diff %s %s", s.old.Name(), s.new.Name())
61 | t.Logf("%s: len(got) = %d", got.Name(), len(gotBuf))
62 | t.Logf("%s: len(exp) = %d", exp.Name(), len(expBuf))
63 | i := matchlen(gotBuf, expBuf)
64 | t.Logf("produced different output at pos %d; %d != %d", i, gotBuf[i], expBuf[i])
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package binarydist implements binary diff and patch as described on
2 | // http://www.daemonology.net/bsdiff/. It reads and writes files
3 | // compatible with the tools there.
4 | package binarydist
5 |
6 | var magic = [8]byte{'B', 'S', 'D', 'I', 'F', 'F', '4', '0'}
7 |
8 | // File format:
9 | // 0 8 "BSDIFF40"
10 | // 8 8 X
11 | // 16 8 Y
12 | // 24 8 sizeof(newfile)
13 | // 32 X bzip2(control block)
14 | // 32+X Y bzip2(diff block)
15 | // 32+X+Y ??? bzip2(extra block)
16 | // with control block a set of triples (x,y,z) meaning "add x bytes
17 | // from oldfile to x bytes from the diff block; copy y bytes from the
18 | // extra block; seek forwards in oldfile by z bytes".
19 | type header struct {
20 | Magic [8]byte
21 | CtrlLen int64
22 | DiffLen int64
23 | NewSize int64
24 | }
25 |
--------------------------------------------------------------------------------
/encoding.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | // SignMagLittleEndian is the numeric encoding used by the bsdiff tools.
4 | // It implements binary.ByteOrder using a sign-magnitude format
5 | // and little-endian byte order. Only methods Uint64 and String
6 | // have been written; the rest panic.
7 | type signMagLittleEndian struct{}
8 |
9 | func (signMagLittleEndian) Uint16(b []byte) uint16 { panic("unimplemented") }
10 |
11 | func (signMagLittleEndian) PutUint16(b []byte, v uint16) { panic("unimplemented") }
12 |
13 | func (signMagLittleEndian) Uint32(b []byte) uint32 { panic("unimplemented") }
14 |
15 | func (signMagLittleEndian) PutUint32(b []byte, v uint32) { panic("unimplemented") }
16 |
17 | func (signMagLittleEndian) Uint64(b []byte) uint64 {
18 | y := int64(b[0]) |
19 | int64(b[1])<<8 |
20 | int64(b[2])<<16 |
21 | int64(b[3])<<24 |
22 | int64(b[4])<<32 |
23 | int64(b[5])<<40 |
24 | int64(b[6])<<48 |
25 | int64(b[7]&0x7f)<<56
26 |
27 | if b[7]&0x80 != 0 {
28 | y = -y
29 | }
30 | return uint64(y)
31 | }
32 |
33 | func (signMagLittleEndian) PutUint64(b []byte, v uint64) {
34 | x := int64(v)
35 | neg := x < 0
36 | if neg {
37 | x = -x
38 | }
39 |
40 | b[0] = byte(x)
41 | b[1] = byte(x >> 8)
42 | b[2] = byte(x >> 16)
43 | b[3] = byte(x >> 24)
44 | b[4] = byte(x >> 32)
45 | b[5] = byte(x >> 40)
46 | b[6] = byte(x >> 48)
47 | b[7] = byte(x >> 56)
48 | if neg {
49 | b[7] |= 0x80
50 | }
51 | }
52 |
53 | func (signMagLittleEndian) String() string { return "signMagLittleEndian" }
54 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module "github.com/kr/binarydist"
2 |
--------------------------------------------------------------------------------
/patch.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "bytes"
5 | "compress/bzip2"
6 | "encoding/binary"
7 | "errors"
8 | "io"
9 | "io/ioutil"
10 | )
11 |
12 | var ErrCorrupt = errors.New("corrupt patch")
13 |
14 | // Patch applies patch to old, according to the bspatch algorithm,
15 | // and writes the result to new.
16 | func Patch(old io.Reader, new io.Writer, patch io.Reader) error {
17 | var hdr header
18 | err := binary.Read(patch, signMagLittleEndian{}, &hdr)
19 | if err != nil {
20 | return err
21 | }
22 | if hdr.Magic != magic {
23 | return ErrCorrupt
24 | }
25 | if hdr.CtrlLen < 0 || hdr.DiffLen < 0 || hdr.NewSize < 0 {
26 | return ErrCorrupt
27 | }
28 |
29 | ctrlbuf := make([]byte, hdr.CtrlLen)
30 | _, err = io.ReadFull(patch, ctrlbuf)
31 | if err != nil {
32 | return err
33 | }
34 | cpfbz2 := bzip2.NewReader(bytes.NewReader(ctrlbuf))
35 |
36 | diffbuf := make([]byte, hdr.DiffLen)
37 | _, err = io.ReadFull(patch, diffbuf)
38 | if err != nil {
39 | return err
40 | }
41 | dpfbz2 := bzip2.NewReader(bytes.NewReader(diffbuf))
42 |
43 | // The entire rest of the file is the extra block.
44 | epfbz2 := bzip2.NewReader(patch)
45 |
46 | obuf, err := ioutil.ReadAll(old)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | nbuf := make([]byte, hdr.NewSize)
52 |
53 | var oldpos, newpos int64
54 | for newpos < hdr.NewSize {
55 | var ctrl struct{ Add, Copy, Seek int64 }
56 | err = binary.Read(cpfbz2, signMagLittleEndian{}, &ctrl)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | // Sanity-check
62 | if newpos+ctrl.Add > hdr.NewSize {
63 | return ErrCorrupt
64 | }
65 |
66 | // Read diff string
67 | _, err = io.ReadFull(dpfbz2, nbuf[newpos:newpos+ctrl.Add])
68 | if err != nil {
69 | return ErrCorrupt
70 | }
71 |
72 | // Add old data to diff string
73 | for i := int64(0); i < ctrl.Add; i++ {
74 | if oldpos+i >= 0 && oldpos+i < int64(len(obuf)) {
75 | nbuf[newpos+i] += obuf[oldpos+i]
76 | }
77 | }
78 |
79 | // Adjust pointers
80 | newpos += ctrl.Add
81 | oldpos += ctrl.Add
82 |
83 | // Sanity-check
84 | if newpos+ctrl.Copy > hdr.NewSize {
85 | return ErrCorrupt
86 | }
87 |
88 | // Read extra string
89 | _, err = io.ReadFull(epfbz2, nbuf[newpos:newpos+ctrl.Copy])
90 | if err != nil {
91 | return ErrCorrupt
92 | }
93 |
94 | // Adjust pointers
95 | newpos += ctrl.Copy
96 | oldpos += ctrl.Seek
97 | }
98 |
99 | // Write the new file
100 | for len(nbuf) > 0 {
101 | n, err := new.Write(nbuf)
102 | if err != nil {
103 | return err
104 | }
105 | nbuf = nbuf[n:]
106 | }
107 |
108 | return nil
109 | }
110 |
--------------------------------------------------------------------------------
/patch_test.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "os/exec"
7 | "testing"
8 | )
9 |
10 | func TestPatch(t *testing.T) {
11 | mustWriteRandFile("test.old", 1e3, 1)
12 | mustWriteRandFile("test.new", 1e3, 2)
13 |
14 | got, err := ioutil.TempFile("/tmp", "bspatch.")
15 | if err != nil {
16 | panic(err)
17 | }
18 | os.Remove(got.Name())
19 |
20 | err = exec.Command("bsdiff", "test.old", "test.new", "test.patch").Run()
21 | if err != nil {
22 | panic(err)
23 | }
24 |
25 | err = Patch(mustOpen("test.old"), got, mustOpen("test.patch"))
26 | if err != nil {
27 | t.Fatal("err", err)
28 | }
29 |
30 | ref, err := got.Seek(0, 2)
31 | if err != nil {
32 | panic(err)
33 | }
34 |
35 | t.Logf("got %d bytes", ref)
36 | if n := fileCmp(got, mustOpen("test.new")); n > -1 {
37 | t.Fatalf("produced different output at pos %d", n)
38 | }
39 | }
40 |
41 | func TestPatchHk(t *testing.T) {
42 | got, err := ioutil.TempFile("/tmp", "bspatch.")
43 | if err != nil {
44 | panic(err)
45 | }
46 | os.Remove(got.Name())
47 |
48 | err = Patch(mustOpen("testdata/sample.old"), got, mustOpen("testdata/sample.patch"))
49 | if err != nil {
50 | t.Fatal("err", err)
51 | }
52 |
53 | ref, err := got.Seek(0, 2)
54 | if err != nil {
55 | panic(err)
56 | }
57 |
58 | t.Logf("got %d bytes", ref)
59 | if n := fileCmp(got, mustOpen("testdata/sample.new")); n > -1 {
60 | t.Fatalf("produced different output at pos %d", n)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/seek.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | type seekBuffer struct {
8 | buf []byte
9 | pos int
10 | }
11 |
12 | func (b *seekBuffer) Write(p []byte) (n int, err error) {
13 | n = copy(b.buf[b.pos:], p)
14 | if n == len(p) {
15 | b.pos += n
16 | return n, nil
17 | }
18 | b.buf = append(b.buf, p[n:]...)
19 | b.pos += len(p)
20 | return len(p), nil
21 | }
22 |
23 | func (b *seekBuffer) Seek(offset int64, whence int) (ret int64, err error) {
24 | var abs int64
25 | switch whence {
26 | case 0:
27 | abs = offset
28 | case 1:
29 | abs = int64(b.pos) + offset
30 | case 2:
31 | abs = int64(len(b.buf)) + offset
32 | default:
33 | return 0, errors.New("binarydist: invalid whence")
34 | }
35 | if abs < 0 {
36 | return 0, errors.New("binarydist: negative position")
37 | }
38 | if abs >= 1<<31 {
39 | return 0, errors.New("binarydist: position out of range")
40 | }
41 | b.pos = int(abs)
42 | return abs, nil
43 | }
44 |
--------------------------------------------------------------------------------
/sort_test.go:
--------------------------------------------------------------------------------
1 | package binarydist
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "testing"
7 | )
8 |
9 | var sortT = [][]byte{
10 | mustRandBytes(1000),
11 | mustReadAll(mustOpen("test.old")),
12 | []byte("abcdefabcdef"),
13 | }
14 |
15 | func TestQsufsort(t *testing.T) {
16 | for _, s := range sortT {
17 | I := qsufsort(s)
18 | for i := 1; i < len(I); i++ {
19 | if bytes.Compare(s[I[i-1]:], s[I[i]:]) > 0 {
20 | t.Fatalf("unsorted at %d", i)
21 | }
22 | }
23 | }
24 | }
25 |
26 | func mustRandBytes(n int) []byte {
27 | b := make([]byte, n)
28 | _, err := rand.Read(b)
29 | if err != nil {
30 | panic(err)
31 | }
32 | return b
33 | }
34 |
--------------------------------------------------------------------------------
/testdata/sample.new:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kr/binarydist/190e7de772656c6127fa8e55c6258ed1b7eabcee/testdata/sample.new
--------------------------------------------------------------------------------
/testdata/sample.old:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kr/binarydist/190e7de772656c6127fa8e55c6258ed1b7eabcee/testdata/sample.old
--------------------------------------------------------------------------------
/testdata/sample.patch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kr/binarydist/190e7de772656c6127fa8e55c6258ed1b7eabcee/testdata/sample.patch
--------------------------------------------------------------------------------