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