blob: db5eea1346f2b922eaecb1fb9da5e545b507b07b [file] [log] [blame]
Christian Heimes7e182542008-01-27 15:20:13 +00001"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
2
3The PropertList (.plist) file format is a simple XML pickle supporting
4basic object types, like dictionaries, lists, numbers and strings.
5Usually the top level object is a dictionary.
6
7To write out a plist file, use the writePlist(rootObject, pathOrFile)
8function. 'rootObject' is the top level object, 'pathOrFile' is a
9filename or a (writable) file object.
10
11To parse a plist from a file, use the readPlist(pathOrFile) function,
12with a file name or a (readable) file object as the only argument. It
13returns the top level object (again, usually a dictionary).
14
15To work with plist data in bytes objects, you can use readPlistFromBytes()
16and writePlistToBytes().
17
18Values can be strings, integers, floats, booleans, tuples, lists,
19dictionaries, Data or datetime.datetime objects. String values (including
20dictionary keys) may be unicode strings -- they will be written out as
21UTF-8.
22
23The <data> plist type is supported through the Data class. This is a
24thin wrapper around a Python bytes object.
25
26Generate Plist example:
27
28 pl = dict(
29 aString="Doodah",
30 aList=["A", "B", 12, 32.1, [1, 2, 3]],
31 aFloat = 0.1,
32 anInt = 728,
33 aDict=dict(
34 anotherString="<hello & hi there!>",
35 aUnicodeValue=u'M\xe4ssig, Ma\xdf',
36 aTrueValue=True,
37 aFalseValue=False,
38 ),
39 someData = Data(b"<binary gunk>"),
40 someMoreData = Data(b"<lots of binary gunk>" * 10),
41 aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
42 )
43 # unicode keys are possible, but a little awkward to use:
44 pl[u'\xc5benraa'] = "That was a unicode key."
45 writePlist(pl, fileName)
46
47Parse Plist example:
48
49 pl = readPlist(pathOrFile)
50 print pl["aKey"]
51"""
52
53
54__all__ = [
55 "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
56 "readPlistFromResource", "writePlistToResource",
57 "Plist", "Data", "Dict"
58]
59# Note: the Plist and Dict classes have been deprecated.
60
61import binascii
62import datetime
63from io import BytesIO
64import re
65
66
67def readPlist(pathOrFile):
68 """Read a .plist file. 'pathOrFile' may either be a file name or a
69 (readable) file object. Return the unpacked root object (which
70 usually is a dictionary).
71 """
72 didOpen = False
73 if isinstance(pathOrFile, str):
74 pathOrFile = open(pathOrFile, 'rb')
75 didOpen = True
76 p = PlistParser()
77 rootObject = p.parse(pathOrFile)
78 if didOpen:
79 pathOrFile.close()
80 return rootObject
81
82
83def writePlist(rootObject, pathOrFile):
84 """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a
85 file name or a (writable) file object.
86 """
87 didOpen = False
88 if isinstance(pathOrFile, str):
89 pathOrFile = open(pathOrFile, 'wb')
90 didOpen = True
91 writer = PlistWriter(pathOrFile)
92 writer.writeln("<plist version=\"1.0\">")
93 writer.writeValue(rootObject)
94 writer.writeln("</plist>")
95 if didOpen:
96 pathOrFile.close()
97
98
99def readPlistFromBytes(data):
100 """Read a plist data from a bytes object. Return the root object.
101 """
102 return readPlist(BytesIO(data))
103
104
105def writePlistToBytes(rootObject):
106 """Return 'rootObject' as a plist-formatted bytes object.
107 """
108 f = BytesIO()
109 writePlist(rootObject, f)
110 return f.getvalue()
111
112
113def readPlistFromResource(path, restype='plst', resid=0):
114 """Read plst resource from the resource fork of path.
115 """
116 from Carbon.File import FSRef, FSGetResourceForkName
117 from Carbon.Files import fsRdPerm
118 from Carbon import Res
119 fsRef = FSRef(path)
120 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdPerm)
121 Res.UseResFile(resNum)
122 plistData = Res.Get1Resource(restype, resid).data
123 Res.CloseResFile(resNum)
124 return readPlistFromString(plistData)
125
126
127def writePlistToResource(rootObject, path, restype='plst', resid=0):
128 """Write 'rootObject' as a plst resource to the resource fork of path.
129 """
130 from Carbon.File import FSRef, FSGetResourceForkName
131 from Carbon.Files import fsRdWrPerm
132 from Carbon import Res
133 plistData = writePlistToString(rootObject)
134 fsRef = FSRef(path)
135 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdWrPerm)
136 Res.UseResFile(resNum)
137 try:
138 Res.Get1Resource(restype, resid).RemoveResource()
139 except Res.Error:
140 pass
141 res = Res.Resource(plistData)
142 res.AddResource(restype, resid, '')
143 res.WriteResource()
144 Res.CloseResFile(resNum)
145
146
147class DumbXMLWriter:
148 def __init__(self, file, indentLevel=0, indent="\t"):
149 self.file = file
150 self.stack = []
151 self.indentLevel = indentLevel
152 self.indent = indent
153
154 def beginElement(self, element):
155 self.stack.append(element)
156 self.writeln("<%s>" % element)
157 self.indentLevel += 1
158
159 def endElement(self, element):
160 assert self.indentLevel > 0
161 assert self.stack.pop() == element
162 self.indentLevel -= 1
163 self.writeln("</%s>" % element)
164
165 def simpleElement(self, element, value=None):
166 if value is not None:
167 value = _escape(value)
168 self.writeln("<%s>%s</%s>" % (element, value, element))
169 else:
170 self.writeln("<%s/>" % element)
171
172 def writeln(self, line):
173 if line:
174 # plist has fixed encoding of utf-8
175 if isinstance(line, str):
176 line = line.encode('utf-8')
177 self.file.write(self.indentLevel * self.indent)
178 self.file.write(line)
179 self.file.write(b'\n')
180
181
182# Contents should conform to a subset of ISO 8601
183# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with
184# a loss of precision)
185_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")
186
187def _dateFromString(s):
188 order = ('year', 'month', 'day', 'hour', 'minute', 'second')
189 gd = _dateParser.match(s).groupdict()
190 lst = []
191 for key in order:
192 val = gd[key]
193 if val is None:
194 break
195 lst.append(int(val))
196 return datetime.datetime(*lst)
197
198def _dateToString(d):
199 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
200 d.year, d.month, d.day,
201 d.hour, d.minute, d.second
202 )
203
204
205# Regex to find any control chars, except for \t \n and \r
206_controlCharPat = re.compile(
207 r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
208 r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
209
210def _escape(text):
211 m = _controlCharPat.search(text)
212 if m is not None:
213 raise ValueError("strings can't contains control characters; "
214 "use plistlib.Data instead")
215 text = text.replace("\r\n", "\n") # convert DOS line endings
216 text = text.replace("\r", "\n") # convert Mac line endings
217 text = text.replace("&", "&amp;") # escape '&'
218 text = text.replace("<", "&lt;") # escape '<'
219 text = text.replace(">", "&gt;") # escape '>'
220 return text
221
222
223PLISTHEADER = b"""\
224<?xml version="1.0" encoding="UTF-8"?>
225<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
226"""
227
228class PlistWriter(DumbXMLWriter):
229
230 def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1):
231 if writeHeader:
232 file.write(PLISTHEADER)
233 DumbXMLWriter.__init__(self, file, indentLevel, indent)
234
235 def writeValue(self, value):
236 if isinstance(value, str):
237 self.simpleElement("string", value)
238 elif isinstance(value, bool):
239 # must switch for bool before int, as bool is a
240 # subclass of int...
241 if value:
242 self.simpleElement("true")
243 else:
244 self.simpleElement("false")
245 elif isinstance(value, int):
246 self.simpleElement("integer", "%d" % value)
247 elif isinstance(value, float):
248 self.simpleElement("real", repr(value))
249 elif isinstance(value, dict):
250 self.writeDict(value)
251 elif isinstance(value, Data):
252 self.writeData(value)
253 elif isinstance(value, datetime.datetime):
254 self.simpleElement("date", _dateToString(value))
255 elif isinstance(value, (tuple, list)):
256 self.writeArray(value)
257 else:
258 raise TypeError("unsuported type: %s" % type(value))
259
260 def writeData(self, data):
261 self.beginElement("data")
262 self.indentLevel -= 1
263 maxlinelength = 76 - len(self.indent.replace(b"\t", b" " * 8) *
264 self.indentLevel)
265 for line in data.asBase64(maxlinelength).split(b"\n"):
266 if line:
267 self.writeln(line)
268 self.indentLevel += 1
269 self.endElement("data")
270
271 def writeDict(self, d):
272 self.beginElement("dict")
273 items = sorted(d.items())
274 for key, value in items:
275 if not isinstance(key, str):
276 raise TypeError("keys must be strings")
277 self.simpleElement("key", key)
278 self.writeValue(value)
279 self.endElement("dict")
280
281 def writeArray(self, array):
282 self.beginElement("array")
283 for value in array:
284 self.writeValue(value)
285 self.endElement("array")
286
287
288class _InternalDict(dict):
289
290 # This class is needed while Dict is scheduled for deprecation:
291 # we only need to warn when a *user* instantiates Dict or when
292 # the "attribute notation for dict keys" is used.
293
294 def __getattr__(self, attr):
295 try:
296 value = self[attr]
297 except KeyError:
298 raise AttributeError(attr)
299 from warnings import warn
300 warn("Attribute access from plist dicts is deprecated, use d[key] "
301 "notation instead", PendingDeprecationWarning)
302 return value
303
304 def __setattr__(self, attr, value):
305 from warnings import warn
306 warn("Attribute access from plist dicts is deprecated, use d[key] "
307 "notation instead", PendingDeprecationWarning)
308 self[attr] = value
309
310 def __delattr__(self, attr):
311 try:
312 del self[attr]
313 except KeyError:
314 raise AttributeError(attr)
315 from warnings import warn
316 warn("Attribute access from plist dicts is deprecated, use d[key] "
317 "notation instead", PendingDeprecationWarning)
318
319class Dict(_InternalDict):
320
321 def __init__(self, **kwargs):
322 from warnings import warn
323 warn("The plistlib.Dict class is deprecated, use builtin dict instead",
324 PendingDeprecationWarning)
325 super().__init__(**kwargs)
326
327
328class Plist(_InternalDict):
329
330 """This class has been deprecated. Use readPlist() and writePlist()
331 functions instead, together with regular dict objects.
332 """
333
334 def __init__(self, **kwargs):
335 from warnings import warn
336 warn("The Plist class is deprecated, use the readPlist() and "
337 "writePlist() functions instead", PendingDeprecationWarning)
338 super().__init__(**kwargs)
339
340 def fromFile(cls, pathOrFile):
341 """Deprecated. Use the readPlist() function instead."""
342 rootObject = readPlist(pathOrFile)
343 plist = cls()
344 plist.update(rootObject)
345 return plist
346 fromFile = classmethod(fromFile)
347
348 def write(self, pathOrFile):
349 """Deprecated. Use the writePlist() function instead."""
350 writePlist(self, pathOrFile)
351
352
353def _encodeBase64(s, maxlinelength=76):
354 # copied from base64.encodestring(), with added maxlinelength argument
355 maxbinsize = (maxlinelength//4)*3
356 pieces = []
357 for i in range(0, len(s), maxbinsize):
358 chunk = s[i : i + maxbinsize]
359 pieces.append(binascii.b2a_base64(chunk))
360 return b''.join(pieces)
361
362class Data:
363
364 """Wrapper for binary data."""
365
366 def __init__(self, data):
367 if not isinstance(data, bytes):
368 raise TypeError("data must be as bytes")
369 self.data = data
370
371 @classmethod
372 def fromBase64(cls, data):
373 # base64.decodestring just calls binascii.a2b_base64;
374 # it seems overkill to use both base64 and binascii.
375 return cls(binascii.a2b_base64(data))
376
377 def asBase64(self, maxlinelength=76):
378 return _encodeBase64(self.data, maxlinelength)
379
380 def __eq__(self, other):
381 if isinstance(other, self.__class__):
382 return self.data == other.data
383 elif isinstance(other, str):
384 return self.data == other
385 else:
386 return id(self) == id(other)
387
388 def __repr__(self):
389 return "%s(%s)" % (self.__class__.__name__, repr(self.data))
390
391
392class PlistParser:
393
394 def __init__(self):
395 self.stack = []
396 self.currentKey = None
397 self.root = None
398
399 def parse(self, fileobj):
400 from xml.parsers.expat import ParserCreate
401 parser = ParserCreate()
402 parser.StartElementHandler = self.handleBeginElement
403 parser.EndElementHandler = self.handleEndElement
404 parser.CharacterDataHandler = self.handleData
405 parser.ParseFile(fileobj)
406 return self.root
407
408 def handleBeginElement(self, element, attrs):
409 self.data = []
410 handler = getattr(self, "begin_" + element, None)
411 if handler is not None:
412 handler(attrs)
413
414 def handleEndElement(self, element):
415 handler = getattr(self, "end_" + element, None)
416 if handler is not None:
417 handler()
418
419 def handleData(self, data):
420 self.data.append(data)
421
422 def addObject(self, value):
423 if self.currentKey is not None:
424 self.stack[-1][self.currentKey] = value
425 self.currentKey = None
426 elif not self.stack:
427 # this is the root object
428 self.root = value
429 else:
430 self.stack[-1].append(value)
431
432 def getData(self):
433 data = ''.join(self.data)
434 self.data = []
435 return data
436
437 # element handlers
438
439 def begin_dict(self, attrs):
440 d = _InternalDict()
441 self.addObject(d)
442 self.stack.append(d)
443 def end_dict(self):
444 self.stack.pop()
445
446 def end_key(self):
447 self.currentKey = self.getData()
448
449 def begin_array(self, attrs):
450 a = []
451 self.addObject(a)
452 self.stack.append(a)
453 def end_array(self):
454 self.stack.pop()
455
456 def end_true(self):
457 self.addObject(True)
458 def end_false(self):
459 self.addObject(False)
460 def end_integer(self):
461 self.addObject(int(self.getData()))
462 def end_real(self):
463 self.addObject(float(self.getData()))
464 def end_string(self):
465 self.addObject(self.getData())
466 def end_data(self):
467 self.addObject(Data.fromBase64(self.getData().encode("utf-8")))
468 def end_date(self):
469 self.addObject(_dateFromString(self.getData()))