Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
corpnewt
GitHub Repository: corpnewt/gibMacOS
Path: blob/master/Scripts/plist.py
175 views
1
### ###
2
# Imports #
3
### ###
4
5
import datetime, os, plistlib, struct, sys, itertools, binascii
6
from io import BytesIO
7
8
if sys.version_info < (3,0):
9
# Force use of StringIO instead of cStringIO as the latter
10
# has issues with Unicode strings
11
from StringIO import StringIO
12
else:
13
from io import StringIO
14
15
try:
16
basestring # Python 2
17
unicode
18
except NameError:
19
basestring = str # Python 3
20
unicode = str
21
22
try:
23
FMT_XML = plistlib.FMT_XML
24
FMT_BINARY = plistlib.FMT_BINARY
25
except AttributeError:
26
FMT_XML = "FMT_XML"
27
FMT_BINARY = "FMT_BINARY"
28
29
### ###
30
# Helper Methods #
31
### ###
32
33
def wrap_data(value):
34
if not _check_py3(): return plistlib.Data(value)
35
return value
36
37
def extract_data(value):
38
if not _check_py3() and isinstance(value,plistlib.Data): return value.data
39
return value
40
41
def _check_py3():
42
return sys.version_info >= (3, 0)
43
44
def _is_binary(fp):
45
if isinstance(fp, basestring):
46
return fp.startswith(b"bplist00")
47
header = fp.read(32)
48
fp.seek(0)
49
return header[:8] == b'bplist00'
50
51
def _seek_past_whitespace(fp):
52
offset = 0
53
while True:
54
byte = fp.read(1)
55
if not byte:
56
# End of file, reset offset and bail
57
offset = 0
58
break
59
if not byte.isspace():
60
# Found our first non-whitespace character
61
break
62
offset += 1
63
# Seek to the first non-whitespace char
64
fp.seek(offset)
65
return offset
66
67
### ###
68
# Deprecated Functions - Remapped #
69
### ###
70
71
def readPlist(pathOrFile):
72
if not isinstance(pathOrFile, basestring):
73
return load(pathOrFile)
74
with open(pathOrFile, "rb") as f:
75
return load(f)
76
77
def writePlist(value, pathOrFile):
78
if not isinstance(pathOrFile, basestring):
79
return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False)
80
with open(pathOrFile, "wb") as f:
81
return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
82
83
### ###
84
# Remapped Functions #
85
### ###
86
87
def load(fp, fmt=None, use_builtin_types=None, dict_type=dict):
88
if _is_binary(fp):
89
use_builtin_types = False if use_builtin_types is None else use_builtin_types
90
try:
91
p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type)
92
except:
93
# Python 3.9 removed use_builtin_types
94
p = _BinaryPlistParser(dict_type=dict_type)
95
return p.parse(fp)
96
elif _check_py3():
97
offset = _seek_past_whitespace(fp)
98
use_builtin_types = True if use_builtin_types is None else use_builtin_types
99
# We need to monkey patch this to allow for hex integers - code taken/modified from
100
# https://github.com/python/cpython/blob/3.8/Lib/plistlib.py
101
if fmt is None:
102
header = fp.read(32)
103
fp.seek(offset)
104
for info in plistlib._FORMATS.values():
105
if info['detect'](header):
106
P = info['parser']
107
break
108
else:
109
raise plistlib.InvalidFileException()
110
else:
111
P = plistlib._FORMATS[fmt]['parser']
112
try:
113
p = P(use_builtin_types=use_builtin_types, dict_type=dict_type)
114
except:
115
# Python 3.9 removed use_builtin_types
116
p = P(dict_type=dict_type)
117
if isinstance(p,plistlib._PlistParser):
118
# Monkey patch!
119
def end_integer():
120
d = p.get_data()
121
value = int(d,16) if d.lower().startswith("0x") else int(d)
122
if -1 << 63 <= value < 1 << 64:
123
p.add_object(value)
124
else:
125
raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber))
126
def end_data():
127
try:
128
p.add_object(plistlib._decode_base64(p.get_data()))
129
except Exception as e:
130
raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e))
131
p.end_integer = end_integer
132
p.end_data = end_data
133
return p.parse(fp)
134
else:
135
offset = _seek_past_whitespace(fp)
136
# Is not binary - assume a string - and try to load
137
# We avoid using readPlistFromString() as that uses
138
# cStringIO and fails when Unicode strings are detected
139
# Don't subclass - keep the parser local
140
from xml.parsers.expat import ParserCreate
141
# Create a new PlistParser object - then we need to set up
142
# the values and parse.
143
p = plistlib.PlistParser()
144
parser = ParserCreate()
145
parser.StartElementHandler = p.handleBeginElement
146
parser.EndElementHandler = p.handleEndElement
147
parser.CharacterDataHandler = p.handleData
148
# We also need to monkey patch this to allow for other dict_types, hex int support
149
# proper line output for data errors, and for unicode string decoding
150
def begin_dict(attrs):
151
d = dict_type()
152
p.addObject(d)
153
p.stack.append(d)
154
def end_integer():
155
d = p.getData()
156
value = int(d,16) if d.lower().startswith("0x") else int(d)
157
if -1 << 63 <= value < 1 << 64:
158
p.addObject(value)
159
else:
160
raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber))
161
def end_data():
162
try:
163
p.addObject(plistlib.Data.fromBase64(p.getData()))
164
except Exception as e:
165
raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e))
166
def end_string():
167
d = p.getData()
168
if isinstance(d,unicode):
169
d = d.encode("utf-8")
170
p.addObject(d)
171
p.begin_dict = begin_dict
172
p.end_integer = end_integer
173
p.end_data = end_data
174
p.end_string = end_string
175
if isinstance(fp, unicode):
176
# Encode unicode -> string; use utf-8 for safety
177
fp = fp.encode("utf-8")
178
if isinstance(fp, basestring):
179
# It's a string - let's wrap it up
180
fp = StringIO(fp)
181
# Parse it
182
parser.ParseFile(fp)
183
return p.root
184
185
def loads(value, fmt=None, use_builtin_types=None, dict_type=dict):
186
if _check_py3() and isinstance(value, basestring):
187
# If it's a string - encode it
188
value = value.encode()
189
try:
190
return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type)
191
except:
192
# Python 3.9 removed use_builtin_types
193
return load(BytesIO(value),fmt=fmt,dict_type=dict_type)
194
195
def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False):
196
if fmt == FMT_BINARY:
197
# Assume binary at this point
198
writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys)
199
writer.write(value)
200
elif fmt == FMT_XML:
201
if _check_py3():
202
plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys)
203
else:
204
# We need to monkey patch a bunch here too in order to avoid auto-sorting
205
# of keys
206
writer = plistlib.PlistWriter(fp)
207
def writeDict(d):
208
if d:
209
writer.beginElement("dict")
210
items = sorted(d.items()) if sort_keys else d.items()
211
for key, value in items:
212
if not isinstance(key, basestring):
213
if skipkeys:
214
continue
215
raise TypeError("keys must be strings")
216
writer.simpleElement("key", key)
217
writer.writeValue(value)
218
writer.endElement("dict")
219
else:
220
writer.simpleElement("dict")
221
writer.writeDict = writeDict
222
writer.writeln("<plist version=\"1.0\">")
223
writer.writeValue(value)
224
writer.writeln("</plist>")
225
else:
226
# Not a proper format
227
raise ValueError("Unsupported format: {}".format(fmt))
228
229
def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True):
230
# We avoid using writePlistToString() as that uses
231
# cStringIO and fails when Unicode strings are detected
232
f = BytesIO() if _check_py3() else StringIO()
233
dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)
234
value = f.getvalue()
235
if _check_py3():
236
value = value.decode("utf-8")
237
return value
238
239
### ###
240
# Binary Plist Stuff For Py2 #
241
### ###
242
243
# From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.11/Lib/plistlib.py
244
# Tweaked to function on both Python 2 and 3
245
246
class UID:
247
def __init__(self, data):
248
if not isinstance(data, int):
249
raise TypeError("data must be an int")
250
# It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in
251
# CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically
252
# allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints,
253
# with the sole function hinting at 64-bits appearing to be a leftover from copying
254
# and pasting integer handling code internally, and this code has not changed since
255
# it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a
256
# 32-bit unsigned int.)
257
#
258
# if data >= 1 << 64:
259
# raise ValueError("UIDs cannot be >= 2**64")
260
if data >= 1 << 32:
261
raise ValueError("UIDs cannot be >= 2**32 (4294967296)")
262
if data < 0:
263
raise ValueError("UIDs must be positive")
264
self.data = data
265
266
def __index__(self):
267
return self.data
268
269
def __repr__(self):
270
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
271
272
def __reduce__(self):
273
return self.__class__, (self.data,)
274
275
def __eq__(self, other):
276
if not isinstance(other, UID):
277
return NotImplemented
278
return self.data == other.data
279
280
def __hash__(self):
281
return hash(self.data)
282
283
class InvalidFileException (ValueError):
284
def __init__(self, message="Invalid file"):
285
ValueError.__init__(self, message)
286
287
_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}
288
289
_undefined = object()
290
291
class _BinaryPlistParser:
292
"""
293
Read or write a binary plist file, following the description of the binary
294
format. Raise InvalidFileException in case of error, otherwise return the
295
root object.
296
see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c
297
"""
298
def __init__(self, use_builtin_types, dict_type):
299
self._use_builtin_types = use_builtin_types
300
self._dict_type = dict_type
301
302
def parse(self, fp):
303
try:
304
# The basic file format:
305
# HEADER
306
# object...
307
# refid->offset...
308
# TRAILER
309
self._fp = fp
310
self._fp.seek(-32, os.SEEK_END)
311
trailer = self._fp.read(32)
312
if len(trailer) != 32:
313
raise InvalidFileException()
314
(
315
offset_size, self._ref_size, num_objects, top_object,
316
offset_table_offset
317
) = struct.unpack('>6xBBQQQ', trailer)
318
self._fp.seek(offset_table_offset)
319
self._object_offsets = self._read_ints(num_objects, offset_size)
320
self._objects = [_undefined] * num_objects
321
return self._read_object(top_object)
322
323
except (OSError, IndexError, struct.error, OverflowError,
324
UnicodeDecodeError):
325
raise InvalidFileException()
326
327
def _get_size(self, tokenL):
328
""" return the size of the next object."""
329
if tokenL == 0xF:
330
m = self._fp.read(1)[0]
331
if not _check_py3():
332
m = ord(m)
333
m = m & 0x3
334
s = 1 << m
335
f = '>' + _BINARY_FORMAT[s]
336
return struct.unpack(f, self._fp.read(s))[0]
337
338
return tokenL
339
340
def _read_ints(self, n, size):
341
data = self._fp.read(size * n)
342
if size in _BINARY_FORMAT:
343
return struct.unpack('>' + _BINARY_FORMAT[size] * n, data)
344
else:
345
if not size or len(data) != size * n:
346
raise InvalidFileException()
347
return tuple(int(binascii.hexlify(data[i: i + size]),16)
348
for i in range(0, size * n, size))
349
'''return tuple(int.from_bytes(data[i: i + size], 'big')
350
for i in range(0, size * n, size))'''
351
352
def _read_refs(self, n):
353
return self._read_ints(n, self._ref_size)
354
355
def _read_object(self, ref):
356
"""
357
read the object by reference.
358
May recursively read sub-objects (content of an array/dict/set)
359
"""
360
result = self._objects[ref]
361
if result is not _undefined:
362
return result
363
364
offset = self._object_offsets[ref]
365
self._fp.seek(offset)
366
token = self._fp.read(1)[0]
367
if not _check_py3():
368
token = ord(token)
369
tokenH, tokenL = token & 0xF0, token & 0x0F
370
371
if token == 0x00: # \x00 or 0x00
372
result = None
373
374
elif token == 0x08: # \x08 or 0x08
375
result = False
376
377
elif token == 0x09: # \x09 or 0x09
378
result = True
379
380
# The referenced source code also mentions URL (0x0c, 0x0d) and
381
# UUID (0x0e), but neither can be generated using the Cocoa libraries.
382
383
elif token == 0x0f: # \x0f or 0x0f
384
result = b''
385
386
elif tokenH == 0x10: # int
387
result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16)
388
if tokenL >= 3: # Signed - adjust
389
result = result-((result & 0x8000000000000000) << 1)
390
391
elif token == 0x22: # real
392
result = struct.unpack('>f', self._fp.read(4))[0]
393
394
elif token == 0x23: # real
395
result = struct.unpack('>d', self._fp.read(8))[0]
396
397
elif token == 0x33: # date
398
f = struct.unpack('>d', self._fp.read(8))[0]
399
# timestamp 0 of binary plists corresponds to 1/1/2001
400
# (year of Mac OS X 10.0), instead of 1/1/1970.
401
result = (datetime.datetime(2001, 1, 1) +
402
datetime.timedelta(seconds=f))
403
404
elif tokenH == 0x40: # data
405
s = self._get_size(tokenL)
406
if self._use_builtin_types or not hasattr(plistlib, "Data"):
407
result = self._fp.read(s)
408
else:
409
result = plistlib.Data(self._fp.read(s))
410
411
elif tokenH == 0x50: # ascii string
412
s = self._get_size(tokenL)
413
result = self._fp.read(s).decode('ascii')
414
result = result
415
416
elif tokenH == 0x60: # unicode string
417
s = self._get_size(tokenL)
418
result = self._fp.read(s * 2).decode('utf-16be')
419
420
elif tokenH == 0x80: # UID
421
# used by Key-Archiver plist files
422
result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16))
423
424
elif tokenH == 0xA0: # array
425
s = self._get_size(tokenL)
426
obj_refs = self._read_refs(s)
427
result = []
428
self._objects[ref] = result
429
result.extend(self._read_object(x) for x in obj_refs)
430
431
# tokenH == 0xB0 is documented as 'ordset', but is not actually
432
# implemented in the Apple reference code.
433
434
# tokenH == 0xC0 is documented as 'set', but sets cannot be used in
435
# plists.
436
437
elif tokenH == 0xD0: # dict
438
s = self._get_size(tokenL)
439
key_refs = self._read_refs(s)
440
obj_refs = self._read_refs(s)
441
result = self._dict_type()
442
self._objects[ref] = result
443
for k, o in zip(key_refs, obj_refs):
444
key = self._read_object(k)
445
if hasattr(plistlib, "Data") and isinstance(key, plistlib.Data):
446
key = key.data
447
result[key] = self._read_object(o)
448
449
else:
450
raise InvalidFileException()
451
452
self._objects[ref] = result
453
return result
454
455
def _count_to_size(count):
456
if count < 1 << 8:
457
return 1
458
459
elif count < 1 << 16:
460
return 2
461
462
elif count < 1 << 32:
463
return 4
464
465
else:
466
return 8
467
468
_scalars = (str, int, float, datetime.datetime, bytes)
469
470
class _BinaryPlistWriter (object):
471
def __init__(self, fp, sort_keys, skipkeys):
472
self._fp = fp
473
self._sort_keys = sort_keys
474
self._skipkeys = skipkeys
475
476
def write(self, value):
477
478
# Flattened object list:
479
self._objlist = []
480
481
# Mappings from object->objectid
482
# First dict has (type(object), object) as the key,
483
# second dict is used when object is not hashable and
484
# has id(object) as the key.
485
self._objtable = {}
486
self._objidtable = {}
487
488
# Create list of all objects in the plist
489
self._flatten(value)
490
491
# Size of object references in serialized containers
492
# depends on the number of objects in the plist.
493
num_objects = len(self._objlist)
494
self._object_offsets = [0]*num_objects
495
self._ref_size = _count_to_size(num_objects)
496
497
self._ref_format = _BINARY_FORMAT[self._ref_size]
498
499
# Write file header
500
self._fp.write(b'bplist00')
501
502
# Write object list
503
for obj in self._objlist:
504
self._write_object(obj)
505
506
# Write refnum->object offset table
507
top_object = self._getrefnum(value)
508
offset_table_offset = self._fp.tell()
509
offset_size = _count_to_size(offset_table_offset)
510
offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
511
self._fp.write(struct.pack(offset_format, *self._object_offsets))
512
513
# Write trailer
514
sort_version = 0
515
trailer = (
516
sort_version, offset_size, self._ref_size, num_objects,
517
top_object, offset_table_offset
518
)
519
self._fp.write(struct.pack('>5xBBBQQQ', *trailer))
520
521
def _flatten(self, value):
522
# First check if the object is in the object table, not used for
523
# containers to ensure that two subcontainers with the same contents
524
# will be serialized as distinct values.
525
if isinstance(value, _scalars):
526
if (type(value), value) in self._objtable:
527
return
528
529
elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data):
530
if (type(value.data), value.data) in self._objtable:
531
return
532
533
elif id(value) in self._objidtable:
534
return
535
536
# Add to objectreference map
537
refnum = len(self._objlist)
538
self._objlist.append(value)
539
if isinstance(value, _scalars):
540
self._objtable[(type(value), value)] = refnum
541
elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data):
542
self._objtable[(type(value.data), value.data)] = refnum
543
else:
544
self._objidtable[id(value)] = refnum
545
546
# And finally recurse into containers
547
if isinstance(value, dict):
548
keys = []
549
values = []
550
items = value.items()
551
if self._sort_keys:
552
items = sorted(items)
553
554
for k, v in items:
555
if not isinstance(k, basestring):
556
if self._skipkeys:
557
continue
558
raise TypeError("keys must be strings")
559
keys.append(k)
560
values.append(v)
561
562
for o in itertools.chain(keys, values):
563
self._flatten(o)
564
565
elif isinstance(value, (list, tuple)):
566
for o in value:
567
self._flatten(o)
568
569
def _getrefnum(self, value):
570
if isinstance(value, _scalars):
571
return self._objtable[(type(value), value)]
572
elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data):
573
return self._objtable[(type(value.data), value.data)]
574
else:
575
return self._objidtable[id(value)]
576
577
def _write_size(self, token, size):
578
if size < 15:
579
self._fp.write(struct.pack('>B', token | size))
580
581
elif size < 1 << 8:
582
self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))
583
584
elif size < 1 << 16:
585
self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))
586
587
elif size < 1 << 32:
588
self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))
589
590
else:
591
self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))
592
593
def _write_object(self, value):
594
ref = self._getrefnum(value)
595
self._object_offsets[ref] = self._fp.tell()
596
if value is None:
597
self._fp.write(b'\x00')
598
599
elif value is False:
600
self._fp.write(b'\x08')
601
602
elif value is True:
603
self._fp.write(b'\x09')
604
605
elif isinstance(value, int):
606
if value < 0:
607
try:
608
self._fp.write(struct.pack('>Bq', 0x13, value))
609
except struct.error:
610
raise OverflowError(value) # from None
611
elif value < 1 << 8:
612
self._fp.write(struct.pack('>BB', 0x10, value))
613
elif value < 1 << 16:
614
self._fp.write(struct.pack('>BH', 0x11, value))
615
elif value < 1 << 32:
616
self._fp.write(struct.pack('>BL', 0x12, value))
617
elif value < 1 << 63:
618
self._fp.write(struct.pack('>BQ', 0x13, value))
619
elif value < 1 << 64:
620
self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True))
621
else:
622
raise OverflowError(value)
623
624
elif isinstance(value, float):
625
self._fp.write(struct.pack('>Bd', 0x23, value))
626
627
elif isinstance(value, datetime.datetime):
628
f = (value - datetime.datetime(2001, 1, 1)).total_seconds()
629
self._fp.write(struct.pack('>Bd', 0x33, f))
630
631
elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, "Data") and isinstance(value, plistlib.Data)):
632
if not isinstance(value, (bytes, bytearray)):
633
value = value.data # Unpack it
634
self._write_size(0x40, len(value))
635
self._fp.write(value)
636
637
elif isinstance(value, basestring):
638
try:
639
t = value.encode('ascii')
640
self._write_size(0x50, len(value))
641
except UnicodeEncodeError:
642
t = value.encode('utf-16be')
643
self._write_size(0x60, len(t) // 2)
644
self._fp.write(t)
645
646
elif isinstance(value, UID) or (hasattr(plistlib,"UID") and isinstance(value, plistlib.UID)):
647
if value.data < 0:
648
raise ValueError("UIDs must be positive")
649
elif value.data < 1 << 8:
650
self._fp.write(struct.pack('>BB', 0x80, value))
651
elif value.data < 1 << 16:
652
self._fp.write(struct.pack('>BH', 0x81, value))
653
elif value.data < 1 << 32:
654
self._fp.write(struct.pack('>BL', 0x83, value))
655
# elif value.data < 1 << 64:
656
# self._fp.write(struct.pack('>BQ', 0x87, value))
657
else:
658
raise OverflowError(value)
659
660
elif isinstance(value, (list, tuple)):
661
refs = [self._getrefnum(o) for o in value]
662
s = len(refs)
663
self._write_size(0xA0, s)
664
self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
665
666
elif isinstance(value, dict):
667
keyRefs, valRefs = [], []
668
669
if self._sort_keys:
670
rootItems = sorted(value.items())
671
else:
672
rootItems = value.items()
673
674
for k, v in rootItems:
675
if not isinstance(k, basestring):
676
if self._skipkeys:
677
continue
678
raise TypeError("keys must be strings")
679
keyRefs.append(self._getrefnum(k))
680
valRefs.append(self._getrefnum(v))
681
682
s = len(keyRefs)
683
self._write_size(0xD0, s)
684
self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
685
self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
686
687
else:
688
raise TypeError(value)
689
690