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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bsdiff4
3
- Version: 1.4.2
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
- 154
93
+ 153
94
94
  >>> bsdiff4.patch(a, p) == b
95
95
  True
@@ -54,6 +54,6 @@ Example:
54
54
  >>> b[100:106] = b' diff '
55
55
  >>> p = bsdiff4.diff(a, bytes(b))
56
56
  >>> len(p)
57
- 154
57
+ 153
58
58
  >>> bsdiff4.patch(a, p) == b
59
59
  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
- __version__ = "1.4.2"
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 (append_control(result->control, lenf, extra_len, z) < 0)
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
- write_patch, read_patch)
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( # noncanonical "negative zero"
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 test_basic(self):
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 test_equal_bytes(self):
80
- n = 10_000
81
- self.assertEqual(core.suffix_array(n * b'a'),
82
- list(range(n, -1, -1)))
83
-
84
- def check_data(self, data):
85
- self.assertEqual(
86
- core.suffix_array(data),
87
- sorted(range(len(data) + 1), key=lambda i: data[i:]))
88
-
89
- def test_repeated_groups(self):
90
- self.check_data(32 * b'a')
91
- self.check_data(100 * b'a' + 100 * b'b')
92
-
93
- def test_two_element_group(self):
94
- # A two-element group must not be mistaken for a singleton.
95
- self.check_data(b'CCBBCCCACCCCBCACBCCCCC')
96
-
97
- def test_random(self):
98
- # randomized comparison
99
- self.check_data(os.urandom(1000))
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'banana', b'bandana',
147
- [(3, 4, 2)], 3 * b'\x00', b'dana')
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"The quick brown fox jumps over the hazy dog."
152
- n = len(src)
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, -8)], bytes(diff), b'')
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, -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, -100)],
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, -100)],
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, -9)],
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 test_patch(self):
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.2
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
- 154
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/__init__.py
13
- pat = re.compile(r'__version__\s*=\s*"(\S+)"', re.M)
14
- data = open('bsdiff4/__init__.py').read()
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