├── .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 --------------------------------------------------------------------------------