~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/xml5.py

  • Committer: John Arbash Meinel
  • Date: 2006-10-24 14:12:53 UTC
  • mto: This revision was merged to the branch mainline in revision 2095.
  • Revision ID: john@arbash-meinel.com-20061024141253-783fba812b197b70
(John Arbash Meinel) Update version information for 0.13 development

Show diffs side-by-side

added added

removed removed

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