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