22
22
- InventoryDeltaSerializer - object to read/write inventory deltas.
25
from __future__ import absolute_import
27
25
__all__ = ['InventoryDeltaSerializer']
30
from ..osutils import basename
31
from . import inventory
32
from ..revision import NULL_REVISION
27
from bzrlib import errors
28
from bzrlib.osutils import basename
29
from bzrlib import inventory
30
from bzrlib.revision import NULL_REVISION
34
FORMAT_1 = b'bzr inventory delta v1 (bzr 1.14)'
32
FORMAT_1 = 'bzr inventory delta v1 (bzr 1.14)'
37
35
class InventoryDeltaError(errors.BzrError):
38
36
"""An error when serializing or deserializing an inventory delta."""
40
38
# Most errors when serializing and deserializing are due to bugs, although
41
39
# damaged input (i.e. a bug in a different process) could cause
42
40
# deserialization errors too.
43
41
internal_error = True
45
def __init__(self, format_string, **kwargs):
46
# Let each error supply a custom format string and arguments.
47
self._fmt = format_string
48
super(InventoryDeltaError, self).__init__(**kwargs)
51
44
class IncompatibleInventoryDelta(errors.BzrError):
52
45
"""The delta could not be deserialised because its contents conflict with
59
52
def _directory_content(entry):
60
53
"""Serialize the content component of entry which is a directory.
62
55
:param entry: An InventoryDirectory.
67
60
def _file_content(entry):
68
61
"""Serialize the content component of entry which is a file.
70
63
:param entry: An InventoryFile.
72
65
if entry.executable:
76
size_exec_sha = entry.text_size, exec_bytes, entry.text_sha1
69
size_exec_sha = (entry.text_size, exec_bytes, entry.text_sha1)
77
70
if None in size_exec_sha:
78
raise InventoryDeltaError(
79
'Missing size or sha for %(fileid)r', fileid=entry.file_id)
80
return b"file\x00%d\x00%s\x00%s" % size_exec_sha
71
raise InventoryDeltaError('Missing size or sha for %s' % entry.file_id)
72
return "file\x00%d\x00%s\x00%s" % size_exec_sha
83
75
def _link_content(entry):
84
76
"""Serialize the content component of entry which is a symlink.
86
78
:param entry: An InventoryLink.
88
80
target = entry.symlink_target
90
raise InventoryDeltaError(
91
'Missing target for %(fileid)r', fileid=entry.file_id)
92
return b"link\x00%s" % target.encode('utf8')
82
raise InventoryDeltaError('Missing target for %s' % entry.file_id)
83
return "link\x00%s" % target.encode('utf8')
95
86
def _reference_content(entry):
96
87
"""Serialize the content component of entry which is a tree-reference.
98
89
:param entry: A TreeReference.
100
91
tree_revision = entry.reference_revision
101
92
if tree_revision is None:
102
93
raise InventoryDeltaError(
103
'Missing reference revision for %(fileid)r', fileid=entry.file_id)
104
return b"tree\x00%s" % tree_revision
94
'Missing reference revision for %s' % entry.file_id)
95
return "tree\x00%s" % tree_revision
107
98
def _dir_to_entry(content, name, parent_id, file_id, last_modified,
108
_type=inventory.InventoryDirectory):
99
_type=inventory.InventoryDirectory):
109
100
"""Convert a dir content record to an InventoryDirectory."""
110
101
result = _type(file_id, name, parent_id)
111
102
result.revision = last_modified
180
171
:return: The serialized delta as lines.
182
if not isinstance(old_name, bytes):
173
if type(old_name) is not str:
183
174
raise TypeError('old_name should be str, got %r' % (old_name,))
184
if not isinstance(new_name, bytes):
175
if type(new_name) is not str:
185
176
raise TypeError('new_name should be str, got %r' % (new_name,))
186
lines = [b'', b'', b'', b'', b'']
177
lines = ['', '', '', '', '']
187
178
to_line = self._delta_item_to_line
188
179
for delta_item in delta_to_new:
189
180
line = to_line(delta_item, new_name)
190
# GZ 2017-06-09: Not really worth asserting this here
191
if line.__class__ != bytes:
181
if line.__class__ != str:
192
182
raise InventoryDeltaError(
193
'to_line gave non-bytes output %(line)r', line=lines[-1])
183
'to_line generated non-str output %r' % lines[-1])
194
184
lines.append(line)
196
lines[0] = b"format: %s\n" % FORMAT_1
197
lines[1] = b"parent: %s\n" % old_name
198
lines[2] = b"version: %s\n" % new_name
199
lines[3] = b"versioned_root: %s\n" % self._serialize_bool(
186
lines[0] = "format: %s\n" % FORMAT_1
187
lines[1] = "parent: %s\n" % old_name
188
lines[2] = "version: %s\n" % new_name
189
lines[3] = "versioned_root: %s\n" % self._serialize_bool(
200
190
self._versioned_root)
201
lines[4] = b"tree_references: %s\n" % self._serialize_bool(
191
lines[4] = "tree_references: %s\n" % self._serialize_bool(
202
192
self._tree_references)
205
195
def _serialize_bool(self, value):
211
201
def _delta_item_to_line(self, delta_item, new_version):
212
202
"""Convert delta_item to a line."""
213
203
oldpath, newpath, file_id, entry = delta_item
214
204
if newpath is None:
216
oldpath_utf8 = b'/' + oldpath.encode('utf8')
217
newpath_utf8 = b'None'
206
oldpath_utf8 = '/' + oldpath.encode('utf8')
207
newpath_utf8 = 'None'
219
209
last_modified = NULL_REVISION
220
content = b'deleted\x00\x00'
210
content = 'deleted\x00\x00'
222
212
if oldpath is None:
223
oldpath_utf8 = b'None'
213
oldpath_utf8 = 'None'
225
oldpath_utf8 = b'/' + oldpath.encode('utf8')
215
oldpath_utf8 = '/' + oldpath.encode('utf8')
226
216
if newpath == '/':
227
217
raise AssertionError(
228
218
"Bad inventory delta: '/' is not a valid newpath "
229
219
"(should be '') in delta item %r" % (delta_item,))
230
220
# TODO: Test real-world utf8 cache hit rate. It may be a win.
231
newpath_utf8 = b'/' + newpath.encode('utf8')
221
newpath_utf8 = '/' + newpath.encode('utf8')
232
222
# Serialize None as ''
233
parent_id = entry.parent_id or b''
223
parent_id = entry.parent_id or ''
234
224
# Serialize unknown revisions as NULL_REVISION
235
225
last_modified = entry.revision
236
226
# special cases for /
237
if newpath_utf8 == b'/' and not self._versioned_root:
227
if newpath_utf8 == '/' and not self._versioned_root:
238
228
# This is an entry for the root, this inventory does not
239
229
# support versioned roots. So this must be an unversioned
240
230
# root, i.e. last_modified == new revision. Otherwise, this
244
234
# xml5 serializer.
245
235
if last_modified != new_version:
246
236
raise InventoryDeltaError(
247
'Version present for / in %(fileid)r'
248
' (%(last)r != %(new)r)',
249
fileid=file_id, last=last_modified, new=new_version)
237
'Version present for / in %s (%s != %s)'
238
% (file_id, last_modified, new_version))
250
239
if last_modified is None:
251
raise InventoryDeltaError(
252
"no version for fileid %(fileid)r", fileid=file_id)
240
raise InventoryDeltaError("no version for fileid %s" % file_id)
253
241
content = self._entry_to_content[entry.kind](entry)
254
return (b"%s\x00%s\x00%s\x00%s\x00%s\x00%s\n" %
255
(oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
242
return ("%s\x00%s\x00%s\x00%s\x00%s\x00%s\n" %
243
(oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
259
247
class InventoryDeltaDeserializer(object):
270
258
self._allow_tree_references = allow_tree_references
272
260
def _deserialize_bool(self, value):
275
elif value == b"false":
263
elif value == "false":
278
raise InventoryDeltaError("value %(val)r is not a bool", val=value)
266
raise InventoryDeltaError("value %r is not a bool" % (value,))
280
def parse_text_bytes(self, lines):
268
def parse_text_bytes(self, bytes):
281
269
"""Parse the text bytes of a serialized inventory delta.
283
271
If versioned_root and/or tree_references flags were set via
284
272
require_flags, then the parsed flags must match or a BzrError will be
287
:param lines: The lines to parse. This can be obtained by calling
275
:param bytes: The bytes to parse. This can be obtained by calling
276
delta_to_lines and then doing ''.join(delta_lines).
289
277
:return: (parent_id, new_id, versioned_root, tree_references,
293
raise InventoryDeltaError(
294
'inventory delta is empty')
295
if not lines[-1].endswith(b'\n'):
296
raise InventoryDeltaError(
297
'last line not empty: %(line)r', line=lines[-1])
298
lines = [line.rstrip(b'\n') for line in lines] # discard the last empty line
299
if not lines or lines[0] != b'format: %s' % FORMAT_1:
300
raise InventoryDeltaError(
301
'unknown format %(line)r', line=lines[0:1])
302
if len(lines) < 2 or not lines[1].startswith(b'parent: '):
280
if bytes[-1:] != '\n':
281
last_line = bytes.rsplit('\n', 1)[-1]
282
raise InventoryDeltaError('last line not empty: %r' % (last_line,))
283
lines = bytes.split('\n')[:-1] # discard the last empty line
284
if not lines or lines[0] != 'format: %s' % FORMAT_1:
285
raise InventoryDeltaError('unknown format %r' % lines[0:1])
286
if len(lines) < 2 or not lines[1].startswith('parent: '):
303
287
raise InventoryDeltaError('missing parent: marker')
304
288
delta_parent_id = lines[1][8:]
305
if len(lines) < 3 or not lines[2].startswith(b'version: '):
289
if len(lines) < 3 or not lines[2].startswith('version: '):
306
290
raise InventoryDeltaError('missing version: marker')
307
291
delta_version_id = lines[2][9:]
308
if len(lines) < 4 or not lines[3].startswith(b'versioned_root: '):
292
if len(lines) < 4 or not lines[3].startswith('versioned_root: '):
309
293
raise InventoryDeltaError('missing versioned_root: marker')
310
294
delta_versioned_root = self._deserialize_bool(lines[3][16:])
311
if len(lines) < 5 or not lines[4].startswith(b'tree_references: '):
295
if len(lines) < 5 or not lines[4].startswith('tree_references: '):
312
296
raise InventoryDeltaError('missing tree_references: marker')
313
297
delta_tree_references = self._deserialize_bool(lines[4][17:])
314
298
if (not self._allow_versioned_root and delta_versioned_root):
318
302
line_iter = iter(lines)
319
303
for i in range(5):
321
305
for line in line_iter:
322
306
(oldpath_utf8, newpath_utf8, file_id, parent_id, last_modified,
323
content) = line.split(b'\x00', 5)
307
content) = line.split('\x00', 5)
324
308
parent_id = parent_id or None
325
309
if file_id in seen_ids:
326
310
raise InventoryDeltaError(
327
"duplicate file id %(fileid)r", fileid=file_id)
311
"duplicate file id in inventory delta %r" % lines)
328
312
seen_ids.add(file_id)
329
if (newpath_utf8 == b'/' and not delta_versioned_root and
330
last_modified != delta_version_id):
331
# Delta claims to be not have a versioned root, yet here's
332
# a root entry with a non-default version.
333
raise InventoryDeltaError(
334
"Versioned root found: %(line)r", line=line)
335
elif newpath_utf8 != b'None' and last_modified[-1:] == b':':
313
if (newpath_utf8 == '/' and not delta_versioned_root and
314
last_modified != delta_version_id):
315
# Delta claims to be not have a versioned root, yet here's
316
# a root entry with a non-default version.
317
raise InventoryDeltaError("Versioned root found: %r" % line)
318
elif newpath_utf8 != 'None' and last_modified[-1] == ':':
336
319
# Deletes have a last_modified of null:, but otherwise special
337
320
# revision ids should not occur.
338
raise InventoryDeltaError(
339
'special revisionid found: %(line)r', line=line)
340
if content.startswith(b'tree\x00'):
321
raise InventoryDeltaError('special revisionid found: %r' % line)
322
if content.startswith('tree\x00'):
341
323
if delta_tree_references is False:
342
324
raise InventoryDeltaError(
343
"Tree reference found (but header said "
344
"tree_references: false): %(line)r", line=line)
325
"Tree reference found (but header said "
326
"tree_references: false): %r" % line)
345
327
elif not self._allow_tree_references:
346
328
raise IncompatibleInventoryDelta(
347
329
"Tree reference not allowed")
348
if oldpath_utf8 == b'None':
330
if oldpath_utf8 == 'None':
350
elif oldpath_utf8[:1] != b'/':
332
elif oldpath_utf8[:1] != '/':
351
333
raise InventoryDeltaError(
352
"oldpath invalid (does not start with /): %(path)r",
334
"oldpath invalid (does not start with /): %r"
355
337
oldpath_utf8 = oldpath_utf8[1:]
356
338
oldpath = oldpath_utf8.decode('utf8')
357
if newpath_utf8 == b'None':
339
if newpath_utf8 == 'None':
359
elif newpath_utf8[:1] != b'/':
341
elif newpath_utf8[:1] != '/':
360
342
raise InventoryDeltaError(
361
"newpath invalid (does not start with /): %(path)r",
343
"newpath invalid (does not start with /): %r"
364
346
# Trim leading slash
365
347
newpath_utf8 = newpath_utf8[1:]
366
348
newpath = newpath_utf8.decode('utf8')
367
content_tuple = tuple(content.split(b'\x00'))
368
if content_tuple[0] == b'deleted':
349
content_tuple = tuple(content.split('\x00'))
350
if content_tuple[0] == 'deleted':
371
353
entry = _parse_entry(