• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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