~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml8.py

  • Committer: Jonathan Riddell
  • Date: 2011-05-16 11:27:37 UTC
  • mto: This revision was merged to the branch mainline in revision 5869.
  • Revision ID: jriddell@canonical.com-20110516112737-gep642p24rtzp3jt
userĀ guideĀ licence

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
 
 
19
17
import cStringIO
 
18
import re
20
19
 
21
20
from bzrlib import (
22
21
    cache_utf8,
 
22
    errors,
 
23
    inventory,
23
24
    lazy_regex,
24
25
    revision as _mod_revision,
25
26
    trace,
28
29
    Element,
29
30
    SubElement,
30
31
    XMLSerializer,
31
 
    encode_and_escape,
32
32
    escape_invalid_chars,
33
 
    get_utf8_or_ascii,
34
 
    serialize_inventory_flat,
35
 
    unpack_inventory_entry,
36
 
    unpack_inventory_flat,
37
33
    )
 
34
from bzrlib.inventory import InventoryEntry
38
35
from bzrlib.revision import Revision
39
36
from bzrlib.errors import BzrError
40
37
 
41
38
 
 
39
_utf8_re = None
 
40
_unicode_re = None
 
41
_xml_escape_map = {
 
42
    "&":'&',
 
43
    "'":"'", # FIXME: overkill
 
44
    "\"":""",
 
45
    "<":"&lt;",
 
46
    ">":"&gt;",
 
47
    }
 
48
 
42
49
_xml_unescape_map = {
43
50
    'apos':"'",
44
51
    'quot':'"',
58
65
        return unichr(int(code[1:])).encode('utf8')
59
66
 
60
67
 
61
 
_unescape_re = lazy_regex.lazy_compile('\&([^;]*);')
 
68
_unescape_re = None
 
69
 
62
70
 
63
71
def _unescape_xml(data):
64
72
    """Unescape predefined XML entities in a string of data."""
 
73
    global _unescape_re
 
74
    if _unescape_re is None:
 
75
        _unescape_re = re.compile('\&([^;]*);')
65
76
    return _unescape_re.sub(_unescaper, data)
66
77
 
67
78
 
 
79
def _ensure_utf8_re():
 
80
    """Make sure the _utf8_re and _unicode_re regexes have been compiled."""
 
81
    global _utf8_re, _unicode_re
 
82
    if _utf8_re is None:
 
83
        _utf8_re = re.compile('[&<>\'\"]|[\x80-\xff]+')
 
84
    if _unicode_re is None:
 
85
        _unicode_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
 
86
 
 
87
 
 
88
def _unicode_escape_replace(match, _map=_xml_escape_map):
 
89
    """Replace a string of non-ascii, non XML safe characters with their escape
 
90
 
 
91
    This will escape both Standard XML escapes, like <>"', etc.
 
92
    As well as escaping non ascii characters, because ElementTree did.
 
93
    This helps us remain compatible to older versions of bzr. We may change
 
94
    our policy in the future, though.
 
95
    """
 
96
    # jam 20060816 Benchmarks show that try/KeyError is faster if you
 
97
    # expect the entity to rarely miss. There is about a 10% difference
 
98
    # in overall time. But if you miss frequently, then if None is much
 
99
    # faster. For our use case, we *rarely* have a revision id, file id
 
100
    # or path name that is unicode. So use try/KeyError.
 
101
    try:
 
102
        return _map[match.group()]
 
103
    except KeyError:
 
104
        return "&#%d;" % ord(match.group())
 
105
 
 
106
 
 
107
def _utf8_escape_replace(match, _map=_xml_escape_map):
 
108
    """Escape utf8 characters into XML safe ones.
 
109
 
 
110
    This uses 2 tricks. It is either escaping "standard" characters, like "&<>,
 
111
    or it is handling characters with the high-bit set. For ascii characters,
 
112
    we just lookup the replacement in the dictionary. For everything else, we
 
113
    decode back into Unicode, and then use the XML escape code.
 
114
    """
 
115
    try:
 
116
        return _map[match.group()]
 
117
    except KeyError:
 
118
        return ''.join('&#%d;' % ord(uni_chr)
 
119
                       for uni_chr in match.group().decode('utf8'))
 
120
 
 
121
 
 
122
_to_escaped_map = {}
 
123
 
 
124
def _encode_and_escape(unicode_or_utf8_str, _map=_to_escaped_map):
 
125
    """Encode the string into utf8, and escape invalid XML characters"""
 
126
    # We frequently get entities we have not seen before, so it is better
 
127
    # to check if None, rather than try/KeyError
 
128
    text = _map.get(unicode_or_utf8_str)
 
129
    if text is None:
 
130
        if unicode_or_utf8_str.__class__ is unicode:
 
131
            # The alternative policy is to do a regular UTF8 encoding
 
132
            # and then escape only XML meta characters.
 
133
            # Performance is equivalent once you use cache_utf8. *However*
 
134
            # this makes the serialized texts incompatible with old versions
 
135
            # of bzr. So no net gain. (Perhaps the read code would handle utf8
 
136
            # better than entity escapes, but cElementTree seems to do just fine
 
137
            # either way)
 
138
            text = str(_unicode_re.sub(_unicode_escape_replace,
 
139
                                       unicode_or_utf8_str)) + '"'
 
140
        else:
 
141
            # Plain strings are considered to already be in utf-8 so we do a
 
142
            # slightly different method for escaping.
 
143
            text = _utf8_re.sub(_utf8_escape_replace,
 
144
                                unicode_or_utf8_str) + '"'
 
145
        _map[unicode_or_utf8_str] = text
 
146
    return text
 
147
 
 
148
 
 
149
def _get_utf8_or_ascii(a_str,
 
150
                       _encode_utf8=cache_utf8.encode,
 
151
                       _get_cached_ascii=cache_utf8.get_cached_ascii):
 
152
    """Return a cached version of the string.
 
153
 
 
154
    cElementTree will return a plain string if the XML is plain ascii. It only
 
155
    returns Unicode when it needs to. We want to work in utf-8 strings. So if
 
156
    cElementTree returns a plain string, we can just return the cached version.
 
157
    If it is Unicode, then we need to encode it.
 
158
 
 
159
    :param a_str: An 8-bit string or Unicode as returned by
 
160
                  cElementTree.Element.get()
 
161
    :return: A utf-8 encoded 8-bit string.
 
162
    """
 
163
    # This is fairly optimized because we know what cElementTree does, this is
 
164
    # not meant as a generic function for all cases. Because it is possible for
 
165
    # an 8-bit string to not be ascii or valid utf8.
 
166
    if a_str.__class__ is unicode:
 
167
        return _encode_utf8(a_str)
 
168
    else:
 
169
        return intern(a_str)
 
170
 
 
171
 
 
172
def _clear_cache():
 
173
    """Clean out the unicode => escaped map"""
 
174
    _to_escaped_map.clear()
 
175
 
 
176
 
68
177
class Serializer_v8(XMLSerializer):
69
178
    """This serialiser adds rich roots.
70
179
 
152
261
            reference_revision, symlink_target.
153
262
        :return: The inventory as a list of lines.
154
263
        """
 
264
        _ensure_utf8_re()
 
265
        self._check_revisions(inv)
155
266
        output = []
156
267
        append = output.append
157
268
        self._append_inventory_root(append, inv)
158
 
        serialize_inventory_flat(inv, append,
159
 
            self.root_id, self.supported_kinds, working)
 
269
        entries = inv.iter_entries()
 
270
        # Skip the root
 
271
        root_path, root_ie = entries.next()
 
272
        for path, ie in entries:
 
273
            if ie.parent_id != self.root_id:
 
274
                parent_str = ' parent_id="'
 
275
                parent_id  = _encode_and_escape(ie.parent_id)
 
276
            else:
 
277
                parent_str = ''
 
278
                parent_id  = ''
 
279
            if ie.kind == 'file':
 
280
                if ie.executable:
 
281
                    executable = ' executable="yes"'
 
282
                else:
 
283
                    executable = ''
 
284
                if not working:
 
285
                    append('<file%s file_id="%s name="%s%s%s revision="%s '
 
286
                        'text_sha1="%s" text_size="%d" />\n' % (
 
287
                        executable, _encode_and_escape(ie.file_id),
 
288
                        _encode_and_escape(ie.name), parent_str, parent_id,
 
289
                        _encode_and_escape(ie.revision), ie.text_sha1,
 
290
                        ie.text_size))
 
291
                else:
 
292
                    append('<file%s file_id="%s name="%s%s%s />\n' % (
 
293
                        executable, _encode_and_escape(ie.file_id),
 
294
                        _encode_and_escape(ie.name), parent_str, parent_id))
 
295
            elif ie.kind == 'directory':
 
296
                if not working:
 
297
                    append('<directory file_id="%s name="%s%s%s revision="%s '
 
298
                        '/>\n' % (
 
299
                        _encode_and_escape(ie.file_id),
 
300
                        _encode_and_escape(ie.name),
 
301
                        parent_str, parent_id,
 
302
                        _encode_and_escape(ie.revision)))
 
303
                else:
 
304
                    append('<directory file_id="%s name="%s%s%s />\n' % (
 
305
                        _encode_and_escape(ie.file_id),
 
306
                        _encode_and_escape(ie.name),
 
307
                        parent_str, parent_id))
 
308
            elif ie.kind == 'symlink':
 
309
                if not working:
 
310
                    append('<symlink file_id="%s name="%s%s%s revision="%s '
 
311
                        'symlink_target="%s />\n' % (
 
312
                        _encode_and_escape(ie.file_id),
 
313
                        _encode_and_escape(ie.name),
 
314
                        parent_str, parent_id,
 
315
                        _encode_and_escape(ie.revision),
 
316
                        _encode_and_escape(ie.symlink_target)))
 
317
                else:
 
318
                    append('<symlink file_id="%s name="%s%s%s />\n' % (
 
319
                        _encode_and_escape(ie.file_id),
 
320
                        _encode_and_escape(ie.name),
 
321
                        parent_str, parent_id))
 
322
            elif ie.kind == 'tree-reference':
 
323
                if ie.kind not in self.supported_kinds:
 
324
                    raise errors.UnsupportedInventoryKind(ie.kind)
 
325
                if not working:
 
326
                    append('<tree-reference file_id="%s name="%s%s%s '
 
327
                        'revision="%s reference_revision="%s />\n' % (
 
328
                        _encode_and_escape(ie.file_id),
 
329
                        _encode_and_escape(ie.name),
 
330
                        parent_str, parent_id,
 
331
                        _encode_and_escape(ie.revision),
 
332
                        _encode_and_escape(ie.reference_revision)))
 
333
                else:
 
334
                    append('<tree-reference file_id="%s name="%s%s%s />\n' % (
 
335
                        _encode_and_escape(ie.file_id),
 
336
                        _encode_and_escape(ie.name),
 
337
                        parent_str, parent_id))
 
338
            else:
 
339
                raise errors.UnsupportedInventoryKind(ie.kind)
 
340
        append('</inventory>\n')
160
341
        if f is not None:
161
342
            f.writelines(output)
162
343
        # Just to keep the cache from growing without bounds
168
349
        """Append the inventory root to output."""
169
350
        if inv.revision_id is not None:
170
351
            revid1 = ' revision_id="'
171
 
            revid2 = encode_and_escape(inv.revision_id)
 
352
            revid2 = _encode_and_escape(inv.revision_id)
172
353
        else:
173
354
            revid1 = ""
174
355
            revid2 = ""
175
356
        append('<inventory format="%s"%s%s>\n' % (
176
357
            self.format_num, revid1, revid2))
177
358
        append('<directory file_id="%s name="%s revision="%s />\n' % (
178
 
            encode_and_escape(inv.root.file_id),
179
 
            encode_and_escape(inv.root.name),
180
 
            encode_and_escape(inv.root.revision)))
 
359
            _encode_and_escape(inv.root.file_id),
 
360
            _encode_and_escape(inv.root.name),
 
361
            _encode_and_escape(inv.root.revision)))
181
362
 
182
363
    def _pack_revision(self, rev):
183
364
        """Revision object -> xml tree"""
227
408
            prop_elt.tail = '\n'
228
409
        top_elt.tail = '\n'
229
410
 
230
 
    def _unpack_entry(self, elt, entry_cache=None, return_from_cache=False):
231
 
        # This is here because it's overridden by xml7
232
 
        return unpack_inventory_entry(elt, entry_cache,
233
 
                return_from_cache)
234
 
 
235
411
    def _unpack_inventory(self, elt, revision_id=None, entry_cache=None,
236
412
                          return_from_cache=False):
237
413
        """Construct from XML Element"""
238
 
        inv = unpack_inventory_flat(elt, self.format_num, self._unpack_entry,
239
 
            entry_cache, return_from_cache)
 
414
        if elt.tag != 'inventory':
 
415
            raise errors.UnexpectedInventoryFormat('Root tag is %r' % elt.tag)
 
416
        format = elt.get('format')
 
417
        if format != self.format_num:
 
418
            raise errors.UnexpectedInventoryFormat('Invalid format version %r'
 
419
                                                   % format)
 
420
        revision_id = elt.get('revision_id')
 
421
        if revision_id is not None:
 
422
            revision_id = cache_utf8.encode(revision_id)
 
423
        inv = inventory.Inventory(root_id=None, revision_id=revision_id)
 
424
        for e in elt:
 
425
            ie = self._unpack_entry(e, entry_cache=entry_cache,
 
426
                                    return_from_cache=return_from_cache)
 
427
            inv.add(ie)
240
428
        self._check_cache_size(len(inv), entry_cache)
241
429
        return inv
242
430
 
 
431
    def _unpack_entry(self, elt, entry_cache=None, return_from_cache=False):
 
432
        elt_get = elt.get
 
433
        file_id = elt_get('file_id')
 
434
        revision = elt_get('revision')
 
435
        # Check and see if we have already unpacked this exact entry
 
436
        # Some timings for "repo.revision_trees(last_100_revs)"
 
437
        #               bzr     mysql
 
438
        #   unmodified  4.1s    40.8s
 
439
        #   using lru   3.5s
 
440
        #   using fifo  2.83s   29.1s
 
441
        #   lru._cache  2.8s
 
442
        #   dict        2.75s   26.8s
 
443
        #   inv.add     2.5s    26.0s
 
444
        #   no_copy     2.00s   20.5s
 
445
        #   no_c,dict   1.95s   18.0s
 
446
        # Note that a cache of 10k nodes is more than sufficient to hold all of
 
447
        # the inventory for the last 100 revs for bzr, but not for mysql (20k
 
448
        # is enough for mysql, which saves the same 2s as using a dict)
 
449
 
 
450
        # Breakdown of mysql using time.clock()
 
451
        #   4.1s    2 calls to element.get for file_id, revision_id
 
452
        #   4.5s    cache_hit lookup
 
453
        #   7.1s    InventoryFile.copy()
 
454
        #   2.4s    InventoryDirectory.copy()
 
455
        #   0.4s    decoding unique entries
 
456
        #   1.6s    decoding entries after FIFO fills up
 
457
        #   0.8s    Adding nodes to FIFO (including flushes)
 
458
        #   0.1s    cache miss lookups
 
459
        # Using an LRU cache
 
460
        #   4.1s    2 calls to element.get for file_id, revision_id
 
461
        #   9.9s    cache_hit lookup
 
462
        #   10.8s   InventoryEntry.copy()
 
463
        #   0.3s    cache miss lookus
 
464
        #   1.2s    decoding entries
 
465
        #   1.0s    adding nodes to LRU
 
466
        if entry_cache is not None and revision is not None:
 
467
            key = (file_id, revision)
 
468
            try:
 
469
                # We copy it, because some operations may mutate it
 
470
                cached_ie = entry_cache[key]
 
471
            except KeyError:
 
472
                pass
 
473
            else:
 
474
                # Only copying directory entries drops us 2.85s => 2.35s
 
475
                if return_from_cache:
 
476
                    if cached_ie.kind == 'directory':
 
477
                        return cached_ie.copy()
 
478
                    return cached_ie
 
479
                return cached_ie.copy()
 
480
 
 
481
        kind = elt.tag
 
482
        if not InventoryEntry.versionable_kind(kind):
 
483
            raise AssertionError('unsupported entry kind %s' % kind)
 
484
 
 
485
        get_cached = _get_utf8_or_ascii
 
486
 
 
487
        file_id = get_cached(file_id)
 
488
        if revision is not None:
 
489
            revision = get_cached(revision)
 
490
        parent_id = elt_get('parent_id')
 
491
        if parent_id is not None:
 
492
            parent_id = get_cached(parent_id)
 
493
 
 
494
        if kind == 'directory':
 
495
            ie = inventory.InventoryDirectory(file_id,
 
496
                                              elt_get('name'),
 
497
                                              parent_id)
 
498
        elif kind == 'file':
 
499
            ie = inventory.InventoryFile(file_id,
 
500
                                         elt_get('name'),
 
501
                                         parent_id)
 
502
            ie.text_sha1 = elt_get('text_sha1')
 
503
            if elt_get('executable') == 'yes':
 
504
                ie.executable = True
 
505
            v = elt_get('text_size')
 
506
            ie.text_size = v and int(v)
 
507
        elif kind == 'symlink':
 
508
            ie = inventory.InventoryLink(file_id,
 
509
                                         elt_get('name'),
 
510
                                         parent_id)
 
511
            ie.symlink_target = elt_get('symlink_target')
 
512
        else:
 
513
            raise errors.UnsupportedInventoryKind(kind)
 
514
        ie.revision = revision
 
515
        if revision is not None and entry_cache is not None:
 
516
            # We cache a copy() because callers like to mutate objects, and
 
517
            # that would cause the item in cache to mutate as well.
 
518
            # This has a small effect on many-inventory performance, because
 
519
            # the majority fraction is spent in cache hits, not misses.
 
520
            entry_cache[key] = ie.copy()
 
521
 
 
522
        return ie
 
523
 
243
524
    def _unpack_revision(self, elt):
244
525
        """XML Element -> Revision object"""
245
526
        format = elt.get('format')
250
531
            if format != format_num:
251
532
                raise BzrError("invalid format version %r on revision"
252
533
                                % format)
253
 
        get_cached = get_utf8_or_ascii
 
534
        get_cached = _get_utf8_or_ascii
254
535
        rev = Revision(committer = elt.get('committer'),
255
536
                       timestamp = float(elt.get('timestamp')),
256
537
                       revision_id = get_cached(elt.get('revision_id')),