1 #!/usr/bin/env python 2 3 """ 4 UEFfile.py - Handle UEF archives. 5 6 Copyright (c) 2001-2010, David Boddie <david@boddie.org.uk> 7 8 This program is free software: you can redistribute it and/or modify 9 it under the terms of the GNU General Public License as published by 10 the Free Software Foundation, either version 3 of the License, or 11 (at your option) any later version. 12 13 This program is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public License 19 along with this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 import exceptions, sys, string, os, gzip, types 23 24 class UEFfile_error(exceptions.Exception): 25 26 pass 27 28 # Determine the platform on which the program is running 29 30 sep = os.sep 31 32 if sys.platform == 'RISCOS': 33 suffix = '/' 34 else: 35 suffix = '.' 36 37 version = '0.20' 38 date = '2010-10-24' 39 40 41 class UEFfile: 42 """instance = UEFfile(filename, creator) 43 44 Create an instance of a UEF container using an existing file. 45 If filename is not defined then create a new UEF container. 46 The creator parameter can be used to override the default 47 creator string. 48 49 """ 50 51 def __init__(self, filename = None, creator = 'UEFfile '+version): 52 """Create a new instance of the UEFfile class.""" 53 54 if filename == None: 55 56 # There are no chunks initially 57 self.chunks = [] 58 # There are no file positions defined 59 self.files = [] 60 61 # Emulator associated with this UEF file 62 self.emulator = 'Unspecified' 63 # Originator/creator of the UEF file 64 self.creator = creator 65 # Target machine 66 self.target_machine = '' 67 # Keyboard layout 68 self.keyboard_layout = '' 69 # Features 70 self.features = '' 71 72 # UEF file format version 73 self.minor = 9 74 self.major = 0 75 76 # List of files 77 self.contents = [] 78 else: 79 # Read in the chunks from the file 80 81 # Open the input file 82 try: 83 in_f = open(filename, 'rb') 84 except IOError: 85 raise UEFfile_error, 'The input file, '+filename+' could not be found.' 86 87 # Is it gzipped? 88 if in_f.read(10) != 'UEF File!\000': 89 90 in_f.close() 91 in_f = gzip.open(filename, 'rb') 92 93 try: 94 if in_f.read(10) != 'UEF File!\000': 95 in_f.close() 96 raise UEFfile_error, 'The input file, '+filename+' is not a UEF file.' 97 except: 98 in_f.close() 99 raise UEFfile_error, 'The input file, '+filename+' could not be read.' 100 101 # Read version self.number of the file format 102 self.minor = self.str2num(1, in_f.read(1)) 103 self.major = self.str2num(1, in_f.read(1)) 104 105 # Decode the UEF file 106 107 # List of chunks 108 self.chunks = [] 109 110 # Read chunks 111 112 while 1: 113 114 # Read chunk ID 115 chunk_id = in_f.read(2) 116 if not chunk_id: 117 break 118 119 chunk_id = self.str2num(2, chunk_id) 120 121 length = self.str2num(4, in_f.read(4)) 122 123 if length != 0: 124 self.chunks.append((chunk_id, in_f.read(length))) 125 else: 126 self.chunks.append((chunk_id, '')) 127 128 # Close the input file 129 in_f.close() 130 131 # UEF file information (placed in "creator", "target_machine", 132 # "keyboard_layout", "emulator" and "features" attributes). 133 self.read_uef_details() 134 135 # Read file contents (placed in the list attribute "contents"). 136 self.read_contents() 137 138 139 def write(self, filename, write_creator_info = True, 140 write_machine_info = True, write_emulator_info = True): 141 """ 142 Write a UEF file containing all the information stored in an 143 instance of UEFfile to the file with the specified filename. 144 145 By default, information about the file's creator, target machine and 146 emulator is written to the file. These can be omitted by calling this 147 method with individual arguments set to False. 148 """ 149 150 # Open the UEF file for writing 151 try: 152 uef = gzip.open(filename, 'wb') 153 except IOError: 154 raise UEFfile_error, "Couldn't open %s for writing." % filename 155 156 # Write the UEF file header 157 self.write_uef_header(uef) 158 159 if write_creator_info: 160 # Write the UEF creator chunk to the file 161 self.write_uef_creator(uef) 162 163 if write_machine_info: 164 # Write the machine information 165 self.write_machine_info(uef) 166 167 if write_emulator_info: 168 # Write the emulator information 169 self.write_emulator_info(uef) 170 171 # Write the chunks to the file 172 self.write_chunks(uef) 173 174 # Close the file 175 uef.close() 176 177 178 def number(self, size, n): 179 """Convert a number to a little endian string of bytes for writing to a binary file.""" 180 181 # Little endian writing 182 183 s = "" 184 185 while size > 0: 186 i = n % 256 187 s = s + chr(i) 188 n = n >> 8 189 size = size - 1 190 191 return s 192 193 194 def str2num(self, size, s): 195 """Convert a string of ASCII characters to an integer.""" 196 197 i = 0 198 n = 0 199 while i < size: 200 201 n = n | (ord(s[i]) << (i*8)) 202 i = i + 1 203 204 return n 205 206 207 def hex2num(self, s): 208 """Convert a string of hexadecimal digits to an integer.""" 209 210 n = 0 211 212 for i in range(0,len(s)): 213 214 a = ord(s[len(s)-i-1]) 215 if (a >= 48) & (a <= 57): 216 n = n | ((a-48) << (i*4)) 217 elif (a >= 65) & (a <= 70): 218 n = n | ((a-65+10) << (i*4)) 219 elif (a >= 97) & (a <= 102): 220 n = n | ((a-97+10) << (i*4)) 221 else: 222 return None 223 224 return n 225 226 227 # CRC calculation routines (begin) 228 229 def rol(self, n, c): 230 231 n = n << 1 232 233 if (n & 256) != 0: 234 carry = 1 235 n = n & 255 236 else: 237 carry = 0 238 239 n = n | c 240 241 return n, carry 242 243 244 def crc(self, s): 245 246 high = 0 247 low = 0 248 249 for i in s: 250 251 high = high ^ ord(i) 252 253 for j in range(0,8): 254 255 a, carry = self.rol(high, 0) 256 257 if carry == 1: 258 high = high ^ 8 259 low = low ^ 16 260 261 low, carry = self.rol(low, carry) 262 high, carry = self.rol(high, carry) 263 264 return high | (low << 8) 265 266 # CRC calculation routines (end) 267 268 def read_contents(self): 269 """Find the positions of files in the list of chunks""" 270 271 # List of files 272 self.contents = [] 273 274 current_file = {} 275 276 position = 0 277 278 while 1: 279 280 position = self.find_next_block(position) 281 282 if position == None: 283 284 # No more blocks, so store the details of the last file in 285 # the contents list 286 if current_file != {}: 287 self.contents.append(current_file) 288 break 289 290 else: 291 292 # Read the block information 293 name, load, exec_addr, data, block_number, last = self.read_block(self.chunks[position]) 294 295 if current_file == {}: 296 297 # No current file, so store details 298 current_file = {'name': name, 'load': load, 'exec': exec_addr, 'blocks': block_number, 'data': data} 299 300 # Locate the first non-block chunk before the block 301 # and store the position of the file 302 current_file['position'] = self.find_file_start(position) 303 # This may also be the position of the last chunk related to 304 # this file in the archive 305 current_file['last position'] = position 306 else: 307 308 # Current file exists 309 if block_number == 0: 310 311 # New file, so write the previous one to the 312 # contents list, but before doing so, find the next 313 # non-block chunk and mark that as the last chunk in 314 # the file 315 316 if current_file != {}: 317 self.contents.append(current_file) 318 319 # Store details of this new file 320 current_file = {'name': name, 'load': load, 'exec': exec_addr, 'blocks': block_number, 'data': data} 321 322 # Locate the first non-block chunk before the block 323 # and store the position of the file 324 current_file['position'] = self.find_file_start(position) 325 # This may also be the position of the last chunk related to 326 # this file in the archive 327 current_file['last position'] = position 328 else: 329 # Not a new file, so update the number of 330 # blocks and append the block data to the 331 # data entry 332 current_file['blocks'] = block_number 333 current_file['data'] = current_file['data'] + data 334 335 # Update the last position information to mark the end of the file 336 current_file['last position'] = position 337 338 # Increase the position 339 position = position + 1 340 341 # We now have a contents list which tells us 342 # 1) the names of files in the archive 343 # 2) the load and execution addresses of them 344 # 3) the number of blocks they contain 345 # 4) their data, and from this their length 346 # 5) their start position (chunk number) in the archive 347 348 349 def chunk(self, f, n, data): 350 """Write a chunk to the file specified by the open file object, chunk number and data supplied.""" 351 352 # Chunk ID 353 f.write(self.number(2, n)) 354 # Chunk length 355 f.write(self.number(4, len(data))) 356 # Data 357 f.write(data) 358 359 360 def read_block(self, chunk): 361 """Read a data block from a tape chunk and return the program name, load and execution addresses, 362 block data, block number and whether the block is supposedly the last in the file.""" 363 364 # Chunk number and data 365 chunk_id = chunk[0] 366 data = chunk[1] 367 368 # For the implicit tape data chunk, just read the block as a series 369 # of bytes, as before 370 if chunk_id == 0x100: 371 372 block = data 373 374 else: # 0x102 375 376 if UEF_major == 0 and UEF_minor < 9: 377 378 # For UEF file versions earlier than 0.9, the number of 379 # excess bits to be ignored at the end of the stream is 380 # set to zero implicitly 381 ignore = 0 382 bit_ptr = 0 383 else: 384 # For later versions, the number of excess bits is 385 # specified in the first byte of the stream 386 ignore = data[0] 387 bit_ptr = 8 388 389 # Convert the data to the implicit format 390 block = [] 391 write_ptr = 0 392 393 after_end = (len(data)*8) - ignore 394 if after_end % 10 != 0: 395 396 # Ensure that the number of bits to be read is a 397 # multiple of ten 398 after_end = after_end - (after_end % 10) 399 400 while bit_ptr < after_end: 401 402 # Skip start bit 403 bit_ptr = bit_ptr + 1 404 405 # Read eight bits of data 406 bit_offset = bit_ptr % 8 407 if bit_offset == 0: 408 # Write the byte to the block 409 block[write_ptr] = data[bit_ptr >> 3] 410 else: 411 # Read the byte containing the first bits 412 b1 = data[bit_ptr >> 3] 413 # Read the byte containing the rest 414 b2 = data[(bit_ptr >> 3) + 1] 415 416 # Construct a byte of data 417 # Shift the first byte right by the bit offset 418 # in that byte 419 b1 = b1 >> bit_offset 420 421 # Shift the rest of the bits from the second 422 # byte to the left and ensure that the result 423 # fits in a byte 424 b2 = (b2 << (8 - bit_offset)) & 0xff 425 426 # OR the two bytes together and write it to 427 # the block 428 block[write_ptr] = b1 | b2 429 430 # Increment the block pointer 431 write_ptr = write_ptr + 1 432 433 # Move the data pointer on eight bits and skip the 434 # stop bit 435 bit_ptr = bit_ptr + 9 436 437 # Read the block 438 name = '' 439 a = 1 440 while 1: 441 c = block[a] 442 if ord(c) != 0: # was > 32: 443 name = name + c 444 a = a + 1 445 if ord(c) == 0: 446 break 447 448 load = self.str2num(4, block[a:a+4]) 449 exec_addr = self.str2num(4, block[a+4:a+8]) 450 block_number = self.str2num(2, block[a+8:a+10]) 451 last = self.str2num(1, block[a+12]) 452 453 if last & 0x80 != 0: 454 last = 1 455 else: 456 last = 0 457 458 return (name, load, exec_addr, block[a+19:-2], block_number, last) 459 460 461 def write_block(self, block, name, load, exe, n): 462 """Write data to a string as a file data block in preparation to be written 463 as chunk data to a UEF file.""" 464 465 # Write the alignment character 466 out = "*"+name[:10]+"\000" 467 468 # Load address 469 out = out + self.number(4, load) 470 471 # Execution address 472 out = out + self.number(4, exe) 473 474 # Block number 475 out = out + self.number(2, n) 476 477 # Block length 478 out = out + self.number(2, len(block)) 479 480 # Block flag (last block) 481 if len(block) == 256: 482 out = out + self.number(1, 0) 483 last = 0 484 else: 485 out = out + self.number(1, 128) # shouldn't be needed 486 last = 1 487 488 # Next address 489 out = out + self.number(2, 0) 490 491 # Unknown 492 out = out + self.number(2, 0) 493 494 # Header CRC 495 out = out + self.number(2, self.crc(out[1:])) 496 497 out = out + block 498 499 # Block CRC 500 out = out + self.number(2, self.crc(block)) 501 502 return out, last 503 504 505 def get_leafname(self, path): 506 """Get the leafname of the specified file.""" 507 508 pos = string.rfind(path, os.sep) 509 if pos != -1: 510 return path[pos+1:] 511 else: 512 return path 513 514 515 def find_next_chunk(self, pos, IDs): 516 """position, chunk = find_next_chunk(start, IDs) 517 Search through the list of chunks from the start position given 518 for the next chunk with an ID in the list of IDs supplied. 519 Return its position in the list of chunks and its details.""" 520 521 while pos < len(self.chunks): 522 523 if self.chunks[pos][0] in IDs: 524 525 # Found a chunk with ID in the list 526 return pos, self.chunks[pos] 527 528 # Otherwise continue looking 529 pos = pos + 1 530 531 return None, None 532 533 534 def find_next_block(self, pos): 535 """Find the next file block in the list of chunks.""" 536 537 while pos < len(self.chunks): 538 539 pos, chunk = self.find_next_chunk(pos, [0x100, 0x102]) 540 541 if pos == None: 542 543 return None 544 else: 545 if len(chunk[1]) > 1: 546 547 # Found a block, return this position 548 return pos 549 550 # Otherwise continue looking 551 pos = pos + 1 552 553 return None 554 555 556 def find_file_start(self, pos): 557 """Find a chunk before the one specified which is not a file block.""" 558 559 pos = pos - 1 560 while pos > 0: 561 562 if self.chunks[pos][0] != 0x100 and self.chunks[pos][0] != 0x102: 563 564 # This is not a block 565 return pos 566 567 else: 568 pos = pos - 1 569 570 return pos 571 572 573 def find_file_end(self, pos): 574 """Find a chunk after the one specified which is not a file block.""" 575 576 pos = pos + 1 577 while pos < len(self.chunks)-1: 578 579 if self.chunks[pos][0] != 0x100 and self.chunks[pos][0] != 0x102: 580 581 # This is not a block 582 return pos 583 584 else: 585 pos = pos + 1 586 587 return pos 588 589 590 def read_uef_details(self): 591 """Return details about the UEF file and its contents.""" 592 593 # Find the creator chunk 594 pos, chunk = self.find_next_chunk(0, [0x0]) 595 596 if pos == None: 597 598 self.creator = 'Unknown' 599 600 elif chunk[1] == '': 601 602 self.creator = 'Unknown' 603 else: 604 self.creator = chunk[1] 605 606 # Delete the creator chunk 607 if pos != None: 608 del self.chunks[pos] 609 610 # Find the target machine chunk 611 pos, chunk = self.find_next_chunk(0, [0x5]) 612 613 if pos == None: 614 615 self.target_machine = 'Unknown' 616 self.keyboard_layout = 'Unknown' 617 else: 618 619 machines = ('BBC Model A', 'Electron', 'BBC Model B', 'BBC Master') 620 keyboards = ('Any layout', 'Physical layout', 'Remapped') 621 622 machine = ord(chunk[1][0]) & 0x0f 623 keyboard = (ord(chunk[1][0]) & 0xf0) >> 4 624 625 if machine < len(machines): 626 self.target_machine = machines[machine] 627 else: 628 self.target_machine = 'Unknown' 629 630 if keyboard < len(keyboards): 631 self.keyboard_layout = keyboards[keyboard] 632 else: 633 self.keyboard_layout = 'Unknown' 634 635 # Delete the target machine chunk 636 del self.chunks[pos] 637 638 # Find the emulator chunk 639 pos, chunk = self.find_next_chunk(0, [0xff00]) 640 641 if pos == None: 642 643 self.emulator = 'Unspecified' 644 645 elif chunk[1] == '': 646 647 self.emulator = 'Unknown' 648 else: 649 self.emulator = chunk[1] 650 651 # Delete the emulator chunk 652 if pos != None: 653 del self.chunks[pos] 654 655 # Remove trailing null bytes 656 while len(self.creator) > 0 and self.creator[-1] == '\000': 657 658 self.creator = self.creator[:-1] 659 660 while len(self.emulator) > 0 and self.emulator[-1] == '\000': 661 662 self.emulator = self.emulator[:-1] 663 664 self.features = '' 665 if self.find_next_chunk(0, [0x1])[0] != None: 666 self.features = self.features + '\n' + 'Instructions' 667 if self.find_next_chunk(0, [0x2])[0] != None: 668 self.features = self.features + '\n' + 'Credits' 669 if self.find_next_chunk(0, [0x3])[0] != None: 670 self.features = self.features + '\n' + 'Inlay' 671 672 673 def write_uef_header(self, file): 674 """Write the UEF file header and version number to a file.""" 675 676 # Write the UEF file header 677 file.write('UEF File!\000') 678 679 # Minor and major version numbers 680 file.write(self.number(1, self.minor) + self.number(1, self.major)) 681 682 683 def write_uef_creator(self, file): 684 """Write a creator chunk to a file.""" 685 686 origin = self.creator + '\000' 687 688 if (len(origin) % 4) != 0: 689 origin = origin + ('\000'*(4-(len(origin) % 4))) 690 691 # Write the creator chunk 692 self.chunk(file, 0, origin) 693 694 695 def write_machine_info(self, file): 696 """Write the target machine and keyboard layout information to a file.""" 697 698 machines = {'BBC Model A': 0, 'Electron': 1, 'BBC Model B': 2, 'BBC Master':3} 699 keyboards = {'any': 0, 'physical': 1, 'logical': 2} 700 701 if machines.has_key(self.target_machine): 702 703 machine = machines[self.target_machine] 704 else: 705 machine = 0 706 707 if keyboards.has_key(self.keyboard_layout): 708 709 keyboard = keyboards[keyboard_layout] 710 else: 711 keyboard = 0 712 713 self.chunk(file, 5, self.number(1, machine | (keyboard << 4) )) 714 715 716 def write_emulator_info(self, file): 717 """Write an emulator chunk to a file.""" 718 719 emulator = self.emulator + '\000' 720 721 if (len(emulator) % 4) != 0: 722 emulator = emulator + ('\000'*(4-(len(emulator) % 4))) 723 724 # Write the creator chunk 725 self.chunk(file, 0xff00, emulator) 726 727 728 def write_chunks(self, file): 729 """Write all the chunks in the list to a file. Saves having loops in other functions to do this.""" 730 731 for c in self.chunks: 732 733 self.chunk(file, c[0], c[1]) 734 735 736 def create_chunks(self, name, load, exe, data): 737 """Create suitable chunks, and insert them into 738 the list of chunks.""" 739 740 # Reset the block number to zero 741 block_number = 0 742 743 # Long gap 744 gap = 1 745 746 new_chunks = [] 747 748 # Write block details 749 while 1: 750 block, last = self.write_block(data[:256], name, load, exe, block_number) 751 752 # Remove the leading 256 bytes as they have been encoded 753 data = data[256:] 754 755 if gap == 1: 756 new_chunks.append((0x110, self.number(2,0x05dc))) 757 gap = 0 758 else: 759 new_chunks.append((0x110, self.number(2,0x0258))) 760 761 # Write the block to the list of new chunks 762 new_chunks.append((0x100, block)) 763 764 if last == 1: 765 break 766 767 # Increment the block number 768 block_number = block_number + 1 769 770 # Return the list of new chunks 771 return new_chunks 772 773 774 def import_files(self, file_position, info): 775 """ 776 Import a file into the UEF file at the specified location in the 777 list of contents. 778 positions is a positive integer or zero 779 780 To insert one file, info can be a sequence: 781 782 info = (name, load, exe, data) where 783 name is the file's name. 784 load is the load address of the file. 785 exe is the execution address. 786 data is the contents of the file. 787 788 For more than one file, info must be a sequence of info sequences. 789 """ 790 791 if file_position < 0: 792 793 raise UEFfile_error, 'Position must be zero or greater.' 794 795 # Find the chunk position which corresponds to the file_position 796 if self.contents != []: 797 798 # There are files already present 799 if file_position >= len(self.contents): 800 801 # Position the new files after the end of the last file 802 position = self.contents[-1]['last position'] + 1 803 804 else: 805 806 # Position the new files before the end of the file 807 # specified 808 position = self.contents[file_position]['position'] 809 else: 810 # There are no files present in the archive, so put them after 811 # all the other chunks 812 position = len(self.chunks) 813 814 # Examine the info sequence passed 815 if len(info) == 0: 816 return 817 818 if type(info[0]) == types.StringType: 819 820 # Assume that the info sequence contains name, load, exe, data 821 info = [info] 822 823 # Read the file details for each file and create chunks to add 824 # to the list of chunks 825 inserted_chunks = [] 826 827 for name, load, exe, data in info: 828 829 inserted_chunks = inserted_chunks + self.create_chunks(name, load, exe, data) 830 831 # Insert the chunks in the list at the specified position 832 self.chunks = self.chunks[:position] + inserted_chunks + self.chunks[position:] 833 834 # Update the contents list 835 self.read_contents() 836 837 838 def chunk_number(self, name): 839 """ 840 Returns the relevant chunk number for the name given. 841 """ 842 843 # Use a convention for determining the chunk number to be used: 844 # Certain names are converted to chunk numbers. These are listed 845 # in the encode_as dictionary. 846 847 encode_as = {'creator': 0x0, 'originator': 0x0, 'instructions': 0x1, 'manual': 0x1, 848 'credits': 0x2, 'inlay': 0x3, 'target': 0x5, 'machine': 0x5, 849 'multi': 0x6, 'multiplexing': 0x6, 'palette': 0x7, 850 'tone': 0x110, 'dummy': 0x111, 'gap': 0x112, 'baud': 0x113, 851 'position': 0x120, 852 'discinfo': 0x200, 'discside': 0x201, 'rom': 0x300, 853 '6502': 0x400, 'ula': 0x401, 'wd1770': 0x402, 'memory': 0x410, 854 'emulator': 0xff00} 855 856 # Attempt to convert name into a chunk number 857 try: 858 return encode_as[string.lower(name)] 859 860 except KeyError: 861 raise UEFfile_error, "Couldn't find suitable chunk number for %s" % name 862 863 864 def export_files(self, file_positions): 865 """ 866 Given a file's location of the list of contents, returns its name, 867 load and execution addresses, and the data contained in the file. 868 If positions is an integer then return a tuple 869 870 info = (name, load, exe, data) 871 872 If positions is a list then return a list of info tuples. 873 """ 874 875 if type(file_positions) == types.IntType: 876 877 file_positions = [file_positions] 878 879 info = [] 880 881 for file_position in file_positions: 882 883 # Find the chunk position which corresponds to the file position 884 if file_position < 0 or file_position >= len(self.contents): 885 886 raise UEFfile_error, 'File position %i does not correspond to an actual file.' % file_position 887 else: 888 # Find the start and end positions 889 name = self.contents[file_position]['name'] 890 load = self.contents[file_position]['load'] 891 exe = self.contents[file_position]['exec'] 892 893 info.append( (name, load, exe, self.contents[file_position]['data']) ) 894 895 if len(info) == 1: 896 info = info[0] 897 898 return info 899 900 901 def chunk_name(self, number): 902 """ 903 Returns the relevant chunk name for the number given. 904 """ 905 906 decode_as = {0x0: 'creator', 0x1: 'manual', 0x2: 'credits', 0x3: 'inlay', 907 0x5: 'machine', 0x6: 'multiplexing', 0x7: 'palette', 908 0x110: 'tone', 0x111: 'dummy', 0x112: 'gap', 0x113: 'baud', 909 0x120: 'position', 910 0x200: 'discinfo', 0x201: 'discside', 0x300: 'rom', 911 0x400: '6502', 0x401: 'ula', 0x402: 'wd1770', 0x410: 'memory', 912 0xff00: 'emulator'} 913 914 try: 915 return decode_as[number] 916 except KeyError: 917 raise UEFfile_error, "Couldn't find name for chunk number %i." % number 918 919 920 def remove_files(self, file_positions): 921 """ 922 Removes files at the positions in the list of contents. 923 positions is either an integer or a list of integers. 924 """ 925 926 if type(file_positions) == types.IntType: 927 928 file_positions = [file_positions] 929 930 positions = [] 931 for file_position in file_positions: 932 933 # Find the chunk position which corresponds to the file position 934 if file_position < 0 or file_position >= len(self.contents): 935 936 print 'File position %i does not correspond to an actual file.' % file_position 937 938 else: 939 # Add the chunk positions within each file to the list of positions 940 positions = positions + range(self.contents[file_position]['position'], 941 self.contents[file_position]['last position'] + 1) 942 943 # Create a new list of chunks without those in the positions list 944 new_chunks = [] 945 for c in range(0, len(self.chunks)): 946 947 if c not in positions: 948 new_chunks.append(self.chunks[c]) 949 950 # Overwrite the chunks list with this new list 951 self.chunks = new_chunks 952 953 # Create a new contents list 954 self.read_contents() 955 956 957 def printable(self, s): 958 959 new = '' 960 for i in s: 961 962 if ord(i) < 32: 963 new = new + '?' 964 else: 965 new = new + i 966 967 return new 968 969 970 # Higher level functions ------------------------------ 971 972 def info(self): 973 """ 974 Provides general information on the target machine, 975 keyboard layout, file creator and target emulator. 976 """ 977 978 # Info command 979 980 # Split paragraphs 981 creator = string.split(self.creator, '\012') 982 983 print 'File creator:' 984 for line in creator: 985 print line 986 print 987 print 'File format version: %i.%i' % (self.major, self.minor) 988 print 989 print 'Target machine : '+self.target_machine 990 print 'Keyboard layout: '+self.keyboard_layout 991 print 'Emulator : '+self.emulator 992 print 993 if self.features != '': 994 995 print 'Contains:' 996 print self.features 997 print 998 print '(%i chunks)' % len(self.chunks) 999 print 1000 1001 def cat(self): 1002 """ 1003 Prints a catalogue of the files stored in the UEF file. 1004 """ 1005 1006 # Catalogue command 1007 1008 if self.contents == []: 1009 1010 print 'No files' 1011 1012 else: 1013 1014 print 'Contents:' 1015 1016 file_number = 0 1017 1018 for file in self.contents: 1019 1020 # Converts non printable characters in the filename 1021 # to ? symbols 1022 new_name = self.printable(file['name']) 1023 1024 print string.expandtabs(string.ljust(str(file_number), 3)+': '+ 1025 string.ljust(new_name, 16)+ 1026 string.upper( 1027 string.ljust(hex(file['load'])[2:], 10) +'\t'+ 1028 string.ljust(hex(file['exec'])[2:], 10) +'\t'+ 1029 string.ljust(hex(len(file['data']))[2:], 6) 1030 ) +'\t'+ 1031 'chunks %i to %i' % (file['position'], file['last position']) ) 1032 1033 file_number = file_number + 1 1034 1035 def show_chunks(self): 1036 """ 1037 Display the chunks in the UEF file in a table format 1038 with the following symbols denoting each type of 1039 chunk: 1040 O Originator information (0x0) 1041 I Instructions/manual (0x1) 1042 C Author credits (0x2) 1043 S Inlay scan (0x3) 1044 M Target machine information (0x5) 1045 X Multiplexing information (0x6) 1046 P Extra palette (0x7) 1047 1048 #, * File data block (0x100,0x102) 1049 #x, *x Multiplexed block (0x101,0x103) 1050 - High tone (inter-block gap) (0x110) 1051 + High tone with dummy byte (0x111) 1052 _ Gap (silence) (0x112) 1053 B Change of baud rate (0x113) 1054 ! Position marker (0x120) 1055 D Disc information (0x200) 1056 d Standard disc side (0x201) 1057 dx Multiplexed disc side (0x202) 1058 R Standard machine ROM (0x300) 1059 Rx Multiplexed machine ROM (0x301) 1060 6 6502 standard state (0x400) 1061 U Electron ULA state (0x401) 1062 W WD1770 state (0x402) 1063 m Standard memory data (0x410) 1064 mx Multiplexed memory data (0x410) 1065 1066 E Emulator identification string (0xff00) 1067 ? Unknown (unsupported chunk) 1068 """ 1069 1070 chunks_symbols = { 1071 0x0: 'O ', # Originator 1072 0x1: 'I ', # Instructions/manual 1073 0x2: 'C ', # Author credits 1074 0x3: 'S ', # Inlay scan 1075 0x5: 'M ', # Target machine info 1076 0x6: 'X ', # Multiplexing information 1077 0x7: 'P ', # Extra palette 1078 0x100: '# ', # Block information (implicit start/stop bit) 1079 0x101: '#x', # Multiplexed (as 0x100) 1080 0x102: '* ', # Generic block information 1081 0x103: '*x', # Multiplexed generic block (as 0x102) 1082 0x110: '- ', # High pitched tone 1083 0x111: '+ ', # High pitched tone with dummy byte 1084 0x112: '_ ', # Gap (silence) 1085 0x113: 'B ', # Change of baud rate 1086 0x120: '! ', # Position marker 1087 0x200: 'D ', # Disc information 1088 0x201: 'd ', # Standard disc side 1089 0x202: 'dx', # Multiplexed disc side 1090 0x300: 'R ', # Standard machine ROM 1091 0x301: 'Rx', # Multiplexed machine ROM 1092 0x400: '6 ', # 6502 standard state 1093 0x401: 'U ', # Electron ULA state 1094 0x402: 'W ', # WD1770 state 1095 0x410: 'm ', # Standard memory data 1096 0x411: 'mx', # Multiplexed memory data 1097 0xff00: 'E ' # Emulator identification string 1098 } 1099 1100 if len(self.chunks) == 0: 1101 print 'No chunks' 1102 return 1103 1104 # Display chunks 1105 print 'Chunks:' 1106 1107 n = 0 1108 1109 for c in self.chunks: 1110 1111 if n % 16 == 0: 1112 sys.stdout.write(string.rjust('%i: '% n, 8)) 1113 1114 if chunks_symbols.has_key(c[0]): 1115 sys.stdout.write(chunks_symbols[c[0]]) 1116 else: 1117 # Unknown 1118 sys.stdout.write('? ') 1119 1120 if n % 16 == 15: 1121 sys.stdout.write('\n') 1122 1123 n = n + 1 1124 1125 print