bsdiff4 1.4.1__tar.gz → 1.4.3__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.3
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
 
@@ -88,6 +90,6 @@ Example:
88
90
  >>> b[100:106] = b' diff '
89
91
  >>> p = bsdiff4.diff(a, bytes(b))
90
92
  >>> len(p)
91
- 154
93
+ 153
92
94
  >>> bsdiff4.patch(a, p) == b
93
95
  True
@@ -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
 
@@ -52,6 +54,6 @@ Example:
52
54
  >>> b[100:106] = b' diff '
53
55
  >>> p = bsdiff4.diff(a, bytes(b))
54
56
  >>> len(p)
55
- 154
57
+ 153
56
58
  >>> bsdiff4.patch(a, p) == b
57
59
  True
@@ -1,11 +1,17 @@
1
+ import bsdiff4.core as core # type: ignore
1
2
  from bsdiff4.format import (diff, patch, file_diff, file_patch,
2
3
  file_patch_inplace)
3
4
 
4
- __version__ = "1.4.1"
5
+
6
+ __version__: str = core.BSDIFF4_VERSION
5
7
 
6
8
  __all__ = ["diff", "patch", "file_diff", "file_patch", "file_patch_inplace"]
7
9
 
8
10
 
9
11
  def test(verbosity=1):
10
- from .test_all import run
12
+ """test(verbosity=1) -> TextTestResult
13
+
14
+ Run self-test, and return `unittest.runner.TextTestResult` object.
15
+ """
16
+ from bsdiff4.tests import run
11
17
  return run(verbosity=verbosity)
@@ -0,0 +1,151 @@
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
+ NonCanonicalError, validate_canonical,
9
+ unpack_header, unpack_control_block)
10
+
11
+
12
+ def human_bytes(n):
13
+ """
14
+ return the number of bytes 'n' in more human readable form
15
+ """
16
+ if n < 1024:
17
+ return '%d B' % n
18
+ k = n / 1024
19
+ if k < 1024:
20
+ return '%.1f K' % k
21
+ m = k / 1024
22
+ if m < 1024:
23
+ return '%.1f M' % m
24
+ g = m / 1024
25
+ return '%.2f G' % g
26
+
27
+
28
+ def show_filesizes(*args):
29
+ for i, name in enumerate(['src', 'dst', 'patch']):
30
+ size = getsize(args[i])
31
+ print('%s: %s (%s)' % (name, args[i], human_bytes(size)))
32
+
33
+
34
+ def show_header(fi):
35
+ total_len = fi.seek(0, 2) # go to EOF and return position
36
+ fi.seek(0)
37
+ control_len, diff_len, dst_len = unpack_header(fi.read(32))
38
+ print("sizes:")
39
+ for name, size in [
40
+ ('total', total_len),
41
+ ('control', control_len),
42
+ ('diff', diff_len),
43
+ ('extra', total_len - 32 - control_len - diff_len),
44
+ ('dst', dst_len),
45
+ ]:
46
+ print('%10s: %10d (%s)' % (name, size, human_bytes(size)))
47
+
48
+
49
+ def show_control(control_block, verbose=False):
50
+ print('\nnumber of control tuples: %d' % (len(control_block) // 24))
51
+ print('%10s %10s %10s %10s' % ('x', 'y', 'z', 'src pos'))
52
+ omitted = False
53
+ src_pos = 0
54
+ totals = (0, 0) # x, y totals
55
+ for i, (x, y, z) in enumerate(unpack_control_block(control_block)):
56
+ if i < 5 or verbose:
57
+ print('%10d %10d %10d %10d' % (x, y, z, src_pos))
58
+ else:
59
+ omitted = True
60
+ totals = (totals[0] + x, totals[1] + y)
61
+ src_pos += x + z
62
+
63
+ if omitted:
64
+ print('%10s %10s %10s' % (3 * ('...',)))
65
+ print('%10s %10s' % (2 * ('------',)))
66
+ print('%10d %10d' % totals)
67
+
68
+
69
+ def show_patch(fi, verbose=False):
70
+ show_header(fi)
71
+ fi.seek(0)
72
+ control_block, diff_block, extra_block = read_patch(fi)
73
+ print("\nuncompressed block sizes:")
74
+ for name, size in [
75
+ ('control', len(control_block)),
76
+ ('diff', len(diff_block)),
77
+ ('extra', len(extra_block)),
78
+ ]:
79
+ note = ""
80
+ if name == 'diff' and diff_block:
81
+ nonzero = len(diff_block) - diff_block.count(b'\x00')
82
+ note = ' (non-zero bytes: %d)' % nonzero
83
+ print('%10s: %10d (%s)%s' % (name, size, human_bytes(size), note))
84
+
85
+ show_control(control_block, verbose)
86
+ # validate patch by applying it to empty source
87
+ core.patch(b'', control_block, diff_block, extra_block)
88
+ try:
89
+ validate_canonical(control_block)
90
+ print("Valid patch has canonical control structure.")
91
+ except NonCanonicalError as e:
92
+ print("Valid patch is non-canonical: %s" % e)
93
+
94
+
95
+ def main_bsdiff4():
96
+ """
97
+ Generate a BSDIFF4-format PATCH from SRC to DST and write it to PATCH.
98
+ """
99
+ p = ArgumentParser(description=main_bsdiff4.__doc__)
100
+ p.add_argument('-v', '--verbose', action='store_true',
101
+ help="show file sizes")
102
+ p.add_argument('--version', action='version',
103
+ version="bsdiff4 %s" % __version__)
104
+ p.add_argument('src', metavar='SRC')
105
+ p.add_argument('dst', metavar='DST')
106
+ p.add_argument('patch', metavar='PATCH')
107
+ args = p.parse_args()
108
+
109
+ paths = args.src, args.dst, args.patch
110
+ try:
111
+ file_diff(*paths)
112
+ except (OSError, MemoryError) as e:
113
+ sys.exit("%s: %s" % (p.prog, str(e) or type(e).__name__))
114
+ if args.verbose:
115
+ show_filesizes(*paths)
116
+
117
+
118
+ def main_bspatch4():
119
+ """
120
+ Generate DST by applying the BSDIFF4-format PATCH file to SRC;
121
+ with only PATCH, display information about and verify the patch.
122
+ """
123
+ p = ArgumentParser(
124
+ usage="%(prog)s [options] SRC DST PATCH\n"
125
+ " %(prog)s [options] PATCH",
126
+ description=main_bspatch4.__doc__,
127
+ )
128
+ p.add_argument('-v', '--verbose', action='store_true',
129
+ help="show file sizes or all control tuples")
130
+ p.add_argument('--version', action='version',
131
+ version="bsdiff4 %s" % __version__)
132
+ p.add_argument('paths', nargs='+', metavar='PATH')
133
+ args = p.parse_args()
134
+
135
+ if len(args.paths) == 1:
136
+ try:
137
+ with open(args.paths[0], 'rb') as fi:
138
+ show_patch(fi, args.verbose)
139
+ except (ValueError, OSError, MemoryError) as e:
140
+ sys.exit("%s: %s" % (p.prog, str(e) or type(e).__name__))
141
+ return
142
+
143
+ if len(args.paths) != 3:
144
+ p.error("requires either PATCH or SRC DST PATCH")
145
+
146
+ try:
147
+ file_patch(*args.paths)
148
+ except (ValueError, OSError, MemoryError) as e:
149
+ sys.exit("%s: %s" % (p.prog, str(e) or type(e).__name__))
150
+ if args.verbose:
151
+ show_filesizes(*args.paths)
@@ -7,8 +7,10 @@
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
 
13
+ #define BSDIFF4_VERSION "1.4.3"
12
14
 
13
15
  /* swap two values */
14
16
  static inline void
@@ -109,6 +111,9 @@ split(Py_ssize_t *sa, Py_ssize_t *rank, Py_ssize_t start,
109
111
  }
110
112
 
111
113
  partition_group(sa, rank, start, len, h, &lower, &upper);
114
+ assert(start <= lower);
115
+ assert(lower < upper);
116
+ assert(upper <= start + len);
112
117
 
113
118
  if (lower > start)
114
119
  split(sa, rank, start, lower - start, h);
@@ -214,7 +219,7 @@ matchlen(const char *a, Py_ssize_t a_len,
214
219
  return i;
215
220
  }
216
221
 
217
- /* return the longest prefix-match length and store its position in *pos */
222
+ /* find a prefix match using the suffix array */
218
223
  static Py_ssize_t
219
224
  search(const Py_ssize_t *sa,
220
225
  const char *a, Py_ssize_t a_len,
@@ -309,6 +314,69 @@ append_control(PyObject *control, Py_ssize_t x, Py_ssize_t y, Py_ssize_t z)
309
314
  return 0;
310
315
  }
311
316
 
317
+ typedef struct {
318
+ Py_ssize_t x, y, z;
319
+ int initialized; /* whether we received any control tuple yet */
320
+ } PendingControl;
321
+
322
+ /* Retain one control tuple so it can be merged with the next. */
323
+ static int
324
+ queue_add(PyObject *control, PendingControl *pending,
325
+ Py_ssize_t x, Py_ssize_t y, Py_ssize_t z)
326
+ {
327
+ if (pending->initialized == 0) {
328
+ pending->x = x;
329
+ pending->y = y;
330
+ pending->z = z;
331
+ pending->initialized = 1;
332
+ return 0;
333
+ }
334
+
335
+ /* A tuple with no diff range does not access the source; any data it
336
+ emits is extra data. Merge it into the preceding tuple:
337
+ (px, py, pz), (0, y, z) -> (px, py + y, pz + z). */
338
+ if (x == 0) {
339
+ assert(y <= PY_SSIZE_T_MAX - pending->y);
340
+ assert((z >= 0 && pending->z <= PY_SSIZE_T_MAX - z) ||
341
+ (z < 0 && pending->z >= -PY_SSIZE_T_MAX - 1 - z));
342
+ pending->y += y;
343
+ pending->z += z;
344
+ return 0;
345
+ }
346
+
347
+ /* The preceding rule may cancel intervening source seeks, leaving
348
+ (px, 0, 0), (x, y, z). Merge these into (px + x, y, z), since
349
+ both diff ranges access consecutive source data. */
350
+ if (pending->y == 0 && pending->z == 0) {
351
+ assert(x <= PY_SSIZE_T_MAX - pending->x);
352
+ pending->x += x;
353
+ pending->y = y;
354
+ pending->z = z;
355
+ return 0;
356
+ }
357
+
358
+ if (append_control(control, pending->x, pending->y, pending->z) < 0)
359
+ return -1;
360
+ pending->x = x;
361
+ pending->y = y;
362
+ pending->z = z;
363
+ return 0;
364
+ }
365
+
366
+ /* Append the final pending tuple, unless it produces no output. */
367
+ static int
368
+ queue_flush(PyObject *control, const PendingControl *pending)
369
+ {
370
+ if (pending->initialized == 0)
371
+ return 0;
372
+
373
+ /* The final source seek is irrelevant. */
374
+ if (pending->x == 0 && pending->y == 0)
375
+ return 0;
376
+
377
+ return append_control(control, pending->x, pending->y, 0);
378
+ }
379
+
312
380
  /* find the best forward match length */
313
381
  static Py_ssize_t
314
382
  forward_match_length(const char *src, Py_ssize_t src_len,
@@ -384,6 +452,7 @@ compute_diff(const char *src, Py_ssize_t src_len,
384
452
  Py_ssize_t lastscan, lastpos, lastoffset;
385
453
  Py_ssize_t scan, pos, len;
386
454
  Py_ssize_t *sa;
455
+ PendingControl pending = {0, 0, 0, 0};
387
456
 
388
457
  result->control = PyByteArray_FromStringAndSize(NULL, 0);
389
458
  if (result->control == NULL)
@@ -446,6 +515,11 @@ compute_diff(const char *src, Py_ssize_t src_len,
446
515
  adjust_overlap(src, dst, lastscan, lastpos, scan, pos,
447
516
  &lenf, &lenb);
448
517
 
518
+ assert(lenf >= 0 && lenb >= 0);
519
+ assert(lastpos >= 0 && lenf <= src_len - lastpos);
520
+ assert(lastscan >= 0 && lenf <= dst_len - lastscan);
521
+ assert(lastscan + lenf <= scan - lenb);
522
+
449
523
  for (i = 0; i < lenf; i++)
450
524
  result->diff[result->diff_len + i] =
451
525
  dst[lastscan + i] - src[lastpos + i];
@@ -458,7 +532,7 @@ compute_diff(const char *src, Py_ssize_t src_len,
458
532
  result->extra_len += extra_len;
459
533
 
460
534
  z = (pos - lenb) - (lastpos + lenf);
461
- if (append_control(result->control, lenf, extra_len, z) < 0)
535
+ if (queue_add(result->control, &pending, lenf, extra_len, z) < 0)
462
536
  goto error;
463
537
 
464
538
  lastscan = scan - lenb;
@@ -467,7 +541,13 @@ compute_diff(const char *src, Py_ssize_t src_len,
467
541
  }
468
542
  }
469
543
 
544
+ if (queue_flush(result->control, &pending) < 0)
545
+ goto error;
546
+
470
547
  PyMem_Free(sa);
548
+ assert(result->diff_len <= dst_len);
549
+ assert(result->extra_len == dst_len - result->diff_len);
550
+ assert(PyByteArray_GET_SIZE(result->control) % 24 == 0);
471
551
  return 0;
472
552
 
473
553
  error:
@@ -514,7 +594,9 @@ mod_diff(PyObject *module, PyObject *args)
514
594
  char *src, *dst;
515
595
  PyObject *result;
516
596
 
517
- if (!PyArg_ParseTuple(args, "s#s#", &src, &src_len, &dst, &dst_len))
597
+ if (!PyArg_ParseTuple(args, "s#s#",
598
+ &src, &src_len,
599
+ &dst, &dst_len))
518
600
  return NULL;
519
601
 
520
602
  if (compute_diff(src, src_len, dst, dst_len, &diff) < 0)
@@ -576,17 +658,19 @@ mod_patch(PyObject *module, PyObject *args)
576
658
  Py_ssize_t dst_pos = 0, i;
577
659
  int64_t src_pos = 0;
578
660
 
579
- if (!PyArg_ParseTuple(args, "s#ns#s#s#",
580
- &src, &src_len, &dst_len,
661
+ if (!PyArg_ParseTuple(args, "s#s#s#s#",
662
+ &src, &src_len,
581
663
  &control_block, &control_len,
582
664
  &diff_block, &diff_remaining,
583
665
  &extra_block, &extra_remaining))
584
666
  return NULL;
585
667
 
586
- if (dst_len < 0) {
587
- CORRUPT("negative destination length");
668
+ if (diff_remaining > PY_SSIZE_T_MAX - extra_remaining) {
669
+ PyErr_NoMemory();
588
670
  return NULL;
589
671
  }
672
+ dst_len = diff_remaining + extra_remaining;
673
+
590
674
  if (control_len % 24) {
591
675
  CORRUPT("invalid control block");
592
676
  return NULL;
@@ -608,10 +692,11 @@ mod_patch(PyObject *module, PyObject *args)
608
692
  memcpy(dst + dst_pos, diff_block, x);
609
693
  diff_block += x;
610
694
  diff_remaining -= x;
611
- /* Check bounds without overflowing src_pos + j. */
612
- for (j = 0; j < x; j++)
695
+ for (j = 0; j < x; j++) {
696
+ /* Check bounds without overflowing src_pos + j. */
613
697
  if (src_pos >= -j && src_pos < src_len - j)
614
698
  dst[dst_pos + j] += src[src_pos + j];
699
+ }
615
700
  dst_pos += x;
616
701
  if (y < 0 || y > dst_len - dst_pos || y > extra_remaining) {
617
702
  CORRUPT("y out of range");
@@ -624,16 +709,15 @@ mod_patch(PyObject *module, PyObject *args)
624
709
  if (update_src_pos(&src_pos, x) < 0 ||
625
710
  update_src_pos(&src_pos, z) < 0)
626
711
  goto error;
712
+ assert(dst_pos + diff_remaining + extra_remaining == dst_len);
627
713
  }
628
714
 
629
715
  if (dst_pos != dst_len) {
630
716
  CORRUPT("incomplete output");
631
717
  goto error;
632
718
  }
633
- if (diff_remaining != 0 || extra_remaining != 0) {
634
- CORRUPT("unused block data");
635
- goto error;
636
- }
719
+ /* Complete output implies that both input blocks were fully consumed. */
720
+ assert(diff_remaining == 0 && extra_remaining == 0);
637
721
 
638
722
  return result;
639
723
 
@@ -734,9 +818,21 @@ PyInit_core(void)
734
818
  if (m == NULL)
735
819
  return NULL;
736
820
 
821
+ if (PyModule_AddStringMacro(m, BSDIFF4_VERSION) < 0)
822
+ goto error;
823
+
737
824
  #ifdef Py_GIL_DISABLED
738
- PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
825
+ if (PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED) < 0)
826
+ goto error;
827
+ #endif
828
+
829
+ #ifndef NDEBUG
830
+ if (PyModule_AddIntConstant(m, "DEBUG", 1) < 0)
831
+ goto error;
739
832
  #endif
740
833
 
741
834
  return m;
835
+ error:
836
+ Py_DECREF(m);
837
+ return NULL;
742
838
  }
@@ -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,12 +32,52 @@ 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))
41
38
 
42
39
 
40
+ class NonCanonicalError(ValueError):
41
+ "Raised when a valid control block is not canonical."
42
+
43
+
44
+ def validate_canonical(control_block, src_len=None):
45
+ "Raise NonCanonicalError unless a valid control block is canonical."
46
+ controls = list(unpack_control_block(control_block))
47
+
48
+ negative_zero = 7 * b'\x00' + b'\x80'
49
+ for j in range(0, len(control_block), 8):
50
+ if control_block[j:j + 8] == negative_zero:
51
+ raise NonCanonicalError("negative zero found")
52
+
53
+ src_pos = 0
54
+ for i, (x, y, z) in enumerate(controls):
55
+ if src_pos < 0:
56
+ raise NonCanonicalError("negative source position")
57
+
58
+ if src_len is not None:
59
+ if src_pos > src_len:
60
+ raise NonCanonicalError("source position not in source range")
61
+
62
+ if src_pos + x > src_len:
63
+ raise NonCanonicalError("diff range not in source range")
64
+
65
+ if x == y == z == 0:
66
+ raise NonCanonicalError("no-op tuple found")
67
+
68
+ if i and x == 0:
69
+ raise NonCanonicalError("empty diff range")
70
+
71
+ if i == len(controls) - 1:
72
+ if z:
73
+ raise NonCanonicalError("final z not zero")
74
+ else:
75
+ if y == z == 0:
76
+ raise NonCanonicalError("adjacent diff ranges not merged")
77
+
78
+ src_pos += x + z
79
+
80
+
43
81
  def check_size(size):
44
82
  if size < 0:
45
83
  raise ValueError("negative size in bsdiff4 patch")
@@ -62,14 +100,19 @@ def read_patch(fi):
62
100
  check_size(size)
63
101
  control_len, diff_len, dst_len = sizes
64
102
  control_block = bz2.decompress(read_exact(fi, control_len))
103
+ if len(control_block) % 24:
104
+ raise ValueError("invalid control block")
65
105
  diff_block = bz2.decompress(read_exact(fi, diff_len))
66
106
  extra_block = bz2.decompress(fi.read())
67
- return dst_len, control_block, diff_block, extra_block
107
+ if dst_len != len(diff_block) + len(extra_block):
108
+ raise ValueError("invalid destination length")
109
+ return control_block, diff_block, extra_block
68
110
 
69
111
 
70
- def write_patch(fo, dst_len, control_block, diff_block, extra_block):
112
+ def write_patch(fo, control_block, diff_block, extra_block):
71
113
  "write a BSDIFF4-format patch to stream 'fo'"
72
114
  # compress each block
115
+ dst_len = len(diff_block) + len(extra_block)
73
116
  control_block = bz2.compress(control_block)
74
117
  diff_block = bz2.compress(diff_block)
75
118
  extra_block = bz2.compress(extra_block)
@@ -90,21 +133,9 @@ def diff(src_bytes: bytes, dst_bytes: bytes) -> bytes:
90
133
 
91
134
  Return a BSDIFF4-format patch (from src_bytes to dst_bytes) as bytes.
92
135
  """
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))
136
+ fo = BytesIO()
137
+ write_patch(fo, *core.diff(src_bytes, dst_bytes))
138
+ return fo.getvalue()
108
139
 
109
140
 
110
141
  def patch(src_bytes: bytes, patch_bytes: bytes) -> bytes:
@@ -115,31 +146,38 @@ def patch(src_bytes: bytes, patch_bytes: bytes) -> bytes:
115
146
  return core.patch(src_bytes, *read_patch(BytesIO(patch_bytes)))
116
147
 
117
148
 
149
+ def file_diff(src_path: str, dst_path: str, patch_path: str) -> None:
150
+ """file_diff(src_path, dst_path, patch_path) -> None
151
+
152
+ Write a BSDIFF4-format patch (from the file src_path to the file dst_path)
153
+ to the file patch_path.
154
+ """
155
+ blocks = core.diff(read_data(src_path), read_data(dst_path))
156
+ with open(patch_path, 'wb') as fo:
157
+ write_patch(fo, *blocks)
158
+
159
+
118
160
  def file_patch_inplace(path: str, patch_path: str) -> None:
119
- """file_patch_inplace(path, patch_path)
161
+ """file_patch_inplace(path, patch_path) -> None
120
162
 
121
163
  Apply the BSDIFF4-format file patch_path to the file 'path' in place.
122
164
  """
123
165
  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()
166
+ blocks = read_patch(fi)
167
+ with open(path, 'r+b') as f:
168
+ dst = core.patch(f.read(), *blocks)
169
+ f.seek(0)
170
+ f.write(dst)
171
+ f.truncate()
129
172
 
130
173
 
131
174
  def file_patch(src_path: str, dst_path: str, patch_path: str) -> None:
132
- """file_patch(src_path, dst_path, patch_path)
175
+ """file_patch(src_path, dst_path, patch_path) -> None
133
176
 
134
177
  Apply the BSDIFF4-format file patch_path to the file src_path and
135
178
  write the result to the file dst_path.
136
179
  """
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
180
  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)))
181
+ dst = core.patch(read_data(src_path), *read_patch(fi))
182
+ with open(dst_path, 'wb') as fo:
183
+ fo.write(dst)