(root)/
Python-3.12.0/
Lib/
plistlib.py
       1  r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
       2  
       3  The property list (.plist) file format is a simple XML pickle supporting
       4  basic object types, like dictionaries, lists, numbers and strings.
       5  Usually the top level object is a dictionary.
       6  
       7  To write out a plist file, use the dump(value, file)
       8  function. 'value' is the top level object, 'file' is
       9  a (writable) file object.
      10  
      11  To parse a plist from a file, use the load(file) function,
      12  with a (readable) file object as the only argument. It
      13  returns the top level object (again, usually a dictionary).
      14  
      15  To work with plist data in bytes objects, you can use loads()
      16  and dumps().
      17  
      18  Values can be strings, integers, floats, booleans, tuples, lists,
      19  dictionaries (but only with string keys), Data, bytes, bytearray, or
      20  datetime.datetime objects.
      21  
      22  Generate Plist example:
      23  
      24      import datetime
      25      import plistlib
      26  
      27      pl = dict(
      28          aString = "Doodah",
      29          aList = ["A", "B", 12, 32.1, [1, 2, 3]],
      30          aFloat = 0.1,
      31          anInt = 728,
      32          aDict = dict(
      33              anotherString = "<hello & hi there!>",
      34              aThirdString = "M\xe4ssig, Ma\xdf",
      35              aTrueValue = True,
      36              aFalseValue = False,
      37          ),
      38          someData = b"<binary gunk>",
      39          someMoreData = b"<lots of binary gunk>" * 10,
      40          aDate = datetime.datetime.now()
      41      )
      42      print(plistlib.dumps(pl).decode())
      43  
      44  Parse Plist example:
      45  
      46      import plistlib
      47  
      48      plist = b'''<plist version="1.0">
      49      <dict>
      50          <key>foo</key>
      51          <string>bar</string>
      52      </dict>
      53      </plist>'''
      54      pl = plistlib.loads(plist)
      55      print(pl["foo"])
      56  """
      57  __all__ = [
      58      "InvalidFileException", "FMT_XML", "FMT_BINARY", "load", "dump", "loads", "dumps", "UID"
      59  ]
      60  
      61  import binascii
      62  import codecs
      63  import datetime
      64  import enum
      65  from io import BytesIO
      66  import itertools
      67  import os
      68  import re
      69  import struct
      70  from xml.parsers.expat import ParserCreate
      71  
      72  
      73  PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
      74  globals().update(PlistFormat.__members__)
      75  
      76  
      77  class ESC[4;38;5;81mUID:
      78      def __init__(self, data):
      79          if not isinstance(data, int):
      80              raise TypeError("data must be an int")
      81          if data >= 1 << 64:
      82              raise ValueError("UIDs cannot be >= 2**64")
      83          if data < 0:
      84              raise ValueError("UIDs must be positive")
      85          self.data = data
      86  
      87      def __index__(self):
      88          return self.data
      89  
      90      def __repr__(self):
      91          return "%s(%s)" % (self.__class__.__name__, repr(self.data))
      92  
      93      def __reduce__(self):
      94          return self.__class__, (self.data,)
      95  
      96      def __eq__(self, other):
      97          if not isinstance(other, UID):
      98              return NotImplemented
      99          return self.data == other.data
     100  
     101      def __hash__(self):
     102          return hash(self.data)
     103  
     104  #
     105  # XML support
     106  #
     107  
     108  
     109  # XML 'header'
     110  PLISTHEADER = b"""\
     111  <?xml version="1.0" encoding="UTF-8"?>
     112  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
     113  """
     114  
     115  
     116  # Regex to find any control chars, except for \t \n and \r
     117  _controlCharPat = re.compile(
     118      r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
     119      r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
     120  
     121  def _encode_base64(s, maxlinelength=76):
     122      # copied from base64.encodebytes(), with added maxlinelength argument
     123      maxbinsize = (maxlinelength//4)*3
     124      pieces = []
     125      for i in range(0, len(s), maxbinsize):
     126          chunk = s[i : i + maxbinsize]
     127          pieces.append(binascii.b2a_base64(chunk))
     128      return b''.join(pieces)
     129  
     130  def _decode_base64(s):
     131      if isinstance(s, str):
     132          return binascii.a2b_base64(s.encode("utf-8"))
     133  
     134      else:
     135          return binascii.a2b_base64(s)
     136  
     137  # Contents should conform to a subset of ISO 8601
     138  # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'.  Smaller units
     139  # may be omitted with #  a loss of precision)
     140  _dateParser = re.compile(r"(?P<year>\d\d\d\d)(?:-(?P<month>\d\d)(?:-(?P<day>\d\d)(?:T(?P<hour>\d\d)(?::(?P<minute>\d\d)(?::(?P<second>\d\d))?)?)?)?)?Z", re.ASCII)
     141  
     142  
     143  def _date_from_string(s):
     144      order = ('year', 'month', 'day', 'hour', 'minute', 'second')
     145      gd = _dateParser.match(s).groupdict()
     146      lst = []
     147      for key in order:
     148          val = gd[key]
     149          if val is None:
     150              break
     151          lst.append(int(val))
     152      return datetime.datetime(*lst)
     153  
     154  
     155  def _date_to_string(d):
     156      return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
     157          d.year, d.month, d.day,
     158          d.hour, d.minute, d.second
     159      )
     160  
     161  def _escape(text):
     162      m = _controlCharPat.search(text)
     163      if m is not None:
     164          raise ValueError("strings can't contain control characters; "
     165                           "use bytes instead")
     166      text = text.replace("\r\n", "\n")       # convert DOS line endings
     167      text = text.replace("\r", "\n")         # convert Mac line endings
     168      text = text.replace("&", "&amp;")       # escape '&'
     169      text = text.replace("<", "&lt;")        # escape '<'
     170      text = text.replace(">", "&gt;")        # escape '>'
     171      return text
     172  
     173  class ESC[4;38;5;81m_PlistParser:
     174      def __init__(self, dict_type):
     175          self.stack = []
     176          self.current_key = None
     177          self.root = None
     178          self._dict_type = dict_type
     179  
     180      def parse(self, fileobj):
     181          self.parser = ParserCreate()
     182          self.parser.StartElementHandler = self.handle_begin_element
     183          self.parser.EndElementHandler = self.handle_end_element
     184          self.parser.CharacterDataHandler = self.handle_data
     185          self.parser.EntityDeclHandler = self.handle_entity_decl
     186          self.parser.ParseFile(fileobj)
     187          return self.root
     188  
     189      def handle_entity_decl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name):
     190          # Reject plist files with entity declarations to avoid XML vulnerabilities in expat.
     191          # Regular plist files don't contain those declarations, and Apple's plutil tool does not
     192          # accept them either.
     193          raise InvalidFileException("XML entity declarations are not supported in plist files")
     194  
     195      def handle_begin_element(self, element, attrs):
     196          self.data = []
     197          handler = getattr(self, "begin_" + element, None)
     198          if handler is not None:
     199              handler(attrs)
     200  
     201      def handle_end_element(self, element):
     202          handler = getattr(self, "end_" + element, None)
     203          if handler is not None:
     204              handler()
     205  
     206      def handle_data(self, data):
     207          self.data.append(data)
     208  
     209      def add_object(self, value):
     210          if self.current_key is not None:
     211              if not isinstance(self.stack[-1], dict):
     212                  raise ValueError("unexpected element at line %d" %
     213                                   self.parser.CurrentLineNumber)
     214              self.stack[-1][self.current_key] = value
     215              self.current_key = None
     216          elif not self.stack:
     217              # this is the root object
     218              self.root = value
     219          else:
     220              if not isinstance(self.stack[-1], list):
     221                  raise ValueError("unexpected element at line %d" %
     222                                   self.parser.CurrentLineNumber)
     223              self.stack[-1].append(value)
     224  
     225      def get_data(self):
     226          data = ''.join(self.data)
     227          self.data = []
     228          return data
     229  
     230      # element handlers
     231  
     232      def begin_dict(self, attrs):
     233          d = self._dict_type()
     234          self.add_object(d)
     235          self.stack.append(d)
     236  
     237      def end_dict(self):
     238          if self.current_key:
     239              raise ValueError("missing value for key '%s' at line %d" %
     240                               (self.current_key,self.parser.CurrentLineNumber))
     241          self.stack.pop()
     242  
     243      def end_key(self):
     244          if self.current_key or not isinstance(self.stack[-1], dict):
     245              raise ValueError("unexpected key at line %d" %
     246                               self.parser.CurrentLineNumber)
     247          self.current_key = self.get_data()
     248  
     249      def begin_array(self, attrs):
     250          a = []
     251          self.add_object(a)
     252          self.stack.append(a)
     253  
     254      def end_array(self):
     255          self.stack.pop()
     256  
     257      def end_true(self):
     258          self.add_object(True)
     259  
     260      def end_false(self):
     261          self.add_object(False)
     262  
     263      def end_integer(self):
     264          raw = self.get_data()
     265          if raw.startswith('0x') or raw.startswith('0X'):
     266              self.add_object(int(raw, 16))
     267          else:
     268              self.add_object(int(raw))
     269  
     270      def end_real(self):
     271          self.add_object(float(self.get_data()))
     272  
     273      def end_string(self):
     274          self.add_object(self.get_data())
     275  
     276      def end_data(self):
     277          self.add_object(_decode_base64(self.get_data()))
     278  
     279      def end_date(self):
     280          self.add_object(_date_from_string(self.get_data()))
     281  
     282  
     283  class ESC[4;38;5;81m_DumbXMLWriter:
     284      def __init__(self, file, indent_level=0, indent="\t"):
     285          self.file = file
     286          self.stack = []
     287          self._indent_level = indent_level
     288          self.indent = indent
     289  
     290      def begin_element(self, element):
     291          self.stack.append(element)
     292          self.writeln("<%s>" % element)
     293          self._indent_level += 1
     294  
     295      def end_element(self, element):
     296          assert self._indent_level > 0
     297          assert self.stack.pop() == element
     298          self._indent_level -= 1
     299          self.writeln("</%s>" % element)
     300  
     301      def simple_element(self, element, value=None):
     302          if value is not None:
     303              value = _escape(value)
     304              self.writeln("<%s>%s</%s>" % (element, value, element))
     305  
     306          else:
     307              self.writeln("<%s/>" % element)
     308  
     309      def writeln(self, line):
     310          if line:
     311              # plist has fixed encoding of utf-8
     312  
     313              # XXX: is this test needed?
     314              if isinstance(line, str):
     315                  line = line.encode('utf-8')
     316              self.file.write(self._indent_level * self.indent)
     317              self.file.write(line)
     318          self.file.write(b'\n')
     319  
     320  
     321  class ESC[4;38;5;81m_PlistWriter(ESC[4;38;5;149m_DumbXMLWriter):
     322      def __init__(
     323              self, file, indent_level=0, indent=b"\t", writeHeader=1,
     324              sort_keys=True, skipkeys=False):
     325  
     326          if writeHeader:
     327              file.write(PLISTHEADER)
     328          _DumbXMLWriter.__init__(self, file, indent_level, indent)
     329          self._sort_keys = sort_keys
     330          self._skipkeys = skipkeys
     331  
     332      def write(self, value):
     333          self.writeln("<plist version=\"1.0\">")
     334          self.write_value(value)
     335          self.writeln("</plist>")
     336  
     337      def write_value(self, value):
     338          if isinstance(value, str):
     339              self.simple_element("string", value)
     340  
     341          elif value is True:
     342              self.simple_element("true")
     343  
     344          elif value is False:
     345              self.simple_element("false")
     346  
     347          elif isinstance(value, int):
     348              if -1 << 63 <= value < 1 << 64:
     349                  self.simple_element("integer", "%d" % value)
     350              else:
     351                  raise OverflowError(value)
     352  
     353          elif isinstance(value, float):
     354              self.simple_element("real", repr(value))
     355  
     356          elif isinstance(value, dict):
     357              self.write_dict(value)
     358  
     359          elif isinstance(value, (bytes, bytearray)):
     360              self.write_bytes(value)
     361  
     362          elif isinstance(value, datetime.datetime):
     363              self.simple_element("date", _date_to_string(value))
     364  
     365          elif isinstance(value, (tuple, list)):
     366              self.write_array(value)
     367  
     368          else:
     369              raise TypeError("unsupported type: %s" % type(value))
     370  
     371      def write_bytes(self, data):
     372          self.begin_element("data")
     373          self._indent_level -= 1
     374          maxlinelength = max(
     375              16,
     376              76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
     377  
     378          for line in _encode_base64(data, maxlinelength).split(b"\n"):
     379              if line:
     380                  self.writeln(line)
     381          self._indent_level += 1
     382          self.end_element("data")
     383  
     384      def write_dict(self, d):
     385          if d:
     386              self.begin_element("dict")
     387              if self._sort_keys:
     388                  items = sorted(d.items())
     389              else:
     390                  items = d.items()
     391  
     392              for key, value in items:
     393                  if not isinstance(key, str):
     394                      if self._skipkeys:
     395                          continue
     396                      raise TypeError("keys must be strings")
     397                  self.simple_element("key", key)
     398                  self.write_value(value)
     399              self.end_element("dict")
     400  
     401          else:
     402              self.simple_element("dict")
     403  
     404      def write_array(self, array):
     405          if array:
     406              self.begin_element("array")
     407              for value in array:
     408                  self.write_value(value)
     409              self.end_element("array")
     410  
     411          else:
     412              self.simple_element("array")
     413  
     414  
     415  def _is_fmt_xml(header):
     416      prefixes = (b'<?xml', b'<plist')
     417  
     418      for pfx in prefixes:
     419          if header.startswith(pfx):
     420              return True
     421  
     422      # Also check for alternative XML encodings, this is slightly
     423      # overkill because the Apple tools (and plistlib) will not
     424      # generate files with these encodings.
     425      for bom, encoding in (
     426                  (codecs.BOM_UTF8, "utf-8"),
     427                  (codecs.BOM_UTF16_BE, "utf-16-be"),
     428                  (codecs.BOM_UTF16_LE, "utf-16-le"),
     429                  # expat does not support utf-32
     430                  #(codecs.BOM_UTF32_BE, "utf-32-be"),
     431                  #(codecs.BOM_UTF32_LE, "utf-32-le"),
     432              ):
     433          if not header.startswith(bom):
     434              continue
     435  
     436          for start in prefixes:
     437              prefix = bom + start.decode('ascii').encode(encoding)
     438              if header[:len(prefix)] == prefix:
     439                  return True
     440  
     441      return False
     442  
     443  #
     444  # Binary Plist
     445  #
     446  
     447  
     448  class ESC[4;38;5;81mInvalidFileException (ESC[4;38;5;149mValueError):
     449      def __init__(self, message="Invalid file"):
     450          ValueError.__init__(self, message)
     451  
     452  _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}
     453  
     454  _undefined = object()
     455  
     456  class ESC[4;38;5;81m_BinaryPlistParser:
     457      """
     458      Read or write a binary plist file, following the description of the binary
     459      format.  Raise InvalidFileException in case of error, otherwise return the
     460      root object.
     461  
     462      see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c
     463      """
     464      def __init__(self, dict_type):
     465          self._dict_type = dict_type
     466  
     467      def parse(self, fp):
     468          try:
     469              # The basic file format:
     470              # HEADER
     471              # object...
     472              # refid->offset...
     473              # TRAILER
     474              self._fp = fp
     475              self._fp.seek(-32, os.SEEK_END)
     476              trailer = self._fp.read(32)
     477              if len(trailer) != 32:
     478                  raise InvalidFileException()
     479              (
     480                  offset_size, self._ref_size, num_objects, top_object,
     481                  offset_table_offset
     482              ) = struct.unpack('>6xBBQQQ', trailer)
     483              self._fp.seek(offset_table_offset)
     484              self._object_offsets = self._read_ints(num_objects, offset_size)
     485              self._objects = [_undefined] * num_objects
     486              return self._read_object(top_object)
     487  
     488          except (OSError, IndexError, struct.error, OverflowError,
     489                  ValueError):
     490              raise InvalidFileException()
     491  
     492      def _get_size(self, tokenL):
     493          """ return the size of the next object."""
     494          if tokenL == 0xF:
     495              m = self._fp.read(1)[0] & 0x3
     496              s = 1 << m
     497              f = '>' + _BINARY_FORMAT[s]
     498              return struct.unpack(f, self._fp.read(s))[0]
     499  
     500          return tokenL
     501  
     502      def _read_ints(self, n, size):
     503          data = self._fp.read(size * n)
     504          if size in _BINARY_FORMAT:
     505              return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
     506          else:
     507              if not size or len(data) != size * n:
     508                  raise InvalidFileException()
     509              return tuple(int.from_bytes(data[i: i + size], 'big')
     510                           for i in range(0, size * n, size))
     511  
     512      def _read_refs(self, n):
     513          return self._read_ints(n, self._ref_size)
     514  
     515      def _read_object(self, ref):
     516          """
     517          read the object by reference.
     518  
     519          May recursively read sub-objects (content of an array/dict/set)
     520          """
     521          result = self._objects[ref]
     522          if result is not _undefined:
     523              return result
     524  
     525          offset = self._object_offsets[ref]
     526          self._fp.seek(offset)
     527          token = self._fp.read(1)[0]
     528          tokenH, tokenL = token & 0xF0, token & 0x0F
     529  
     530          if token == 0x00:
     531              result = None
     532  
     533          elif token == 0x08:
     534              result = False
     535  
     536          elif token == 0x09:
     537              result = True
     538  
     539          # The referenced source code also mentions URL (0x0c, 0x0d) and
     540          # UUID (0x0e), but neither can be generated using the Cocoa libraries.
     541  
     542          elif token == 0x0f:
     543              result = b''
     544  
     545          elif tokenH == 0x10:  # int
     546              result = int.from_bytes(self._fp.read(1 << tokenL),
     547                                      'big', signed=tokenL >= 3)
     548  
     549          elif token == 0x22: # real
     550              result = struct.unpack('>f', self._fp.read(4))[0]
     551  
     552          elif token == 0x23: # real
     553              result = struct.unpack('>d', self._fp.read(8))[0]
     554  
     555          elif token == 0x33:  # date
     556              f = struct.unpack('>d', self._fp.read(8))[0]
     557              # timestamp 0 of binary plists corresponds to 1/1/2001
     558              # (year of Mac OS X 10.0), instead of 1/1/1970.
     559              result = (datetime.datetime(2001, 1, 1) +
     560                        datetime.timedelta(seconds=f))
     561  
     562          elif tokenH == 0x40:  # data
     563              s = self._get_size(tokenL)
     564              result = self._fp.read(s)
     565              if len(result) != s:
     566                  raise InvalidFileException()
     567  
     568          elif tokenH == 0x50:  # ascii string
     569              s = self._get_size(tokenL)
     570              data = self._fp.read(s)
     571              if len(data) != s:
     572                  raise InvalidFileException()
     573              result = data.decode('ascii')
     574  
     575          elif tokenH == 0x60:  # unicode string
     576              s = self._get_size(tokenL) * 2
     577              data = self._fp.read(s)
     578              if len(data) != s:
     579                  raise InvalidFileException()
     580              result = data.decode('utf-16be')
     581  
     582          elif tokenH == 0x80:  # UID
     583              # used by Key-Archiver plist files
     584              result = UID(int.from_bytes(self._fp.read(1 + tokenL), 'big'))
     585  
     586          elif tokenH == 0xA0:  # array
     587              s = self._get_size(tokenL)
     588              obj_refs = self._read_refs(s)
     589              result = []
     590              self._objects[ref] = result
     591              result.extend(self._read_object(x) for x in obj_refs)
     592  
     593          # tokenH == 0xB0 is documented as 'ordset', but is not actually
     594          # implemented in the Apple reference code.
     595  
     596          # tokenH == 0xC0 is documented as 'set', but sets cannot be used in
     597          # plists.
     598  
     599          elif tokenH == 0xD0:  # dict
     600              s = self._get_size(tokenL)
     601              key_refs = self._read_refs(s)
     602              obj_refs = self._read_refs(s)
     603              result = self._dict_type()
     604              self._objects[ref] = result
     605              try:
     606                  for k, o in zip(key_refs, obj_refs):
     607                      result[self._read_object(k)] = self._read_object(o)
     608              except TypeError:
     609                  raise InvalidFileException()
     610          else:
     611              raise InvalidFileException()
     612  
     613          self._objects[ref] = result
     614          return result
     615  
     616  def _count_to_size(count):
     617      if count < 1 << 8:
     618          return 1
     619  
     620      elif count < 1 << 16:
     621          return 2
     622  
     623      elif count < 1 << 32:
     624          return 4
     625  
     626      else:
     627          return 8
     628  
     629  _scalars = (str, int, float, datetime.datetime, bytes)
     630  
     631  class ESC[4;38;5;81m_BinaryPlistWriter (ESC[4;38;5;149mobject):
     632      def __init__(self, fp, sort_keys, skipkeys):
     633          self._fp = fp
     634          self._sort_keys = sort_keys
     635          self._skipkeys = skipkeys
     636  
     637      def write(self, value):
     638  
     639          # Flattened object list:
     640          self._objlist = []
     641  
     642          # Mappings from object->objectid
     643          # First dict has (type(object), object) as the key,
     644          # second dict is used when object is not hashable and
     645          # has id(object) as the key.
     646          self._objtable = {}
     647          self._objidtable = {}
     648  
     649          # Create list of all objects in the plist
     650          self._flatten(value)
     651  
     652          # Size of object references in serialized containers
     653          # depends on the number of objects in the plist.
     654          num_objects = len(self._objlist)
     655          self._object_offsets = [0]*num_objects
     656          self._ref_size = _count_to_size(num_objects)
     657  
     658          self._ref_format = _BINARY_FORMAT[self._ref_size]
     659  
     660          # Write file header
     661          self._fp.write(b'bplist00')
     662  
     663          # Write object list
     664          for obj in self._objlist:
     665              self._write_object(obj)
     666  
     667          # Write refnum->object offset table
     668          top_object = self._getrefnum(value)
     669          offset_table_offset = self._fp.tell()
     670          offset_size = _count_to_size(offset_table_offset)
     671          offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
     672          self._fp.write(struct.pack(offset_format, *self._object_offsets))
     673  
     674          # Write trailer
     675          sort_version = 0
     676          trailer = (
     677              sort_version, offset_size, self._ref_size, num_objects,
     678              top_object, offset_table_offset
     679          )
     680          self._fp.write(struct.pack('>5xBBBQQQ', *trailer))
     681  
     682      def _flatten(self, value):
     683          # First check if the object is in the object table, not used for
     684          # containers to ensure that two subcontainers with the same contents
     685          # will be serialized as distinct values.
     686          if isinstance(value, _scalars):
     687              if (type(value), value) in self._objtable:
     688                  return
     689  
     690          elif id(value) in self._objidtable:
     691              return
     692  
     693          # Add to objectreference map
     694          refnum = len(self._objlist)
     695          self._objlist.append(value)
     696          if isinstance(value, _scalars):
     697              self._objtable[(type(value), value)] = refnum
     698          else:
     699              self._objidtable[id(value)] = refnum
     700  
     701          # And finally recurse into containers
     702          if isinstance(value, dict):
     703              keys = []
     704              values = []
     705              items = value.items()
     706              if self._sort_keys:
     707                  items = sorted(items)
     708  
     709              for k, v in items:
     710                  if not isinstance(k, str):
     711                      if self._skipkeys:
     712                          continue
     713                      raise TypeError("keys must be strings")
     714                  keys.append(k)
     715                  values.append(v)
     716  
     717              for o in itertools.chain(keys, values):
     718                  self._flatten(o)
     719  
     720          elif isinstance(value, (list, tuple)):
     721              for o in value:
     722                  self._flatten(o)
     723  
     724      def _getrefnum(self, value):
     725          if isinstance(value, _scalars):
     726              return self._objtable[(type(value), value)]
     727          else:
     728              return self._objidtable[id(value)]
     729  
     730      def _write_size(self, token, size):
     731          if size < 15:
     732              self._fp.write(struct.pack('>B', token | size))
     733  
     734          elif size < 1 << 8:
     735              self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))
     736  
     737          elif size < 1 << 16:
     738              self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))
     739  
     740          elif size < 1 << 32:
     741              self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))
     742  
     743          else:
     744              self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))
     745  
     746      def _write_object(self, value):
     747          ref = self._getrefnum(value)
     748          self._object_offsets[ref] = self._fp.tell()
     749          if value is None:
     750              self._fp.write(b'\x00')
     751  
     752          elif value is False:
     753              self._fp.write(b'\x08')
     754  
     755          elif value is True:
     756              self._fp.write(b'\x09')
     757  
     758          elif isinstance(value, int):
     759              if value < 0:
     760                  try:
     761                      self._fp.write(struct.pack('>Bq', 0x13, value))
     762                  except struct.error:
     763                      raise OverflowError(value) from None
     764              elif value < 1 << 8:
     765                  self._fp.write(struct.pack('>BB', 0x10, value))
     766              elif value < 1 << 16:
     767                  self._fp.write(struct.pack('>BH', 0x11, value))
     768              elif value < 1 << 32:
     769                  self._fp.write(struct.pack('>BL', 0x12, value))
     770              elif value < 1 << 63:
     771                  self._fp.write(struct.pack('>BQ', 0x13, value))
     772              elif value < 1 << 64:
     773                  self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True))
     774              else:
     775                  raise OverflowError(value)
     776  
     777          elif isinstance(value, float):
     778              self._fp.write(struct.pack('>Bd', 0x23, value))
     779  
     780          elif isinstance(value, datetime.datetime):
     781              f = (value - datetime.datetime(2001, 1, 1)).total_seconds()
     782              self._fp.write(struct.pack('>Bd', 0x33, f))
     783  
     784          elif isinstance(value, (bytes, bytearray)):
     785              self._write_size(0x40, len(value))
     786              self._fp.write(value)
     787  
     788          elif isinstance(value, str):
     789              try:
     790                  t = value.encode('ascii')
     791                  self._write_size(0x50, len(value))
     792              except UnicodeEncodeError:
     793                  t = value.encode('utf-16be')
     794                  self._write_size(0x60, len(t) // 2)
     795  
     796              self._fp.write(t)
     797  
     798          elif isinstance(value, UID):
     799              if value.data < 0:
     800                  raise ValueError("UIDs must be positive")
     801              elif value.data < 1 << 8:
     802                  self._fp.write(struct.pack('>BB', 0x80, value))
     803              elif value.data < 1 << 16:
     804                  self._fp.write(struct.pack('>BH', 0x81, value))
     805              elif value.data < 1 << 32:
     806                  self._fp.write(struct.pack('>BL', 0x83, value))
     807              elif value.data < 1 << 64:
     808                  self._fp.write(struct.pack('>BQ', 0x87, value))
     809              else:
     810                  raise OverflowError(value)
     811  
     812          elif isinstance(value, (list, tuple)):
     813              refs = [self._getrefnum(o) for o in value]
     814              s = len(refs)
     815              self._write_size(0xA0, s)
     816              self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
     817  
     818          elif isinstance(value, dict):
     819              keyRefs, valRefs = [], []
     820  
     821              if self._sort_keys:
     822                  rootItems = sorted(value.items())
     823              else:
     824                  rootItems = value.items()
     825  
     826              for k, v in rootItems:
     827                  if not isinstance(k, str):
     828                      if self._skipkeys:
     829                          continue
     830                      raise TypeError("keys must be strings")
     831                  keyRefs.append(self._getrefnum(k))
     832                  valRefs.append(self._getrefnum(v))
     833  
     834              s = len(keyRefs)
     835              self._write_size(0xD0, s)
     836              self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
     837              self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
     838  
     839          else:
     840              raise TypeError(value)
     841  
     842  
     843  def _is_fmt_binary(header):
     844      return header[:8] == b'bplist00'
     845  
     846  
     847  #
     848  # Generic bits
     849  #
     850  
     851  _FORMATS={
     852      FMT_XML: dict(
     853          detect=_is_fmt_xml,
     854          parser=_PlistParser,
     855          writer=_PlistWriter,
     856      ),
     857      FMT_BINARY: dict(
     858          detect=_is_fmt_binary,
     859          parser=_BinaryPlistParser,
     860          writer=_BinaryPlistWriter,
     861      )
     862  }
     863  
     864  
     865  def load(fp, *, fmt=None, dict_type=dict):
     866      """Read a .plist file. 'fp' should be a readable and binary file object.
     867      Return the unpacked root object (which usually is a dictionary).
     868      """
     869      if fmt is None:
     870          header = fp.read(32)
     871          fp.seek(0)
     872          for info in _FORMATS.values():
     873              if info['detect'](header):
     874                  P = info['parser']
     875                  break
     876  
     877          else:
     878              raise InvalidFileException()
     879  
     880      else:
     881          P = _FORMATS[fmt]['parser']
     882  
     883      p = P(dict_type=dict_type)
     884      return p.parse(fp)
     885  
     886  
     887  def loads(value, *, fmt=None, dict_type=dict):
     888      """Read a .plist file from a bytes object.
     889      Return the unpacked root object (which usually is a dictionary).
     890      """
     891      fp = BytesIO(value)
     892      return load(fp, fmt=fmt, dict_type=dict_type)
     893  
     894  
     895  def dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False):
     896      """Write 'value' to a .plist file. 'fp' should be a writable,
     897      binary file object.
     898      """
     899      if fmt not in _FORMATS:
     900          raise ValueError("Unsupported format: %r"%(fmt,))
     901  
     902      writer = _FORMATS[fmt]["writer"](fp, sort_keys=sort_keys, skipkeys=skipkeys)
     903      writer.write(value)
     904  
     905  
     906  def dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True):
     907      """Return a bytes object with the contents for a .plist file.
     908      """
     909      fp = BytesIO()
     910      dump(value, fp, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)
     911      return fp.getvalue()