1import sys 2import os 3import datetime 4import codecs 5import collections 6from io import BytesIO 7from numbers import Integral 8from fontTools.misc import etree 9from fontTools.misc import plistlib 10from fontTools.misc.textTools import tostr 11import pytest 12from collections.abc import Mapping 13 14 15# The testdata is generated using https://github.com/python/cpython/... 16# Mac/Tools/plistlib_generate_testdata.py 17# which uses PyObjC to control the Cocoa classes for generating plists 18datadir = os.path.join(os.path.dirname(__file__), "testdata") 19with open(os.path.join(datadir, "test.plist"), "rb") as fp: 20 TESTDATA = fp.read() 21 22 23def _test_pl(use_builtin_types): 24 DataClass = bytes if use_builtin_types else plistlib.Data 25 pl = dict( 26 aString="Doodah", 27 aList=["A", "B", 12, 32.5, [1, 2, 3]], 28 aFloat=0.5, 29 anInt=728, 30 aBigInt=2**63 - 44, 31 aBigInt2=2**63 + 44, 32 aNegativeInt=-5, 33 aNegativeBigInt=-80000000000, 34 aDict=dict( 35 anotherString="<hello & 'hi' there!>", 36 aUnicodeValue="M\xe4ssig, Ma\xdf", 37 aTrueValue=True, 38 aFalseValue=False, 39 deeperDict=dict(a=17, b=32.5, c=[1, 2, "text"]), 40 ), 41 someData=DataClass(b"<binary gunk>"), 42 someMoreData=DataClass(b"<lots of binary gunk>\0\1\2\3" * 10), 43 nestedData=[DataClass(b"<lots of binary gunk>\0\1\2\3" * 10)], 44 aDate=datetime.datetime(2004, 10, 26, 10, 33, 33), 45 anEmptyDict=dict(), 46 anEmptyList=list(), 47 ) 48 pl["\xc5benraa"] = "That was a unicode key." 49 return pl 50 51 52@pytest.fixture 53def pl(): 54 return _test_pl(use_builtin_types=True) 55 56 57@pytest.fixture 58def pl_no_builtin_types(): 59 return _test_pl(use_builtin_types=False) 60 61 62@pytest.fixture( 63 params=[True, False], 64 ids=["builtin=True", "builtin=False"], 65) 66def use_builtin_types(request): 67 return request.param 68 69 70@pytest.fixture 71def parametrized_pl(use_builtin_types): 72 return _test_pl(use_builtin_types), use_builtin_types 73 74 75def test__test_pl(): 76 # sanity test that checks that the two values are equivalent 77 # (plistlib.Data implements __eq__ against bytes values) 78 pl = _test_pl(use_builtin_types=False) 79 pl2 = _test_pl(use_builtin_types=True) 80 assert pl == pl2 81 82 83def test_io(tmpdir, parametrized_pl): 84 pl, use_builtin_types = parametrized_pl 85 testpath = tmpdir / "test.plist" 86 with testpath.open("wb") as fp: 87 plistlib.dump(pl, fp, use_builtin_types=use_builtin_types) 88 89 with testpath.open("rb") as fp: 90 pl2 = plistlib.load(fp, use_builtin_types=use_builtin_types) 91 92 assert pl == pl2 93 94 with pytest.raises(AttributeError): 95 plistlib.dump(pl, "filename") 96 97 with pytest.raises(AttributeError): 98 plistlib.load("filename") 99 100 101def test_invalid_type(): 102 pl = [object()] 103 104 with pytest.raises(TypeError): 105 plistlib.dumps(pl) 106 107 108@pytest.mark.parametrize( 109 "pl", 110 [ 111 0, 112 2**8 - 1, 113 2**8, 114 2**16 - 1, 115 2**16, 116 2**32 - 1, 117 2**32, 118 2**63 - 1, 119 2**64 - 1, 120 1, 121 -(2**63), 122 ], 123) 124def test_int(pl): 125 data = plistlib.dumps(pl) 126 pl2 = plistlib.loads(data) 127 assert isinstance(pl2, Integral) 128 assert pl == pl2 129 data2 = plistlib.dumps(pl2) 130 assert data == data2 131 132 133@pytest.mark.parametrize("pl", [2**64 + 1, 2**127 - 1, -(2**64), -(2**127)]) 134def test_int_overflow(pl): 135 with pytest.raises(OverflowError): 136 plistlib.dumps(pl) 137 138 139def test_bytearray(use_builtin_types): 140 DataClass = bytes if use_builtin_types else plistlib.Data 141 pl = DataClass(b"<binary gunk\0\1\2\3>") 142 array = bytearray(pl) if use_builtin_types else bytearray(pl.data) 143 data = plistlib.dumps(array) 144 pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types) 145 assert isinstance(pl2, DataClass) 146 assert pl2 == pl 147 data2 = plistlib.dumps(pl2, use_builtin_types=use_builtin_types) 148 assert data == data2 149 150 151@pytest.mark.parametrize( 152 "DataClass, use_builtin_types", 153 [(bytes, True), (plistlib.Data, True), (plistlib.Data, False)], 154 ids=[ 155 "bytes|builtin_types=True", 156 "Data|builtin_types=True", 157 "Data|builtin_types=False", 158 ], 159) 160def test_bytes_data(DataClass, use_builtin_types): 161 pl = DataClass(b"<binary gunk\0\1\2\3>") 162 data = plistlib.dumps(pl, use_builtin_types=use_builtin_types) 163 pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types) 164 assert isinstance(pl2, bytes if use_builtin_types else plistlib.Data) 165 assert pl2 == pl 166 data2 = plistlib.dumps(pl2, use_builtin_types=use_builtin_types) 167 assert data == data2 168 169 170def test_bytes_string(use_builtin_types): 171 pl = b"some ASCII bytes" 172 data = plistlib.dumps(pl, use_builtin_types=False) 173 pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types) 174 assert isinstance(pl2, str) # it's always a <string> 175 assert pl2 == pl.decode() 176 177 178def test_indentation_array(): 179 data = [[[[[[[[{"test": "aaaaaa"}]]]]]]]] 180 assert plistlib.loads(plistlib.dumps(data)) == data 181 182 183def test_indentation_dict(): 184 data = {"1": {"2": {"3": {"4": {"5": {"6": {"7": {"8": {"9": "aaaaaa"}}}}}}}}} 185 assert plistlib.loads(plistlib.dumps(data)) == data 186 187 188def test_indentation_dict_mix(): 189 data = {"1": {"2": [{"3": [[[[[{"test": "aaaaaa"}]]]]]}]}} 190 assert plistlib.loads(plistlib.dumps(data)) == data 191 192 193@pytest.mark.xfail(reason="we use two spaces, Apple uses tabs") 194def test_apple_formatting(parametrized_pl): 195 # we also split base64 data into multiple lines differently: 196 # both right-justify data to 76 chars, but Apple's treats tabs 197 # as 8 spaces, whereas we use 2 spaces 198 pl, use_builtin_types = parametrized_pl 199 pl = plistlib.loads(TESTDATA, use_builtin_types=use_builtin_types) 200 data = plistlib.dumps(pl, use_builtin_types=use_builtin_types) 201 assert data == TESTDATA 202 203 204def test_apple_formatting_fromliteral(parametrized_pl): 205 pl, use_builtin_types = parametrized_pl 206 pl2 = plistlib.loads(TESTDATA, use_builtin_types=use_builtin_types) 207 assert pl == pl2 208 209 210def test_apple_roundtrips(use_builtin_types): 211 pl = plistlib.loads(TESTDATA, use_builtin_types=use_builtin_types) 212 data = plistlib.dumps(pl, use_builtin_types=use_builtin_types) 213 pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types) 214 data2 = plistlib.dumps(pl2, use_builtin_types=use_builtin_types) 215 assert data == data2 216 217 218def test_bytesio(parametrized_pl): 219 pl, use_builtin_types = parametrized_pl 220 b = BytesIO() 221 plistlib.dump(pl, b, use_builtin_types=use_builtin_types) 222 pl2 = plistlib.load(BytesIO(b.getvalue()), use_builtin_types=use_builtin_types) 223 assert pl == pl2 224 225 226@pytest.mark.parametrize("sort_keys", [False, True]) 227def test_keysort_bytesio(sort_keys): 228 pl = collections.OrderedDict() 229 pl["b"] = 1 230 pl["a"] = 2 231 pl["c"] = 3 232 233 b = BytesIO() 234 235 plistlib.dump(pl, b, sort_keys=sort_keys) 236 pl2 = plistlib.load(BytesIO(b.getvalue()), dict_type=collections.OrderedDict) 237 238 assert dict(pl) == dict(pl2) 239 if sort_keys: 240 assert list(pl2.keys()) == ["a", "b", "c"] 241 else: 242 assert list(pl2.keys()) == ["b", "a", "c"] 243 244 245@pytest.mark.parametrize("sort_keys", [False, True]) 246def test_keysort(sort_keys): 247 pl = collections.OrderedDict() 248 pl["b"] = 1 249 pl["a"] = 2 250 pl["c"] = 3 251 252 data = plistlib.dumps(pl, sort_keys=sort_keys) 253 pl2 = plistlib.loads(data, dict_type=collections.OrderedDict) 254 255 assert dict(pl) == dict(pl2) 256 if sort_keys: 257 assert list(pl2.keys()) == ["a", "b", "c"] 258 else: 259 assert list(pl2.keys()) == ["b", "a", "c"] 260 261 262def test_keys_no_string(): 263 pl = {42: "aNumber"} 264 265 with pytest.raises(TypeError): 266 plistlib.dumps(pl) 267 268 b = BytesIO() 269 with pytest.raises(TypeError): 270 plistlib.dump(pl, b) 271 272 273def test_skipkeys(): 274 pl = {42: "aNumber", "snake": "aWord"} 275 276 data = plistlib.dumps(pl, skipkeys=True, sort_keys=False) 277 278 pl2 = plistlib.loads(data) 279 assert pl2 == {"snake": "aWord"} 280 281 fp = BytesIO() 282 plistlib.dump(pl, fp, skipkeys=True, sort_keys=False) 283 data = fp.getvalue() 284 pl2 = plistlib.loads(fp.getvalue()) 285 assert pl2 == {"snake": "aWord"} 286 287 288def test_tuple_members(): 289 pl = {"first": (1, 2), "second": (1, 2), "third": (3, 4)} 290 291 data = plistlib.dumps(pl) 292 pl2 = plistlib.loads(data) 293 assert pl2 == {"first": [1, 2], "second": [1, 2], "third": [3, 4]} 294 assert pl2["first"] is not pl2["second"] 295 296 297def test_list_members(): 298 pl = {"first": [1, 2], "second": [1, 2], "third": [3, 4]} 299 300 data = plistlib.dumps(pl) 301 pl2 = plistlib.loads(data) 302 assert pl2 == {"first": [1, 2], "second": [1, 2], "third": [3, 4]} 303 assert pl2["first"] is not pl2["second"] 304 305 306def test_dict_members(): 307 pl = {"first": {"a": 1}, "second": {"a": 1}, "third": {"b": 2}} 308 309 data = plistlib.dumps(pl) 310 pl2 = plistlib.loads(data) 311 assert pl2 == {"first": {"a": 1}, "second": {"a": 1}, "third": {"b": 2}} 312 assert pl2["first"] is not pl2["second"] 313 314 315def test_controlcharacters(): 316 for i in range(128): 317 c = chr(i) 318 testString = "string containing %s" % c 319 if i >= 32 or c in "\r\n\t": 320 # \r, \n and \t are the only legal control chars in XML 321 data = plistlib.dumps(testString) 322 # the stdlib's plistlib writer, as well as the elementtree 323 # parser, always replace \r with \n inside string values; 324 # lxml doesn't (the ctrl character is escaped), so it roundtrips 325 if c != "\r" or etree._have_lxml: 326 assert plistlib.loads(data) == testString 327 else: 328 with pytest.raises(ValueError): 329 plistlib.dumps(testString) 330 331 332def test_non_bmp_characters(): 333 pl = {"python": "\U0001f40d"} 334 data = plistlib.dumps(pl) 335 assert plistlib.loads(data) == pl 336 337 338def test_nondictroot(): 339 test1 = "abc" 340 test2 = [1, 2, 3, "abc"] 341 result1 = plistlib.loads(plistlib.dumps(test1)) 342 result2 = plistlib.loads(plistlib.dumps(test2)) 343 assert test1 == result1 344 assert test2 == result2 345 346 347def test_invalidarray(): 348 for i in [ 349 "<key>key inside an array</key>", 350 "<key>key inside an array2</key><real>3</real>", 351 "<true/><key>key inside an array3</key>", 352 ]: 353 with pytest.raises(ValueError): 354 plistlib.loads(("<plist><array>%s</array></plist>" % i).encode("utf-8")) 355 356 357def test_invaliddict(): 358 for i in [ 359 "<key><true/>k</key><string>compound key</string>", 360 "<key>single key</key>", 361 "<string>missing key</string>", 362 "<key>k1</key><string>v1</string><real>5.3</real>" 363 "<key>k1</key><key>k2</key><string>double key</string>", 364 ]: 365 with pytest.raises(ValueError): 366 plistlib.loads(("<plist><dict>%s</dict></plist>" % i).encode()) 367 with pytest.raises(ValueError): 368 plistlib.loads( 369 ("<plist><array><dict>%s</dict></array></plist>" % i).encode() 370 ) 371 372 373def test_invalidinteger(): 374 with pytest.raises(ValueError): 375 plistlib.loads(b"<plist><integer>not integer</integer></plist>") 376 377 378def test_invalidreal(): 379 with pytest.raises(ValueError): 380 plistlib.loads(b"<plist><integer>not real</integer></plist>") 381 382 383@pytest.mark.parametrize( 384 "xml_encoding, encoding, bom", 385 [ 386 (b"utf-8", "utf-8", codecs.BOM_UTF8), 387 (b"utf-16", "utf-16-le", codecs.BOM_UTF16_LE), 388 (b"utf-16", "utf-16-be", codecs.BOM_UTF16_BE), 389 # expat parser (used by ElementTree) does't support UTF-32 390 # (b"utf-32", "utf-32-le", codecs.BOM_UTF32_LE), 391 # (b"utf-32", "utf-32-be", codecs.BOM_UTF32_BE), 392 ], 393) 394def test_xml_encodings(parametrized_pl, xml_encoding, encoding, bom): 395 pl, use_builtin_types = parametrized_pl 396 data = TESTDATA.replace(b"UTF-8", xml_encoding) 397 data = bom + data.decode("utf-8").encode(encoding) 398 pl2 = plistlib.loads(data, use_builtin_types=use_builtin_types) 399 assert pl == pl2 400 401 402def test_fromtree(parametrized_pl): 403 pl, use_builtin_types = parametrized_pl 404 tree = etree.fromstring(TESTDATA) 405 pl2 = plistlib.fromtree(tree, use_builtin_types=use_builtin_types) 406 assert pl == pl2 407 408 409def _strip(txt): 410 return ( 411 "".join(l.strip() for l in tostr(txt, "utf-8").splitlines()) 412 if txt is not None 413 else "" 414 ) 415 416 417def test_totree(parametrized_pl): 418 pl, use_builtin_types = parametrized_pl 419 tree = etree.fromstring(TESTDATA)[0] # ignore root 'plist' element 420 tree2 = plistlib.totree(pl, use_builtin_types=use_builtin_types) 421 assert tree.tag == tree2.tag == "dict" 422 for (_, e1), (_, e2) in zip(etree.iterwalk(tree), etree.iterwalk(tree2)): 423 assert e1.tag == e2.tag 424 assert e1.attrib == e2.attrib 425 assert len(e1) == len(e2) 426 # ignore whitespace 427 assert _strip(e1.text) == _strip(e2.text) 428 429 430def test_no_pretty_print(use_builtin_types): 431 data = plistlib.dumps( 432 {"data": b"hello" if use_builtin_types else plistlib.Data(b"hello")}, 433 pretty_print=False, 434 use_builtin_types=use_builtin_types, 435 ) 436 assert data == ( 437 plistlib.XML_DECLARATION + plistlib.PLIST_DOCTYPE + b'<plist version="1.0">' 438 b"<dict>" 439 b"<key>data</key>" 440 b"<data>aGVsbG8=</data>" 441 b"</dict>" 442 b"</plist>" 443 ) 444 445 446def test_readPlist_from_path(pl): 447 old_plistlib = pytest.importorskip("fontTools.ufoLib.plistlib") 448 path = os.path.join(datadir, "test.plist") 449 pl2 = old_plistlib.readPlist(path) 450 assert isinstance(pl2["someData"], plistlib.Data) 451 assert pl2 == pl 452 453 454def test_readPlist_from_file(pl): 455 old_plistlib = pytest.importorskip("fontTools.ufoLib.plistlib") 456 with open(os.path.join(datadir, "test.plist"), "rb") as f: 457 pl2 = old_plistlib.readPlist(f) 458 assert isinstance(pl2["someData"], plistlib.Data) 459 assert pl2 == pl 460 assert not f.closed 461 462 463def test_readPlistFromString(pl): 464 old_plistlib = pytest.importorskip("fontTools.ufoLib.plistlib") 465 pl2 = old_plistlib.readPlistFromString(TESTDATA) 466 assert isinstance(pl2["someData"], plistlib.Data) 467 assert pl2 == pl 468 469 470def test_writePlist_to_path(tmpdir, pl_no_builtin_types): 471 old_plistlib = pytest.importorskip("fontTools.ufoLib.plistlib") 472 testpath = tmpdir / "test.plist" 473 old_plistlib.writePlist(pl_no_builtin_types, str(testpath)) 474 with testpath.open("rb") as fp: 475 pl2 = plistlib.load(fp, use_builtin_types=False) 476 assert pl2 == pl_no_builtin_types 477 478 479def test_writePlist_to_file(tmpdir, pl_no_builtin_types): 480 old_plistlib = pytest.importorskip("fontTools.ufoLib.plistlib") 481 testpath = tmpdir / "test.plist" 482 with testpath.open("wb") as fp: 483 old_plistlib.writePlist(pl_no_builtin_types, fp) 484 with testpath.open("rb") as fp: 485 pl2 = plistlib.load(fp, use_builtin_types=False) 486 assert pl2 == pl_no_builtin_types 487 488 489def test_writePlistToString(pl_no_builtin_types): 490 old_plistlib = pytest.importorskip("fontTools.ufoLib.plistlib") 491 data = old_plistlib.writePlistToString(pl_no_builtin_types) 492 pl2 = plistlib.loads(data) 493 assert pl2 == pl_no_builtin_types 494 495 496def test_load_use_builtin_types_default(): 497 pl = plistlib.loads(TESTDATA) 498 assert isinstance(pl["someData"], bytes) 499 500 501def test_dump_use_builtin_types_default(pl_no_builtin_types): 502 data = plistlib.dumps(pl_no_builtin_types) 503 pl2 = plistlib.loads(data) 504 assert isinstance(pl2["someData"], bytes) 505 assert pl2 == pl_no_builtin_types 506 507 508def test_non_ascii_bytes(): 509 with pytest.raises(ValueError, match="invalid non-ASCII bytes"): 510 plistlib.dumps("\U0001f40d".encode("utf-8"), use_builtin_types=False) 511 512 513class CustomMapping(Mapping): 514 a = {"a": 1, "b": 2} 515 516 def __getitem__(self, key): 517 return self.a[key] 518 519 def __iter__(self): 520 return iter(self.a) 521 522 def __len__(self): 523 return len(self.a) 524 525 526def test_custom_mapping(): 527 test_mapping = CustomMapping() 528 data = plistlib.dumps(test_mapping) 529 assert plistlib.loads(data) == {"a": 1, "b": 2} 530 531 532if __name__ == "__main__": 533 import sys 534 535 sys.exit(pytest.main(sys.argv)) 536