bsdiff4 1.4.1__tar.gz → 1.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bsdiff4
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
4
  Summary: binary diff and patch using the BSDIFF4-format
5
5
  Home-page: https://github.com/ilanschnell/bsdiff4
6
6
  Author: Ilan Schnell
@@ -47,9 +47,11 @@ Key features of this package include:
47
47
 
48
48
  * high-level ``diff`` and ``patch`` functions
49
49
  * an interface to the BSDIFF4 format
50
- * improved error handling for malformed patches
50
+ * stricter validation and improved error handling of malformed patches than
51
+ the standard ``bspatch`` tool; in particular overlong blocks are rejected
51
52
  * functions for creating patches from files and applying them to files
52
- * command-line interfaces: ``bsdiff4`` and ``bspatch4``
53
+ * command-line interfaces: ``bsdiff4`` and ``bspatch4`` (the latter can also
54
+ inspect patches using ``bspatch4 PATCH``)
53
55
  * support for free-threaded Python
54
56
  * type hints
55
57
  * extensive tests
@@ -65,15 +67,15 @@ The bsdiff4 package defines the following high level functions:
65
67
  Apply the BSDIFF4-format ``patch_bytes`` to ``src_bytes`` and return
66
68
  the bytes.
67
69
 
68
- ``file_diff(src_path, dst_path, patch_path)``
70
+ ``file_diff(src_path, dst_path, patch_path)`` -> None
69
71
  Write a BSDIFF4-format patch (from the file ``src_path`` to the
70
72
  file ``dst_path``) to the file ``patch_path``.
71
73
 
72
- ``file_patch(src_path, dst_path, patch_path)``
74
+ ``file_patch(src_path, dst_path, patch_path)`` -> None
73
75
  Apply the BSDIFF4-format file ``patch_path`` to the file ``src_path``
74
76
  and write the result to the file ``dst_path``.
75
77
 
76
- ``file_patch_inplace(path, patch_path)``
78
+ ``file_patch_inplace(path, patch_path)`` -> None
77
79
  Apply the BSDIFF4-format file ``patch_path`` to the file ``path``
78
80
  in place.
79
81
 
@@ -11,9 +11,11 @@ Key features of this package include:
11
11
 
12
12
  * high-level ``diff`` and ``patch`` functions
13
13
  * an interface to the BSDIFF4 format
14
- * improved error handling for malformed patches
14
+ * stricter validation and improved error handling of malformed patches than
15
+ the standard ``bspatch`` tool; in particular overlong blocks are rejected
15
16
  * functions for creating patches from files and applying them to files
16
- * command-line interfaces: ``bsdiff4`` and ``bspatch4``
17
+ * command-line interfaces: ``bsdiff4`` and ``bspatch4`` (the latter can also
18
+ inspect patches using ``bspatch4 PATCH``)
17
19
  * support for free-threaded Python
18
20
  * type hints
19
21
  * extensive tests
@@ -29,15 +31,15 @@ The bsdiff4 package defines the following high level functions:
29
31
  Apply the BSDIFF4-format ``patch_bytes`` to ``src_bytes`` and return
30
32
  the bytes.
31
33
 
32
- ``file_diff(src_path, dst_path, patch_path)``
34
+ ``file_diff(src_path, dst_path, patch_path)`` -> None
33
35
  Write a BSDIFF4-format patch (from the file ``src_path`` to the
34
36
  file ``dst_path``) to the file ``patch_path``.
35
37
 
36
- ``file_patch(src_path, dst_path, patch_path)``
38
+ ``file_patch(src_path, dst_path, patch_path)`` -> None
37
39
  Apply the BSDIFF4-format file ``patch_path`` to the file ``src_path``
38
40
  and write the result to the file ``dst_path``.
39
41
 
40
- ``file_patch_inplace(path, patch_path)``
42
+ ``file_patch_inplace(path, patch_path)`` -> None
41
43
  Apply the BSDIFF4-format file ``patch_path`` to the file ``path``
42
44
  in place.
43
45
 
@@ -1,11 +1,15 @@
1
1
  from bsdiff4.format import (diff, patch, file_diff, file_patch,
2
2
  file_patch_inplace)
3
3
 
4
- __version__ = "1.4.1"
4
+ __version__ = "1.4.2"
5
5
 
6
6
  __all__ = ["diff", "patch", "file_diff", "file_patch", "file_patch_inplace"]
7
7
 
8
8
 
9
9
  def test(verbosity=1):
10
- from .test_all import run
10
+ """test(verbosity=1) -> TextTestResult
11
+
12
+ Run self-test, and return `unittest.runner.TextTestResult` object.
13
+ """
14
+ from bsdiff4.tests import run
11
15
  return run(verbosity=verbosity)
@@ -0,0 +1,145 @@
1
+ import sys
2
+ from argparse import ArgumentParser
3
+ from os.path import getsize
4
+
5
+ import bsdiff4.core as core # type: ignore
6
+ from bsdiff4 import __version__
7
+ from bsdiff4.format import (file_diff, file_patch, read_patch,
8
+ unpack_header, unpack_control_block)
9
+
10
+
11
+ def human_bytes(n):
12
+ """
13
+ return the number of bytes 'n' in more human readable form
14
+ """
15
+ if n < 1024:
16
+ return '%d B' % n
17
+ k = n / 1024
18
+ if k < 1024:
19
+ return '%.1f K' % k
20
+ m = k / 1024
21
+ if m < 1024:
22
+ return '%.1f M' % m
23
+ g = m / 1024
24
+ return '%.2f G' % g
25
+
26
+
27
+ def show_filesizes(*args):
28
+ for i, name in enumerate(['src', 'dst', 'patch']):
29
+ size = getsize(args[i])
30
+ print('%s: %s (%s)' % (name, args[i], human_bytes(size)))
31
+
32
+
33
+ def show_header(fi):
34
+ total_len = fi.seek(0, 2) # go to EOF and return position
35
+ fi.seek(0)
36
+ control_len, diff_len, dst_len = unpack_header(fi.read(32))
37
+ print("sizes:")
38
+ for name, size in [
39
+ ('total', total_len),
40
+ ('control', control_len),
41
+ ('diff', diff_len),
42
+ ('extra', total_len - 32 - control_len - diff_len),
43
+ ('dst', dst_len),
44
+ ]:
45
+ print('%10s: %10d (%s)' % (name, size, human_bytes(size)))
46
+
47
+
48
+ def show_control(control_block, verbose=False):
49
+ print('\nnumber of control tuples: %d' % (len(control_block) // 24))
50
+ print('%10s %10s %10s %10s' % ('x', 'y', 'z', 'src pos'))
51
+ omitted = False
52
+ src_pos = 0
53
+ totals = (0, 0) # x, y totals
54
+ for i, (x, y, z) in enumerate(unpack_control_block(control_block)):
55
+ if i < 5 or verbose:
56
+ print('%10d %10d %10d %10d' % (x, y, z, src_pos))
57
+ else:
58
+ omitted = True
59
+ totals = (totals[0] + x, totals[1] + y)
60
+ src_pos += x + z
61
+
62
+ if omitted:
63
+ print('%10s %10s %10s' % (3 * ('...',)))
64
+ print('%10s %10s' % (2 * ('------',)))
65
+ print('%10d %10d' % totals)
66
+
67
+
68
+ def show_patch(fi, verbose=False):
69
+ show_header(fi)
70
+ fi.seek(0)
71
+ control_block, diff_block, extra_block = read_patch(fi)
72
+ print("\nuncompressed block sizes:")
73
+ for name, size in [
74
+ ('control', len(control_block)),
75
+ ('diff', len(diff_block)),
76
+ ('extra', len(extra_block)),
77
+ ]:
78
+ note = ""
79
+ if name == 'diff' and diff_block:
80
+ nonzero = len(diff_block) - diff_block.count(b'\x00')
81
+ note = ' (non-zero bytes: %d)' % nonzero
82
+ print('%10s: %10d (%s)%s' % (name, size, human_bytes(size), note))
83
+
84
+ show_control(control_block, verbose)
85
+ # validate patch by applying it to empty source
86
+ core.patch(b'', control_block, diff_block, extra_block)
87
+
88
+
89
+ def main_bsdiff4():
90
+ """
91
+ Generate a BSDIFF4-format PATCH from SRC to DST and write it to PATCH.
92
+ """
93
+ p = ArgumentParser(description=main_bsdiff4.__doc__)
94
+ p.add_argument('-v', '--verbose', action='store_true',
95
+ help="show file sizes")
96
+ p.add_argument('--version', action='version',
97
+ version="bsdiff4 %s" % __version__)
98
+ p.add_argument('src', metavar='SRC')
99
+ p.add_argument('dst', metavar='DST')
100
+ p.add_argument('patch', metavar='PATCH')
101
+ args = p.parse_args()
102
+
103
+ paths = args.src, args.dst, args.patch
104
+ try:
105
+ file_diff(*paths)
106
+ except (OSError, MemoryError) as e:
107
+ sys.exit("%s: %s" % (p.prog, str(e) or type(e).__name__))
108
+ if args.verbose:
109
+ show_filesizes(*paths)
110
+
111
+
112
+ def main_bspatch4():
113
+ """
114
+ Generate DST by applying the BSDIFF4-format PATCH file to SRC;
115
+ with only PATCH, display information about and verify the patch.
116
+ """
117
+ p = ArgumentParser(
118
+ usage="%(prog)s [options] SRC DST PATCH\n"
119
+ " %(prog)s [options] PATCH",
120
+ description=main_bspatch4.__doc__,
121
+ )
122
+ p.add_argument('-v', '--verbose', action='store_true',
123
+ help="show file sizes or all control tuples")
124
+ p.add_argument('--version', action='version',
125
+ version="bsdiff4 %s" % __version__)
126
+ p.add_argument('paths', nargs='+', metavar='PATH')
127
+ args = p.parse_args()
128
+
129
+ if len(args.paths) == 1:
130
+ try:
131
+ with open(args.paths[0], 'rb') as fi:
132
+ show_patch(fi, args.verbose)
133
+ except (ValueError, OSError, MemoryError) as e:
134
+ sys.exit("%s: %s" % (p.prog, str(e) or type(e).__name__))
135
+ return
136
+
137
+ if len(args.paths) != 3:
138
+ p.error("requires either PATCH or SRC DST PATCH")
139
+
140
+ try:
141
+ file_patch(*args.paths)
142
+ except (ValueError, OSError, MemoryError) as e:
143
+ sys.exit("%s: %s" % (p.prog, str(e) or type(e).__name__))
144
+ if args.verbose:
145
+ show_filesizes(*args.paths)
@@ -7,6 +7,7 @@
7
7
 
8
8
  #define PY_SSIZE_T_CLEAN
9
9
  #include <Python.h>
10
+ #include <assert.h>
10
11
  #include <stdint.h>
11
12
 
12
13
 
@@ -109,6 +110,9 @@ split(Py_ssize_t *sa, Py_ssize_t *rank, Py_ssize_t start,
109
110
  }
110
111
 
111
112
  partition_group(sa, rank, start, len, h, &lower, &upper);
113
+ assert(start <= lower);
114
+ assert(lower < upper);
115
+ assert(upper <= start + len);
112
116
 
113
117
  if (lower > start)
114
118
  split(sa, rank, start, lower - start, h);
@@ -214,7 +218,7 @@ matchlen(const char *a, Py_ssize_t a_len,
214
218
  return i;
215
219
  }
216
220
 
217
- /* return the longest prefix-match length and store its position in *pos */
221
+ /* find a prefix match using the suffix array */
218
222
  static Py_ssize_t
219
223
  search(const Py_ssize_t *sa,
220
224
  const char *a, Py_ssize_t a_len,
@@ -446,6 +450,11 @@ compute_diff(const char *src, Py_ssize_t src_len,
446
450
  adjust_overlap(src, dst, lastscan, lastpos, scan, pos,
447
451
  &lenf, &lenb);
448
452
 
453
+ assert(lenf >= 0 && lenb >= 0);
454
+ assert(lastpos >= 0 && lenf <= src_len - lastpos);
455
+ assert(lastscan >= 0 && lenf <= dst_len - lastscan);
456
+ assert(lastscan + lenf <= scan - lenb);
457
+
449
458
  for (i = 0; i < lenf; i++)
450
459
  result->diff[result->diff_len + i] =
451
460
  dst[lastscan + i] - src[lastpos + i];
@@ -468,6 +477,9 @@ compute_diff(const char *src, Py_ssize_t src_len,
468
477
  }
469
478
 
470
479
  PyMem_Free(sa);
480
+ assert(result->diff_len <= dst_len);
481
+ assert(result->extra_len == dst_len - result->diff_len);
482
+ assert(PyByteArray_GET_SIZE(result->control) % 24 == 0);
471
483
  return 0;
472
484
 
473
485
  error:
@@ -514,7 +526,9 @@ mod_diff(PyObject *module, PyObject *args)
514
526
  char *src, *dst;
515
527
  PyObject *result;
516
528
 
517
- if (!PyArg_ParseTuple(args, "s#s#", &src, &src_len, &dst, &dst_len))
529
+ if (!PyArg_ParseTuple(args, "s#s#",
530
+ &src, &src_len,
531
+ &dst, &dst_len))
518
532
  return NULL;
519
533
 
520
534
  if (compute_diff(src, src_len, dst, dst_len, &diff) < 0)
@@ -576,17 +590,19 @@ mod_patch(PyObject *module, PyObject *args)
576
590
  Py_ssize_t dst_pos = 0, i;
577
591
  int64_t src_pos = 0;
578
592
 
579
- if (!PyArg_ParseTuple(args, "s#ns#s#s#",
580
- &src, &src_len, &dst_len,
593
+ if (!PyArg_ParseTuple(args, "s#s#s#s#",
594
+ &src, &src_len,
581
595
  &control_block, &control_len,
582
596
  &diff_block, &diff_remaining,
583
597
  &extra_block, &extra_remaining))
584
598
  return NULL;
585
599
 
586
- if (dst_len < 0) {
587
- CORRUPT("negative destination length");
600
+ if (diff_remaining > PY_SSIZE_T_MAX - extra_remaining) {
601
+ PyErr_NoMemory();
588
602
  return NULL;
589
603
  }
604
+ dst_len = diff_remaining + extra_remaining;
605
+
590
606
  if (control_len % 24) {
591
607
  CORRUPT("invalid control block");
592
608
  return NULL;
@@ -608,10 +624,11 @@ mod_patch(PyObject *module, PyObject *args)
608
624
  memcpy(dst + dst_pos, diff_block, x);
609
625
  diff_block += x;
610
626
  diff_remaining -= x;
611
- /* Check bounds without overflowing src_pos + j. */
612
- for (j = 0; j < x; j++)
627
+ for (j = 0; j < x; j++) {
628
+ /* Check bounds without overflowing src_pos + j. */
613
629
  if (src_pos >= -j && src_pos < src_len - j)
614
630
  dst[dst_pos + j] += src[src_pos + j];
631
+ }
615
632
  dst_pos += x;
616
633
  if (y < 0 || y > dst_len - dst_pos || y > extra_remaining) {
617
634
  CORRUPT("y out of range");
@@ -630,10 +647,7 @@ mod_patch(PyObject *module, PyObject *args)
630
647
  CORRUPT("incomplete output");
631
648
  goto error;
632
649
  }
633
- if (diff_remaining != 0 || extra_remaining != 0) {
634
- CORRUPT("unused block data");
635
- goto error;
636
- }
650
+ assert(diff_remaining == 0 && extra_remaining == 0);
637
651
 
638
652
  return result;
639
653
 
@@ -9,17 +9,15 @@ MAGIC = b'BSDIFF40'
9
9
 
10
10
 
11
11
  def pack_header(control_len, diff_len, dst_len):
12
- return (MAGIC +
13
- core.encode_int64(control_len) +
14
- core.encode_int64(diff_len) +
15
- core.encode_int64(dst_len))
12
+ lengths = control_len, diff_len, dst_len
13
+ return MAGIC + b''.join(core.encode_int64(n) for n in lengths)
16
14
 
17
15
 
18
16
  def unpack_header(header):
19
17
  if len(header) != 32:
20
18
  raise ValueError("invalid header size")
21
- if header[:7] != MAGIC[:7]:
22
- raise ValueError("invalid magic bsdiff4 header")
19
+ if header[:8] != MAGIC:
20
+ raise ValueError("invalid BSDIFF40 magic header, got %r" % header[:8])
23
21
  return tuple(core.decode_int64(header[i:i + 8]) for i in range(8, 32, 8))
24
22
 
25
23
 
@@ -34,7 +32,6 @@ def unpack_control_block(control_block):
34
32
  "unpack the binary control block and generate control tuples"
35
33
  if len(control_block) % 24:
36
34
  raise ValueError("invalid control block")
37
-
38
35
  for i in range(0, len(control_block), 24):
39
36
  yield tuple(core.decode_int64(control_block[j:j + 8])
40
37
  for j in range(i, i + 24, 8))
@@ -62,14 +59,19 @@ def read_patch(fi):
62
59
  check_size(size)
63
60
  control_len, diff_len, dst_len = sizes
64
61
  control_block = bz2.decompress(read_exact(fi, control_len))
62
+ if len(control_block) % 24:
63
+ raise ValueError("invalid control block")
65
64
  diff_block = bz2.decompress(read_exact(fi, diff_len))
66
65
  extra_block = bz2.decompress(fi.read())
67
- return dst_len, control_block, diff_block, extra_block
66
+ if dst_len != len(diff_block) + len(extra_block):
67
+ raise ValueError("invalid destination length")
68
+ return control_block, diff_block, extra_block
68
69
 
69
70
 
70
- def write_patch(fo, dst_len, control_block, diff_block, extra_block):
71
+ def write_patch(fo, control_block, diff_block, extra_block):
71
72
  "write a BSDIFF4-format patch to stream 'fo'"
72
73
  # compress each block
74
+ dst_len = len(diff_block) + len(extra_block)
73
75
  control_block = bz2.compress(control_block)
74
76
  diff_block = bz2.compress(diff_block)
75
77
  extra_block = bz2.compress(extra_block)
@@ -90,21 +92,9 @@ def diff(src_bytes: bytes, dst_bytes: bytes) -> bytes:
90
92
 
91
93
  Return a BSDIFF4-format patch (from src_bytes to dst_bytes) as bytes.
92
94
  """
93
- faux = BytesIO()
94
- write_patch(faux, len(dst_bytes), *core.diff(src_bytes, dst_bytes))
95
- return faux.getvalue()
96
-
97
-
98
- def file_diff(src_path: str, dst_path: str, patch_path: str) -> None:
99
- """file_diff(src_path, dst_path, patch_path)
100
-
101
- Write a BSDIFF4-format patch (from the file src_path to the file dst_path)
102
- to the file patch_path.
103
- """
104
- src = read_data(src_path)
105
- dst = read_data(dst_path)
106
- with open(patch_path, 'wb') as fo:
107
- write_patch(fo, len(dst), *core.diff(src, dst))
95
+ fo = BytesIO()
96
+ write_patch(fo, *core.diff(src_bytes, dst_bytes))
97
+ return fo.getvalue()
108
98
 
109
99
 
110
100
  def patch(src_bytes: bytes, patch_bytes: bytes) -> bytes:
@@ -115,31 +105,38 @@ def patch(src_bytes: bytes, patch_bytes: bytes) -> bytes:
115
105
  return core.patch(src_bytes, *read_patch(BytesIO(patch_bytes)))
116
106
 
117
107
 
108
+ def file_diff(src_path: str, dst_path: str, patch_path: str) -> None:
109
+ """file_diff(src_path, dst_path, patch_path) -> None
110
+
111
+ Write a BSDIFF4-format patch (from the file src_path to the file dst_path)
112
+ to the file patch_path.
113
+ """
114
+ blocks = core.diff(read_data(src_path), read_data(dst_path))
115
+ with open(patch_path, 'wb') as fo:
116
+ write_patch(fo, *blocks)
117
+
118
+
118
119
  def file_patch_inplace(path: str, patch_path: str) -> None:
119
- """file_patch_inplace(path, patch_path)
120
+ """file_patch_inplace(path, patch_path) -> None
120
121
 
121
122
  Apply the BSDIFF4-format file patch_path to the file 'path' in place.
122
123
  """
123
124
  with open(patch_path, 'rb') as fi:
124
- with open(path, 'r+b') as fo:
125
- data = fo.read()
126
- fo.seek(0)
127
- fo.write(core.patch(data, *read_patch(fi)))
128
- fo.truncate()
125
+ blocks = read_patch(fi)
126
+ with open(path, 'r+b') as f:
127
+ dst = core.patch(f.read(), *blocks)
128
+ f.seek(0)
129
+ f.write(dst)
130
+ f.truncate()
129
131
 
130
132
 
131
133
  def file_patch(src_path: str, dst_path: str, patch_path: str) -> None:
132
- """file_patch(src_path, dst_path, patch_path)
134
+ """file_patch(src_path, dst_path, patch_path) -> None
133
135
 
134
136
  Apply the BSDIFF4-format file patch_path to the file src_path and
135
137
  write the result to the file dst_path.
136
138
  """
137
- from os.path import abspath
138
-
139
- if abspath(dst_path) == abspath(src_path):
140
- file_patch_inplace(src_path, patch_path)
141
- return
142
-
143
139
  with open(patch_path, 'rb') as fi:
144
- with open(dst_path, 'wb') as fo:
145
- fo.write(core.patch(read_data(src_path), *read_patch(fi)))
140
+ dst = core.patch(read_data(src_path), *read_patch(fi))
141
+ with open(dst_path, 'wb') as fo:
142
+ fo.write(dst)
@@ -6,14 +6,15 @@ import shutil
6
6
  import unittest
7
7
  import tempfile
8
8
  from io import BytesIO
9
+ from os.path import exists, samefile
9
10
  from sysconfig import get_config_var
10
11
  from concurrent.futures import ThreadPoolExecutor
11
12
  from threading import Barrier
12
13
 
13
14
  import bsdiff4.core as core # type: ignore
14
- from bsdiff4.format import (MAGIC, pack_header, unpack_header,
15
+ from bsdiff4.format import (pack_header, unpack_header,
15
16
  pack_control_block, unpack_control_block,
16
- write_patch, read_patch, read_data)
17
+ write_patch, read_patch)
17
18
  from bsdiff4 import diff, patch, file_diff, file_patch, file_patch_inplace
18
19
 
19
20
 
@@ -49,12 +50,12 @@ class TestCoreInt64(unittest.TestCase):
49
50
  core.decode_int64(bytes.fromhex('00 00 00 00 00 00 00 80')), 0)
50
51
 
51
52
  def test_errors(self):
52
- self.assertRaises(TypeError, core.encode_int64, 'x')
53
+ self.assertRaises(TypeError, core.encode_int64, 8 * b'\x00')
53
54
  M = 1 << 63
54
55
  for n in -M -1, -M, M, M + 1:
55
56
  self.assertRaises(OverflowError, core.encode_int64, n)
56
57
 
57
- self.assertRaises(TypeError, core.decode_int64, 12345)
58
+ self.assertRaises(TypeError, core.decode_int64, 0)
58
59
  for n in 0, 1, 7, 9, 16:
59
60
  self.assertRaisesRegex(ValueError, "8 bytes expected",
60
61
  core.decode_int64, n * b'\x00')
@@ -98,6 +99,8 @@ class TestSuffixArray(unittest.TestCase):
98
99
  self.check_data(os.urandom(1000))
99
100
 
100
101
 
102
+ RND_200 = os.urandom(200)
103
+
101
104
  ROUND_TRIP_CASES = [
102
105
  (b'', b''),
103
106
  (b'', b'x'),
@@ -107,19 +110,22 @@ ROUND_TRIP_CASES = [
107
110
  (b'abc', b'xbc'),
108
111
  (b'abc', b'xybc'),
109
112
  (b'123456789 987654321', b'123456789000987654321'), # issue #14
110
- (b'', os.urandom(200)),
111
- (os.urandom(200), os.urandom(200)),
113
+ (b'', RND_200),
114
+ (RND_200, RND_200),
115
+ (RND_200, os.urandom(20) + RND_200),
116
+ (RND_200, RND_200 + os.urandom(20)),
112
117
  ]
113
118
 
119
+ BYTES_CASES = [b'', b'a', b'abc', 100 * b'a', RND_200]
120
+
114
121
 
115
122
  class TestCoreDiffPatch(unittest.TestCase):
116
123
 
117
124
  def round_trip(self, src, dst):
118
- control_block, diff_block, extra_block = core.diff(src, dst)
119
- for b in control_block, diff_block, extra_block:
120
- self.assertIs(type(b), bytes)
121
- dst2 = core.patch(src, len(dst),
122
- control_block, diff_block, extra_block)
125
+ blocks = core.diff(src, dst)
126
+ for block in blocks:
127
+ self.assertIs(type(block), bytes)
128
+ dst2 = core.patch(src, *blocks)
123
129
  self.assertIs(type(dst2), bytes)
124
130
  self.assertEqual(dst2, dst)
125
131
 
@@ -128,117 +134,167 @@ class TestCoreDiffPatch(unittest.TestCase):
128
134
  self.round_trip(a, b)
129
135
  self.round_trip(b, a)
130
136
 
131
- def test_diff_blocks(self):
137
+ def check_diff_blocks(self, src, dst, controls, diff, extra):
132
138
  # Check construction of control, diff and extra blocks.
133
- self.assertEqual(core.diff(b'banana', b'bandana'),
134
- (pack_control_block((3, 4, 2)),
135
- 3 * b'\x00', b'dana'))
136
-
139
+ blocks = core.diff(src, dst)
140
+ self.assertEqual(list(unpack_control_block(blocks[0])), controls)
141
+ self.assertEqual(blocks[1], diff)
142
+ self.assertEqual(blocks[2], extra)
143
+ self.assertEqual(core.patch(src, *blocks), dst)
144
+
145
+ def test_diff_simple_example(self):
146
+ self.check_diff_blocks(b'banana', b'bandana',
147
+ [(3, 4, 2)], 3 * b'\x00', b'dana')
148
+
149
+ def test_nonzero_diff(self):
150
+ src = b"The quick brown fox jumps over the lazy dog."
151
+ dst = b"The quick brown fox jumps over the hazy dog."
152
+ n = len(src)
153
+ diff = bytearray(n)
154
+ diff[35] = (ord(b'h') - ord(b'l')) & 0xff
155
+ self.check_diff_blocks(src, dst, [(n, 0, -8)], bytes(diff), b'')
156
+
157
+ def test_diff_insert_byte(self):
137
158
  src = 32 * b'a'
138
- dst = 16 * b'a' + b'b' + 16 * b'a'
139
- self.assertEqual(core.diff(src, dst),
140
- (pack_control_block((32, 1, -1)),
141
- 16 * b'\x00' + b'\x01' + 15 * b'\x00', b'a'))
142
-
143
- src = 100 * b'a' + 100 * b'b'
144
- dst = 100 * b'b' + 100 * b'a'
145
- self.assertEqual(core.diff(src, dst),
146
- (pack_control_block((0, 100, 0), (100, 0, -100)),
147
- 100 * b'\x00', 100 * b'b'))
159
+ dst = src[:16] + b'b' + src[16:]
160
+ self.check_diff_blocks(src, dst, [(32, 1, -1)],
161
+ diff=16 * b'\x00' + b'\x01' + 15 * b'\x00',
162
+ extra=b'a')
163
+
164
+ def test_diff_swap_repeated_blocks(self):
165
+ A, B = 100 * b'A', 100 * b'B'
166
+ self.check_diff_blocks(A + B, B + A,
167
+ [(0, 100, 0), (100, 0, -100)],
168
+ diff=100 * b'\x00', extra=B)
169
+
170
+ def test_diff_swap_blocks(self):
171
+ src = bytes(range(200))
172
+ dst = src[100:] + src[:100]
173
+ self.check_diff_blocks(src, dst,
174
+ [(0, 0, 100), (100, 0, -200), (100, 0, -100)],
175
+ diff=200 * b'\x00', extra=b'')
148
176
 
149
177
  def test_diff_adjust_overlap(self):
150
178
  # Ensure that an overlapping byte is assigned to the better
151
179
  # matching block.
152
- self.assertEqual(core.diff(b'ccccdabcadcb', b'ccccdcdabcadcb'),
153
- (pack_control_block((5, 0, -2), (9, 0, -9)),
154
- 14 * b'\x00', b''))
180
+ self.check_diff_blocks(b'ccccdabcadcb',
181
+ b'ccccdcdabcadcb',
182
+ [(5, 0, -2), (9, 0, -9)],
183
+ 14 * b'\x00', b'')
184
+
185
+ def test_empty_dst(self):
186
+ for src in BYTES_CASES:
187
+ self.check_diff_blocks(src, b'', [], b'', b'')
188
+
189
+ def test_empty_src(self):
190
+ for dst in BYTES_CASES:
191
+ self.check_diff_blocks(b'', dst,
192
+ [(0, len(dst), 0)] if dst else [],
193
+ diff=b'', extra=dst)
194
+
195
+ def test_identical_src_dst(self):
196
+ for src in BYTES_CASES:
197
+ control, diff, extra = core.diff(src, src)
198
+ controls = list(unpack_control_block(control))
199
+
200
+ if src:
201
+ self.assertEqual(len(controls), 1)
202
+ x, y, z = controls[0]
203
+ self.assertEqual(x, len(src))
204
+ self.assertEqual(y, 0)
205
+ # final source-position adjustment z does not affect output
206
+ else:
207
+ self.assertEqual(controls, [])
208
+
209
+ self.assertEqual(diff, len(src) * b'\x00')
210
+ self.assertEqual(extra, b'')
211
+ self.assertEqual(core.patch(src, control, diff, extra), src)
212
+
213
+ def test_patch_addition(self):
214
+ src = bytes(range(256))
215
+ dst = bytes((value + 1) % 256 for value in src)
216
+ bcontrol = pack_control_block((len(src), 0, 0))
217
+ blocks = bcontrol, len(src) * b'\x01', b''
218
+ self.assertEqual(core.patch(src, *blocks), dst)
155
219
 
156
220
  def test_patch(self):
157
- for controls in [
158
- [(3, 4, 2)],
159
- [(3, 4, 2), (0, 0, 0)], # (0, 0, 0) is a no-op control tuple
160
- [(0, 0, 0), (3, 4, 2)],
221
+ noop = pack_control_block((0, 0, 0)) # no-op control tuple
222
+ noop2 = 3 * (7 * b'\x00' + b'\x80') # negative zero no-op
223
+ operation = pack_control_block((3, 4, 2))
224
+ for bcontrol in [
225
+ operation,
226
+ noop + operation + noop,
227
+ noop2 + operation + noop + noop2,
161
228
  ]:
162
- dst = core.patch(b'banana', 7, pack_control_block(*controls),
163
- 3 * b'\x00', b'dana')
229
+ dst = core.patch(b'banana', bcontrol, 3 * b'\x00', b'dana')
164
230
  self.assertEqual(dst, b'bandana')
165
231
 
166
232
  # The control block must be binary data, not a list of tuples.
167
- self.assertRaises(TypeError, core.patch, b'', 0, [], b'', b'')
233
+ self.assertRaises(TypeError, core.patch, b'', [], b'', b'')
168
234
 
169
235
  # ----- Patch edge cases and error handling -----
170
236
 
237
+ def assert_corrupt(self, reason, control=b'', diff=b'', extra=b''):
238
+ msg = r"corrupt patch \(%s\)" % reason
239
+ with self.assertRaisesRegex(ValueError, msg):
240
+ core.patch(b'', control, diff, extra)
241
+
171
242
  def test_x_y_out_of_range(self):
243
+ # regression test for PR #29
172
244
  for control, var_name in [((-1, 0, 0), 'x'),
173
245
  ((0, -1, 0), 'y'),
174
246
  ((5, 0, 0), 'x'),
175
247
  ((0, 5, 0), 'y')]:
176
248
  bcontrol = pack_control_block(control)
177
- msg = r"corrupt patch \(%s out of range\)" % var_name
178
- self.assertRaisesRegex(ValueError, msg,
179
- core.patch, b'abcd', 4,
180
- bcontrol, 4 * b'\x00', b'')
181
-
182
- def test_negative_destination_length(self):
183
- msg = r"corrupt patch \(negative destination length\)"
184
- self.assertRaisesRegex(ValueError, msg,
185
- core.patch, b'', -1, b'', b'', b'')
249
+ self.assert_corrupt("%s out of range" % var_name,
250
+ bcontrol, diff=4 * b'\x00', extra=b'abcd')
186
251
 
187
252
  def test_invalid_control_block(self):
188
- msg = r"corrupt patch \(invalid control block\)"
189
253
  for n in 1, 23, 25, 47, 49:
190
- self.assertRaisesRegex(ValueError, msg,
191
- core.patch, b'', 0, n * b'\x00', b'', b'')
254
+ self.assert_corrupt("invalid control block", control=n * b'\x00')
192
255
 
193
256
  def test_final_state(self):
194
- for args, msg in [
195
- ((1, b'', b'', b''), "incomplete output"),
196
- ((0, b'', b'x', b''), "unused block data"),
197
- ((0, b'', b'', b'x'), "unused block data"),
198
- ]:
199
- self.assertRaisesRegex(ValueError, r"corrupt patch \(%s\)" % msg,
200
- core.patch, b'', *args)
257
+ self.assert_corrupt("incomplete output", diff=b'x')
258
+ self.assert_corrupt("incomplete output", extra=b'y')
259
+ self.assert_corrupt("incomplete output", diff=b'x', extra=b'y')
201
260
 
202
261
  def test_src_pos_outside_source(self):
203
- bcontrol = pack_control_block((0, 0, -1), (2, 0, 0))
204
- self.assertEqual(
205
- core.patch(b'ab', 2, bcontrol, b'\x01\x01', b''), b'\x01b')
206
-
207
- bcontrol = pack_control_block((0, 0, 2), (2, 0, 0))
208
- self.assertEqual(
209
- core.patch(b'ab', 2, bcontrol, b'\x01\x02', b''), b'\x01\x02')
262
+ src = b'ab'
263
+ for controls, bdiff, dst in [
264
+ ([(0, 0, -1), (2, 0, 0)], b'\x01\x01', b'\x01b'),
265
+ ([(0, 0, 2), (2, 0, 0)], b'\x01\x02', b'\x01\x02'),
266
+ ]:
267
+ bcontrol = pack_control_block(*controls)
268
+ self.assertEqual(core.patch(src, bcontrol, bdiff, b''), dst)
210
269
 
211
270
  def test_src_pos_overflow(self):
212
271
  M = 1 << 63
213
272
  for z in -M + 1, M - 1:
214
273
  bcontrol = pack_control_block((0, 0, z))
215
- self.assertEqual(core.patch(b'', 0, bcontrol, b'', b''), b'')
274
+ self.assertEqual(core.patch(b'', bcontrol, b'', b''), b'')
216
275
 
217
- for dst_len, controls, diff_block in [
218
- (0, [(0, 0, M - 1), (0, 0, 1)], b''),
219
- (0, [(0, 0, -M + 1), (0, 0, -1), (0, 0, -1)], b''),
220
- (2, [(0, 0, M - 1), (2, 0, 0)], b'\x00\x00'),
276
+ for controls, diff_len in [
277
+ ([(0, 0, M - 1), (0, 0, 1)], 0),
278
+ ([(0, 0, -M + 1), (0, 0, -1), (0, 0, -1)], 0),
279
+ ([(0, 0, M - 1), (2, 0, 0)], 2),
221
280
  ]:
222
281
  bcontrol = pack_control_block(*controls)
223
- self.assertRaisesRegex(ValueError,
224
- r"corrupt patch \(offset overflow\)",
225
- core.patch, b'', dst_len,
226
- bcontrol, diff_block, b'')
282
+ self.assert_corrupt("offset overflow",
283
+ bcontrol, diff=diff_len * b'\x00')
227
284
 
228
285
  # ----------------------- BSDIFF4 format layer ---------------------------
229
286
 
230
287
  class TestHelpers(unittest.TestCase):
231
288
 
232
289
  def test_header(self):
233
- bheader = b'BSDIFF40' + bytes.fromhex("""
234
- c9 02 00 00 00 00 00 00
235
- a2 07 00 00 00 00 00 00
236
- c0 0f 02 00 00 00 00 00 """)
290
+ bheader = bytes.fromhex("""
291
+ 42 53 44 49 46 46 34 30 c9 02 00 00 00 00 00 00
292
+ a2 07 00 00 00 00 00 00 c0 0f 02 00 00 00 00 00 """)
237
293
  sizes = (0x2c9, 0x7a2, 0x20fc0)
238
294
 
239
295
  self.assertEqual(pack_header(*sizes), bheader)
240
296
  self.assertEqual(unpack_header(bheader), sizes)
241
- self.assertRaisesRegex(ValueError, "invalid magic bsdiff4 header",
297
+ self.assertRaisesRegex(ValueError, "invalid BSDIFF40 magic header",
242
298
  unpack_header, 32 * b'\x00')
243
299
  for n in 0, 1, 31, 33:
244
300
  self.assertRaisesRegex(ValueError, "invalid header size",
@@ -247,8 +303,7 @@ class TestHelpers(unittest.TestCase):
247
303
  def test_control_block(self):
248
304
  bcontrol = bytes.fromhex("""
249
305
  1f00000000000000 a007000000000000 0100000000000080
250
- 0200000000000000 3000000000000080 0000000000000000
251
- """)
306
+ 0200000000000000 3000000000000080 0000000000000000 """)
252
307
  controls = [(0x1f, 0x7a0, -1), (2, -0x30, 0)]
253
308
 
254
309
  self.assertEqual(pack_control_block(*controls), bcontrol)
@@ -267,44 +322,36 @@ class Example:
267
322
  dst = b'abd!'
268
323
  # BSDIFF40 patch from b'abc' to b'abd!', verified with bspatch.
269
324
  # 42 5a 68 39 = BZh9
270
- data = bytes.fromhex("""
325
+ patch = bytes.fromhex("""
271
326
  42534449 46463430 2a000000 00000000 25000000 00000000 04000000 00000000
272
327
  425a6839 31415926 5359d014 9a290000 04c00068 08200030 cd34193f 5209593c
273
328
  5dc914e1 42434052 68a4425a 68393141 59265359 4c5a9f48 00000040 00600020
274
329
  00210082 8c5dc914 e1424131 6a7d2042 5a683931 41592653 592d15eb 1c000000
275
- 10002000 20002118 4682ee48 a70a1205 a2bd6380
276
- """)
330
+ 10002000 20002118 4682ee48 a70a1205 a2bd6380 """)
277
331
  header_sizes = (0x2a, 0x25, 0x04) # control_len, diff_len, dst_len
278
332
  control_tuples = [(3, 1, 0)]
279
333
  diff_block = b'\x00\x00\x01'
280
334
  extra_block = b'!'
335
+ blocks = (pack_control_block(*control_tuples), diff_block, extra_block)
281
336
 
282
337
 
283
338
  class TestExample(unittest.TestCase):
284
339
 
285
340
  def test_compatibility(self):
286
- self.assertEqual(patch(Example.src, Example.data), Example.dst)
341
+ self.assertEqual(patch(Example.src, Example.patch), Example.dst)
287
342
 
288
343
  def test_header(self):
289
- self.assertEqual(unpack_header(Example.data[:32]),
344
+ self.assertEqual(unpack_header(Example.patch[:32]),
290
345
  Example.header_sizes)
291
346
 
292
- def test_read_patch(self):
293
- f = BytesIO(Example.data)
294
- dst_len, control_block, diff_block, extra_block = read_patch(f)
295
- self.assertEqual(dst_len, len(Example.dst))
296
- self.assertEqual(list(unpack_control_block(control_block)),
297
- Example.control_tuples)
298
- self.assertEqual(diff_block, Example.diff_block)
299
- self.assertEqual(extra_block, Example.extra_block)
300
-
301
- def test_write_patch(self):
347
+ def test_read_write_roundtrip(self):
348
+ blocks = read_patch(BytesIO(Example.patch))
349
+ self.assertEqual(blocks, Example.blocks)
302
350
  f = BytesIO()
303
- write_patch(f, len(Example.dst),
304
- pack_control_block(*Example.control_tuples),
305
- Example.diff_block,
306
- Example.extra_block)
307
- self.assertEqual(f.getvalue(), Example.data)
351
+ write_patch(f, *blocks)
352
+ self.assertEqual(f.getvalue(), Example.patch)
353
+ f.seek(0)
354
+ self.assertEqual(read_patch(f), blocks)
308
355
 
309
356
 
310
357
  class TestFormat(unittest.TestCase):
@@ -329,12 +376,6 @@ class TestFormat(unittest.TestCase):
329
376
  self.round_trip(a, b)
330
377
  self.round_trip(b, a)
331
378
 
332
- def test_extra(self):
333
- src = os.urandom(1000)
334
- dst = src + os.urandom(10)
335
- self.round_trip(src, dst)
336
- self.round_trip(dst, src)
337
-
338
379
  def test_random(self):
339
380
  for _ in range(100):
340
381
  n1 = random.randint(0, 1000)
@@ -354,14 +395,16 @@ EMPTY = bz2.compress(b'') # compressed empty block
354
395
  class TestFormatErrors(unittest.TestCase):
355
396
 
356
397
  def test_invalid_header(self):
357
- p = b"BSDIFF30" + 24 * b'\x00'
358
- self.assertRaisesRegex(ValueError, "invalid magic bsdiff4 header",
398
+ p = b"BSDIFF41" + 24 * b'\x00'
399
+ self.assertRaisesRegex(ValueError, "invalid BSDIFF40 magic header",
359
400
  read_patch, BytesIO(p))
360
401
 
361
402
  def test_truncated_patch(self):
362
403
  for data in [
363
- MAGIC,
364
- MAGIC + 23 * b'\x00',
404
+ b'',
405
+ b'ABC',
406
+ b'BSDIFF40',
407
+ b'BSDIFF40' + 23 * b'\x00',
365
408
  pack_header(len(EMPTY) + 1, 0, 0) + EMPTY,
366
409
  pack_header(len(EMPTY), len(EMPTY) + 1, 0) + 2 * EMPTY,
367
410
  ]:
@@ -375,6 +418,14 @@ class TestFormatErrors(unittest.TestCase):
375
418
  "negative size in bsdiff4 patch",
376
419
  read_patch, BytesIO(data))
377
420
 
421
+ def test_invalid_control_block(self):
422
+ for n in 1, 23, 25, 47, 49:
423
+ f = BytesIO()
424
+ write_patch(f, n * b'\x00', b'', b'')
425
+ f.seek(0)
426
+ self.assertRaisesRegex(ValueError, "invalid control block",
427
+ read_patch, f)
428
+
378
429
  def test_invalid_blocks(self):
379
430
  for data in [
380
431
  pack_header(1, len(EMPTY), 0) + b'x' + EMPTY + EMPTY,
@@ -383,11 +434,17 @@ class TestFormatErrors(unittest.TestCase):
383
434
  ]:
384
435
  self.assertRaises(OSError, read_patch, BytesIO(data))
385
436
 
437
+ def test_dst_length(self):
438
+ dst_len = 1 # Conflicts with the empty diff and extra block.
439
+ data = pack_header(len(EMPTY), len(EMPTY), dst_len) + 3 * EMPTY
440
+ self.assertRaisesRegex(ValueError, "invalid destination length",
441
+ read_patch, BytesIO(data))
442
+
386
443
  @unittest.skipUnless(sys.maxsize < (1 << 31), "requires 32-bit Python")
387
444
  def test_header_overflow(self):
388
445
  M = 1 << 31
389
446
  for lengths in [(M, 0, 0), (0, M, 0), (0, 0, M)]:
390
- data = pack_header(*lengths) + 3 * bz2.compress(b'')
447
+ data = pack_header(*lengths) + 3 * EMPTY
391
448
  self.assertRaisesRegex(ValueError,
392
449
  "size too large in bsdiff4 patch",
393
450
  read_patch, BytesIO(data))
@@ -396,14 +453,14 @@ class TestFormatErrors(unittest.TestCase):
396
453
  class TestMutate(unittest.TestCase):
397
454
 
398
455
  def test_truncated_patch(self):
399
- for i in range(len(Example.data)):
456
+ for i in range(len(Example.patch)):
400
457
  # truncating patch
401
458
  self.assertRaises(ValueError, patch,
402
- Example.src, Example.data[:i])
459
+ Example.src, Example.patch[:i])
403
460
 
404
461
  @unittest.skipIf(UNDER_QEMU, "slow under QEMU")
405
462
  def test_mutated_patch(self):
406
- data = Example.data
463
+ data = Example.patch
407
464
  for i in range(len(data)):
408
465
  mut = bytearray(data)
409
466
  for j in range(8):
@@ -429,9 +486,17 @@ class TestFile(unittest.TestCase):
429
486
  def path(self, fn):
430
487
  return os.path.join(self.tmpdir, fn)
431
488
 
489
+ def read_data(self, fn):
490
+ with open(self.path(fn), 'rb') as fi:
491
+ return fi.read()
492
+
493
+ def write_data(self, fn, data):
494
+ with open(self.path(fn), 'wb') as fo:
495
+ fo.write(data)
496
+
432
497
  def assert_same_file_content(self, fn1, fn2):
433
- data1 = read_data(self.path(fn1))
434
- data2 = read_data(self.path(fn2))
498
+ data1 = self.read_data(fn1)
499
+ data2 = self.read_data(fn2)
435
500
  self.assertEqual(data1, data2)
436
501
 
437
502
  def round_trip(self):
@@ -445,25 +510,94 @@ class TestFile(unittest.TestCase):
445
510
  file_patch(self.path('src'), self.path('src'), self.path('patch'))
446
511
  self.assert_same_file_content('src', 'dst')
447
512
 
448
- def write_data(self, fn, data):
449
- with open(self.path(fn), 'wb') as fo:
450
- fo.write(data)
451
-
452
513
  def test_cases(self):
453
514
  for a, b in ROUND_TRIP_CASES:
454
515
  self.write_data('src', a)
455
516
  self.write_data('dst', b)
456
517
  self.round_trip()
457
518
 
458
- def test_inplace(self):
459
- a = 1000 * b'ABCDE'
460
- b = 1000 * b'XYZ'
461
- self.write_data('src', a + os.urandom(100) + b)
462
- self.write_data('dst', a + os.urandom(100) + b)
463
- file_diff(self.path('src'), self.path('dst'), self.path('patch'))
519
+ def test_example(self):
520
+ self.write_data('src', Example.src)
521
+ self.write_data('patch', Example.patch)
522
+ file_patch(self.path('src'), self.path('dst'), self.path('patch'))
523
+ self.assertEqual(self.read_data('dst'), Example.dst)
524
+
525
+ def test_example_inplace(self):
526
+ self.write_data('src', Example.src)
527
+ self.write_data('patch', Example.patch)
464
528
  file_patch_inplace(self.path('src'), self.path('patch'))
529
+ self.assertEqual(self.read_data('src'), Example.dst)
530
+
531
+ def check_preserves_file(self, mode):
532
+ # Verify that patching preserves the inode and its hard-link
533
+ # relationship.
534
+ self.write_data('src', b'source data will be patched with new data')
535
+ self.write_data('dst', b'destination data')
536
+ patch_path = self.path('patch')
537
+ file_diff(self.path('src'), self.path('dst'), patch_path)
538
+
539
+ src = self.path('src')
540
+ link = self.path('link')
541
+
542
+ os.link(src, link)
543
+ stat_before = os.stat(src)
544
+
545
+ if mode == 'inplace':
546
+ file_patch_inplace(src, patch_path)
547
+ elif mode == 'samepath':
548
+ file_patch(src, src, patch_path)
549
+ elif mode == 'samefile':
550
+ file_patch(src, link, patch_path)
551
+ else:
552
+ raise ValueError(mode)
553
+
554
+ # Ensure that `src` still refers to the same file.
555
+ stat_after = os.stat(src)
556
+ self.assertEqual((stat_after.st_dev, stat_after.st_ino),
557
+ (stat_before.st_dev, stat_before.st_ino))
558
+ # Ensure that the hard link was not broken.
559
+ self.assertTrue(samefile(src, link))
465
560
  self.assert_same_file_content('src', 'dst')
466
561
 
562
+ def test_preserves_file_inplace(self):
563
+ self.check_preserves_file('inplace')
564
+
565
+ def test_preserves_file_samepath(self):
566
+ self.check_preserves_file('samepath')
567
+
568
+ def test_preserves_file_samefile(self):
569
+ self.check_preserves_file('samefile')
570
+
571
+ def test_patch_error_preserves_files(self):
572
+ # Verify that an invalid patch leaves the target file unchanged.
573
+ self.write_data('src', b'source data')
574
+ self.write_data('dst', b'destination data')
575
+ self.write_data('patch', b'invalid patch')
576
+
577
+ self.assertRaises(ValueError, file_patch,
578
+ self.path('src'), self.path('dst'),
579
+ self.path('patch'))
580
+ self.assertEqual(self.read_data('dst'), b'destination data')
581
+
582
+ self.assertRaises(ValueError, file_patch_inplace,
583
+ self.path('src'), self.path('patch'))
584
+ self.assertEqual(self.read_data('src'), b'source data')
585
+
586
+ self.assertRaises(ValueError, file_patch,
587
+ self.path('src'), self.path('src'),
588
+ self.path('patch'))
589
+ self.assertEqual(self.read_data('src'), b'source data')
590
+
591
+ def test_patch_error_does_not_create_dst(self):
592
+ # Verify that an invalid patch does not create the target file.
593
+ self.write_data('src', b'source data')
594
+ self.write_data('patch', b'invalid patch')
595
+
596
+ self.assertRaises(ValueError, file_patch,
597
+ self.path('src'), self.path('dst'),
598
+ self.path('patch'))
599
+ self.assertFalse(exists(self.path('dst')))
600
+
467
601
  # -------------------------- free threading ------------------------------
468
602
 
469
603
  NTHREADS = 4
@@ -500,7 +634,7 @@ class TestFreeThreading(unittest.TestCase):
500
634
  blocks = core.diff(src, dst)
501
635
  if blocks != expected_blocks:
502
636
  return False
503
- if core.patch(src, len(dst), *blocks) != dst:
637
+ if core.patch(src, *blocks) != dst:
504
638
  return False
505
639
  if core.suffix_array(src) != expected_sa:
506
640
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bsdiff4
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
4
  Summary: binary diff and patch using the BSDIFF4-format
5
5
  Home-page: https://github.com/ilanschnell/bsdiff4
6
6
  Author: Ilan Schnell
@@ -47,9 +47,11 @@ Key features of this package include:
47
47
 
48
48
  * high-level ``diff`` and ``patch`` functions
49
49
  * an interface to the BSDIFF4 format
50
- * improved error handling for malformed patches
50
+ * stricter validation and improved error handling of malformed patches than
51
+ the standard ``bspatch`` tool; in particular overlong blocks are rejected
51
52
  * functions for creating patches from files and applying them to files
52
- * command-line interfaces: ``bsdiff4`` and ``bspatch4``
53
+ * command-line interfaces: ``bsdiff4`` and ``bspatch4`` (the latter can also
54
+ inspect patches using ``bspatch4 PATCH``)
53
55
  * support for free-threaded Python
54
56
  * type hints
55
57
  * extensive tests
@@ -65,15 +67,15 @@ The bsdiff4 package defines the following high level functions:
65
67
  Apply the BSDIFF4-format ``patch_bytes`` to ``src_bytes`` and return
66
68
  the bytes.
67
69
 
68
- ``file_diff(src_path, dst_path, patch_path)``
70
+ ``file_diff(src_path, dst_path, patch_path)`` -> None
69
71
  Write a BSDIFF4-format patch (from the file ``src_path`` to the
70
72
  file ``dst_path``) to the file ``patch_path``.
71
73
 
72
- ``file_patch(src_path, dst_path, patch_path)``
74
+ ``file_patch(src_path, dst_path, patch_path)`` -> None
73
75
  Apply the BSDIFF4-format file ``patch_path`` to the file ``src_path``
74
76
  and write the result to the file ``dst_path``.
75
77
 
76
- ``file_patch_inplace(path, patch_path)``
78
+ ``file_patch_inplace(path, patch_path)`` -> None
77
79
  Apply the BSDIFF4-format file ``patch_path`` to the file ``path``
78
80
  in place.
79
81
 
@@ -7,7 +7,7 @@ bsdiff4/cli.py
7
7
  bsdiff4/core.c
8
8
  bsdiff4/format.py
9
9
  bsdiff4/py.typed
10
- bsdiff4/test_all.py
10
+ bsdiff4/tests.py
11
11
  bsdiff4.egg-info/PKG-INFO
12
12
  bsdiff4.egg-info/SOURCES.txt
13
13
  bsdiff4.egg-info/dependency_links.txt
@@ -9,7 +9,7 @@ except ImportError:
9
9
  kwds = {}
10
10
  kwds['long_description'] = open('README.rst').read()
11
11
 
12
- # Read version from bsdiff/__init__.py
12
+ # Read version from bsdiff4/__init__.py
13
13
  pat = re.compile(r'__version__\s*=\s*"(\S+)"', re.M)
14
14
  data = open('bsdiff4/__init__.py').read()
15
15
  kwds['version'] = pat.search(data).group(1)
@@ -1,103 +0,0 @@
1
- from argparse import ArgumentParser
2
- from os.path import getsize
3
-
4
- from bsdiff4 import __version__
5
- from bsdiff4.format import (file_diff, file_patch, read_patch,
6
- unpack_header, unpack_control_block)
7
-
8
-
9
- def human_bytes(n):
10
- """
11
- return the number of bytes 'n' in more human readable form
12
- """
13
- if n < 1024:
14
- return '%d B' % n
15
- k = n / 1024
16
- if k < 1024:
17
- return '%.1f K' % k
18
- m = k / 1024
19
- if m < 1024:
20
- return '%.1f M' % m
21
- g = m / 1024
22
- return '%.2f G' % g
23
-
24
-
25
- def show_filesizes(*args):
26
- for i, name in enumerate(['src', 'dst', 'patch']):
27
- size = getsize(args[i])
28
- print('%s: %s (%s)' % (name, args[i], human_bytes(size)))
29
-
30
-
31
- def main_bsdiff4():
32
- p = ArgumentParser(
33
- description=("generate a BSDIFF4-format PATCH from SRC to DST "
34
- "and write it to PATCH"))
35
- p.add_argument('-v', '--verbose', action='store_true',
36
- help="show file sizes")
37
- p.add_argument('--version', action='version',
38
- version="bsdiff4 %s" % __version__)
39
- p.add_argument('src', metavar='SRC')
40
- p.add_argument('dst', metavar='DST')
41
- p.add_argument('patch', metavar='PATCH')
42
- args = p.parse_args()
43
-
44
- paths = args.src, args.dst, args.patch
45
- file_diff(*paths)
46
- if args.verbose:
47
- show_filesizes(*paths)
48
-
49
-
50
- def show_patch(patch_path, verbose=False):
51
- total_len = getsize(patch_path)
52
- with open(patch_path, 'rb') as fi:
53
- control_len, diff_len, dst_len = unpack_header(fi.read(32))
54
- print("sizes:")
55
- for name, size in [
56
- ('total', total_len),
57
- ('control', control_len),
58
- ('diff', diff_len),
59
- ('extra', total_len - 32 - control_len - diff_len),
60
- ('dst', dst_len),
61
- ]:
62
- print('%10s: %10d (%s)' % (name, size, human_bytes(size)))
63
-
64
- fi.seek(0)
65
- _, control_block, diff_block, extra_block = read_patch(fi)
66
- print("\nuncompressed block sizes:")
67
- for name, size in [
68
- ('control', len(control_block)),
69
- ('diff', len(diff_block)),
70
- ('extra', len(extra_block)),
71
- ]:
72
- print('%10s: %10d (%s)' % (name, size, human_bytes(size)))
73
-
74
- print('\nnumber of control tuples: %d' % (len(control_block) // 24))
75
- if verbose:
76
- print('%10s %10s %10s' % ('x', 'y', 'z'))
77
- for ct in unpack_control_block(control_block):
78
- print('%10d %10d %10d' % ct)
79
-
80
-
81
- def main_bspatch4():
82
- p = ArgumentParser(
83
- usage="%(prog)s [options] SRC DST PATCH\n"
84
- " %(prog)s [options] PATCH",
85
- description=("generate DST by applying the BSDIFF4-format PATCH "
86
- "file to SRC, or display information about PATCH"))
87
- p.add_argument('-v', '--verbose', action='store_true',
88
- help="show file sizes or control tuples")
89
- p.add_argument('--version', action='version',
90
- version="bsdiff4 %s" % __version__)
91
- p.add_argument('paths', nargs='+', metavar='PATH')
92
- args = p.parse_args()
93
-
94
- if len(args.paths) == 1:
95
- show_patch(args.paths[0], args.verbose)
96
- return
97
-
98
- if len(args.paths) != 3:
99
- p.error("requires either PATCH or SRC DST PATCH")
100
-
101
- file_patch(*args.paths)
102
- if args.verbose:
103
- show_filesizes(*args.paths)
File without changes
File without changes
File without changes
File without changes