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