Skip to content

Commit c981ad1

Browse files
bigfootjonserhiy-storchaka
authored andcommitted
bpo-26707: Enable plistlib to read UID keys. (pythonGH-12153)
Plistlib currently throws an exception when asked to decode a valid .plist file that was generated by Apple's NSKeyedArchiver. Specifically, this is caused by a byte 0x80 (signifying a UID) not being understood. This fixes the problem by enabling the binary plist reader and writer to read and write plistlib.UID objects.
1 parent e307e5c commit c981ad1

File tree

7 files changed

+169
-5
lines changed

7 files changed

+169
-5
lines changed

Doc/library/plistlib.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ or :class:`datetime.datetime` objects.
3636
.. versionchanged:: 3.4
3737
New API, old API deprecated. Support for binary format plists added.
3838

39+
.. versionchanged:: 3.8
40+
Support added for reading and writing :class:`UID` tokens in binary plists as used
41+
by NSKeyedArchiver and NSKeyedUnarchiver.
42+
3943
.. seealso::
4044

4145
`PList manual page <https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/>`_
@@ -179,6 +183,16 @@ The following classes are available:
179183

180184
.. deprecated:: 3.4 Use a :class:`bytes` object instead.
181185

186+
.. class:: UID(data)
187+
188+
Wraps an :class:`int`. This is used when reading or writing NSKeyedArchiver
189+
encoded data, which contains UID (see PList manual).
190+
191+
It has one attribute, :attr:`data` which can be used to retrieve the int value
192+
of the UID. :attr:`data` must be in the range `0 <= data <= 2**64`.
193+
194+
.. versionadded:: 3.8
195+
182196

183197
The following constants are available:
184198

Doc/whatsnew/3.8.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,14 @@ to a path.
394394
(Contributed by Joannah Nanjekye in :issue:`26978`)
395395

396396

397+
plistlib
398+
--------
399+
400+
Added new :class:`plistlib.UID` and enabled support for reading and writing
401+
NSKeyedArchiver-encoded binary plists.
402+
(Contributed by Jon Janzen in :issue:`26707`.)
403+
404+
397405
socket
398406
------
399407

Lib/plistlib.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
__all__ = [
4949
"readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
5050
"Data", "InvalidFileException", "FMT_XML", "FMT_BINARY",
51-
"load", "dump", "loads", "dumps"
51+
"load", "dump", "loads", "dumps", "UID"
5252
]
5353

5454
import binascii
@@ -175,6 +175,34 @@ def __repr__(self):
175175
#
176176

177177

178+
class UID:
179+
def __init__(self, data):
180+
if not isinstance(data, int):
181+
raise TypeError("data must be an int")
182+
if data >= 1 << 64:
183+
raise ValueError("UIDs cannot be >= 2**64")
184+
if data < 0:
185+
raise ValueError("UIDs must be positive")
186+
self.data = data
187+
188+
def __index__(self):
189+
return self.data
190+
191+
def __repr__(self):
192+
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
193+
194+
def __reduce__(self):
195+
return self.__class__, (self.data,)
196+
197+
def __eq__(self, other):
198+
if not isinstance(other, UID):
199+
return NotImplemented
200+
return self.data == other.data
201+
202+
def __hash__(self):
203+
return hash(self.data)
204+
205+
178206
#
179207
# XML support
180208
#
@@ -649,8 +677,9 @@ def _read_object(self, ref):
649677
s = self._get_size(tokenL)
650678
result = self._fp.read(s * 2).decode('utf-16be')
651679

652-
# tokenH == 0x80 is documented as 'UID' and appears to be used for
653-
# keyed-archiving, not in plists.
680+
elif tokenH == 0x80: # UID
681+
# used by Key-Archiver plist files
682+
result = UID(int.from_bytes(self._fp.read(1 + tokenL), 'big'))
654683

655684
elif tokenH == 0xA0: # array
656685
s = self._get_size(tokenL)
@@ -874,6 +903,20 @@ def _write_object(self, value):
874903

875904
self._fp.write(t)
876905

906+
elif isinstance(value, UID):
907+
if value.data < 0:
908+
raise ValueError("UIDs must be positive")
909+
elif value.data < 1 << 8:
910+
self._fp.write(struct.pack('>BB', 0x80, value))
911+
elif value.data < 1 << 16:
912+
self._fp.write(struct.pack('>BH', 0x81, value))
913+
elif value.data < 1 << 32:
914+
self._fp.write(struct.pack('>BL', 0x83, value))
915+
elif value.data < 1 << 64:
916+
self._fp.write(struct.pack('>BQ', 0x87, value))
917+
else:
918+
raise OverflowError(value)
919+
877920
elif isinstance(value, (list, tuple)):
878921
refs = [self._getrefnum(o) for o in value]
879922
s = len(refs)

Lib/test/test_plistlib.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Copyright (C) 2003-2013 Python Software Foundation
2-
2+
import copy
3+
import operator
4+
import pickle
35
import unittest
46
import plistlib
57
import os
@@ -10,6 +12,8 @@
1012
from test import support
1113
from io import BytesIO
1214

15+
from plistlib import UID
16+
1317
ALL_FORMATS=(plistlib.FMT_XML, plistlib.FMT_BINARY)
1418

1519
# The testdata is generated using Mac/Tools/plistlib_generate_testdata.py
@@ -88,6 +92,17 @@
8892
ZwB0AHwAiACUAJoApQCuALsAygDTAOQA7QD4AQQBDwEdASsBNgE3ATgBTwFn
8993
AW4BcAFyAXQBdgF/AYMBhQGHAYwBlQGbAZ0BnwGhAaUBpwGwAbkBwAHBAcIB
9094
xQHHAsQC0gAAAAAAAAIBAAAAAAAAADkAAAAAAAAAAAAAAAAAAALs'''),
95+
'KEYED_ARCHIVE': binascii.a2b_base64(b'''
96+
YnBsaXN0MDDUAQIDBAUGHB1YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVy
97+
VCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVnB5dHlwZVYkY2xhc3NZTlMu
98+
c3RyaW5nEAGAAl8QE0tleUFyY2hpdmUgVUlEIFRlc3TTEBESExQZWiRjbGFz
99+
c25hbWVYJGNsYXNzZXNbJGNsYXNzaGludHNfEBdPQ19CdWlsdGluUHl0aG9u
100+
VW5pY29kZaQVFhcYXxAXT0NfQnVpbHRpblB5dGhvblVuaWNvZGVfEBBPQ19Q
101+
eXRob25Vbmljb2RlWE5TU3RyaW5nWE5TT2JqZWN0ohobXxAPT0NfUHl0aG9u
102+
U3RyaW5nWE5TU3RyaW5nXxAPTlNLZXllZEFyY2hpdmVy0R4fVHJvb3SAAQAI
103+
ABEAGgAjAC0AMgA3ADsAQQBIAE8AVgBgAGIAZAB6AIEAjACVAKEAuwDAANoA
104+
7QD2AP8BAgEUAR0BLwEyATcAAAAAAAACAQAAAAAAAAAgAAAAAAAAAAAAAAAA
105+
AAABOQ=='''),
91106
}
92107

93108

@@ -151,6 +166,14 @@ def test_invalid_type(self):
151166
with self.subTest(fmt=fmt):
152167
self.assertRaises(TypeError, plistlib.dumps, pl, fmt=fmt)
153168

169+
def test_invalid_uid(self):
170+
with self.assertRaises(TypeError):
171+
UID("not an int")
172+
with self.assertRaises(ValueError):
173+
UID(2 ** 64)
174+
with self.assertRaises(ValueError):
175+
UID(-19)
176+
154177
def test_int(self):
155178
for pl in [0, 2**8-1, 2**8, 2**16-1, 2**16, 2**32-1, 2**32,
156179
2**63-1, 2**64-1, 1, -2**63]:
@@ -200,6 +223,45 @@ def test_indentation_dict_mix(self):
200223
data = {'1': {'2': [{'3': [[[[[{'test': b'aaaaaa'}]]]]]}]}}
201224
self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
202225

226+
def test_uid(self):
227+
data = UID(1)
228+
self.assertEqual(plistlib.loads(plistlib.dumps(data, fmt=plistlib.FMT_BINARY)), data)
229+
dict_data = {
230+
'uid0': UID(0),
231+
'uid2': UID(2),
232+
'uid8': UID(2 ** 8),
233+
'uid16': UID(2 ** 16),
234+
'uid32': UID(2 ** 32),
235+
'uid63': UID(2 ** 63)
236+
}
237+
self.assertEqual(plistlib.loads(plistlib.dumps(dict_data, fmt=plistlib.FMT_BINARY)), dict_data)
238+
239+
def test_uid_data(self):
240+
uid = UID(1)
241+
self.assertEqual(uid.data, 1)
242+
243+
def test_uid_eq(self):
244+
self.assertEqual(UID(1), UID(1))
245+
self.assertNotEqual(UID(1), UID(2))
246+
self.assertNotEqual(UID(1), "not uid")
247+
248+
def test_uid_hash(self):
249+
self.assertEqual(hash(UID(1)), hash(UID(1)))
250+
251+
def test_uid_repr(self):
252+
self.assertEqual(repr(UID(1)), "UID(1)")
253+
254+
def test_uid_index(self):
255+
self.assertEqual(operator.index(UID(1)), 1)
256+
257+
def test_uid_pickle(self):
258+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
259+
self.assertEqual(pickle.loads(pickle.dumps(UID(19), protocol=proto)), UID(19))
260+
261+
def test_uid_copy(self):
262+
self.assertEqual(copy.copy(UID(1)), UID(1))
263+
self.assertEqual(copy.deepcopy(UID(1)), UID(1))
264+
203265
def test_appleformatting(self):
204266
for use_builtin_types in (True, False):
205267
for fmt in ALL_FORMATS:
@@ -648,14 +710,46 @@ def test_dataobject_deprecated(self):
648710
self.assertEqual(cur, in_data)
649711

650712

713+
class TestKeyedArchive(unittest.TestCase):
714+
def test_keyed_archive_data(self):
715+
# This is the structure of a NSKeyedArchive packed plist
716+
data = {
717+
'$version': 100000,
718+
'$objects': [
719+
'$null', {
720+
'pytype': 1,
721+
'$class': UID(2),
722+
'NS.string': 'KeyArchive UID Test'
723+
},
724+
{
725+
'$classname': 'OC_BuiltinPythonUnicode',
726+
'$classes': [
727+
'OC_BuiltinPythonUnicode',
728+
'OC_PythonUnicode',
729+
'NSString',
730+
'NSObject'
731+
],
732+
'$classhints': [
733+
'OC_PythonString', 'NSString'
734+
]
735+
}
736+
],
737+
'$archiver': 'NSKeyedArchiver',
738+
'$top': {
739+
'root': UID(1)
740+
}
741+
}
742+
self.assertEqual(plistlib.loads(TESTDATA["KEYED_ARCHIVE"]), data)
743+
744+
651745
class MiscTestCase(unittest.TestCase):
652746
def test__all__(self):
653747
blacklist = {"PlistFormat", "PLISTHEADER"}
654748
support.check__all__(self, plistlib, blacklist=blacklist)
655749

656750

657751
def test_main():
658-
support.run_unittest(TestPlistlib, TestPlistlibDeprecated, MiscTestCase)
752+
support.run_unittest(TestPlistlib, TestPlistlibDeprecated, TestKeyedArchive, MiscTestCase)
659753

660754

661755
if __name__ == '__main__':

Mac/Tools/plistlib_generate_testdata.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from Cocoa import NSPropertyListXMLFormat_v1_0, NSPropertyListBinaryFormat_v1_0
66
from Cocoa import CFUUIDCreateFromString, NSNull, NSUUID, CFPropertyListCreateData
77
from Cocoa import NSURL
8+
from Cocoa import NSKeyedArchiver
89

910
import datetime
1011
from collections import OrderedDict
@@ -89,6 +90,8 @@ def main():
8990
else:
9091
print(" %s: binascii.a2b_base64(b'''\n %s'''),"%(fmt_name, _encode_base64(bytes(data)).decode('ascii')[:-1]))
9192

93+
keyed_archive_data = NSKeyedArchiver.archivedDataWithRootObject_("KeyArchive UID Test")
94+
print(" 'KEYED_ARCHIVE': binascii.a2b_base64(b'''\n %s''')," % (_encode_base64(bytes(keyed_archive_data)).decode('ascii')[:-1]))
9295
print("}")
9396
print()
9497

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ Geert Jansen
754754
Jack Jansen
755755
Hans-Peter Jansen
756756
Bill Janssen
757+
Jon Janzen
757758
Thomas Jarosch
758759
Juhana Jauhiainen
759760
Rajagopalasarma Jayakrishnan
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Enable plistlib to read and write binary plist files that were created as a KeyedArchive file. Specifically, this allows the plistlib to process 0x80 tokens as UID objects.

0 commit comments

Comments
 (0)