bpo-26707: Enable plistlib to read UID keys. (GH-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.
This commit is contained in:
Jon Janzen 2019-05-15 22:14:38 +02:00 committed by Serhiy Storchaka
parent e307e5cd06
commit c981ad16b0
7 changed files with 169 additions and 5 deletions

View file

@ -36,6 +36,10 @@ or :class:`datetime.datetime` objects.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
New API, old API deprecated. Support for binary format plists added. New API, old API deprecated. Support for binary format plists added.
.. versionchanged:: 3.8
Support added for reading and writing :class:`UID` tokens in binary plists as used
by NSKeyedArchiver and NSKeyedUnarchiver.
.. seealso:: .. seealso::
`PList manual page <https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/>`_ `PList manual page <https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/>`_
@ -179,6 +183,16 @@ The following classes are available:
.. deprecated:: 3.4 Use a :class:`bytes` object instead. .. deprecated:: 3.4 Use a :class:`bytes` object instead.
.. class:: UID(data)
Wraps an :class:`int`. This is used when reading or writing NSKeyedArchiver
encoded data, which contains UID (see PList manual).
It has one attribute, :attr:`data` which can be used to retrieve the int value
of the UID. :attr:`data` must be in the range `0 <= data <= 2**64`.
.. versionadded:: 3.8
The following constants are available: The following constants are available:

View file

@ -394,6 +394,14 @@ to a path.
(Contributed by Joannah Nanjekye in :issue:`26978`) (Contributed by Joannah Nanjekye in :issue:`26978`)
plistlib
--------
Added new :class:`plistlib.UID` and enabled support for reading and writing
NSKeyedArchiver-encoded binary plists.
(Contributed by Jon Janzen in :issue:`26707`.)
socket socket
------ ------

View file

@ -48,7 +48,7 @@ Parse Plist example:
__all__ = [ __all__ = [
"readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes", "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
"Data", "InvalidFileException", "FMT_XML", "FMT_BINARY", "Data", "InvalidFileException", "FMT_XML", "FMT_BINARY",
"load", "dump", "loads", "dumps" "load", "dump", "loads", "dumps", "UID"
] ]
import binascii import binascii
@ -175,6 +175,34 @@ class Data:
# #
class UID:
def __init__(self, data):
if not isinstance(data, int):
raise TypeError("data must be an int")
if data >= 1 << 64:
raise ValueError("UIDs cannot be >= 2**64")
if data < 0:
raise ValueError("UIDs must be positive")
self.data = data
def __index__(self):
return self.data
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
def __reduce__(self):
return self.__class__, (self.data,)
def __eq__(self, other):
if not isinstance(other, UID):
return NotImplemented
return self.data == other.data
def __hash__(self):
return hash(self.data)
# #
# XML support # XML support
# #
@ -649,8 +677,9 @@ class _BinaryPlistParser:
s = self._get_size(tokenL) s = self._get_size(tokenL)
result = self._fp.read(s * 2).decode('utf-16be') result = self._fp.read(s * 2).decode('utf-16be')
# tokenH == 0x80 is documented as 'UID' and appears to be used for elif tokenH == 0x80: # UID
# keyed-archiving, not in plists. # used by Key-Archiver plist files
result = UID(int.from_bytes(self._fp.read(1 + tokenL), 'big'))
elif tokenH == 0xA0: # array elif tokenH == 0xA0: # array
s = self._get_size(tokenL) s = self._get_size(tokenL)
@ -874,6 +903,20 @@ class _BinaryPlistWriter (object):
self._fp.write(t) self._fp.write(t)
elif isinstance(value, UID):
if value.data < 0:
raise ValueError("UIDs must be positive")
elif value.data < 1 << 8:
self._fp.write(struct.pack('>BB', 0x80, value))
elif value.data < 1 << 16:
self._fp.write(struct.pack('>BH', 0x81, value))
elif value.data < 1 << 32:
self._fp.write(struct.pack('>BL', 0x83, value))
elif value.data < 1 << 64:
self._fp.write(struct.pack('>BQ', 0x87, value))
else:
raise OverflowError(value)
elif isinstance(value, (list, tuple)): elif isinstance(value, (list, tuple)):
refs = [self._getrefnum(o) for o in value] refs = [self._getrefnum(o) for o in value]
s = len(refs) s = len(refs)

View file

@ -1,5 +1,7 @@
# Copyright (C) 2003-2013 Python Software Foundation # Copyright (C) 2003-2013 Python Software Foundation
import copy
import operator
import pickle
import unittest import unittest
import plistlib import plistlib
import os import os
@ -10,6 +12,8 @@ import collections
from test import support from test import support
from io import BytesIO from io import BytesIO
from plistlib import UID
ALL_FORMATS=(plistlib.FMT_XML, plistlib.FMT_BINARY) ALL_FORMATS=(plistlib.FMT_XML, plistlib.FMT_BINARY)
# The testdata is generated using Mac/Tools/plistlib_generate_testdata.py # The testdata is generated using Mac/Tools/plistlib_generate_testdata.py
@ -88,6 +92,17 @@ TESTDATA={
ZwB0AHwAiACUAJoApQCuALsAygDTAOQA7QD4AQQBDwEdASsBNgE3ATgBTwFn ZwB0AHwAiACUAJoApQCuALsAygDTAOQA7QD4AQQBDwEdASsBNgE3ATgBTwFn
AW4BcAFyAXQBdgF/AYMBhQGHAYwBlQGbAZ0BnwGhAaUBpwGwAbkBwAHBAcIB AW4BcAFyAXQBdgF/AYMBhQGHAYwBlQGbAZ0BnwGhAaUBpwGwAbkBwAHBAcIB
xQHHAsQC0gAAAAAAAAIBAAAAAAAAADkAAAAAAAAAAAAAAAAAAALs'''), xQHHAsQC0gAAAAAAAAIBAAAAAAAAADkAAAAAAAAAAAAAAAAAAALs'''),
'KEYED_ARCHIVE': binascii.a2b_base64(b'''
YnBsaXN0MDDUAQIDBAUGHB1YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVy
VCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVnB5dHlwZVYkY2xhc3NZTlMu
c3RyaW5nEAGAAl8QE0tleUFyY2hpdmUgVUlEIFRlc3TTEBESExQZWiRjbGFz
c25hbWVYJGNsYXNzZXNbJGNsYXNzaGludHNfEBdPQ19CdWlsdGluUHl0aG9u
VW5pY29kZaQVFhcYXxAXT0NfQnVpbHRpblB5dGhvblVuaWNvZGVfEBBPQ19Q
eXRob25Vbmljb2RlWE5TU3RyaW5nWE5TT2JqZWN0ohobXxAPT0NfUHl0aG9u
U3RyaW5nWE5TU3RyaW5nXxAPTlNLZXllZEFyY2hpdmVy0R4fVHJvb3SAAQAI
ABEAGgAjAC0AMgA3ADsAQQBIAE8AVgBgAGIAZAB6AIEAjACVAKEAuwDAANoA
7QD2AP8BAgEUAR0BLwEyATcAAAAAAAACAQAAAAAAAAAgAAAAAAAAAAAAAAAA
AAABOQ=='''),
} }
@ -151,6 +166,14 @@ class TestPlistlib(unittest.TestCase):
with self.subTest(fmt=fmt): with self.subTest(fmt=fmt):
self.assertRaises(TypeError, plistlib.dumps, pl, fmt=fmt) self.assertRaises(TypeError, plistlib.dumps, pl, fmt=fmt)
def test_invalid_uid(self):
with self.assertRaises(TypeError):
UID("not an int")
with self.assertRaises(ValueError):
UID(2 ** 64)
with self.assertRaises(ValueError):
UID(-19)
def test_int(self): def test_int(self):
for pl in [0, 2**8-1, 2**8, 2**16-1, 2**16, 2**32-1, 2**32, for pl in [0, 2**8-1, 2**8, 2**16-1, 2**16, 2**32-1, 2**32,
2**63-1, 2**64-1, 1, -2**63]: 2**63-1, 2**64-1, 1, -2**63]:
@ -200,6 +223,45 @@ class TestPlistlib(unittest.TestCase):
data = {'1': {'2': [{'3': [[[[[{'test': b'aaaaaa'}]]]]]}]}} data = {'1': {'2': [{'3': [[[[[{'test': b'aaaaaa'}]]]]]}]}}
self.assertEqual(plistlib.loads(plistlib.dumps(data)), data) self.assertEqual(plistlib.loads(plistlib.dumps(data)), data)
def test_uid(self):
data = UID(1)
self.assertEqual(plistlib.loads(plistlib.dumps(data, fmt=plistlib.FMT_BINARY)), data)
dict_data = {
'uid0': UID(0),
'uid2': UID(2),
'uid8': UID(2 ** 8),
'uid16': UID(2 ** 16),
'uid32': UID(2 ** 32),
'uid63': UID(2 ** 63)
}
self.assertEqual(plistlib.loads(plistlib.dumps(dict_data, fmt=plistlib.FMT_BINARY)), dict_data)
def test_uid_data(self):
uid = UID(1)
self.assertEqual(uid.data, 1)
def test_uid_eq(self):
self.assertEqual(UID(1), UID(1))
self.assertNotEqual(UID(1), UID(2))
self.assertNotEqual(UID(1), "not uid")
def test_uid_hash(self):
self.assertEqual(hash(UID(1)), hash(UID(1)))
def test_uid_repr(self):
self.assertEqual(repr(UID(1)), "UID(1)")
def test_uid_index(self):
self.assertEqual(operator.index(UID(1)), 1)
def test_uid_pickle(self):
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
self.assertEqual(pickle.loads(pickle.dumps(UID(19), protocol=proto)), UID(19))
def test_uid_copy(self):
self.assertEqual(copy.copy(UID(1)), UID(1))
self.assertEqual(copy.deepcopy(UID(1)), UID(1))
def test_appleformatting(self): def test_appleformatting(self):
for use_builtin_types in (True, False): for use_builtin_types in (True, False):
for fmt in ALL_FORMATS: for fmt in ALL_FORMATS:
@ -648,6 +710,38 @@ class TestPlistlibDeprecated(unittest.TestCase):
self.assertEqual(cur, in_data) self.assertEqual(cur, in_data)
class TestKeyedArchive(unittest.TestCase):
def test_keyed_archive_data(self):
# This is the structure of a NSKeyedArchive packed plist
data = {
'$version': 100000,
'$objects': [
'$null', {
'pytype': 1,
'$class': UID(2),
'NS.string': 'KeyArchive UID Test'
},
{
'$classname': 'OC_BuiltinPythonUnicode',
'$classes': [
'OC_BuiltinPythonUnicode',
'OC_PythonUnicode',
'NSString',
'NSObject'
],
'$classhints': [
'OC_PythonString', 'NSString'
]
}
],
'$archiver': 'NSKeyedArchiver',
'$top': {
'root': UID(1)
}
}
self.assertEqual(plistlib.loads(TESTDATA["KEYED_ARCHIVE"]), data)
class MiscTestCase(unittest.TestCase): class MiscTestCase(unittest.TestCase):
def test__all__(self): def test__all__(self):
blacklist = {"PlistFormat", "PLISTHEADER"} blacklist = {"PlistFormat", "PLISTHEADER"}
@ -655,7 +749,7 @@ class MiscTestCase(unittest.TestCase):
def test_main(): def test_main():
support.run_unittest(TestPlistlib, TestPlistlibDeprecated, MiscTestCase) support.run_unittest(TestPlistlib, TestPlistlibDeprecated, TestKeyedArchive, MiscTestCase)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -5,6 +5,7 @@ from Cocoa import NSPropertyListSerialization, NSPropertyListOpenStepFormat
from Cocoa import NSPropertyListXMLFormat_v1_0, NSPropertyListBinaryFormat_v1_0 from Cocoa import NSPropertyListXMLFormat_v1_0, NSPropertyListBinaryFormat_v1_0
from Cocoa import CFUUIDCreateFromString, NSNull, NSUUID, CFPropertyListCreateData from Cocoa import CFUUIDCreateFromString, NSNull, NSUUID, CFPropertyListCreateData
from Cocoa import NSURL from Cocoa import NSURL
from Cocoa import NSKeyedArchiver
import datetime import datetime
from collections import OrderedDict from collections import OrderedDict
@ -89,6 +90,8 @@ def main():
else: else:
print(" %s: binascii.a2b_base64(b'''\n %s'''),"%(fmt_name, _encode_base64(bytes(data)).decode('ascii')[:-1])) print(" %s: binascii.a2b_base64(b'''\n %s'''),"%(fmt_name, _encode_base64(bytes(data)).decode('ascii')[:-1]))
keyed_archive_data = NSKeyedArchiver.archivedDataWithRootObject_("KeyArchive UID Test")
print(" 'KEYED_ARCHIVE': binascii.a2b_base64(b'''\n %s''')," % (_encode_base64(bytes(keyed_archive_data)).decode('ascii')[:-1]))
print("}") print("}")
print() print()

View file

@ -754,6 +754,7 @@ Geert Jansen
Jack Jansen Jack Jansen
Hans-Peter Jansen Hans-Peter Jansen
Bill Janssen Bill Janssen
Jon Janzen
Thomas Jarosch Thomas Jarosch
Juhana Jauhiainen Juhana Jauhiainen
Rajagopalasarma Jayakrishnan Rajagopalasarma Jayakrishnan

View file

@ -0,0 +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.