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.
- {bsdiff4-1.4.1/bsdiff4.egg-info → bsdiff4-1.4.3}/PKG-INFO +9 -7
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/README.rst +8 -6
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4/__init__.py +8 -2
- bsdiff4-1.4.3/bsdiff4/cli.py +151 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4/core.c +110 -14
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4/format.py +77 -39
- bsdiff4-1.4.3/bsdiff4/tests.py +822 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3/bsdiff4.egg-info}/PKG-INFO +9 -7
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4.egg-info/SOURCES.txt +1 -1
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/setup.py +3 -3
- bsdiff4-1.4.1/bsdiff4/cli.py +0 -103
- bsdiff4-1.4.1/bsdiff4/test_all.py +0 -546
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/LICENSE +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/MANIFEST.in +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4/py.typed +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4.egg-info/dependency_links.txt +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4.egg-info/entry_points.txt +0 -0
- {bsdiff4-1.4.1 → bsdiff4-1.4.3}/bsdiff4.egg-info/top_level.txt +0 -0
- {bsdiff4-1.4.1 → 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
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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 (
|
|
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#",
|
|
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#
|
|
580
|
-
&src, &src_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 (
|
|
587
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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,
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
145
|
-
|
|
181
|
+
dst = core.patch(read_data(src_path), *read_patch(fi))
|
|
182
|
+
with open(dst_path, 'wb') as fo:
|
|
183
|
+
fo.write(dst)
|