/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/xml5.py

  • Committer: John Arbash Meinel
  • Date: 2007-02-09 18:11:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2294.
  • Revision ID: john@arbash-meinel.com-20070209181144-3cxnt3e4jre3e317
Update WorkingTree to use safe_revision_id when appropriate

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import cStringIO
 
18
import re
 
19
 
 
20
from bzrlib import (
 
21
    cache_utf8,
 
22
    inventory,
 
23
    )
 
24
from bzrlib.xml_serializer import SubElement, Element, Serializer
 
25
from bzrlib.inventory import ROOT_ID, Inventory, InventoryEntry
 
26
from bzrlib.revision import Revision
 
27
from bzrlib.errors import BzrError
 
28
 
 
29
 
 
30
_utf8_re = None
 
31
_utf8_escape_map = {
 
32
    "&":'&',
 
33
    "'":"'", # FIXME: overkill
 
34
    "\"":""",
 
35
    "<":"&lt;",
 
36
    ">":"&gt;",
 
37
    }
 
38
 
 
39
 
 
40
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):
 
49
    """Replace a string of non-ascii, non XML safe characters with their escape
 
50
 
 
51
    This will escape both Standard XML escapes, like <>"', etc.
 
52
    As well as escaping non ascii characters, because ElementTree did.
 
53
    This helps us remain compatible to older versions of bzr. We may change
 
54
    our policy in the future, though.
 
55
    """
 
56
    # jam 20060816 Benchmarks show that try/KeyError is faster if you
 
57
    # expect the entity to rarely miss. There is about a 10% difference
 
58
    # in overall time. But if you miss frequently, then if None is much
 
59
    # faster. For our use case, we *rarely* have a revision id, file id
 
60
    # or path name that is unicode. So use try/KeyError.
 
61
    try:
 
62
        return _map[match.group()]
 
63
    except KeyError:
 
64
        return "&#%d;" % ord(match.group())
 
65
 
 
66
 
 
67
_unicode_to_escaped_map = {}
 
68
 
 
69
def _encode_and_escape(unicode_str, _map=_unicode_to_escaped_map):
 
70
    """Encode the string into utf8, and escape invalid XML characters"""
 
71
    # We frequently get entities we have not seen before, so it is better
 
72
    # to check if None, rather than try/KeyError
 
73
    text = _map.get(unicode_str)
 
74
    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
 
84
    return text
 
85
 
 
86
 
 
87
def _get_utf8_or_ascii(a_str,
 
88
                       _encode_utf8=cache_utf8.encode,
 
89
                       _get_cached_ascii=cache_utf8.get_cached_ascii):
 
90
    """Return a cached version of the string.
 
91
 
 
92
    cElementTree will return a plain string if the XML is plain ascii. It only
 
93
    returns Unicode when it needs to. We want to work in utf-8 strings. So if
 
94
    cElementTree returns a plain string, we can just return the cached version.
 
95
    If it is Unicode, then we need to encode it.
 
96
 
 
97
    :param a_str: An 8-bit string or Unicode as returned by
 
98
                  cElementTree.Element.get()
 
99
    :return: A utf-8 encoded 8-bit string.
 
100
    """
 
101
    # This is fairly optimized because we know what cElementTree does, this is
 
102
    # not meant as a generic function for all cases. Because it is possible for
 
103
    # an 8-bit string to not be ascii or valid utf8.
 
104
    if a_str.__class__ == unicode:
 
105
        return _encode_utf8(a_str)
 
106
    else:
 
107
        return _get_cached_ascii(a_str)
 
108
 
 
109
 
 
110
def _clear_cache():
 
111
    """Clean out the unicode => escaped map"""
 
112
    _unicode_to_escaped_map.clear()
 
113
 
 
114
 
 
115
class Serializer_v5(Serializer):
 
116
    """Version 5 serializer
 
117
 
 
118
    Packs objects into XML and vice versa.
 
119
    """
 
120
    
 
121
    __slots__ = []
 
122
 
 
123
    support_altered_by_hack = True
 
124
    # This format supports the altered-by hack that reads file ids directly out
 
125
    # of the versionedfile, without doing XML parsing.
 
126
 
 
127
    def write_inventory_to_string(self, inv):
 
128
        """Just call write_inventory with a StringIO and return the value"""
 
129
        sio = cStringIO.StringIO()
 
130
        self.write_inventory(inv, sio)
 
131
        return sio.getvalue()
 
132
 
 
133
    def write_inventory(self, inv, f):
 
134
        """Write inventory to a file.
 
135
        
 
136
        :param inv: the inventory to write.
 
137
        :param f: the file to write.
 
138
        """
 
139
        _ensure_utf8_re()
 
140
        output = []
 
141
        append = output.append
 
142
        self._append_inventory_root(append, inv)
 
143
        entries = inv.iter_entries()
 
144
        # Skip the root
 
145
        root_path, root_ie = entries.next()
 
146
        for path, ie in entries:
 
147
            self._append_entry(append, ie)
 
148
        append('</inventory>\n')
 
149
        f.writelines(output)
 
150
        # Just to keep the cache from growing without bounds
 
151
        # but we may actually not want to do clear the cache
 
152
        #_clear_cache()
 
153
 
 
154
    def _append_inventory_root(self, append, inv):
 
155
        """Append the inventory root to output."""
 
156
        append('<inventory')
 
157
        if inv.root.file_id not in (None, ROOT_ID):
 
158
            append(' file_id="')
 
159
            append(_encode_and_escape(inv.root.file_id))
 
160
        append(' format="5"')
 
161
        if inv.revision_id is not None:
 
162
            append(' revision_id="')
 
163
            append(_encode_and_escape(inv.revision_id))
 
164
        append('>\n')
 
165
        
 
166
    def _append_entry(self, append, ie):
 
167
        """Convert InventoryEntry to XML element and append to output."""
 
168
        # TODO: should just be a plain assertion
 
169
        assert InventoryEntry.versionable_kind(ie.kind), \
 
170
            'unsupported entry kind %s' % ie.kind
 
171
 
 
172
        append("<")
 
173
        append(ie.kind)
 
174
        if ie.executable:
 
175
            append(' executable="yes"')
 
176
        append(' file_id="')
 
177
        append(_encode_and_escape(ie.file_id))
 
178
        append(' name="')
 
179
        append(_encode_and_escape(ie.name))
 
180
        if self._parent_condition(ie):
 
181
            assert isinstance(ie.parent_id, basestring)
 
182
            append(' parent_id="')
 
183
            append(_encode_and_escape(ie.parent_id))
 
184
        if ie.revision is not None:
 
185
            append(' revision="')
 
186
            append(_encode_and_escape(ie.revision))
 
187
        if ie.symlink_target is not None:
 
188
            append(' symlink_target="')
 
189
            append(_encode_and_escape(ie.symlink_target))
 
190
        if ie.text_sha1 is not None:
 
191
            append(' text_sha1="')
 
192
            append(ie.text_sha1)
 
193
            append('"')
 
194
        if ie.text_size is not None:
 
195
            append(' text_size="%d"' % ie.text_size)
 
196
        append(" />\n")
 
197
        return
 
198
 
 
199
    def _parent_condition(self, ie):
 
200
        return ie.parent_id != ROOT_ID
 
201
 
 
202
    def _pack_revision(self, rev):
 
203
        """Revision object -> xml tree"""
 
204
        # For the XML format, we need to write them as Unicode rather than as
 
205
        # utf-8 strings. So that cElementTree can handle properly escaping
 
206
        # them.
 
207
        decode_utf8 = cache_utf8.decode
 
208
        revision_id = rev.revision_id
 
209
        if isinstance(revision_id, str):
 
210
            revision_id = decode_utf8(revision_id)
 
211
        root = Element('revision',
 
212
                       committer = rev.committer,
 
213
                       timestamp = '%.3f' % rev.timestamp,
 
214
                       revision_id = revision_id,
 
215
                       inventory_sha1 = rev.inventory_sha1,
 
216
                       format='5',
 
217
                       )
 
218
        if rev.timezone is not None:
 
219
            root.set('timezone', str(rev.timezone))
 
220
        root.text = '\n'
 
221
        msg = SubElement(root, 'message')
 
222
        msg.text = rev.message
 
223
        msg.tail = '\n'
 
224
        if rev.parent_ids:
 
225
            pelts = SubElement(root, 'parents')
 
226
            pelts.tail = pelts.text = '\n'
 
227
            for parent_id in rev.parent_ids:
 
228
                assert isinstance(parent_id, basestring)
 
229
                p = SubElement(pelts, 'revision_ref')
 
230
                p.tail = '\n'
 
231
                if isinstance(parent_id, str):
 
232
                    parent_id = decode_utf8(parent_id)
 
233
                p.set('revision_id', parent_id)
 
234
        if rev.properties:
 
235
            self._pack_revision_properties(rev, root)
 
236
        return root
 
237
 
 
238
    def _pack_revision_properties(self, rev, under_element):
 
239
        top_elt = SubElement(under_element, 'properties')
 
240
        for prop_name, prop_value in sorted(rev.properties.items()):
 
241
            assert isinstance(prop_name, basestring) 
 
242
            assert isinstance(prop_value, basestring) 
 
243
            prop_elt = SubElement(top_elt, 'property')
 
244
            prop_elt.set('name', prop_name)
 
245
            prop_elt.text = prop_value
 
246
            prop_elt.tail = '\n'
 
247
        top_elt.tail = '\n'
 
248
 
 
249
    def _unpack_inventory(self, elt):
 
250
        """Construct from XML Element
 
251
        """
 
252
        assert elt.tag == 'inventory'
 
253
        root_id = elt.get('file_id') or ROOT_ID
 
254
        format = elt.get('format')
 
255
        if format is not None:
 
256
            if format != '5':
 
257
                raise BzrError("invalid format version %r on inventory"
 
258
                                % format)
 
259
        revision_id = elt.get('revision_id')
 
260
        if revision_id is not None:
 
261
            revision_id = cache_utf8.encode(revision_id)
 
262
        inv = Inventory(root_id, revision_id=revision_id)
 
263
        for e in elt:
 
264
            ie = self._unpack_entry(e)
 
265
            if ie.parent_id == ROOT_ID:
 
266
                ie.parent_id = root_id
 
267
            inv.add(ie)
 
268
        return inv
 
269
 
 
270
    def _unpack_entry(self, elt, none_parents=False):
 
271
        kind = elt.tag
 
272
        if not InventoryEntry.versionable_kind(kind):
 
273
            raise AssertionError('unsupported entry kind %s' % kind)
 
274
 
 
275
        get_cached = _get_utf8_or_ascii
 
276
 
 
277
        parent_id = elt.get('parent_id')
 
278
        if parent_id is None and not none_parents:
 
279
            parent_id = ROOT_ID
 
280
        # TODO: jam 20060817 At present, caching file ids costs us too 
 
281
        #       much time. It slows down overall read performances from
 
282
        #       approx 500ms to 700ms. And doesn't improve future reads.
 
283
        #       it might be because revision ids and file ids are mixing.
 
284
        #       Consider caching *just* the file ids, for a limited period
 
285
        #       of time.
 
286
        #parent_id = get_cached(parent_id)
 
287
        #file_id = get_cached(elt.get('file_id'))
 
288
        file_id = elt.get('file_id')
 
289
 
 
290
        if kind == 'directory':
 
291
            ie = inventory.InventoryDirectory(file_id,
 
292
                                              elt.get('name'),
 
293
                                              parent_id)
 
294
        elif kind == 'file':
 
295
            ie = inventory.InventoryFile(file_id,
 
296
                                         elt.get('name'),
 
297
                                         parent_id)
 
298
            ie.text_sha1 = elt.get('text_sha1')
 
299
            if elt.get('executable') == 'yes':
 
300
                ie.executable = True
 
301
            v = elt.get('text_size')
 
302
            ie.text_size = v and int(v)
 
303
        elif kind == 'symlink':
 
304
            ie = inventory.InventoryLink(file_id,
 
305
                                         elt.get('name'),
 
306
                                         parent_id)
 
307
            ie.symlink_target = elt.get('symlink_target')
 
308
        else:
 
309
            raise BzrError("unknown kind %r" % kind)
 
310
        revision = elt.get('revision')
 
311
        if revision is not None:
 
312
            revision = get_cached(revision)
 
313
        ie.revision = revision
 
314
 
 
315
        return ie
 
316
 
 
317
    def _unpack_revision(self, elt):
 
318
        """XML Element -> Revision object"""
 
319
        assert elt.tag == 'revision'
 
320
        format = elt.get('format')
 
321
        if format is not None:
 
322
            if format != '5':
 
323
                raise BzrError("invalid format version %r on inventory"
 
324
                                % format)
 
325
        get_cached = _get_utf8_or_ascii
 
326
        rev = Revision(committer = elt.get('committer'),
 
327
                       timestamp = float(elt.get('timestamp')),
 
328
                       revision_id = get_cached(elt.get('revision_id')),
 
329
                       inventory_sha1 = elt.get('inventory_sha1')
 
330
                       )
 
331
        parents = elt.find('parents') or []
 
332
        for p in parents:
 
333
            assert p.tag == 'revision_ref', \
 
334
                   "bad parent node tag %r" % p.tag
 
335
            rev.parent_ids.append(get_cached(p.get('revision_id')))
 
336
        self._unpack_revision_properties(elt, rev)
 
337
        v = elt.get('timezone')
 
338
        if v is None:
 
339
            rev.timezone = 0
 
340
        else:
 
341
            rev.timezone = int(v)
 
342
        rev.message = elt.findtext('message') # text of <message>
 
343
        return rev
 
344
 
 
345
    def _unpack_revision_properties(self, elt, rev):
 
346
        """Unpack properties onto a revision."""
 
347
        props_elt = elt.find('properties')
 
348
        assert len(rev.properties) == 0
 
349
        if not props_elt:
 
350
            return
 
351
        for prop_elt in props_elt:
 
352
            assert prop_elt.tag == 'property', \
 
353
                "bad tag under properties list: %r" % prop_elt.tag
 
354
            name = prop_elt.get('name')
 
355
            value = prop_elt.text
 
356
            # If a property had an empty value ('') cElementTree reads
 
357
            # that back as None, convert it back to '', so that all
 
358
            # properties have string values
 
359
            if value is None:
 
360
                value = ''
 
361
            assert name not in rev.properties, \
 
362
                "repeated property %r" % name
 
363
            rev.properties[name] = value
 
364
 
 
365
 
 
366
serializer_v5 = Serializer_v5()