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