bsdiff4 1.4.2__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.
- {bsdiff4-1.4.2/bsdiff4.egg-info → bsdiff4-1.4.3}/PKG-INFO +2 -2
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/README.rst +1 -1
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4/__init__.py +3 -1
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4/cli.py +6 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4/core.c +84 -2
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4/format.py +41 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4/tests.py +180 -38
- {bsdiff4-1.4.2 → bsdiff4-1.4.3/bsdiff4.egg-info}/PKG-INFO +2 -2
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/setup.py +3 -3
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/LICENSE +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/MANIFEST.in +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4/py.typed +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4.egg-info/SOURCES.txt +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4.egg-info/dependency_links.txt +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4.egg-info/entry_points.txt +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/bsdiff4.egg-info/top_level.txt +0 -0
- {bsdiff4-1.4.2 → bsdiff4-1.4.3}/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.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
|
|
@@ -90,6 +90,6 @@ Example:
|
|
|
90
90
|
>>> b[100:106] = b' diff '
|
|
91
91
|
>>> p = bsdiff4.diff(a, bytes(b))
|
|
92
92
|
>>> len(p)
|
|
93
|
-
|
|
93
|
+
153
|
|
94
94
|
>>> bsdiff4.patch(a, p) == b
|
|
95
95
|
True
|
|
@@ -1,7 +1,9 @@
|
|
|
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
|
-
|
|
5
|
+
|
|
6
|
+
__version__: str = core.BSDIFF4_VERSION
|
|
5
7
|
|
|
6
8
|
__all__ = ["diff", "patch", "file_diff", "file_patch", "file_patch_inplace"]
|
|
7
9
|
|
|
@@ -5,6 +5,7 @@ from os.path import getsize
|
|
|
5
5
|
import bsdiff4.core as core # type: ignore
|
|
6
6
|
from bsdiff4 import __version__
|
|
7
7
|
from bsdiff4.format import (file_diff, file_patch, read_patch,
|
|
8
|
+
NonCanonicalError, validate_canonical,
|
|
8
9
|
unpack_header, unpack_control_block)
|
|
9
10
|
|
|
10
11
|
|
|
@@ -84,6 +85,11 @@ def show_patch(fi, verbose=False):
|
|
|
84
85
|
show_control(control_block, verbose)
|
|
85
86
|
# validate patch by applying it to empty source
|
|
86
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)
|
|
87
93
|
|
|
88
94
|
|
|
89
95
|
def main_bsdiff4():
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#include <assert.h>
|
|
11
11
|
#include <stdint.h>
|
|
12
12
|
|
|
13
|
+
#define BSDIFF4_VERSION "1.4.3"
|
|
13
14
|
|
|
14
15
|
/* swap two values */
|
|
15
16
|
static inline void
|
|
@@ -313,6 +314,69 @@ append_control(PyObject *control, Py_ssize_t x, Py_ssize_t y, Py_ssize_t z)
|
|
|
313
314
|
return 0;
|
|
314
315
|
}
|
|
315
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
|
+
|
|
316
380
|
/* find the best forward match length */
|
|
317
381
|
static Py_ssize_t
|
|
318
382
|
forward_match_length(const char *src, Py_ssize_t src_len,
|
|
@@ -388,6 +452,7 @@ compute_diff(const char *src, Py_ssize_t src_len,
|
|
|
388
452
|
Py_ssize_t lastscan, lastpos, lastoffset;
|
|
389
453
|
Py_ssize_t scan, pos, len;
|
|
390
454
|
Py_ssize_t *sa;
|
|
455
|
+
PendingControl pending = {0, 0, 0, 0};
|
|
391
456
|
|
|
392
457
|
result->control = PyByteArray_FromStringAndSize(NULL, 0);
|
|
393
458
|
if (result->control == NULL)
|
|
@@ -467,7 +532,7 @@ compute_diff(const char *src, Py_ssize_t src_len,
|
|
|
467
532
|
result->extra_len += extra_len;
|
|
468
533
|
|
|
469
534
|
z = (pos - lenb) - (lastpos + lenf);
|
|
470
|
-
if (
|
|
535
|
+
if (queue_add(result->control, &pending, lenf, extra_len, z) < 0)
|
|
471
536
|
goto error;
|
|
472
537
|
|
|
473
538
|
lastscan = scan - lenb;
|
|
@@ -476,6 +541,9 @@ compute_diff(const char *src, Py_ssize_t src_len,
|
|
|
476
541
|
}
|
|
477
542
|
}
|
|
478
543
|
|
|
544
|
+
if (queue_flush(result->control, &pending) < 0)
|
|
545
|
+
goto error;
|
|
546
|
+
|
|
479
547
|
PyMem_Free(sa);
|
|
480
548
|
assert(result->diff_len <= dst_len);
|
|
481
549
|
assert(result->extra_len == dst_len - result->diff_len);
|
|
@@ -641,12 +709,14 @@ mod_patch(PyObject *module, PyObject *args)
|
|
|
641
709
|
if (update_src_pos(&src_pos, x) < 0 ||
|
|
642
710
|
update_src_pos(&src_pos, z) < 0)
|
|
643
711
|
goto error;
|
|
712
|
+
assert(dst_pos + diff_remaining + extra_remaining == dst_len);
|
|
644
713
|
}
|
|
645
714
|
|
|
646
715
|
if (dst_pos != dst_len) {
|
|
647
716
|
CORRUPT("incomplete output");
|
|
648
717
|
goto error;
|
|
649
718
|
}
|
|
719
|
+
/* Complete output implies that both input blocks were fully consumed. */
|
|
650
720
|
assert(diff_remaining == 0 && extra_remaining == 0);
|
|
651
721
|
|
|
652
722
|
return result;
|
|
@@ -748,9 +818,21 @@ PyInit_core(void)
|
|
|
748
818
|
if (m == NULL)
|
|
749
819
|
return NULL;
|
|
750
820
|
|
|
821
|
+
if (PyModule_AddStringMacro(m, BSDIFF4_VERSION) < 0)
|
|
822
|
+
goto error;
|
|
823
|
+
|
|
751
824
|
#ifdef Py_GIL_DISABLED
|
|
752
|
-
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;
|
|
753
832
|
#endif
|
|
754
833
|
|
|
755
834
|
return m;
|
|
835
|
+
error:
|
|
836
|
+
Py_DECREF(m);
|
|
837
|
+
return NULL;
|
|
756
838
|
}
|
|
@@ -37,6 +37,47 @@ def unpack_control_block(control_block):
|
|
|
37
37
|
for j in range(i, i + 24, 8))
|
|
38
38
|
|
|
39
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
|
+
|
|
40
81
|
def check_size(size):
|
|
41
82
|
if size < 0:
|
|
42
83
|
raise ValueError("negative size in bsdiff4 patch")
|
|
@@ -14,7 +14,8 @@ from threading import Barrier
|
|
|
14
14
|
import bsdiff4.core as core # type: ignore
|
|
15
15
|
from bsdiff4.format import (pack_header, unpack_header,
|
|
16
16
|
pack_control_block, unpack_control_block,
|
|
17
|
-
|
|
17
|
+
NonCanonicalError, validate_canonical,
|
|
18
|
+
read_patch, write_patch)
|
|
18
19
|
from bsdiff4 import diff, patch, file_diff, file_patch, file_patch_inplace
|
|
19
20
|
|
|
20
21
|
|
|
@@ -46,7 +47,7 @@ class TestCoreInt64(unittest.TestCase):
|
|
|
46
47
|
self.assertEqual(core.encode_int64(n), b)
|
|
47
48
|
self.assertEqual(core.decode_int64(b), n)
|
|
48
49
|
|
|
49
|
-
self.assertEqual( #
|
|
50
|
+
self.assertEqual( # non-canonical "negative zero"
|
|
50
51
|
core.decode_int64(bytes.fromhex('00 00 00 00 00 00 00 80')), 0)
|
|
51
52
|
|
|
52
53
|
def test_errors(self):
|
|
@@ -69,55 +70,79 @@ class TestCoreInt64(unittest.TestCase):
|
|
|
69
70
|
self.assertEqual(core.decode_int64(b), n)
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
RND_200 = os.urandom(200)
|
|
74
|
+
|
|
75
|
+
BYTES_CASES = [b'', b'a', b'abc', 50 * b'A', 50 * b'A' + 50 * b'B', RND_200]
|
|
76
|
+
|
|
77
|
+
|
|
72
78
|
class TestSuffixArray(unittest.TestCase):
|
|
73
79
|
|
|
74
|
-
def
|
|
80
|
+
def test_examples(self):
|
|
75
81
|
SA = core.suffix_array
|
|
76
82
|
self.assertEqual(SA(b''), [0])
|
|
77
83
|
self.assertEqual(SA(b'banana'), [6, 5, 3, 1, 0, 4, 2])
|
|
78
84
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
def test_cases(self):
|
|
86
|
+
for data in BYTES_CASES:
|
|
87
|
+
self.assertEqual(
|
|
88
|
+
core.suffix_array(data),
|
|
89
|
+
sorted(range(len(data) + 1), key=lambda i: data[i:]))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Although non-canonical, each control sequence is valid and transforms
|
|
93
|
+
# b'Banana' into b'bandana' with diff b'\x20\x00\x00' and extra b'dana'.
|
|
94
|
+
NON_CANONICAL_CONTROLS = [
|
|
95
|
+
# Control tuples not merged.
|
|
96
|
+
([(3, 0, 0), (0, 4, 0)], "adjacent"),
|
|
97
|
+
# Consecutive diffs.
|
|
98
|
+
([(2, 0, 0), (1, 4, 0)], "adjacent"),
|
|
99
|
+
([(1, 0, 0), (2, 0, 0), (0, 4, 0)], "adjacent"),
|
|
100
|
+
# Consecutive extras.
|
|
101
|
+
([(3, 2, 0), (0, 2, 0)], "empty diff range"),
|
|
102
|
+
([(3, 0, 0), (0, 2, 0), (0, 2, 0)], "adjacent"),
|
|
103
|
+
# Consecutive z values.
|
|
104
|
+
([(0, 0, 11), (0, 0, -4), (0, 0, -7), (3, 4, 0)], "^source position"),
|
|
105
|
+
# Final z value.
|
|
106
|
+
([(3, 4, 2)], "final z not zero"),
|
|
107
|
+
# No-op control tuple.
|
|
108
|
+
([(0, 0, 0), (3, 4, 0)], "no-op"),
|
|
109
|
+
]
|
|
101
110
|
|
|
102
|
-
RND_200 = os.urandom(200)
|
|
103
111
|
|
|
104
112
|
ROUND_TRIP_CASES = [
|
|
105
113
|
(b'', b''),
|
|
106
114
|
(b'', b'x'),
|
|
107
115
|
(b'abc', b'abc'),
|
|
108
116
|
(b'abc', b'abx'),
|
|
117
|
+
(b'abc', b'axc'),
|
|
109
118
|
(b'abc', b'abxy'),
|
|
110
119
|
(b'abc', b'xbc'),
|
|
111
120
|
(b'abc', b'xybc'),
|
|
121
|
+
(b'Banana', b'bandana'),
|
|
122
|
+
(b'ccccdabcadcb', b'ccccdcdabcadcb'),
|
|
112
123
|
(b'123456789 987654321', b'123456789000987654321'), # issue #14
|
|
124
|
+
# These inputs cause control tuples to be merged during diff generation.
|
|
125
|
+
(b'ababababcdababababc', b'&ababababcdabababab'),
|
|
126
|
+
(b'aaaaaaaaaaaaaaaaaaaabxtrabytes',
|
|
127
|
+
b'ccaaaaaaaaaaaaaaaaaabexbytesbextrabytes'),
|
|
128
|
+
(b'gaiififkegaiififkegaiififkejcdldhialkegaiifif',
|
|
129
|
+
b'kaiififkegaiififkedmcfajfbbkegaiififkegaiifif'),
|
|
130
|
+
(b'srfljlkeoxqdhiusrfljlkeoxqdhiusrfljcadrfljlkeoxqdhius',
|
|
131
|
+
b'irfljlkeoxqdhiusrfljtwyvgpnbmeoxqdhiusrfljlkeox'),
|
|
132
|
+
# swap repeated blocks
|
|
133
|
+
(50 * b'a' + 50 * b'b', 50 * b'b' + 50 * b'a'),
|
|
134
|
+
# swap random blocks
|
|
135
|
+
(RND_200, RND_200[100:] + RND_200[:100]),
|
|
113
136
|
(b'', RND_200),
|
|
114
137
|
(RND_200, RND_200),
|
|
115
138
|
(RND_200, os.urandom(20) + RND_200),
|
|
116
139
|
(RND_200, RND_200 + os.urandom(20)),
|
|
140
|
+
# Three bytes in the middle replaced.
|
|
141
|
+
(RND_200, RND_200[:98] + os.urandom(3) + RND_200[101:]),
|
|
142
|
+
# 10 bytes replaced by 15 new bytes
|
|
143
|
+
(RND_200, RND_200[:95] + os.urandom(15) + RND_200[105:]),
|
|
117
144
|
]
|
|
118
145
|
|
|
119
|
-
BYTES_CASES = [b'', b'a', b'abc', 100 * b'a', RND_200]
|
|
120
|
-
|
|
121
146
|
|
|
122
147
|
class TestCoreDiffPatch(unittest.TestCase):
|
|
123
148
|
|
|
@@ -125,6 +150,7 @@ class TestCoreDiffPatch(unittest.TestCase):
|
|
|
125
150
|
blocks = core.diff(src, dst)
|
|
126
151
|
for block in blocks:
|
|
127
152
|
self.assertIs(type(block), bytes)
|
|
153
|
+
validate_canonical(blocks[0], len(src))
|
|
128
154
|
dst2 = core.patch(src, *blocks)
|
|
129
155
|
self.assertIs(type(dst2), bytes)
|
|
130
156
|
self.assertEqual(dst2, dst)
|
|
@@ -143,35 +169,74 @@ class TestCoreDiffPatch(unittest.TestCase):
|
|
|
143
169
|
self.assertEqual(core.patch(src, *blocks), dst)
|
|
144
170
|
|
|
145
171
|
def test_diff_simple_example(self):
|
|
146
|
-
self.check_diff_blocks(b'
|
|
147
|
-
[(3, 4,
|
|
172
|
+
self.check_diff_blocks(b'Banana', b'bandana',
|
|
173
|
+
[(3, 4, 0)], b'\x20\x00\x00', b'dana')
|
|
148
174
|
|
|
149
175
|
def test_nonzero_diff(self):
|
|
150
176
|
src = b"The quick brown fox jumps over the lazy dog."
|
|
151
|
-
dst = b"
|
|
152
|
-
n = len(
|
|
177
|
+
dst = b"the quick brown fox jumps over the hazy dog."
|
|
178
|
+
n = len(dst)
|
|
153
179
|
diff = bytearray(n)
|
|
180
|
+
diff[0] = 32
|
|
154
181
|
diff[35] = (ord(b'h') - ord(b'l')) & 0xff
|
|
155
|
-
self.check_diff_blocks(src, dst, [(n, 0,
|
|
182
|
+
self.check_diff_blocks(src, dst, [(n, 0, 0)], bytes(diff), b'')
|
|
183
|
+
|
|
184
|
+
def test_merge_rules(self):
|
|
185
|
+
# ...................
|
|
186
|
+
src = b'SRFLJLKEOXQDHIUSRFLJLKEOXQDHIUSRFLJCADRFLJLKEOXQDHIUS'
|
|
187
|
+
dst = b'IRFLJLKEOXQDHIUSRFLJTWYVGPNBMEOXQDHIUSRFLJLKEOX'
|
|
188
|
+
# ^^^^^^^^^^^^^^^^^^^
|
|
189
|
+
# dst[1:20] occurs in src at offsets 1 and 16. The matcher first
|
|
190
|
+
# selects the latter occurrence, then returns to the former.
|
|
191
|
+
self.assertTrue(src[1:20] == src[16:35] == dst[1:20])
|
|
192
|
+
# Without merging, controls would be:
|
|
193
|
+
# [(1, 0, 15), (0, 0, -15), (19, 27, 0)]
|
|
194
|
+
# Branch 1 merges the first two tuples:
|
|
195
|
+
# [(1, 0, 0), (19, 27, 0)]
|
|
196
|
+
# Branch 2 can then see that the two diff ranges access consecutive
|
|
197
|
+
# source positions and merge them:
|
|
198
|
+
# [(20, 27, 0)]
|
|
199
|
+
self.check_diff_blocks(src, dst,
|
|
200
|
+
controls=[(20, 27, 0)],
|
|
201
|
+
diff=b'\xf6' + 19 * b'\x00',
|
|
202
|
+
extra=b'TWYVGPNBMEOXQDHIUSRFLJLKEOX')
|
|
203
|
+
|
|
204
|
+
def test_merge_rules_diff_only(self):
|
|
205
|
+
A = b'RCdrQsXKTOo1N5Ml7Xa'
|
|
206
|
+
B = b'332230220'
|
|
207
|
+
C = b'cGIm8gt4E'
|
|
208
|
+
D = b'ZHDSOJjqK'
|
|
209
|
+
src = A + A + B + C + A + B + B
|
|
210
|
+
dst = A + A + D + B + A + B + B
|
|
211
|
+
# Without merging, controls would be:
|
|
212
|
+
# [(19, 0, 37), (0, 0, -37), (74, 0, 0)]
|
|
213
|
+
# The first merge rule gives:
|
|
214
|
+
# [(19, 0, 0), (74, 0, 0)]
|
|
215
|
+
# The second gives the canonical result:
|
|
216
|
+
# [(93, 0, 0)]
|
|
217
|
+
# The seeks cancel, so the diff block is simply the bytewise
|
|
218
|
+
# difference between dst and src.
|
|
219
|
+
bdiff = bytes((b - a) & 0xff for a, b in zip(src, dst))
|
|
220
|
+
self.check_diff_blocks(src, dst, [(93, 0, 0)], bdiff, b'')
|
|
156
221
|
|
|
157
222
|
def test_diff_insert_byte(self):
|
|
158
223
|
src = 32 * b'a'
|
|
159
224
|
dst = src[:16] + b'b' + src[16:]
|
|
160
|
-
self.check_diff_blocks(src, dst, [(32, 1,
|
|
225
|
+
self.check_diff_blocks(src, dst, [(32, 1, 0)],
|
|
161
226
|
diff=16 * b'\x00' + b'\x01' + 15 * b'\x00',
|
|
162
227
|
extra=b'a')
|
|
163
228
|
|
|
164
229
|
def test_diff_swap_repeated_blocks(self):
|
|
165
230
|
A, B = 100 * b'A', 100 * b'B'
|
|
166
231
|
self.check_diff_blocks(A + B, B + A,
|
|
167
|
-
[(0, 100, 0), (100, 0,
|
|
232
|
+
[(0, 100, 0), (100, 0, 0)],
|
|
168
233
|
diff=100 * b'\x00', extra=B)
|
|
169
234
|
|
|
170
235
|
def test_diff_swap_blocks(self):
|
|
171
236
|
src = bytes(range(200))
|
|
172
237
|
dst = src[100:] + src[:100]
|
|
173
238
|
self.check_diff_blocks(src, dst,
|
|
174
|
-
[(0, 0, 100), (100, 0, -200), (100, 0,
|
|
239
|
+
[(0, 0, 100), (100, 0, -200), (100, 0, 0)],
|
|
175
240
|
diff=200 * b'\x00', extra=b'')
|
|
176
241
|
|
|
177
242
|
def test_diff_adjust_overlap(self):
|
|
@@ -179,7 +244,7 @@ class TestCoreDiffPatch(unittest.TestCase):
|
|
|
179
244
|
# matching block.
|
|
180
245
|
self.check_diff_blocks(b'ccccdabcadcb',
|
|
181
246
|
b'ccccdcdabcadcb',
|
|
182
|
-
[(5, 0, -2), (9, 0,
|
|
247
|
+
[(5, 0, -2), (9, 0, 0)],
|
|
183
248
|
14 * b'\x00', b'')
|
|
184
249
|
|
|
185
250
|
def test_empty_dst(self):
|
|
@@ -217,7 +282,42 @@ class TestCoreDiffPatch(unittest.TestCase):
|
|
|
217
282
|
blocks = bcontrol, len(src) * b'\x01', b''
|
|
218
283
|
self.assertEqual(core.patch(src, *blocks), dst)
|
|
219
284
|
|
|
220
|
-
def
|
|
285
|
+
def check_banana_patch(self, controls,
|
|
286
|
+
diff=b'\x20\x00\x00', extra=b'dana', msg=None):
|
|
287
|
+
src = b'Banana'
|
|
288
|
+
dst = b'bandana'
|
|
289
|
+
bcontrol = pack_control_block(*controls)
|
|
290
|
+
self.assertEqual(core.patch(src, bcontrol, diff, extra), dst)
|
|
291
|
+
if msg is None:
|
|
292
|
+
validate_canonical(bcontrol, len(src))
|
|
293
|
+
else:
|
|
294
|
+
with self.assertRaisesRegex(NonCanonicalError, msg):
|
|
295
|
+
validate_canonical(bcontrol, len(src))
|
|
296
|
+
|
|
297
|
+
def test_patch_non_canonical(self):
|
|
298
|
+
dst = b'bandana'
|
|
299
|
+
check = self.check_banana_patch
|
|
300
|
+
|
|
301
|
+
# canonical patch
|
|
302
|
+
check([(3, 4, 0)])
|
|
303
|
+
# Also canonical, but not returned by core.diff().
|
|
304
|
+
check([(6, 1, 0)], b'\x20\x00\x00\x03\xf3\x0d', b'a')
|
|
305
|
+
check([(4, 3, 0)], b'\x20\x00\x00\x03', b'ana')
|
|
306
|
+
check([(0, 7, 0)], b'', dst)
|
|
307
|
+
|
|
308
|
+
for controls, msg in NON_CANONICAL_CONTROLS:
|
|
309
|
+
check(controls, msg=msg)
|
|
310
|
+
|
|
311
|
+
# Negative source position.
|
|
312
|
+
check([(0, 0, -7), (7, 0, 0)], dst, b'', msg="negative source")
|
|
313
|
+
# Diff range extends one byte beyond the source.
|
|
314
|
+
check([(7, 0, 0)], b'\x20\x00\x00\x03\xf3\x0da', b'',
|
|
315
|
+
msg="diff range not in source range")
|
|
316
|
+
# Source position starts beyond the end of the source.
|
|
317
|
+
check([(0, 0, 7), (1, 6, 0)], diff=b'b', extra=b'andana',
|
|
318
|
+
msg="source position not in source range")
|
|
319
|
+
|
|
320
|
+
def test_patch_noop(self):
|
|
221
321
|
noop = pack_control_block((0, 0, 0)) # no-op control tuple
|
|
222
322
|
noop2 = 3 * (7 * b'\x00' + b'\x80') # negative zero no-op
|
|
223
323
|
operation = pack_control_block((3, 4, 2))
|
|
@@ -317,6 +417,47 @@ class TestHelpers(unittest.TestCase):
|
|
|
317
417
|
list, bcontrol) # force iteration
|
|
318
418
|
|
|
319
419
|
|
|
420
|
+
class TestValidateCanonical(unittest.TestCase):
|
|
421
|
+
|
|
422
|
+
# Additional tests for validate_canonical() beyond those in
|
|
423
|
+
# test_patch_non_canonical.
|
|
424
|
+
|
|
425
|
+
def test_error_type(self):
|
|
426
|
+
msg = "invalid control block"
|
|
427
|
+
with self.assertRaisesRegex(ValueError, msg) as cm:
|
|
428
|
+
validate_canonical(30 * b'\x00')
|
|
429
|
+
self.assertIs(type(cm.exception), ValueError)
|
|
430
|
+
|
|
431
|
+
with self.assertRaisesRegex(NonCanonicalError, "no-op"):
|
|
432
|
+
validate_canonical(24 * b'\x00')
|
|
433
|
+
|
|
434
|
+
def test_negative_zero(self):
|
|
435
|
+
zero = 8 * b'\x00'
|
|
436
|
+
negzero = 7 * b'\x00' + b'\x80'
|
|
437
|
+
|
|
438
|
+
for i in range(6):
|
|
439
|
+
values = 6 * [zero]
|
|
440
|
+
values[i] = negzero
|
|
441
|
+
with self.assertRaisesRegex(NonCanonicalError, "negative zero"):
|
|
442
|
+
validate_canonical(b''.join(values))
|
|
443
|
+
|
|
444
|
+
def test_source_independent(self):
|
|
445
|
+
bcontrol = pack_control_block((0, 0, 7), (1, 6, 0))
|
|
446
|
+
# Without a source length, the control structure is canonical.
|
|
447
|
+
validate_canonical(bcontrol)
|
|
448
|
+
|
|
449
|
+
# With a source length of 6, the source position starts beyond
|
|
450
|
+
# the end of the source.
|
|
451
|
+
self.assertRaisesRegex(NonCanonicalError,
|
|
452
|
+
"source position not in source range",
|
|
453
|
+
validate_canonical, bcontrol, 6)
|
|
454
|
+
|
|
455
|
+
def test_empty(self):
|
|
456
|
+
# An empty control block is always canonical.
|
|
457
|
+
for src_len in None, 0, 1, 123:
|
|
458
|
+
validate_canonical(b'', src_len)
|
|
459
|
+
|
|
460
|
+
|
|
320
461
|
class Example:
|
|
321
462
|
src = b'abc'
|
|
322
463
|
dst = b'abd!'
|
|
@@ -668,6 +809,7 @@ def run(verbosity=1):
|
|
|
668
809
|
print('bsdiff4 version: %s' % __version__)
|
|
669
810
|
print('UNDER_QEMU: %s' % UNDER_QEMU)
|
|
670
811
|
print('GIL_DISABLED: %s' % GIL_DISABLED)
|
|
812
|
+
print('DEBUG: %s' % getattr(core, "DEBUG", 0))
|
|
671
813
|
|
|
672
814
|
loader = unittest.TestLoader()
|
|
673
815
|
suite = unittest.TestSuite()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bsdiff4
|
|
3
|
-
Version: 1.4.
|
|
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
|
|
@@ -90,6 +90,6 @@ Example:
|
|
|
90
90
|
>>> b[100:106] = b' diff '
|
|
91
91
|
>>> p = bsdiff4.diff(a, bytes(b))
|
|
92
92
|
>>> len(p)
|
|
93
|
-
|
|
93
|
+
153
|
|
94
94
|
>>> bsdiff4.patch(a, p) == b
|
|
95
95
|
True
|
|
@@ -9,9 +9,9 @@ except ImportError:
|
|
|
9
9
|
kwds = {}
|
|
10
10
|
kwds['long_description'] = open('README.rst').read()
|
|
11
11
|
|
|
12
|
-
# Read version from bsdiff4/
|
|
13
|
-
pat = re.compile(r'
|
|
14
|
-
data = open('bsdiff4/
|
|
12
|
+
# Read version from bsdiff4/core.c
|
|
13
|
+
pat = re.compile(r'#define\s+BSDIFF4_VERSION\s+"(\S+)"', re.M)
|
|
14
|
+
data = open('bsdiff4/core.c').read()
|
|
15
15
|
kwds['version'] = pat.search(data).group(1)
|
|
16
16
|
|
|
17
17
|
macros = []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|