xref: /aosp_15_r20/external/fonttools/Tests/misc/plistlib_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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