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