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