• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import absolute_import, unicode_literals
2import sys
3import re
4from io import BytesIO
5from datetime import datetime
6from base64 import b64encode, b64decode
7from numbers import Integral
8
9try:
10    from collections.abc import Mapping # python >= 3.3
11except ImportError:
12    from collections import Mapping
13
14try:
15    from functools import singledispatch
16except ImportError:
17    try:
18        from singledispatch import singledispatch
19    except ImportError:
20        singledispatch = None
21
22from fontTools.misc import etree
23
24from fontTools.misc.py23 import (
25    unicode,
26    basestring,
27    tounicode,
28    tobytes,
29    SimpleNamespace,
30    range,
31)
32
33# On python3, by default we deserialize <data> elements as bytes, whereas on
34# python2 we deserialize <data> elements as plistlib.Data objects, in order
35# to distinguish them from the built-in str type (which is bytes on python2).
36# Similarly, by default on python3 we serialize bytes as <data> elements;
37# however, on python2 we serialize bytes as <string> elements (they must
38# only contain ASCII characters in this case).
39# You can pass use_builtin_types=[True|False] to load/dump etc. functions to
40# enforce the same treatment of bytes across python 2 and 3.
41# NOTE that unicode type always maps to <string> element, and plistlib.Data
42# always maps to <data> element, regardless of use_builtin_types.
43PY3 = sys.version_info[0] > 2
44if PY3:
45    USE_BUILTIN_TYPES = True
46else:
47    USE_BUILTIN_TYPES = False
48
49XML_DECLARATION = b"""<?xml version='1.0' encoding='UTF-8'?>"""
50
51PLIST_DOCTYPE = (
52    b'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
53    b'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
54)
55
56# Date should conform to a subset of ISO 8601:
57# YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'
58_date_parser = re.compile(
59    r"(?P<year>\d\d\d\d)"
60    r"(?:-(?P<month>\d\d)"
61    r"(?:-(?P<day>\d\d)"
62    r"(?:T(?P<hour>\d\d)"
63    r"(?::(?P<minute>\d\d)"
64    r"(?::(?P<second>\d\d))"
65    r"?)?)?)?)?Z",
66    getattr(re, "ASCII", 0),  # py3-only
67)
68
69
70def _date_from_string(s):
71    order = ("year", "month", "day", "hour", "minute", "second")
72    gd = _date_parser.match(s).groupdict()
73    lst = []
74    for key in order:
75        val = gd[key]
76        if val is None:
77            break
78        lst.append(int(val))
79    return datetime(*lst)
80
81
82def _date_to_string(d):
83    return "%04d-%02d-%02dT%02d:%02d:%02dZ" % (
84        d.year,
85        d.month,
86        d.day,
87        d.hour,
88        d.minute,
89        d.second,
90    )
91
92
93def _encode_base64(data, maxlinelength=76, indent_level=1):
94    data = b64encode(data)
95    if data and maxlinelength:
96        # split into multiple lines right-justified to 'maxlinelength' chars
97        indent = b"\n" + b"  " * indent_level
98        max_length = max(16, maxlinelength - len(indent))
99        chunks = []
100        for i in range(0, len(data), max_length):
101            chunks.append(indent)
102            chunks.append(data[i : i + max_length])
103        chunks.append(indent)
104        data = b"".join(chunks)
105    return data
106
107
108class Data:
109    """Wrapper for binary data returned in place of the built-in bytes type
110    when loading property list data with use_builtin_types=False.
111    """
112
113    def __init__(self, data):
114        if not isinstance(data, bytes):
115            raise TypeError("Expected bytes, found %s" % type(data).__name__)
116        self.data = data
117
118    @classmethod
119    def fromBase64(cls, data):
120        return cls(b64decode(data))
121
122    def asBase64(self, maxlinelength=76, indent_level=1):
123        return _encode_base64(
124            self.data, maxlinelength=maxlinelength, indent_level=indent_level
125        )
126
127    def __eq__(self, other):
128        if isinstance(other, self.__class__):
129            return self.data == other.data
130        elif isinstance(other, bytes):
131            return self.data == other
132        else:
133            return NotImplemented
134
135    def __repr__(self):
136        return "%s(%s)" % (self.__class__.__name__, repr(self.data))
137
138
139class PlistTarget(object):
140    """ Event handler using the ElementTree Target API that can be
141    passed to a XMLParser to produce property list objects from XML.
142    It is based on the CPython plistlib module's _PlistParser class,
143    but does not use the expat parser.
144
145    >>> from fontTools.misc import etree
146    >>> parser = etree.XMLParser(target=PlistTarget())
147    >>> result = etree.XML(
148    ...     "<dict>"
149    ...     "    <key>something</key>"
150    ...     "    <string>blah</string>"
151    ...     "</dict>",
152    ...     parser=parser)
153    >>> result == {"something": "blah"}
154    True
155
156    Links:
157    https://github.com/python/cpython/blob/master/Lib/plistlib.py
158    http://lxml.de/parsing.html#the-target-parser-interface
159    """
160
161    def __init__(self, use_builtin_types=None, dict_type=dict):
162        self.stack = []
163        self.current_key = None
164        self.root = None
165        if use_builtin_types is None:
166            self._use_builtin_types = USE_BUILTIN_TYPES
167        else:
168            self._use_builtin_types = use_builtin_types
169        self._dict_type = dict_type
170
171    def start(self, tag, attrib):
172        self._data = []
173        handler = _TARGET_START_HANDLERS.get(tag)
174        if handler is not None:
175            handler(self)
176
177    def end(self, tag):
178        handler = _TARGET_END_HANDLERS.get(tag)
179        if handler is not None:
180            handler(self)
181
182    def data(self, data):
183        self._data.append(data)
184
185    def close(self):
186        return self.root
187
188    # helpers
189
190    def add_object(self, value):
191        if self.current_key is not None:
192            if not isinstance(self.stack[-1], type({})):
193                raise ValueError("unexpected element: %r" % self.stack[-1])
194            self.stack[-1][self.current_key] = value
195            self.current_key = None
196        elif not self.stack:
197            # this is the root object
198            self.root = value
199        else:
200            if not isinstance(self.stack[-1], type([])):
201                raise ValueError("unexpected element: %r" % self.stack[-1])
202            self.stack[-1].append(value)
203
204    def get_data(self):
205        data = "".join(self._data)
206        self._data = []
207        return data
208
209
210# event handlers
211
212
213def start_dict(self):
214    d = self._dict_type()
215    self.add_object(d)
216    self.stack.append(d)
217
218
219def end_dict(self):
220    if self.current_key:
221        raise ValueError("missing value for key '%s'" % self.current_key)
222    self.stack.pop()
223
224
225def end_key(self):
226    if self.current_key or not isinstance(self.stack[-1], type({})):
227        raise ValueError("unexpected key")
228    self.current_key = self.get_data()
229
230
231def start_array(self):
232    a = []
233    self.add_object(a)
234    self.stack.append(a)
235
236
237def end_array(self):
238    self.stack.pop()
239
240
241def end_true(self):
242    self.add_object(True)
243
244
245def end_false(self):
246    self.add_object(False)
247
248
249def end_integer(self):
250    self.add_object(int(self.get_data()))
251
252
253def end_real(self):
254    self.add_object(float(self.get_data()))
255
256
257def end_string(self):
258    self.add_object(self.get_data())
259
260
261def end_data(self):
262    if self._use_builtin_types:
263        self.add_object(b64decode(self.get_data()))
264    else:
265        self.add_object(Data.fromBase64(self.get_data()))
266
267
268def end_date(self):
269    self.add_object(_date_from_string(self.get_data()))
270
271
272_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array}
273
274_TARGET_END_HANDLERS = {
275    "dict": end_dict,
276    "array": end_array,
277    "key": end_key,
278    "true": end_true,
279    "false": end_false,
280    "integer": end_integer,
281    "real": end_real,
282    "string": end_string,
283    "data": end_data,
284    "date": end_date,
285}
286
287
288# functions to build element tree from plist data
289
290
291def _string_element(value, ctx):
292    el = etree.Element("string")
293    el.text = value
294    return el
295
296
297def _bool_element(value, ctx):
298    if value:
299        return etree.Element("true")
300    else:
301        return etree.Element("false")
302
303
304def _integer_element(value, ctx):
305    if -1 << 63 <= value < 1 << 64:
306        el = etree.Element("integer")
307        el.text = "%d" % value
308        return el
309    else:
310        raise OverflowError(value)
311
312
313def _real_element(value, ctx):
314    el = etree.Element("real")
315    el.text = repr(value)
316    return el
317
318
319def _dict_element(d, ctx):
320    el = etree.Element("dict")
321    items = d.items()
322    if ctx.sort_keys:
323        items = sorted(items)
324    ctx.indent_level += 1
325    for key, value in items:
326        if not isinstance(key, basestring):
327            if ctx.skipkeys:
328                continue
329            raise TypeError("keys must be strings")
330        k = etree.SubElement(el, "key")
331        k.text = tounicode(key, "utf-8")
332        el.append(_make_element(value, ctx))
333    ctx.indent_level -= 1
334    return el
335
336
337def _array_element(array, ctx):
338    el = etree.Element("array")
339    if len(array) == 0:
340        return el
341    ctx.indent_level += 1
342    for value in array:
343        el.append(_make_element(value, ctx))
344    ctx.indent_level -= 1
345    return el
346
347
348def _date_element(date, ctx):
349    el = etree.Element("date")
350    el.text = _date_to_string(date)
351    return el
352
353
354def _data_element(data, ctx):
355    el = etree.Element("data")
356    el.text = _encode_base64(
357        data,
358        maxlinelength=(76 if ctx.pretty_print else None),
359        indent_level=ctx.indent_level,
360    )
361    return el
362
363
364def _string_or_data_element(raw_bytes, ctx):
365    if ctx.use_builtin_types:
366        return _data_element(raw_bytes, ctx)
367    else:
368        try:
369            string = raw_bytes.decode(encoding="ascii", errors="strict")
370        except UnicodeDecodeError:
371            raise ValueError(
372                "invalid non-ASCII bytes; use unicode string instead: %r"
373                % raw_bytes
374            )
375        return _string_element(string, ctx)
376
377
378# if singledispatch is available, we use a generic '_make_element' function
379# and register overloaded implementations that are run based on the type of
380# the first argument
381
382if singledispatch is not None:
383
384    @singledispatch
385    def _make_element(value, ctx):
386        raise TypeError("unsupported type: %s" % type(value))
387
388    _make_element.register(unicode)(_string_element)
389    _make_element.register(bool)(_bool_element)
390    _make_element.register(Integral)(_integer_element)
391    _make_element.register(float)(_real_element)
392    _make_element.register(Mapping)(_dict_element)
393    _make_element.register(list)(_array_element)
394    _make_element.register(tuple)(_array_element)
395    _make_element.register(datetime)(_date_element)
396    _make_element.register(bytes)(_string_or_data_element)
397    _make_element.register(bytearray)(_data_element)
398    _make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx))
399
400else:
401    # otherwise we use a long switch-like if statement
402
403    def _make_element(value, ctx):
404        if isinstance(value, unicode):
405            return _string_element(value, ctx)
406        elif isinstance(value, bool):
407            return _bool_element(value, ctx)
408        elif isinstance(value, Integral):
409            return _integer_element(value, ctx)
410        elif isinstance(value, float):
411            return _real_element(value, ctx)
412        elif isinstance(value, Mapping):
413            return _dict_element(value, ctx)
414        elif isinstance(value, (list, tuple)):
415            return _array_element(value, ctx)
416        elif isinstance(value, datetime):
417            return _date_element(value, ctx)
418        elif isinstance(value, bytes):
419            return _string_or_data_element(value, ctx)
420        elif isinstance(value, bytearray):
421            return _data_element(value, ctx)
422        elif isinstance(value, Data):
423            return _data_element(value.data, ctx)
424
425
426# Public functions to create element tree from plist-compatible python
427# data structures and viceversa, for use when (de)serializing GLIF xml.
428
429
430def totree(
431    value,
432    sort_keys=True,
433    skipkeys=False,
434    use_builtin_types=None,
435    pretty_print=True,
436    indent_level=1,
437):
438    if use_builtin_types is None:
439        use_builtin_types = USE_BUILTIN_TYPES
440    else:
441        use_builtin_types = use_builtin_types
442    context = SimpleNamespace(
443        sort_keys=sort_keys,
444        skipkeys=skipkeys,
445        use_builtin_types=use_builtin_types,
446        pretty_print=pretty_print,
447        indent_level=indent_level,
448    )
449    return _make_element(value, context)
450
451
452def fromtree(tree, use_builtin_types=None, dict_type=dict):
453    target = PlistTarget(
454        use_builtin_types=use_builtin_types, dict_type=dict_type
455    )
456    for action, element in etree.iterwalk(tree, events=("start", "end")):
457        if action == "start":
458            target.start(element.tag, element.attrib)
459        elif action == "end":
460            # if there are no children, parse the leaf's data
461            if not len(element):
462                # always pass str, not None
463                target.data(element.text or "")
464            target.end(element.tag)
465    return target.close()
466
467
468# python3 plistlib API
469
470
471def load(fp, use_builtin_types=None, dict_type=dict):
472    if not hasattr(fp, "read"):
473        raise AttributeError(
474            "'%s' object has no attribute 'read'" % type(fp).__name__
475        )
476    target = PlistTarget(
477        use_builtin_types=use_builtin_types, dict_type=dict_type
478    )
479    parser = etree.XMLParser(target=target)
480    result = etree.parse(fp, parser=parser)
481    # lxml returns the target object directly, while ElementTree wraps
482    # it as the root of an ElementTree object
483    try:
484        return result.getroot()
485    except AttributeError:
486        return result
487
488
489def loads(value, use_builtin_types=None, dict_type=dict):
490    fp = BytesIO(value)
491    return load(fp, use_builtin_types=use_builtin_types, dict_type=dict_type)
492
493
494def dump(
495    value,
496    fp,
497    sort_keys=True,
498    skipkeys=False,
499    use_builtin_types=None,
500    pretty_print=True,
501):
502    if not hasattr(fp, "write"):
503        raise AttributeError(
504            "'%s' object has no attribute 'write'" % type(fp).__name__
505        )
506    root = etree.Element("plist", version="1.0")
507    el = totree(
508        value,
509        sort_keys=sort_keys,
510        skipkeys=skipkeys,
511        use_builtin_types=use_builtin_types,
512        pretty_print=pretty_print,
513    )
514    root.append(el)
515    tree = etree.ElementTree(root)
516    # we write the doctype ourselves instead of using the 'doctype' argument
517    # of 'write' method, becuse lxml will force adding a '\n' even when
518    # pretty_print is False.
519    if pretty_print:
520        header = b"\n".join((XML_DECLARATION, PLIST_DOCTYPE, b""))
521    else:
522        header = XML_DECLARATION + PLIST_DOCTYPE
523    fp.write(header)
524    tree.write(
525        fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False
526    )
527
528
529def dumps(
530    value,
531    sort_keys=True,
532    skipkeys=False,
533    use_builtin_types=None,
534    pretty_print=True,
535):
536    fp = BytesIO()
537    dump(
538        value,
539        fp,
540        sort_keys=sort_keys,
541        skipkeys=skipkeys,
542        use_builtin_types=use_builtin_types,
543        pretty_print=pretty_print,
544    )
545    return fp.getvalue()
546