1import cgi 2import os 3import sys 4import tempfile 5import unittest 6from collections import namedtuple 7from io import StringIO, BytesIO 8from test import support 9 10class HackedSysModule: 11 # The regression test will have real values in sys.argv, which 12 # will completely confuse the test of the cgi module 13 argv = [] 14 stdin = sys.stdin 15 16cgi.sys = HackedSysModule() 17 18class ComparableException: 19 def __init__(self, err): 20 self.err = err 21 22 def __str__(self): 23 return str(self.err) 24 25 def __eq__(self, anExc): 26 if not isinstance(anExc, Exception): 27 return NotImplemented 28 return (self.err.__class__ == anExc.__class__ and 29 self.err.args == anExc.args) 30 31 def __getattr__(self, attr): 32 return getattr(self.err, attr) 33 34def do_test(buf, method): 35 env = {} 36 if method == "GET": 37 fp = None 38 env['REQUEST_METHOD'] = 'GET' 39 env['QUERY_STRING'] = buf 40 elif method == "POST": 41 fp = BytesIO(buf.encode('latin-1')) # FieldStorage expects bytes 42 env['REQUEST_METHOD'] = 'POST' 43 env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' 44 env['CONTENT_LENGTH'] = str(len(buf)) 45 else: 46 raise ValueError("unknown method: %s" % method) 47 try: 48 return cgi.parse(fp, env, strict_parsing=1) 49 except Exception as err: 50 return ComparableException(err) 51 52parse_strict_test_cases = [ 53 ("", ValueError("bad query field: ''")), 54 ("&", ValueError("bad query field: ''")), 55 ("&&", ValueError("bad query field: ''")), 56 # Should the next few really be valid? 57 ("=", {}), 58 ("=&=", {}), 59 # This rest seem to make sense 60 ("=a", {'': ['a']}), 61 ("&=a", ValueError("bad query field: ''")), 62 ("=a&", ValueError("bad query field: ''")), 63 ("=&a", ValueError("bad query field: 'a'")), 64 ("b=a", {'b': ['a']}), 65 ("b+=a", {'b ': ['a']}), 66 ("a=b=a", {'a': ['b=a']}), 67 ("a=+b=a", {'a': [' b=a']}), 68 ("&b=a", ValueError("bad query field: ''")), 69 ("b&=a", ValueError("bad query field: 'b'")), 70 ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}), 71 ("a=a+b&a=b+a", {'a': ['a b', 'b a']}), 72 ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}), 73 ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env", 74 {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'], 75 'cuyer': ['r'], 76 'expire': ['964546263'], 77 'kid': ['130003.300038'], 78 'lobale': ['en-US'], 79 'order_id': ['0bb2e248638833d48cb7fed300000f1b'], 80 'ss': ['env'], 81 'view': ['bustomer'], 82 }), 83 84 ("group_id=5470&set=custom&_assigned_to=31392&_status=1&_category=100&SUBMIT=Browse", 85 {'SUBMIT': ['Browse'], 86 '_assigned_to': ['31392'], 87 '_category': ['100'], 88 '_status': ['1'], 89 'group_id': ['5470'], 90 'set': ['custom'], 91 }) 92 ] 93 94def norm(seq): 95 return sorted(seq, key=repr) 96 97def first_elts(list): 98 return [p[0] for p in list] 99 100def first_second_elts(list): 101 return [(p[0], p[1][0]) for p in list] 102 103def gen_result(data, environ): 104 encoding = 'latin-1' 105 fake_stdin = BytesIO(data.encode(encoding)) 106 fake_stdin.seek(0) 107 form = cgi.FieldStorage(fp=fake_stdin, environ=environ, encoding=encoding) 108 109 result = {} 110 for k, v in dict(form).items(): 111 result[k] = isinstance(v, list) and form.getlist(k) or v.value 112 113 return result 114 115class CgiTests(unittest.TestCase): 116 117 def test_parse_multipart(self): 118 fp = BytesIO(POSTDATA.encode('latin1')) 119 env = {'boundary': BOUNDARY.encode('latin1'), 120 'CONTENT-LENGTH': '558'} 121 result = cgi.parse_multipart(fp, env) 122 expected = {'submit': [' Add '], 'id': ['1234'], 123 'file': [b'Testing 123.\n'], 'title': ['']} 124 self.assertEqual(result, expected) 125 126 def test_parse_multipart_without_content_length(self): 127 POSTDATA = '''--JfISa01 128Content-Disposition: form-data; name="submit-name" 129 130just a string 131 132--JfISa01-- 133''' 134 fp = BytesIO(POSTDATA.encode('latin1')) 135 env = {'boundary': 'JfISa01'.encode('latin1')} 136 result = cgi.parse_multipart(fp, env) 137 expected = {'submit-name': ['just a string\n']} 138 self.assertEqual(result, expected) 139 140 def test_parse_multipart_invalid_encoding(self): 141 BOUNDARY = "JfISa01" 142 POSTDATA = """--JfISa01 143Content-Disposition: form-data; name="submit-name" 144Content-Length: 3 145 146\u2603 147--JfISa01""" 148 fp = BytesIO(POSTDATA.encode('utf8')) 149 env = {'boundary': BOUNDARY.encode('latin1'), 150 'CONTENT-LENGTH': str(len(POSTDATA.encode('utf8')))} 151 result = cgi.parse_multipart(fp, env, encoding="ascii", 152 errors="surrogateescape") 153 expected = {'submit-name': ["\udce2\udc98\udc83"]} 154 self.assertEqual(result, expected) 155 self.assertEqual("\u2603".encode('utf8'), 156 result["submit-name"][0].encode('utf8', 'surrogateescape')) 157 158 def test_fieldstorage_properties(self): 159 fs = cgi.FieldStorage() 160 self.assertFalse(fs) 161 self.assertIn("FieldStorage", repr(fs)) 162 self.assertEqual(list(fs), list(fs.keys())) 163 fs.list.append(namedtuple('MockFieldStorage', 'name')('fieldvalue')) 164 self.assertTrue(fs) 165 166 def test_fieldstorage_invalid(self): 167 self.assertRaises(TypeError, cgi.FieldStorage, "not-a-file-obj", 168 environ={"REQUEST_METHOD":"PUT"}) 169 self.assertRaises(TypeError, cgi.FieldStorage, "foo", "bar") 170 fs = cgi.FieldStorage(headers={'content-type':'text/plain'}) 171 self.assertRaises(TypeError, bool, fs) 172 173 def test_strict(self): 174 for orig, expect in parse_strict_test_cases: 175 # Test basic parsing 176 d = do_test(orig, "GET") 177 self.assertEqual(d, expect, "Error parsing %s method GET" % repr(orig)) 178 d = do_test(orig, "POST") 179 self.assertEqual(d, expect, "Error parsing %s method POST" % repr(orig)) 180 181 env = {'QUERY_STRING': orig} 182 fs = cgi.FieldStorage(environ=env) 183 if isinstance(expect, dict): 184 # test dict interface 185 self.assertEqual(len(expect), len(fs)) 186 self.assertCountEqual(expect.keys(), fs.keys()) 187 ##self.assertEqual(norm(expect.values()), norm(fs.values())) 188 ##self.assertEqual(norm(expect.items()), norm(fs.items())) 189 self.assertEqual(fs.getvalue("nonexistent field", "default"), "default") 190 # test individual fields 191 for key in expect.keys(): 192 expect_val = expect[key] 193 self.assertIn(key, fs) 194 if len(expect_val) > 1: 195 self.assertEqual(fs.getvalue(key), expect_val) 196 else: 197 self.assertEqual(fs.getvalue(key), expect_val[0]) 198 199 def test_separator(self): 200 parse_semicolon = [ 201 ("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}), 202 ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}), 203 (";", ValueError("bad query field: ''")), 204 (";;", ValueError("bad query field: ''")), 205 ("=;a", ValueError("bad query field: 'a'")), 206 (";b=a", ValueError("bad query field: ''")), 207 ("b;=a", ValueError("bad query field: 'b'")), 208 ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}), 209 ("a=a+b;a=b+a", {'a': ['a b', 'b a']}), 210 ] 211 for orig, expect in parse_semicolon: 212 env = {'QUERY_STRING': orig} 213 fs = cgi.FieldStorage(separator=';', environ=env) 214 if isinstance(expect, dict): 215 for key in expect.keys(): 216 expect_val = expect[key] 217 self.assertIn(key, fs) 218 if len(expect_val) > 1: 219 self.assertEqual(fs.getvalue(key), expect_val) 220 else: 221 self.assertEqual(fs.getvalue(key), expect_val[0]) 222 223 def test_log(self): 224 cgi.log("Testing") 225 226 cgi.logfp = StringIO() 227 cgi.initlog("%s", "Testing initlog 1") 228 cgi.log("%s", "Testing log 2") 229 self.assertEqual(cgi.logfp.getvalue(), "Testing initlog 1\nTesting log 2\n") 230 if os.path.exists(os.devnull): 231 cgi.logfp = None 232 cgi.logfile = os.devnull 233 cgi.initlog("%s", "Testing log 3") 234 self.addCleanup(cgi.closelog) 235 cgi.log("Testing log 4") 236 237 def test_fieldstorage_readline(self): 238 # FieldStorage uses readline, which has the capacity to read all 239 # contents of the input file into memory; we use readline's size argument 240 # to prevent that for files that do not contain any newlines in 241 # non-GET/HEAD requests 242 class TestReadlineFile: 243 def __init__(self, file): 244 self.file = file 245 self.numcalls = 0 246 247 def readline(self, size=None): 248 self.numcalls += 1 249 if size: 250 return self.file.readline(size) 251 else: 252 return self.file.readline() 253 254 def __getattr__(self, name): 255 file = self.__dict__['file'] 256 a = getattr(file, name) 257 if not isinstance(a, int): 258 setattr(self, name, a) 259 return a 260 261 f = TestReadlineFile(tempfile.TemporaryFile("wb+")) 262 self.addCleanup(f.close) 263 f.write(b'x' * 256 * 1024) 264 f.seek(0) 265 env = {'REQUEST_METHOD':'PUT'} 266 fs = cgi.FieldStorage(fp=f, environ=env) 267 self.addCleanup(fs.file.close) 268 # if we're not chunking properly, readline is only called twice 269 # (by read_binary); if we are chunking properly, it will be called 5 times 270 # as long as the chunksize is 1 << 16. 271 self.assertGreater(f.numcalls, 2) 272 f.close() 273 274 def test_fieldstorage_multipart(self): 275 #Test basic FieldStorage multipart parsing 276 env = { 277 'REQUEST_METHOD': 'POST', 278 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 279 'CONTENT_LENGTH': '558'} 280 fp = BytesIO(POSTDATA.encode('latin-1')) 281 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 282 self.assertEqual(len(fs.list), 4) 283 expect = [{'name':'id', 'filename':None, 'value':'1234'}, 284 {'name':'title', 'filename':None, 'value':''}, 285 {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'}, 286 {'name':'submit', 'filename':None, 'value':' Add '}] 287 for x in range(len(fs.list)): 288 for k, exp in expect[x].items(): 289 got = getattr(fs.list[x], k) 290 self.assertEqual(got, exp) 291 292 def test_fieldstorage_multipart_leading_whitespace(self): 293 env = { 294 'REQUEST_METHOD': 'POST', 295 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 296 'CONTENT_LENGTH': '560'} 297 # Add some leading whitespace to our post data that will cause the 298 # first line to not be the innerboundary. 299 fp = BytesIO(b"\r\n" + POSTDATA.encode('latin-1')) 300 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 301 self.assertEqual(len(fs.list), 4) 302 expect = [{'name':'id', 'filename':None, 'value':'1234'}, 303 {'name':'title', 'filename':None, 'value':''}, 304 {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'}, 305 {'name':'submit', 'filename':None, 'value':' Add '}] 306 for x in range(len(fs.list)): 307 for k, exp in expect[x].items(): 308 got = getattr(fs.list[x], k) 309 self.assertEqual(got, exp) 310 311 def test_fieldstorage_multipart_non_ascii(self): 312 #Test basic FieldStorage multipart parsing 313 env = {'REQUEST_METHOD':'POST', 314 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 315 'CONTENT_LENGTH':'558'} 316 for encoding in ['iso-8859-1','utf-8']: 317 fp = BytesIO(POSTDATA_NON_ASCII.encode(encoding)) 318 fs = cgi.FieldStorage(fp, environ=env,encoding=encoding) 319 self.assertEqual(len(fs.list), 1) 320 expect = [{'name':'id', 'filename':None, 'value':'\xe7\xf1\x80'}] 321 for x in range(len(fs.list)): 322 for k, exp in expect[x].items(): 323 got = getattr(fs.list[x], k) 324 self.assertEqual(got, exp) 325 326 def test_fieldstorage_multipart_maxline(self): 327 # Issue #18167 328 maxline = 1 << 16 329 self.maxDiff = None 330 def check(content): 331 data = """---123 332Content-Disposition: form-data; name="upload"; filename="fake.txt" 333Content-Type: text/plain 334 335%s 336---123-- 337""".replace('\n', '\r\n') % content 338 environ = { 339 'CONTENT_LENGTH': str(len(data)), 340 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 341 'REQUEST_METHOD': 'POST', 342 } 343 self.assertEqual(gen_result(data, environ), 344 {'upload': content.encode('latin1')}) 345 check('x' * (maxline - 1)) 346 check('x' * (maxline - 1) + '\r') 347 check('x' * (maxline - 1) + '\r' + 'y' * (maxline - 1)) 348 349 def test_fieldstorage_multipart_w3c(self): 350 # Test basic FieldStorage multipart parsing (W3C sample) 351 env = { 352 'REQUEST_METHOD': 'POST', 353 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY_W3), 354 'CONTENT_LENGTH': str(len(POSTDATA_W3))} 355 fp = BytesIO(POSTDATA_W3.encode('latin-1')) 356 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 357 self.assertEqual(len(fs.list), 2) 358 self.assertEqual(fs.list[0].name, 'submit-name') 359 self.assertEqual(fs.list[0].value, 'Larry') 360 self.assertEqual(fs.list[1].name, 'files') 361 files = fs.list[1].value 362 self.assertEqual(len(files), 2) 363 expect = [{'name': None, 'filename': 'file1.txt', 'value': b'... contents of file1.txt ...'}, 364 {'name': None, 'filename': 'file2.gif', 'value': b'...contents of file2.gif...'}] 365 for x in range(len(files)): 366 for k, exp in expect[x].items(): 367 got = getattr(files[x], k) 368 self.assertEqual(got, exp) 369 370 def test_fieldstorage_part_content_length(self): 371 BOUNDARY = "JfISa01" 372 POSTDATA = """--JfISa01 373Content-Disposition: form-data; name="submit-name" 374Content-Length: 5 375 376Larry 377--JfISa01""" 378 env = { 379 'REQUEST_METHOD': 'POST', 380 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 381 'CONTENT_LENGTH': str(len(POSTDATA))} 382 fp = BytesIO(POSTDATA.encode('latin-1')) 383 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 384 self.assertEqual(len(fs.list), 1) 385 self.assertEqual(fs.list[0].name, 'submit-name') 386 self.assertEqual(fs.list[0].value, 'Larry') 387 388 def test_field_storage_multipart_no_content_length(self): 389 fp = BytesIO(b"""--MyBoundary 390Content-Disposition: form-data; name="my-arg"; filename="foo" 391 392Test 393 394--MyBoundary-- 395""") 396 env = { 397 "REQUEST_METHOD": "POST", 398 "CONTENT_TYPE": "multipart/form-data; boundary=MyBoundary", 399 "wsgi.input": fp, 400 } 401 fields = cgi.FieldStorage(fp, environ=env) 402 403 self.assertEqual(len(fields["my-arg"].file.read()), 5) 404 405 def test_fieldstorage_as_context_manager(self): 406 fp = BytesIO(b'x' * 10) 407 env = {'REQUEST_METHOD': 'PUT'} 408 with cgi.FieldStorage(fp=fp, environ=env) as fs: 409 content = fs.file.read() 410 self.assertFalse(fs.file.closed) 411 self.assertTrue(fs.file.closed) 412 self.assertEqual(content, 'x' * 10) 413 with self.assertRaisesRegex(ValueError, 'I/O operation on closed file'): 414 fs.file.read() 415 416 _qs_result = { 417 'key1': 'value1', 418 'key2': ['value2x', 'value2y'], 419 'key3': 'value3', 420 'key4': 'value4' 421 } 422 def testQSAndUrlEncode(self): 423 data = "key2=value2x&key3=value3&key4=value4" 424 environ = { 425 'CONTENT_LENGTH': str(len(data)), 426 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 427 'QUERY_STRING': 'key1=value1&key2=value2y', 428 'REQUEST_METHOD': 'POST', 429 } 430 v = gen_result(data, environ) 431 self.assertEqual(self._qs_result, v) 432 433 def test_max_num_fields(self): 434 # For application/x-www-form-urlencoded 435 data = '&'.join(['a=a']*11) 436 environ = { 437 'CONTENT_LENGTH': str(len(data)), 438 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 439 'REQUEST_METHOD': 'POST', 440 } 441 442 with self.assertRaises(ValueError): 443 cgi.FieldStorage( 444 fp=BytesIO(data.encode()), 445 environ=environ, 446 max_num_fields=10, 447 ) 448 449 # For multipart/form-data 450 data = """---123 451Content-Disposition: form-data; name="a" 452 4533 454---123 455Content-Type: application/x-www-form-urlencoded 456 457a=4 458---123 459Content-Type: application/x-www-form-urlencoded 460 461a=5 462---123-- 463""" 464 environ = { 465 'CONTENT_LENGTH': str(len(data)), 466 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 467 'QUERY_STRING': 'a=1&a=2', 468 'REQUEST_METHOD': 'POST', 469 } 470 471 # 2 GET entities 472 # 1 top level POST entities 473 # 1 entity within the second POST entity 474 # 1 entity within the third POST entity 475 with self.assertRaises(ValueError): 476 cgi.FieldStorage( 477 fp=BytesIO(data.encode()), 478 environ=environ, 479 max_num_fields=4, 480 ) 481 cgi.FieldStorage( 482 fp=BytesIO(data.encode()), 483 environ=environ, 484 max_num_fields=5, 485 ) 486 487 def testQSAndFormData(self): 488 data = """---123 489Content-Disposition: form-data; name="key2" 490 491value2y 492---123 493Content-Disposition: form-data; name="key3" 494 495value3 496---123 497Content-Disposition: form-data; name="key4" 498 499value4 500---123-- 501""" 502 environ = { 503 'CONTENT_LENGTH': str(len(data)), 504 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 505 'QUERY_STRING': 'key1=value1&key2=value2x', 506 'REQUEST_METHOD': 'POST', 507 } 508 v = gen_result(data, environ) 509 self.assertEqual(self._qs_result, v) 510 511 def testQSAndFormDataFile(self): 512 data = """---123 513Content-Disposition: form-data; name="key2" 514 515value2y 516---123 517Content-Disposition: form-data; name="key3" 518 519value3 520---123 521Content-Disposition: form-data; name="key4" 522 523value4 524---123 525Content-Disposition: form-data; name="upload"; filename="fake.txt" 526Content-Type: text/plain 527 528this is the content of the fake file 529 530---123-- 531""" 532 environ = { 533 'CONTENT_LENGTH': str(len(data)), 534 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 535 'QUERY_STRING': 'key1=value1&key2=value2x', 536 'REQUEST_METHOD': 'POST', 537 } 538 result = self._qs_result.copy() 539 result.update({ 540 'upload': b'this is the content of the fake file\n' 541 }) 542 v = gen_result(data, environ) 543 self.assertEqual(result, v) 544 545 def test_parse_header(self): 546 self.assertEqual( 547 cgi.parse_header("text/plain"), 548 ("text/plain", {})) 549 self.assertEqual( 550 cgi.parse_header("text/vnd.just.made.this.up ; "), 551 ("text/vnd.just.made.this.up", {})) 552 self.assertEqual( 553 cgi.parse_header("text/plain;charset=us-ascii"), 554 ("text/plain", {"charset": "us-ascii"})) 555 self.assertEqual( 556 cgi.parse_header('text/plain ; charset="us-ascii"'), 557 ("text/plain", {"charset": "us-ascii"})) 558 self.assertEqual( 559 cgi.parse_header('text/plain ; charset="us-ascii"; another=opt'), 560 ("text/plain", {"charset": "us-ascii", "another": "opt"})) 561 self.assertEqual( 562 cgi.parse_header('attachment; filename="silly.txt"'), 563 ("attachment", {"filename": "silly.txt"})) 564 self.assertEqual( 565 cgi.parse_header('attachment; filename="strange;name"'), 566 ("attachment", {"filename": "strange;name"})) 567 self.assertEqual( 568 cgi.parse_header('attachment; filename="strange;name";size=123;'), 569 ("attachment", {"filename": "strange;name", "size": "123"})) 570 self.assertEqual( 571 cgi.parse_header('form-data; name="files"; filename="fo\\"o;bar"'), 572 ("form-data", {"name": "files", "filename": 'fo"o;bar'})) 573 574 def test_all(self): 575 blacklist = {"logfile", "logfp", "initlog", "dolog", "nolog", 576 "closelog", "log", "maxlen", "valid_boundary"} 577 support.check__all__(self, cgi, blacklist=blacklist) 578 579 580BOUNDARY = "---------------------------721837373350705526688164684" 581 582POSTDATA = """-----------------------------721837373350705526688164684 583Content-Disposition: form-data; name="id" 584 5851234 586-----------------------------721837373350705526688164684 587Content-Disposition: form-data; name="title" 588 589 590-----------------------------721837373350705526688164684 591Content-Disposition: form-data; name="file"; filename="test.txt" 592Content-Type: text/plain 593 594Testing 123. 595 596-----------------------------721837373350705526688164684 597Content-Disposition: form-data; name="submit" 598 599 Add\x20 600-----------------------------721837373350705526688164684-- 601""" 602 603POSTDATA_NON_ASCII = """-----------------------------721837373350705526688164684 604Content-Disposition: form-data; name="id" 605 606\xe7\xf1\x80 607-----------------------------721837373350705526688164684 608""" 609 610# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4 611BOUNDARY_W3 = "AaB03x" 612POSTDATA_W3 = """--AaB03x 613Content-Disposition: form-data; name="submit-name" 614 615Larry 616--AaB03x 617Content-Disposition: form-data; name="files" 618Content-Type: multipart/mixed; boundary=BbC04y 619 620--BbC04y 621Content-Disposition: file; filename="file1.txt" 622Content-Type: text/plain 623 624... contents of file1.txt ... 625--BbC04y 626Content-Disposition: file; filename="file2.gif" 627Content-Type: image/gif 628Content-Transfer-Encoding: binary 629 630...contents of file2.gif... 631--BbC04y-- 632--AaB03x-- 633""" 634 635if __name__ == '__main__': 636 unittest.main() 637