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.
- {bsdiff4-1.4.1/bsdiff4.egg-info → bsdiff4-1.4.2}/PKG-INFO +8 -6
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/README.rst +7 -5
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4/__init__.py +6 -2
- bsdiff4-1.4.2/bsdiff4/cli.py +145 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4/core.c +26 -12
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4/format.py +36 -39
- bsdiff4-1.4.1/bsdiff4/test_all.py → bsdiff4-1.4.2/bsdiff4/tests.py +259 -125
- {bsdiff4-1.4.1 → bsdiff4-1.4.2/bsdiff4.egg-info}/PKG-INFO +8 -6
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4.egg-info/SOURCES.txt +1 -1
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/setup.py +1 -1
- bsdiff4-1.4.1/bsdiff4/cli.py +0 -103
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/LICENSE +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/MANIFEST.in +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4/py.typed +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4.egg-info/dependency_links.txt +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4.egg-info/entry_points.txt +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/bsdiff4.egg-info/top_level.txt +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bsdiff4
|
|
3
|
-
Version: 1.4.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
/*
|
|
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#",
|
|
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#
|
|
580
|
-
&src, &src_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 (
|
|
587
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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[:
|
|
22
|
-
raise ValueError("invalid magic
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
94
|
-
write_patch(
|
|
95
|
-
return
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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 (
|
|
15
|
+
from bsdiff4.format import (pack_header, unpack_header,
|
|
15
16
|
pack_control_block, unpack_control_block,
|
|
16
|
-
write_patch, read_patch
|
|
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, '
|
|
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,
|
|
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'',
|
|
111
|
-
(
|
|
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
|
-
|
|
119
|
-
for
|
|
120
|
-
self.assertIs(type(
|
|
121
|
-
dst2 = core.patch(src,
|
|
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
|
|
137
|
+
def check_diff_blocks(self, src, dst, controls, diff, extra):
|
|
132
138
|
# Check construction of control, diff and extra blocks.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
self.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
self.
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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',
|
|
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'',
|
|
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
|
-
|
|
178
|
-
|
|
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.
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
core.patch(
|
|
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'',
|
|
274
|
+
self.assertEqual(core.patch(b'', bcontrol, b'', b''), b'')
|
|
216
275
|
|
|
217
|
-
for
|
|
218
|
-
(
|
|
219
|
-
(
|
|
220
|
-
(
|
|
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.
|
|
224
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
341
|
+
self.assertEqual(patch(Example.src, Example.patch), Example.dst)
|
|
287
342
|
|
|
288
343
|
def test_header(self):
|
|
289
|
-
self.assertEqual(unpack_header(Example.
|
|
344
|
+
self.assertEqual(unpack_header(Example.patch[:32]),
|
|
290
345
|
Example.header_sizes)
|
|
291
346
|
|
|
292
|
-
def
|
|
293
|
-
|
|
294
|
-
|
|
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,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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"
|
|
358
|
-
self.assertRaisesRegex(ValueError, "invalid magic
|
|
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
|
-
|
|
364
|
-
|
|
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 *
|
|
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.
|
|
456
|
+
for i in range(len(Example.patch)):
|
|
400
457
|
# truncating patch
|
|
401
458
|
self.assertRaises(ValueError, patch,
|
|
402
|
-
Example.src, Example.
|
|
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.
|
|
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 =
|
|
434
|
-
data2 =
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
self.
|
|
462
|
-
self.
|
|
463
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -9,7 +9,7 @@ except ImportError:
|
|
|
9
9
|
kwds = {}
|
|
10
10
|
kwds['long_description'] = open('README.rst').read()
|
|
11
11
|
|
|
12
|
-
# Read version from
|
|
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)
|
bsdiff4-1.4.1/bsdiff4/cli.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|