1# -*- coding: utf-8 -*-
2
3import datetime
4import io
5import os
6import subprocess
7import sys
8import tempfile
9import unittest
10
11from biplist import *
12from biplist import PlistWriter
13from test_utils import *
14
15try:
16    unicode
17    unicodeStr = lambda x: x.decode('utf-8')
18    toUnicode = lambda x: x.decode('unicode-escape')
19except NameError:
20    unicode = str
21    unicodeStr = lambda x: x
22    toUnicode = lambda x: x
23try:
24    xrange
25except NameError:
26    xrange = range
27
28class TestWritePlist(unittest.TestCase):
29    def setUp(self):
30        pass
31
32    def roundTrip(self, case, xml=False, expected=None, reprTest=True):
33        # reprTest may fail randomly if True and the values being encoded include a dictionary with more
34        # than one key.
35
36        # convert to plist string
37        plist = writePlistToString(case, binary=(not xml))
38        self.assertTrue(len(plist) > 0)
39
40        # confirm that lint is happy with the result
41        self.lintPlist(plist)
42
43        # convert back
44        readResult = readPlistFromString(plist)
45
46        # test equality
47        if reprTest is True:
48            self.assertEqual(repr(case if expected is None else expected), repr(readResult))
49        else:
50            self.assertEqual((case if expected is None else expected), readResult)
51
52        # write to file
53        plistFile = tempfile.NamedTemporaryFile(mode='wb+', suffix='.plist')
54        writePlist(case, plistFile, binary=(xml is False))
55        plistFile.seek(0)
56
57        # confirm that lint is happy with the result
58        self.lintPlist(plistFile)
59
60        # read back from file
61        fileResult = readPlist(plistFile)
62
63        # test equality
64        if reprTest is True:
65            self.assertEqual(repr(case if expected is None else expected), repr(fileResult))
66        else:
67            self.assertEqual((case if expected is None else expected), fileResult)
68
69    def lintPlist(self, plist):
70        if os.access('/usr/bin/plutil', os.X_OK):
71            plistFile = None
72            plistFilePath = None
73
74            if hasattr(plist, 'name'):
75                plistFilePath = plist.name
76            else:
77                if hasattr(plist, 'read'):
78                    plistFile = tempfile.NamedTemporaryFile('w%s' % ('b' if 'b' in plist.mode else ''))
79                    plistFile.write(plist.read())
80                else:
81                    plistFile = tempfile.NamedTemporaryFile('w%s' % ('b' if isinstance(plist, bytes) else ''))
82                    plistFile.write(plist)
83                plistFilePath = plistFile.name
84                plistFile.flush()
85
86            status, output = run_command(['/usr/bin/plutil', '-lint', plistFilePath])
87            if status != 0:
88                self.fail("plutil verification failed (status %d): %s" % (status, output))
89
90    def testXMLPlist(self):
91        self.roundTrip({'hello':'world'}, xml=True)
92
93    def testXMLPlistWithData(self):
94        for binmode in (True, False):
95            binplist = writePlistToString({'data': Data(b'\x01\xac\xf0\xff')}, binary=binmode)
96            plist = readPlistFromString(binplist)
97            self.assertTrue(isinstance(plist['data'], (Data, bytes)), \
98                "unable to encode then decode Data into %s plist" % ("binary" if binmode else "XML"))
99
100    def testConvertToXMLPlistWithData(self):
101        binplist = writePlistToString({'data': Data(b'\x01\xac\xf0\xff')})
102        plist = readPlistFromString(binplist)
103        xmlplist = writePlistToString(plist, binary=False)
104        self.assertTrue(len(xmlplist) > 0, "unable to convert plist with Data from binary to XML")
105
106    def testBoolRoot(self):
107        self.roundTrip(True)
108        self.roundTrip(False)
109
110    def testDuplicate(self):
111        l = ["foo" for i in xrange(0, 100)]
112        self.roundTrip(l)
113
114    def testListRoot(self):
115        self.roundTrip([1, 2, 3])
116
117    def testDictRoot(self):
118        self.roundTrip({'a':1, 'B':'d'}, reprTest=False)
119
120    def mixedNumericTypesHelper(self, cases):
121        result = readPlistFromString(writePlistToString(cases))
122        for i in xrange(0, len(cases)):
123            self.assertTrue(cases[i] == result[i])
124            self.assertEqual(type(cases[i]), type(result[i]), "Type mismatch on %d: %s != %s" % (i, repr(cases[i]), repr(result[i])))
125
126    def testBoolsAndIntegersMixed(self):
127        self.mixedNumericTypesHelper([0, 1, True, False, None])
128        self.mixedNumericTypesHelper([False, True, 0, 1, None])
129        self.roundTrip({'1':[True, False, 1, 0], '0':[1, 2, 0, {'2':[1, 0, False]}]}, reprTest=False)
130        self.roundTrip([1, 1, 1, 1, 1, True, True, True, True])
131
132    def testFloatsAndIntegersMixed(self):
133        self.mixedNumericTypesHelper([0, 1, 1.0, 0.0, None])
134        self.mixedNumericTypesHelper([0.0, 1.0, 0, 1, None])
135        self.roundTrip({'1':[1.0, 0.0, 1, 0], '0':[1, 2, 0, {'2':[1, 0, 0.0]}]}, reprTest=False)
136        self.roundTrip([1, 1, 1, 1, 1, 1.0, 1.0, 1.0, 1.0])
137
138    def testSetRoot(self):
139        self.roundTrip(set((1, 2, 3)))
140
141    def testDatetime(self):
142        now = datetime.datetime.utcnow()
143        now = now.replace(microsecond=0)
144        self.roundTrip([now])
145
146    def testFloat(self):
147        self.roundTrip({'aFloat':1.23})
148
149    def testTuple(self):
150        result = writePlistToString({'aTuple':(1, 2.0, 'a'), 'dupTuple':('a', 'a', 'a', 'b', 'b')})
151        self.assertTrue(len(result) > 0)
152        readResult = readPlistFromString(result)
153        self.assertEqual(readResult['aTuple'], [1, 2.0, 'a'])
154        self.assertEqual(readResult['dupTuple'], ['a', 'a', 'a', 'b', 'b'])
155
156    def testComplicated(self):
157        root = {'preference':[1, 2, {'hi there':['a', 1, 2, {'yarrrr':123}]}]}
158        self.lintPlist(writePlistToString(root))
159        self.roundTrip(root)
160
161    def testBytes(self):
162        self.roundTrip(b'0')
163        self.roundTrip(b'')
164
165        self.roundTrip([b'0'])
166        self.roundTrip([b''])
167
168        self.roundTrip({'a': b'0'})
169        self.roundTrip({'a': b''})
170
171    def testString(self):
172        self.roundTrip('')
173        self.roundTrip('a')
174        self.roundTrip('1')
175
176        self.roundTrip([''])
177        self.roundTrip(['a'])
178        self.roundTrip(['1'])
179
180        self.roundTrip({'a':''})
181        self.roundTrip({'a':'a'})
182        self.roundTrip({'1':'a'})
183
184        self.roundTrip({'a':'a'})
185        self.roundTrip({'a':'1'})
186
187    def testUnicode(self):
188        # defaulting to 1 byte strings
189        if str != unicode:
190            self.roundTrip(unicodeStr(r''), expected='')
191            self.roundTrip(unicodeStr(r'a'), expected='a')
192
193            self.roundTrip([unicodeStr(r'a')], expected=['a'])
194
195            self.roundTrip({'a':unicodeStr(r'a')}, expected={'a':'a'})
196            self.roundTrip({unicodeStr(r'a'):'a'}, expected={'a':'a'})
197            self.roundTrip({unicodeStr(r''):unicodeStr(r'')}, expected={'':''})
198
199        # TODO: need a 4-byte unicode character
200        self.roundTrip(unicodeStr(r'ü'))
201        self.roundTrip([unicodeStr(r'ü')])
202        self.roundTrip({'a':unicodeStr(r'ü')})
203        self.roundTrip({unicodeStr(r'ü'):'a'})
204
205        self.roundTrip(toUnicode('\u00b6'))
206        self.roundTrip([toUnicode('\u00b6')])
207        self.roundTrip({toUnicode('\u00b6'):toUnicode('\u00b6')})
208
209        self.roundTrip(toUnicode('\u1D161'))
210        self.roundTrip([toUnicode('\u1D161')])
211        self.roundTrip({toUnicode('\u1D161'):toUnicode('\u1D161')})
212
213        # Smiley face emoji
214        self.roundTrip(toUnicode('\U0001f604'))
215        self.roundTrip([toUnicode('\U0001f604'), toUnicode('\U0001f604')])
216        self.roundTrip({toUnicode('\U0001f604'):toUnicode('\U0001f604')})
217
218    def testNone(self):
219        self.roundTrip(None)
220        self.roundTrip({'1':None})
221        self.roundTrip([None, None, None])
222
223    def testBools(self):
224        self.roundTrip(True)
225        self.roundTrip(False)
226
227        self.roundTrip([True, False])
228
229        self.roundTrip({'a':True, 'b':False}, reprTest=False)
230
231    def testUniques(self):
232        root = {'hi':'there', 'halloo':'there'}
233        self.roundTrip(root, reprTest=False)
234
235    def testAllEmpties(self):
236        '''Primarily testint that an empty unicode and bytes are not mixed up'''
237        self.roundTrip([unicodeStr(''), '', b'', [], {}], expected=['', '', b'', [], {}])
238
239    def testLargeDict(self):
240        d = dict((str(x), str(x)) for x in xrange(0, 1000))
241        self.roundTrip(d, reprTest=False)
242
243    def testWriteToFile(self):
244        for is_binary in [True, False]:
245            with tempfile.NamedTemporaryFile(mode='w%s' % ('b' if is_binary else ''), suffix='.plist') as plistFile:
246                # clear out the created file
247                os.unlink(plistFile.name)
248                self.assertFalse(os.path.exists(plistFile.name))
249
250                # write to disk
251                writePlist([1, 2, 3], plistFile.name, binary=is_binary)
252                self.assertTrue(os.path.exists(plistFile.name))
253
254                with open(plistFile.name, 'r%s' % ('b' if is_binary else '')) as f:
255                    fileContents = f.read()
256                    self.lintPlist(fileContents)
257
258    def testBadKeys(self):
259        try:
260            self.roundTrip({None:1})
261            self.fail("None is not a valid key in Cocoa.")
262        except InvalidPlistException as e:
263            pass
264        try:
265            self.roundTrip({Data(b"hello world"):1})
266            self.fail("Data is not a valid key in Cocoa.")
267        except InvalidPlistException as e:
268            pass
269        try:
270            self.roundTrip({1:1})
271            self.fail("Number is not a valid key in Cocoa.")
272        except InvalidPlistException as e:
273            pass
274
275    def testIntBoundaries(self):
276        edges = [0xff, 0xffff, 0xffffffff]
277        for edge in edges:
278            cases = [edge, edge-1, edge+1, edge-2, edge+2, edge*2, edge/2]
279            self.roundTrip(cases)
280        edges = [-pow(2, 7), pow(2, 7) - 1,
281                 -pow(2, 15), pow(2, 15) - 1,
282                 -pow(2, 31), pow(2, 31) - 1,
283                 -pow(2, 63), pow(2, 64) - 1]
284        self.roundTrip(edges, reprTest=False)
285
286        ioBytes = io.BytesIO()
287        writer = PlistWriter(ioBytes)
288        bytes = [(1, [pow(2, 7) - 1]),
289                 (2, [pow(2, 15) - 1]),
290                 (4, [pow(2, 31) - 1]),
291                 (8, [-pow(2, 7), -pow(2, 15), -pow(2, 31), -pow(2, 63), pow(2, 63) - 1]),
292                 (16, [pow(2, 64) - 1])
293            ]
294        for bytelen, tests in bytes:
295            for test in tests:
296                got = writer.intSize(test)
297                self.assertEqual(bytelen, got, "Byte size is wrong. Expected %d, got %d" % (bytelen, got))
298
299        bytes_lists = [list(x) for x in bytes]
300        self.roundTrip(bytes_lists, reprTest=False)
301
302        try:
303            self.roundTrip([0x10000000000000000, pow(2, 64)])
304            self.fail("2^64 should be too large for Core Foundation to handle.")
305        except InvalidPlistException as e:
306            pass
307
308    def testUnicode2(self):
309        unicodeRoot = toUnicode("Mirror's Edge\u2122 for iPad")
310        self.roundTrip(unicodeRoot)
311        unicodeStrings = [toUnicode("Mirror's Edge\u2122 for iPad"), toUnicode('Weightbot \u2014 Track your Weight in Style')]
312        self.roundTrip(unicodeStrings)
313        self.roundTrip({toUnicode(""):toUnicode("")}, expected={'':''})
314        self.roundTrip(toUnicode(""), expected='')
315
316    def testWriteData(self):
317        self.roundTrip(Data(b"woohoo"))
318
319    def testEmptyData(self):
320        data = Data(b'')
321        binplist = writePlistToString(data)
322        plist = readPlistFromString(binplist)
323        self.assertEqual(plist, data)
324        self.assertEqual(type(plist), type(data))
325
326    def testUidWrite(self):
327        self.roundTrip({'$version': 100000,
328            '$objects':
329                ['$null',
330                 {'$class': Uid(3), 'somekey': Uid(2)},
331                 'object value as string',
332                 {'$classes': ['Archived', 'NSObject'], '$classname': 'Archived'}
333                 ],
334            '$top': {'root': Uid(1)}, '$archiver': 'NSKeyedArchiver'}, reprTest=False)
335
336    def testUidRoundTrip(self):
337        # Per https://github.com/wooster/biplist/issues/9
338        self.roundTrip(Uid(1))
339        self.roundTrip([Uid(1), 1])
340        self.roundTrip([1, Uid(1)])
341        self.roundTrip([Uid(1), Uid(1)])
342
343    def testRecursiveWrite(self):
344        # Apple libraries disallow recursive containers, so we should fail on
345        # trying to write those.
346        root = []
347        child = [root]
348        root.extend(child)
349        try:
350            writePlistToString(root)
351            self.fail("Should not be able to write plists with recursive containers.")
352        except InvalidPlistException as e:
353            pass
354        except:
355            self.fail("Should get an invalid plist exception for recursive containers.")
356
357if __name__ == '__main__':
358    unittest.main()
359