~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml5.py

  • Committer: Robert Collins
  • Date: 2007-07-04 08:08:13 UTC
  • mfrom: (2572 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2587.
  • Revision ID: robertc@robertcollins.net-20070704080813-wzebx0r88fvwj5rq
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
19
19
 
20
20
from bzrlib import (
21
21
    cache_utf8,
 
22
    errors,
22
23
    inventory,
23
24
    )
24
25
from bzrlib.xml_serializer import SubElement, Element, Serializer
28
29
 
29
30
 
30
31
_utf8_re = None
31
 
_utf8_escape_map = {
 
32
_unicode_re = None
 
33
_xml_escape_map = {
32
34
    "&":'&',
33
35
    "'":"'", # FIXME: overkill
34
36
    "\"":""",
38
40
 
39
41
 
40
42
def _ensure_utf8_re():
41
 
    """Make sure the _utf8_re regex has been compiled"""
42
 
    global _utf8_re
43
 
    if _utf8_re is not None:
44
 
        return
45
 
    _utf8_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
46
 
 
47
 
 
48
 
def _utf8_escape_replace(match, _map=_utf8_escape_map):
 
43
    """Make sure the _utf8_re and _unicode_re regexes have been compiled."""
 
44
    global _utf8_re, _unicode_re
 
45
    if _utf8_re is None:
 
46
        _utf8_re = re.compile('[&<>\'\"]|[\x80-\xff]+')
 
47
    if _unicode_re is None:
 
48
        _unicode_re = re.compile(u'[&<>\'\"\u0080-\uffff]')
 
49
 
 
50
 
 
51
def _unicode_escape_replace(match, _map=_xml_escape_map):
49
52
    """Replace a string of non-ascii, non XML safe characters with their escape
50
53
 
51
54
    This will escape both Standard XML escapes, like <>"', etc.
64
67
        return "&#%d;" % ord(match.group())
65
68
 
66
69
 
67
 
_unicode_to_escaped_map = {}
68
 
 
69
 
def _encode_and_escape(unicode_str, _map=_unicode_to_escaped_map):
 
70
def _utf8_escape_replace(match, _map=_xml_escape_map):
 
71
    """Escape utf8 characters into XML safe ones.
 
72
 
 
73
    This uses 2 tricks. It is either escaping "standard" characters, like "&<>,
 
74
    or it is handling characters with the high-bit set. For ascii characters,
 
75
    we just lookup the replacement in the dictionary. For everything else, we
 
76
    decode back into Unicode, and then use the XML escape code.
 
77
    """
 
78
    try:
 
79
        return _map[match.group()]
 
80
    except KeyError:
 
81
        return ''.join('&#%d;' % ord(uni_chr)
 
82
                       for uni_chr in match.group().decode('utf8'))
 
83
 
 
84
 
 
85
_to_escaped_map = {}
 
86
 
 
87
def _encode_and_escape(unicode_or_utf8_str, _map=_to_escaped_map):
70
88
    """Encode the string into utf8, and escape invalid XML characters"""
71
89
    # We frequently get entities we have not seen before, so it is better
72
90
    # to check if None, rather than try/KeyError
73
 
    text = _map.get(unicode_str)
 
91
    text = _map.get(unicode_or_utf8_str)
74
92
    if text is None:
75
 
        # The alternative policy is to do a regular UTF8 encoding
76
 
        # and then escape only XML meta characters.
77
 
        # Performance is equivalent once you use cache_utf8. *However*
78
 
        # this makes the serialized texts incompatible with old versions
79
 
        # of bzr. So no net gain. (Perhaps the read code would handle utf8
80
 
        # better than entity escapes, but cElementTree seems to do just fine
81
 
        # either way)
82
 
        text = str(_utf8_re.sub(_utf8_escape_replace, unicode_str)) + '"'
83
 
        _map[unicode_str] = text
 
93
        if unicode_or_utf8_str.__class__ == unicode:
 
94
            # The alternative policy is to do a regular UTF8 encoding
 
95
            # and then escape only XML meta characters.
 
96
            # Performance is equivalent once you use cache_utf8. *However*
 
97
            # this makes the serialized texts incompatible with old versions
 
98
            # of bzr. So no net gain. (Perhaps the read code would handle utf8
 
99
            # better than entity escapes, but cElementTree seems to do just fine
 
100
            # either way)
 
101
            text = str(_unicode_re.sub(_unicode_escape_replace,
 
102
                                       unicode_or_utf8_str)) + '"'
 
103
        else:
 
104
            # Plain strings are considered to already be in utf-8 so we do a
 
105
            # slightly different method for escaping.
 
106
            text = _utf8_re.sub(_utf8_escape_replace,
 
107
                                unicode_or_utf8_str) + '"'
 
108
        _map[unicode_or_utf8_str] = text
84
109
    return text
85
110
 
86
111
 
 
112
def _get_utf8_or_ascii(a_str,
 
113
                       _encode_utf8=cache_utf8.encode,
 
114
                       _get_cached_ascii=cache_utf8.get_cached_ascii):
 
115
    """Return a cached version of the string.
 
116
 
 
117
    cElementTree will return a plain string if the XML is plain ascii. It only
 
118
    returns Unicode when it needs to. We want to work in utf-8 strings. So if
 
119
    cElementTree returns a plain string, we can just return the cached version.
 
120
    If it is Unicode, then we need to encode it.
 
121
 
 
122
    :param a_str: An 8-bit string or Unicode as returned by
 
123
                  cElementTree.Element.get()
 
124
    :return: A utf-8 encoded 8-bit string.
 
125
    """
 
126
    # This is fairly optimized because we know what cElementTree does, this is
 
127
    # not meant as a generic function for all cases. Because it is possible for
 
128
    # an 8-bit string to not be ascii or valid utf8.
 
129
    if a_str.__class__ == unicode:
 
130
        return _encode_utf8(a_str)
 
131
    else:
 
132
        return _get_cached_ascii(a_str)
 
133
 
 
134
 
87
135
def _clear_cache():
88
136
    """Clean out the unicode => escaped map"""
89
 
    _unicode_to_escaped_map.clear()
 
137
    _to_escaped_map.clear()
90
138
 
91
139
 
92
140
class Serializer_v5(Serializer):
101
149
    # This format supports the altered-by hack that reads file ids directly out
102
150
    # of the versionedfile, without doing XML parsing.
103
151
 
 
152
    supported_kinds = set(['file', 'directory', 'symlink'])
 
153
 
104
154
    def write_inventory_to_string(self, inv):
105
155
        """Just call write_inventory with a StringIO and return the value"""
106
156
        sio = cStringIO.StringIO()
143
193
    def _append_entry(self, append, ie):
144
194
        """Convert InventoryEntry to XML element and append to output."""
145
195
        # TODO: should just be a plain assertion
146
 
        assert InventoryEntry.versionable_kind(ie.kind), \
147
 
            'unsupported entry kind %s' % ie.kind
 
196
        if ie.kind not in self.supported_kinds:
 
197
            raise errors.UnsupportedInventoryKind(ie.kind)
148
198
 
149
199
        append("<")
150
200
        append(ie.kind)
170
220
            append('"')
171
221
        if ie.text_size is not None:
172
222
            append(' text_size="%d"' % ie.text_size)
 
223
        if getattr(ie, 'reference_revision', None) is not None:
 
224
            append(' reference_revision="')
 
225
            append(_encode_and_escape(ie.reference_revision))
173
226
        append(" />\n")
174
227
        return
175
228
 
178
231
 
179
232
    def _pack_revision(self, rev):
180
233
        """Revision object -> xml tree"""
 
234
        # For the XML format, we need to write them as Unicode rather than as
 
235
        # utf-8 strings. So that cElementTree can handle properly escaping
 
236
        # them.
 
237
        decode_utf8 = cache_utf8.decode
 
238
        revision_id = rev.revision_id
 
239
        if isinstance(revision_id, str):
 
240
            revision_id = decode_utf8(revision_id)
181
241
        root = Element('revision',
182
242
                       committer = rev.committer,
183
 
                       timestamp = '%.9f' % rev.timestamp,
184
 
                       revision_id = rev.revision_id,
 
243
                       timestamp = '%.3f' % rev.timestamp,
 
244
                       revision_id = revision_id,
185
245
                       inventory_sha1 = rev.inventory_sha1,
186
246
                       format='5',
187
247
                       )
198
258
                assert isinstance(parent_id, basestring)
199
259
                p = SubElement(pelts, 'revision_ref')
200
260
                p.tail = '\n'
 
261
                if isinstance(parent_id, str):
 
262
                    parent_id = decode_utf8(parent_id)
201
263
                p.set('revision_id', parent_id)
202
264
        if rev.properties:
203
265
            self._pack_revision_properties(rev, root)
219
281
        """
220
282
        assert elt.tag == 'inventory'
221
283
        root_id = elt.get('file_id') or ROOT_ID
 
284
        root_id = _get_utf8_or_ascii(root_id)
 
285
 
222
286
        format = elt.get('format')
223
287
        if format is not None:
224
288
            if format != '5':
226
290
                                % format)
227
291
        revision_id = elt.get('revision_id')
228
292
        if revision_id is not None:
229
 
            revision_id = cache_utf8.get_cached_unicode(revision_id)
 
293
            revision_id = cache_utf8.encode(revision_id)
230
294
        inv = Inventory(root_id, revision_id=revision_id)
231
295
        for e in elt:
232
296
            ie = self._unpack_entry(e)
233
 
            if ie.parent_id == ROOT_ID:
 
297
            if ie.parent_id is None:
234
298
                ie.parent_id = root_id
235
299
            inv.add(ie)
236
300
        return inv
237
301
 
238
 
    def _unpack_entry(self, elt, none_parents=False):
 
302
    def _unpack_entry(self, elt):
239
303
        kind = elt.tag
240
304
        if not InventoryEntry.versionable_kind(kind):
241
305
            raise AssertionError('unsupported entry kind %s' % kind)
242
306
 
243
 
        get_cached = cache_utf8.get_cached_unicode
 
307
        get_cached = _get_utf8_or_ascii
244
308
 
245
309
        parent_id = elt.get('parent_id')
246
 
        if parent_id is None and not none_parents:
247
 
            parent_id = ROOT_ID
248
 
        # TODO: jam 20060817 At present, caching file ids costs us too 
249
 
        #       much time. It slows down overall read performances from
250
 
        #       approx 500ms to 700ms. And doesn't improve future reads.
251
 
        #       it might be because revision ids and file ids are mixing.
252
 
        #       Consider caching *just* the file ids, for a limited period
253
 
        #       of time.
254
 
        #parent_id = get_cached(parent_id)
255
 
        #file_id = get_cached(elt.get('file_id'))
256
 
        file_id = elt.get('file_id')
 
310
        if parent_id is not None:
 
311
            parent_id = get_cached(parent_id)
 
312
        file_id = get_cached(elt.get('file_id'))
257
313
 
258
314
        if kind == 'directory':
259
315
            ie = inventory.InventoryDirectory(file_id,
274
330
                                         parent_id)
275
331
            ie.symlink_target = elt.get('symlink_target')
276
332
        else:
277
 
            raise BzrError("unknown kind %r" % kind)
 
333
            raise errors.UnsupportedInventoryKind(kind)
278
334
        revision = elt.get('revision')
279
335
        if revision is not None:
280
336
            revision = get_cached(revision)
290
346
            if format != '5':
291
347
                raise BzrError("invalid format version %r on inventory"
292
348
                                % format)
293
 
        get_cached = cache_utf8.get_cached_unicode
 
349
        get_cached = _get_utf8_or_ascii
294
350
        rev = Revision(committer = elt.get('committer'),
295
351
                       timestamp = float(elt.get('timestamp')),
296
352
                       revision_id = get_cached(elt.get('revision_id')),