~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml8.py

  • Committer: Jelmer Vernooij
  • Date: 2012-02-01 19:18:09 UTC
  • mfrom: (6459 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6460.
  • Revision ID: jelmer@samba.org-20120201191809-xn340a5i5v4fqsfu
Merge bzr.dev.

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
 
17
19
import cStringIO
18
 
import re
19
20
 
20
21
from bzrlib import (
21
22
    cache_utf8,
22
 
    errors,
23
 
    inventory,
24
23
    lazy_regex,
25
24
    revision as _mod_revision,
26
25
    trace,
29
28
    Element,
30
29
    SubElement,
31
30
    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,
33
37
    )
34
 
from bzrlib.inventory import InventoryEntry
35
38
from bzrlib.revision import Revision
36
39
from bzrlib.errors import BzrError
37
40
 
38
41
 
39
 
_utf8_re = None
40
 
_unicode_re = None
41
 
_xml_escape_map = {
42
 
    "&":'&',
43
 
    "'":"'", # FIXME: overkill
44
 
    "\"":""",
45
 
    "<":"&lt;",
46
 
    ">":"&gt;",
47
 
    }
48
 
 
49
42
_xml_unescape_map = {
50
43
    'apos':"'",
51
44
    'quot':'"',
65
58
        return unichr(int(code[1:])).encode('utf8')
66
59
 
67
60
 
68
 
_unescape_re = None
69
 
 
 
61
_unescape_re = lazy_regex.lazy_compile('\&([^;]*);')
70
62
 
71
63
def _unescape_xml(data):
72
64
    """Unescape predefined XML entities in a string of data."""
73
 
    global _unescape_re
74
 
    if _unescape_re is None:
75
 
        _unescape_re = re.compile('\&([^;]*);')
76
65
    return _unescape_re.sub(_unescaper, data)
77
66
 
78
67
 
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
 
 
177
68
class Serializer_v8(XMLSerializer):
178
69
    """This serialiser adds rich roots.
179
70
 
261
152
            reference_revision, symlink_target.
262
153
        :return: The inventory as a list of lines.
263
154
        """
264
 
        _ensure_utf8_re()
265
 
        self._check_revisions(inv)
266
155
        output = []
267
156
        append = output.append
268
157
        self._append_inventory_root(append, inv)
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')
 
158
        serialize_inventory_flat(inv, append,
 
159
            self.root_id, self.supported_kinds, working)
341
160
        if f is not None:
342
161
            f.writelines(output)
343
162
        # Just to keep the cache from growing without bounds
349
168
        """Append the inventory root to output."""
350
169
        if inv.revision_id is not None:
351
170
            revid1 = ' revision_id="'
352
 
            revid2 = _encode_and_escape(inv.revision_id)
 
171
            revid2 = encode_and_escape(inv.revision_id)
353
172
        else:
354
173
            revid1 = ""
355
174
            revid2 = ""
356
175
        append('<inventory format="%s"%s%s>\n' % (
357
176
            self.format_num, revid1, revid2))
358
177
        append('<directory file_id="%s name="%s revision="%s />\n' % (
359
 
            _encode_and_escape(inv.root.file_id),
360
 
            _encode_and_escape(inv.root.name),
361
 
            _encode_and_escape(inv.root.revision)))
 
178
            encode_and_escape(inv.root.file_id),
 
179
            encode_and_escape(inv.root.name),
 
180
            encode_and_escape(inv.root.revision)))
362
181
 
363
182
    def _pack_revision(self, rev):
364
183
        """Revision object -> xml tree"""
408
227
            prop_elt.tail = '\n'
409
228
        top_elt.tail = '\n'
410
229
 
 
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
 
411
235
    def _unpack_inventory(self, elt, revision_id=None, entry_cache=None,
412
236
                          return_from_cache=False):
413
237
        """Construct from XML Element"""
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)
 
238
        inv = unpack_inventory_flat(elt, self.format_num, self._unpack_entry,
 
239
            entry_cache, return_from_cache)
428
240
        self._check_cache_size(len(inv), entry_cache)
429
241
        return inv
430
242
 
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
 
 
524
243
    def _unpack_revision(self, elt):
525
244
        """XML Element -> Revision object"""
526
245
        format = elt.get('format')
531
250
            if format != format_num:
532
251
                raise BzrError("invalid format version %r on revision"
533
252
                                % format)
534
 
        get_cached = _get_utf8_or_ascii
 
253
        get_cached = get_utf8_or_ascii
535
254
        rev = Revision(committer = elt.get('committer'),
536
255
                       timestamp = float(elt.get('timestamp')),
537
256
                       revision_id = get_cached(elt.get('revision_id')),