junglejourney
changeset 225:685e7583ff59
Added the actual file, not a symbolic link.
| author | David Boddie <david@boddie.org.uk> |
|---|---|
| date | Fri Jan 06 20:34:41 2012 +0100 |
| parents | 130973af1036 |
| children | bcad03cec5b9 |
| files | UEFfile.py |
| diffstat | 1 files changed, 1125 insertions(+), 1 deletions(-) [+] |
line diff
1.1 --- a/UEFfile.py Fri Jan 06 20:32:35 2012 +0100 1.2 +++ b/UEFfile.py Fri Jan 06 20:34:41 2012 +0100 1.3 @@ -1,1 +1,1125 @@ 1.4 -../Game/UEFfile.py 1.5 \ No newline at end of file 1.6 +#!/usr/bin/env python 1.7 + 1.8 +""" 1.9 +UEFfile.py - Handle UEF archives. 1.10 + 1.11 +Copyright (c) 2001-2010, David Boddie <david@boddie.org.uk> 1.12 + 1.13 +This program is free software: you can redistribute it and/or modify 1.14 +it under the terms of the GNU General Public License as published by 1.15 +the Free Software Foundation, either version 3 of the License, or 1.16 +(at your option) any later version. 1.17 + 1.18 +This program is distributed in the hope that it will be useful, 1.19 +but WITHOUT ANY WARRANTY; without even the implied warranty of 1.20 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1.21 +GNU General Public License for more details. 1.22 + 1.23 +You should have received a copy of the GNU General Public License 1.24 +along with this program. If not, see <http://www.gnu.org/licenses/>. 1.25 +""" 1.26 + 1.27 +import exceptions, sys, string, os, gzip, types 1.28 + 1.29 +class UEFfile_error(exceptions.Exception): 1.30 + 1.31 + pass 1.32 + 1.33 +# Determine the platform on which the program is running 1.34 + 1.35 +sep = os.sep 1.36 + 1.37 +if sys.platform == 'RISCOS': 1.38 + suffix = '/' 1.39 +else: 1.40 + suffix = '.' 1.41 + 1.42 +version = '0.20' 1.43 +date = '2010-10-24' 1.44 + 1.45 + 1.46 +class UEFfile: 1.47 + """instance = UEFfile(filename, creator) 1.48 + 1.49 + Create an instance of a UEF container using an existing file. 1.50 + If filename is not defined then create a new UEF container. 1.51 + The creator parameter can be used to override the default 1.52 + creator string. 1.53 + 1.54 + """ 1.55 + 1.56 + def __init__(self, filename = None, creator = 'UEFfile '+version): 1.57 + """Create a new instance of the UEFfile class.""" 1.58 + 1.59 + if filename == None: 1.60 + 1.61 + # There are no chunks initially 1.62 + self.chunks = [] 1.63 + # There are no file positions defined 1.64 + self.files = [] 1.65 + 1.66 + # Emulator associated with this UEF file 1.67 + self.emulator = 'Unspecified' 1.68 + # Originator/creator of the UEF file 1.69 + self.creator = creator 1.70 + # Target machine 1.71 + self.target_machine = '' 1.72 + # Keyboard layout 1.73 + self.keyboard_layout = '' 1.74 + # Features 1.75 + self.features = '' 1.76 + 1.77 + # UEF file format version 1.78 + self.minor = 9 1.79 + self.major = 0 1.80 + 1.81 + # List of files 1.82 + self.contents = [] 1.83 + else: 1.84 + # Read in the chunks from the file 1.85 + 1.86 + # Open the input file 1.87 + try: 1.88 + in_f = open(filename, 'rb') 1.89 + except IOError: 1.90 + raise UEFfile_error, 'The input file, '+filename+' could not be found.' 1.91 + 1.92 + # Is it gzipped? 1.93 + if in_f.read(10) != 'UEF File!\000': 1.94 + 1.95 + in_f.close() 1.96 + in_f = gzip.open(filename, 'rb') 1.97 + 1.98 + try: 1.99 + if in_f.read(10) != 'UEF File!\000': 1.100 + in_f.close() 1.101 + raise UEFfile_error, 'The input file, '+filename+' is not a UEF file.' 1.102 + except: 1.103 + in_f.close() 1.104 + raise UEFfile_error, 'The input file, '+filename+' could not be read.' 1.105 + 1.106 + # Read version self.number of the file format 1.107 + self.minor = self.str2num(1, in_f.read(1)) 1.108 + self.major = self.str2num(1, in_f.read(1)) 1.109 + 1.110 + # Decode the UEF file 1.111 + 1.112 + # List of chunks 1.113 + self.chunks = [] 1.114 + 1.115 + # Read chunks 1.116 + 1.117 + while 1: 1.118 + 1.119 + # Read chunk ID 1.120 + chunk_id = in_f.read(2) 1.121 + if not chunk_id: 1.122 + break 1.123 + 1.124 + chunk_id = self.str2num(2, chunk_id) 1.125 + 1.126 + length = self.str2num(4, in_f.read(4)) 1.127 + 1.128 + if length != 0: 1.129 + self.chunks.append((chunk_id, in_f.read(length))) 1.130 + else: 1.131 + self.chunks.append((chunk_id, '')) 1.132 + 1.133 + # Close the input file 1.134 + in_f.close() 1.135 + 1.136 + # UEF file information (placed in "creator", "target_machine", 1.137 + # "keyboard_layout", "emulator" and "features" attributes). 1.138 + self.read_uef_details() 1.139 + 1.140 + # Read file contents (placed in the list attribute "contents"). 1.141 + self.read_contents() 1.142 + 1.143 + 1.144 + def write(self, filename, write_creator_info = True, 1.145 + write_machine_info = True, write_emulator_info = True): 1.146 + """ 1.147 + Write a UEF file containing all the information stored in an 1.148 + instance of UEFfile to the file with the specified filename. 1.149 + 1.150 + By default, information about the file's creator, target machine and 1.151 + emulator is written to the file. These can be omitted by calling this 1.152 + method with individual arguments set to False. 1.153 + """ 1.154 + 1.155 + # Open the UEF file for writing 1.156 + try: 1.157 + uef = gzip.open(filename, 'wb') 1.158 + except IOError: 1.159 + raise UEFfile_error, "Couldn't open %s for writing." % filename 1.160 + 1.161 + # Write the UEF file header 1.162 + self.write_uef_header(uef) 1.163 + 1.164 + if write_creator_info: 1.165 + # Write the UEF creator chunk to the file 1.166 + self.write_uef_creator(uef) 1.167 + 1.168 + if write_machine_info: 1.169 + # Write the machine information 1.170 + self.write_machine_info(uef) 1.171 + 1.172 + if write_emulator_info: 1.173 + # Write the emulator information 1.174 + self.write_emulator_info(uef) 1.175 + 1.176 + # Write the chunks to the file 1.177 + self.write_chunks(uef) 1.178 + 1.179 + # Close the file 1.180 + uef.close() 1.181 + 1.182 + 1.183 + def number(self, size, n): 1.184 + """Convert a number to a little endian string of bytes for writing to a binary file.""" 1.185 + 1.186 + # Little endian writing 1.187 + 1.188 + s = "" 1.189 + 1.190 + while size > 0: 1.191 + i = n % 256 1.192 + s = s + chr(i) 1.193 + n = n >> 8 1.194 + size = size - 1 1.195 + 1.196 + return s 1.197 + 1.198 + 1.199 + def str2num(self, size, s): 1.200 + """Convert a string of ASCII characters to an integer.""" 1.201 + 1.202 + i = 0 1.203 + n = 0 1.204 + while i < size: 1.205 + 1.206 + n = n | (ord(s[i]) << (i*8)) 1.207 + i = i + 1 1.208 + 1.209 + return n 1.210 + 1.211 + 1.212 + def hex2num(self, s): 1.213 + """Convert a string of hexadecimal digits to an integer.""" 1.214 + 1.215 + n = 0 1.216 + 1.217 + for i in range(0,len(s)): 1.218 + 1.219 + a = ord(s[len(s)-i-1]) 1.220 + if (a >= 48) & (a <= 57): 1.221 + n = n | ((a-48) << (i*4)) 1.222 + elif (a >= 65) & (a <= 70): 1.223 + n = n | ((a-65+10) << (i*4)) 1.224 + elif (a >= 97) & (a <= 102): 1.225 + n = n | ((a-97+10) << (i*4)) 1.226 + else: 1.227 + return None 1.228 + 1.229 + return n 1.230 + 1.231 + 1.232 + # CRC calculation routines (begin) 1.233 + 1.234 + def rol(self, n, c): 1.235 + 1.236 + n = n << 1 1.237 + 1.238 + if (n & 256) != 0: 1.239 + carry = 1 1.240 + n = n & 255 1.241 + else: 1.242 + carry = 0 1.243 + 1.244 + n = n | c 1.245 + 1.246 + return n, carry 1.247 + 1.248 + 1.249 + def crc(self, s): 1.250 + 1.251 + high = 0 1.252 + low = 0 1.253 + 1.254 + for i in s: 1.255 + 1.256 + high = high ^ ord(i) 1.257 + 1.258 + for j in range(0,8): 1.259 + 1.260 + a, carry = self.rol(high, 0) 1.261 + 1.262 + if carry == 1: 1.263 + high = high ^ 8 1.264 + low = low ^ 16 1.265 + 1.266 + low, carry = self.rol(low, carry) 1.267 + high, carry = self.rol(high, carry) 1.268 + 1.269 + return high | (low << 8) 1.270 + 1.271 + # CRC calculation routines (end) 1.272 + 1.273 + def read_contents(self): 1.274 + """Find the positions of files in the list of chunks""" 1.275 + 1.276 + # List of files 1.277 + self.contents = [] 1.278 + 1.279 + current_file = {} 1.280 + 1.281 + position = 0 1.282 + 1.283 + while 1: 1.284 + 1.285 + position = self.find_next_block(position) 1.286 + 1.287 + if position == None: 1.288 + 1.289 + # No more blocks, so store the details of the last file in 1.290 + # the contents list 1.291 + if current_file != {}: 1.292 + self.contents.append(current_file) 1.293 + break 1.294 + 1.295 + else: 1.296 + 1.297 + # Read the block information 1.298 + name, load, exec_addr, data, block_number, last = self.read_block(self.chunks[position]) 1.299 + 1.300 + if current_file == {}: 1.301 + 1.302 + # No current file, so store details 1.303 + current_file = {'name': name, 'load': load, 'exec': exec_addr, 'blocks': block_number, 'data': data} 1.304 + 1.305 + # Locate the first non-block chunk before the block 1.306 + # and store the position of the file 1.307 + current_file['position'] = self.find_file_start(position) 1.308 + # This may also be the position of the last chunk related to 1.309 + # this file in the archive 1.310 + current_file['last position'] = position 1.311 + else: 1.312 + 1.313 + # Current file exists 1.314 + if block_number == 0: 1.315 + 1.316 + # New file, so write the previous one to the 1.317 + # contents list, but before doing so, find the next 1.318 + # non-block chunk and mark that as the last chunk in 1.319 + # the file 1.320 + 1.321 + if current_file != {}: 1.322 + self.contents.append(current_file) 1.323 + 1.324 + # Store details of this new file 1.325 + current_file = {'name': name, 'load': load, 'exec': exec_addr, 'blocks': block_number, 'data': data} 1.326 + 1.327 + # Locate the first non-block chunk before the block 1.328 + # and store the position of the file 1.329 + current_file['position'] = self.find_file_start(position) 1.330 + # This may also be the position of the last chunk related to 1.331 + # this file in the archive 1.332 + current_file['last position'] = position 1.333 + else: 1.334 + # Not a new file, so update the number of 1.335 + # blocks and append the block data to the 1.336 + # data entry 1.337 + current_file['blocks'] = block_number 1.338 + current_file['data'] = current_file['data'] + data 1.339 + 1.340 + # Update the last position information to mark the end of the file 1.341 + current_file['last position'] = position 1.342 + 1.343 + # Increase the position 1.344 + position = position + 1 1.345 + 1.346 + # We now have a contents list which tells us 1.347 + # 1) the names of files in the archive 1.348 + # 2) the load and execution addresses of them 1.349 + # 3) the number of blocks they contain 1.350 + # 4) their data, and from this their length 1.351 + # 5) their start position (chunk number) in the archive 1.352 + 1.353 + 1.354 + def chunk(self, f, n, data): 1.355 + """Write a chunk to the file specified by the open file object, chunk number and data supplied.""" 1.356 + 1.357 + # Chunk ID 1.358 + f.write(self.number(2, n)) 1.359 + # Chunk length 1.360 + f.write(self.number(4, len(data))) 1.361 + # Data 1.362 + f.write(data) 1.363 + 1.364 + 1.365 + def read_block(self, chunk): 1.366 + """Read a data block from a tape chunk and return the program name, load and execution addresses, 1.367 + block data, block number and whether the block is supposedly the last in the file.""" 1.368 + 1.369 + # Chunk number and data 1.370 + chunk_id = chunk[0] 1.371 + data = chunk[1] 1.372 + 1.373 + # For the implicit tape data chunk, just read the block as a series 1.374 + # of bytes, as before 1.375 + if chunk_id == 0x100: 1.376 + 1.377 + block = data 1.378 + 1.379 + else: # 0x102 1.380 + 1.381 + if UEF_major == 0 and UEF_minor < 9: 1.382 + 1.383 + # For UEF file versions earlier than 0.9, the number of 1.384 + # excess bits to be ignored at the end of the stream is 1.385 + # set to zero implicitly 1.386 + ignore = 0 1.387 + bit_ptr = 0 1.388 + else: 1.389 + # For later versions, the number of excess bits is 1.390 + # specified in the first byte of the stream 1.391 + ignore = data[0] 1.392 + bit_ptr = 8 1.393 + 1.394 + # Convert the data to the implicit format 1.395 + block = [] 1.396 + write_ptr = 0 1.397 + 1.398 + after_end = (len(data)*8) - ignore 1.399 + if after_end % 10 != 0: 1.400 + 1.401 + # Ensure that the number of bits to be read is a 1.402 + # multiple of ten 1.403 + after_end = after_end - (after_end % 10) 1.404 + 1.405 + while bit_ptr < after_end: 1.406 + 1.407 + # Skip start bit 1.408 + bit_ptr = bit_ptr + 1 1.409 + 1.410 + # Read eight bits of data 1.411 + bit_offset = bit_ptr % 8 1.412 + if bit_offset == 0: 1.413 + # Write the byte to the block 1.414 + block[write_ptr] = data[bit_ptr >> 3] 1.415 + else: 1.416 + # Read the byte containing the first bits 1.417 + b1 = data[bit_ptr >> 3] 1.418 + # Read the byte containing the rest 1.419 + b2 = data[(bit_ptr >> 3) + 1] 1.420 + 1.421 + # Construct a byte of data 1.422 + # Shift the first byte right by the bit offset 1.423 + # in that byte 1.424 + b1 = b1 >> bit_offset 1.425 + 1.426 + # Shift the rest of the bits from the second 1.427 + # byte to the left and ensure that the result 1.428 + # fits in a byte 1.429 + b2 = (b2 << (8 - bit_offset)) & 0xff 1.430 + 1.431 + # OR the two bytes together and write it to 1.432 + # the block 1.433 + block[write_ptr] = b1 | b2 1.434 + 1.435 + # Increment the block pointer 1.436 + write_ptr = write_ptr + 1 1.437 + 1.438 + # Move the data pointer on eight bits and skip the 1.439 + # stop bit 1.440 + bit_ptr = bit_ptr + 9 1.441 + 1.442 + # Read the block 1.443 + name = '' 1.444 + a = 1 1.445 + while 1: 1.446 + c = block[a] 1.447 + if ord(c) != 0: # was > 32: 1.448 + name = name + c 1.449 + a = a + 1 1.450 + if ord(c) == 0: 1.451 + break 1.452 + 1.453 + load = self.str2num(4, block[a:a+4]) 1.454 + exec_addr = self.str2num(4, block[a+4:a+8]) 1.455 + block_number = self.str2num(2, block[a+8:a+10]) 1.456 + last = self.str2num(1, block[a+12]) 1.457 + 1.458 + if last & 0x80 != 0: 1.459 + last = 1 1.460 + else: 1.461 + last = 0 1.462 + 1.463 + return (name, load, exec_addr, block[a+19:-2], block_number, last) 1.464 + 1.465 + 1.466 + def write_block(self, block, name, load, exe, n): 1.467 + """Write data to a string as a file data block in preparation to be written 1.468 + as chunk data to a UEF file.""" 1.469 + 1.470 + # Write the alignment character 1.471 + out = "*"+name[:10]+"\000" 1.472 + 1.473 + # Load address 1.474 + out = out + self.number(4, load) 1.475 + 1.476 + # Execution address 1.477 + out = out + self.number(4, exe) 1.478 + 1.479 + # Block number 1.480 + out = out + self.number(2, n) 1.481 + 1.482 + # Block length 1.483 + out = out + self.number(2, len(block)) 1.484 + 1.485 + # Block flag (last block) 1.486 + if len(block) == 256: 1.487 + out = out + self.number(1, 0) 1.488 + last = 0 1.489 + else: 1.490 + out = out + self.number(1, 128) # shouldn't be needed 1.491 + last = 1 1.492 + 1.493 + # Next address 1.494 + out = out + self.number(2, 0) 1.495 + 1.496 + # Unknown 1.497 + out = out + self.number(2, 0) 1.498 + 1.499 + # Header CRC 1.500 + out = out + self.number(2, self.crc(out[1:])) 1.501 + 1.502 + out = out + block 1.503 + 1.504 + # Block CRC 1.505 + out = out + self.number(2, self.crc(block)) 1.506 + 1.507 + return out, last 1.508 + 1.509 + 1.510 + def get_leafname(self, path): 1.511 + """Get the leafname of the specified file.""" 1.512 + 1.513 + pos = string.rfind(path, os.sep) 1.514 + if pos != -1: 1.515 + return path[pos+1:] 1.516 + else: 1.517 + return path 1.518 + 1.519 + 1.520 + def find_next_chunk(self, pos, IDs): 1.521 + """position, chunk = find_next_chunk(start, IDs) 1.522 + Search through the list of chunks from the start position given 1.523 + for the next chunk with an ID in the list of IDs supplied. 1.524 + Return its position in the list of chunks and its details.""" 1.525 + 1.526 + while pos < len(self.chunks): 1.527 + 1.528 + if self.chunks[pos][0] in IDs: 1.529 + 1.530 + # Found a chunk with ID in the list 1.531 + return pos, self.chunks[pos] 1.532 + 1.533 + # Otherwise continue looking 1.534 + pos = pos + 1 1.535 + 1.536 + return None, None 1.537 + 1.538 + 1.539 + def find_next_block(self, pos): 1.540 + """Find the next file block in the list of chunks.""" 1.541 + 1.542 + while pos < len(self.chunks): 1.543 + 1.544 + pos, chunk = self.find_next_chunk(pos, [0x100, 0x102]) 1.545 + 1.546 + if pos == None: 1.547 + 1.548 + return None 1.549 + else: 1.550 + if len(chunk[1]) > 1: 1.551 + 1.552 + # Found a block, return this position 1.553 + return pos 1.554 + 1.555 + # Otherwise continue looking 1.556 + pos = pos + 1 1.557 + 1.558 + return None 1.559 + 1.560 + 1.561 + def find_file_start(self, pos): 1.562 + """Find a chunk before the one specified which is not a file block.""" 1.563 + 1.564 + pos = pos - 1 1.565 + while pos > 0: 1.566 + 1.567 + if self.chunks[pos][0] != 0x100 and self.chunks[pos][0] != 0x102: 1.568 + 1.569 + # This is not a block 1.570 + return pos 1.571 + 1.572 + else: 1.573 + pos = pos - 1 1.574 + 1.575 + return pos 1.576 + 1.577 + 1.578 + def find_file_end(self, pos): 1.579 + """Find a chunk after the one specified which is not a file block.""" 1.580 + 1.581 + pos = pos + 1 1.582 + while pos < len(self.chunks)-1: 1.583 + 1.584 + if self.chunks[pos][0] != 0x100 and self.chunks[pos][0] != 0x102: 1.585 + 1.586 + # This is not a block 1.587 + return pos 1.588 + 1.589 + else: 1.590 + pos = pos + 1 1.591 + 1.592 + return pos 1.593 + 1.594 + 1.595 + def read_uef_details(self): 1.596 + """Return details about the UEF file and its contents.""" 1.597 + 1.598 + # Find the creator chunk 1.599 + pos, chunk = self.find_next_chunk(0, [0x0]) 1.600 + 1.601 + if pos == None: 1.602 + 1.603 + self.creator = 'Unknown' 1.604 + 1.605 + elif chunk[1] == '': 1.606 + 1.607 + self.creator = 'Unknown' 1.608 + else: 1.609 + self.creator = chunk[1] 1.610 + 1.611 + # Delete the creator chunk 1.612 + if pos != None: 1.613 + del self.chunks[pos] 1.614 + 1.615 + # Find the target machine chunk 1.616 + pos, chunk = self.find_next_chunk(0, [0x5]) 1.617 + 1.618 + if pos == None: 1.619 + 1.620 + self.target_machine = 'Unknown' 1.621 + self.keyboard_layout = 'Unknown' 1.622 + else: 1.623 + 1.624 + machines = ('BBC Model A', 'Electron', 'BBC Model B', 'BBC Master') 1.625 + keyboards = ('Any layout', 'Physical layout', 'Remapped') 1.626 + 1.627 + machine = ord(chunk[1][0]) & 0x0f 1.628 + keyboard = (ord(chunk[1][0]) & 0xf0) >> 4 1.629 + 1.630 + if machine < len(machines): 1.631 + self.target_machine = machines[machine] 1.632 + else: 1.633 + self.target_machine = 'Unknown' 1.634 + 1.635 + if keyboard < len(keyboards): 1.636 + self.keyboard_layout = keyboards[keyboard] 1.637 + else: 1.638 + self.keyboard_layout = 'Unknown' 1.639 + 1.640 + # Delete the target machine chunk 1.641 + del self.chunks[pos] 1.642 + 1.643 + # Find the emulator chunk 1.644 + pos, chunk = self.find_next_chunk(0, [0xff00]) 1.645 + 1.646 + if pos == None: 1.647 + 1.648 + self.emulator = 'Unspecified' 1.649 + 1.650 + elif chunk[1] == '': 1.651 + 1.652 + self.emulator = 'Unknown' 1.653 + else: 1.654 + self.emulator = chunk[1] 1.655 + 1.656 + # Delete the emulator chunk 1.657 + if pos != None: 1.658 + del self.chunks[pos] 1.659 + 1.660 + # Remove trailing null bytes 1.661 + while len(self.creator) > 0 and self.creator[-1] == '\000': 1.662 + 1.663 + self.creator = self.creator[:-1] 1.664 + 1.665 + while len(self.emulator) > 0 and self.emulator[-1] == '\000': 1.666 + 1.667 + self.emulator = self.emulator[:-1] 1.668 + 1.669 + self.features = '' 1.670 + if self.find_next_chunk(0, [0x1])[0] != None: 1.671 + self.features = self.features + '\n' + 'Instructions' 1.672 + if self.find_next_chunk(0, [0x2])[0] != None: 1.673 + self.features = self.features + '\n' + 'Credits' 1.674 + if self.find_next_chunk(0, [0x3])[0] != None: 1.675 + self.features = self.features + '\n' + 'Inlay' 1.676 + 1.677 + 1.678 + def write_uef_header(self, file): 1.679 + """Write the UEF file header and version number to a file.""" 1.680 + 1.681 + # Write the UEF file header 1.682 + file.write('UEF File!\000') 1.683 + 1.684 + # Minor and major version numbers 1.685 + file.write(self.number(1, self.minor) + self.number(1, self.major)) 1.686 + 1.687 + 1.688 + def write_uef_creator(self, file): 1.689 + """Write a creator chunk to a file.""" 1.690 + 1.691 + origin = self.creator + '\000' 1.692 + 1.693 + if (len(origin) % 4) != 0: 1.694 + origin = origin + ('\000'*(4-(len(origin) % 4))) 1.695 + 1.696 + # Write the creator chunk 1.697 + self.chunk(file, 0, origin) 1.698 + 1.699 + 1.700 + def write_machine_info(self, file): 1.701 + """Write the target machine and keyboard layout information to a file.""" 1.702 + 1.703 + machines = {'BBC Model A': 0, 'Electron': 1, 'BBC Model B': 2, 'BBC Master':3} 1.704 + keyboards = {'any': 0, 'physical': 1, 'logical': 2} 1.705 + 1.706 + if machines.has_key(self.target_machine): 1.707 + 1.708 + machine = machines[self.target_machine] 1.709 + else: 1.710 + machine = 0 1.711 + 1.712 + if keyboards.has_key(self.keyboard_layout): 1.713 + 1.714 + keyboard = keyboards[keyboard_layout] 1.715 + else: 1.716 + keyboard = 0 1.717 + 1.718 + self.chunk(file, 5, self.number(1, machine | (keyboard << 4) )) 1.719 + 1.720 + 1.721 + def write_emulator_info(self, file): 1.722 + """Write an emulator chunk to a file.""" 1.723 + 1.724 + emulator = self.emulator + '\000' 1.725 + 1.726 + if (len(emulator) % 4) != 0: 1.727 + emulator = emulator + ('\000'*(4-(len(emulator) % 4))) 1.728 + 1.729 + # Write the creator chunk 1.730 + self.chunk(file, 0xff00, emulator) 1.731 + 1.732 + 1.733 + def write_chunks(self, file): 1.734 + """Write all the chunks in the list to a file. Saves having loops in other functions to do this.""" 1.735 + 1.736 + for c in self.chunks: 1.737 + 1.738 + self.chunk(file, c[0], c[1]) 1.739 + 1.740 + 1.741 + def create_chunks(self, name, load, exe, data): 1.742 + """Create suitable chunks, and insert them into 1.743 + the list of chunks.""" 1.744 + 1.745 + # Reset the block number to zero 1.746 + block_number = 0 1.747 + 1.748 + # Long gap 1.749 + gap = 1 1.750 + 1.751 + new_chunks = [] 1.752 + 1.753 + # Write block details 1.754 + while 1: 1.755 + block, last = self.write_block(data[:256], name, load, exe, block_number) 1.756 + 1.757 + # Remove the leading 256 bytes as they have been encoded 1.758 + data = data[256:] 1.759 + 1.760 + if gap == 1: 1.761 + new_chunks.append((0x110, self.number(2,0x05dc))) 1.762 + gap = 0 1.763 + else: 1.764 + new_chunks.append((0x110, self.number(2,0x0258))) 1.765 + 1.766 + # Write the block to the list of new chunks 1.767 + new_chunks.append((0x100, block)) 1.768 + 1.769 + if last == 1: 1.770 + break 1.771 + 1.772 + # Increment the block number 1.773 + block_number = block_number + 1 1.774 + 1.775 + # Return the list of new chunks 1.776 + return new_chunks 1.777 + 1.778 + 1.779 + def import_files(self, file_position, info): 1.780 + """ 1.781 + Import a file into the UEF file at the specified location in the 1.782 + list of contents. 1.783 + positions is a positive integer or zero 1.784 + 1.785 + To insert one file, info can be a sequence: 1.786 + 1.787 + info = (name, load, exe, data) where 1.788 + name is the file's name. 1.789 + load is the load address of the file. 1.790 + exe is the execution address. 1.791 + data is the contents of the file. 1.792 + 1.793 + For more than one file, info must be a sequence of info sequences. 1.794 + """ 1.795 + 1.796 + if file_position < 0: 1.797 + 1.798 + raise UEFfile_error, 'Position must be zero or greater.' 1.799 + 1.800 + # Find the chunk position which corresponds to the file_position 1.801 + if self.contents != []: 1.802 + 1.803 + # There are files already present 1.804 + if file_position >= len(self.contents): 1.805 + 1.806 + # Position the new files after the end of the last file 1.807 + position = self.contents[-1]['last position'] + 1 1.808 + 1.809 + else: 1.810 + 1.811 + # Position the new files before the end of the file 1.812 + # specified 1.813 + position = self.contents[file_position]['position'] 1.814 + else: 1.815 + # There are no files present in the archive, so put them after 1.816 + # all the other chunks 1.817 + position = len(self.chunks) 1.818 + 1.819 + # Examine the info sequence passed 1.820 + if len(info) == 0: 1.821 + return 1.822 + 1.823 + if type(info[0]) == types.StringType: 1.824 + 1.825 + # Assume that the info sequence contains name, load, exe, data 1.826 + info = [info] 1.827 + 1.828 + # Read the file details for each file and create chunks to add 1.829 + # to the list of chunks 1.830 + inserted_chunks = [] 1.831 + 1.832 + for name, load, exe, data in info: 1.833 + 1.834 + inserted_chunks = inserted_chunks + self.create_chunks(name, load, exe, data) 1.835 + 1.836 + # Insert the chunks in the list at the specified position 1.837 + self.chunks = self.chunks[:position] + inserted_chunks + self.chunks[position:] 1.838 + 1.839 + # Update the contents list 1.840 + self.read_contents() 1.841 + 1.842 + 1.843 + def chunk_number(self, name): 1.844 + """ 1.845 + Returns the relevant chunk number for the name given. 1.846 + """ 1.847 + 1.848 + # Use a convention for determining the chunk number to be used: 1.849 + # Certain names are converted to chunk numbers. These are listed 1.850 + # in the encode_as dictionary. 1.851 + 1.852 + encode_as = {'creator': 0x0, 'originator': 0x0, 'instructions': 0x1, 'manual': 0x1, 1.853 + 'credits': 0x2, 'inlay': 0x3, 'target': 0x5, 'machine': 0x5, 1.854 + 'multi': 0x6, 'multiplexing': 0x6, 'palette': 0x7, 1.855 + 'tone': 0x110, 'dummy': 0x111, 'gap': 0x112, 'baud': 0x113, 1.856 + 'position': 0x120, 1.857 + 'discinfo': 0x200, 'discside': 0x201, 'rom': 0x300, 1.858 + '6502': 0x400, 'ula': 0x401, 'wd1770': 0x402, 'memory': 0x410, 1.859 + 'emulator': 0xff00} 1.860 + 1.861 + # Attempt to convert name into a chunk number 1.862 + try: 1.863 + return encode_as[string.lower(name)] 1.864 + 1.865 + except KeyError: 1.866 + raise UEFfile_error, "Couldn't find suitable chunk number for %s" % name 1.867 + 1.868 + 1.869 + def export_files(self, file_positions): 1.870 + """ 1.871 + Given a file's location of the list of contents, returns its name, 1.872 + load and execution addresses, and the data contained in the file. 1.873 + If positions is an integer then return a tuple 1.874 + 1.875 + info = (name, load, exe, data) 1.876 + 1.877 + If positions is a list then return a list of info tuples. 1.878 + """ 1.879 + 1.880 + if type(file_positions) == types.IntType: 1.881 + 1.882 + file_positions = [file_positions] 1.883 + 1.884 + info = [] 1.885 + 1.886 + for file_position in file_positions: 1.887 + 1.888 + # Find the chunk position which corresponds to the file position 1.889 + if file_position < 0 or file_position >= len(self.contents): 1.890 + 1.891 + raise UEFfile_error, 'File position %i does not correspond to an actual file.' % file_position 1.892 + else: 1.893 + # Find the start and end positions 1.894 + name = self.contents[file_position]['name'] 1.895 + load = self.contents[file_position]['load'] 1.896 + exe = self.contents[file_position]['exec'] 1.897 + 1.898 + info.append( (name, load, exe, self.contents[file_position]['data']) ) 1.899 + 1.900 + if len(info) == 1: 1.901 + info = info[0] 1.902 + 1.903 + return info 1.904 + 1.905 + 1.906 + def chunk_name(self, number): 1.907 + """ 1.908 + Returns the relevant chunk name for the number given. 1.909 + """ 1.910 + 1.911 + decode_as = {0x0: 'creator', 0x1: 'manual', 0x2: 'credits', 0x3: 'inlay', 1.912 + 0x5: 'machine', 0x6: 'multiplexing', 0x7: 'palette', 1.913 + 0x110: 'tone', 0x111: 'dummy', 0x112: 'gap', 0x113: 'baud', 1.914 + 0x120: 'position', 1.915 + 0x200: 'discinfo', 0x201: 'discside', 0x300: 'rom', 1.916 + 0x400: '6502', 0x401: 'ula', 0x402: 'wd1770', 0x410: 'memory', 1.917 + 0xff00: 'emulator'} 1.918 + 1.919 + try: 1.920 + return decode_as[number] 1.921 + except KeyError: 1.922 + raise UEFfile_error, "Couldn't find name for chunk number %i." % number 1.923 + 1.924 + 1.925 + def remove_files(self, file_positions): 1.926 + """ 1.927 + Removes files at the positions in the list of contents. 1.928 + positions is either an integer or a list of integers. 1.929 + """ 1.930 + 1.931 + if type(file_positions) == types.IntType: 1.932 + 1.933 + file_positions = [file_positions] 1.934 + 1.935 + positions = [] 1.936 + for file_position in file_positions: 1.937 + 1.938 + # Find the chunk position which corresponds to the file position 1.939 + if file_position < 0 or file_position >= len(self.contents): 1.940 + 1.941 + print 'File position %i does not correspond to an actual file.' % file_position 1.942 + 1.943 + else: 1.944 + # Add the chunk positions within each file to the list of positions 1.945 + positions = positions + range(self.contents[file_position]['position'], 1.946 + self.contents[file_position]['last position'] + 1) 1.947 + 1.948 + # Create a new list of chunks without those in the positions list 1.949 + new_chunks = [] 1.950 + for c in range(0, len(self.chunks)): 1.951 + 1.952 + if c not in positions: 1.953 + new_chunks.append(self.chunks[c]) 1.954 + 1.955 + # Overwrite the chunks list with this new list 1.956 + self.chunks = new_chunks 1.957 + 1.958 + # Create a new contents list 1.959 + self.read_contents() 1.960 + 1.961 + 1.962 + def printable(self, s): 1.963 + 1.964 + new = '' 1.965 + for i in s: 1.966 + 1.967 + if ord(i) < 32: 1.968 + new = new + '?' 1.969 + else: 1.970 + new = new + i 1.971 + 1.972 + return new 1.973 + 1.974 + 1.975 + # Higher level functions ------------------------------ 1.976 + 1.977 + def info(self): 1.978 + """ 1.979 + Provides general information on the target machine, 1.980 + keyboard layout, file creator and target emulator. 1.981 + """ 1.982 + 1.983 + # Info command 1.984 + 1.985 + # Split paragraphs 1.986 + creator = string.split(self.creator, '\012') 1.987 + 1.988 + print 'File creator:' 1.989 + for line in creator: 1.990 + print line 1.991 + print 1.992 + print 'File format version: %i.%i' % (self.major, self.minor) 1.993 + print 1.994 + print 'Target machine : '+self.target_machine 1.995 + print 'Keyboard layout: '+self.keyboard_layout 1.996 + print 'Emulator : '+self.emulator 1.997 + print 1.998 + if self.features != '': 1.999 + 1.1000 + print 'Contains:' 1.1001 + print self.features 1.1002 + print 1.1003 + print '(%i chunks)' % len(self.chunks) 1.1004 + print 1.1005 + 1.1006 + def cat(self): 1.1007 + """ 1.1008 + Prints a catalogue of the files stored in the UEF file. 1.1009 + """ 1.1010 + 1.1011 + # Catalogue command 1.1012 + 1.1013 + if self.contents == []: 1.1014 + 1.1015 + print 'No files' 1.1016 + 1.1017 + else: 1.1018 + 1.1019 + print 'Contents:' 1.1020 + 1.1021 + file_number = 0 1.1022 + 1.1023 + for file in self.contents: 1.1024 + 1.1025 + # Converts non printable characters in the filename 1.1026 + # to ? symbols 1.1027 + new_name = self.printable(file['name']) 1.1028 + 1.1029 + print string.expandtabs(string.ljust(str(file_number), 3)+': '+ 1.1030 + string.ljust(new_name, 16)+ 1.1031 + string.upper( 1.1032 + string.ljust(hex(file['load'])[2:], 10) +'\t'+ 1.1033 + string.ljust(hex(file['exec'])[2:], 10) +'\t'+ 1.1034 + string.ljust(hex(len(file['data']))[2:], 6) 1.1035 + ) +'\t'+ 1.1036 + 'chunks %i to %i' % (file['position'], file['last position']) ) 1.1037 + 1.1038 + file_number = file_number + 1 1.1039 + 1.1040 + def show_chunks(self): 1.1041 + """ 1.1042 + Display the chunks in the UEF file in a table format 1.1043 + with the following symbols denoting each type of 1.1044 + chunk: 1.1045 + O Originator information (0x0) 1.1046 + I Instructions/manual (0x1) 1.1047 + C Author credits (0x2) 1.1048 + S Inlay scan (0x3) 1.1049 + M Target machine information (0x5) 1.1050 + X Multiplexing information (0x6) 1.1051 + P Extra palette (0x7) 1.1052 + 1.1053 + #, * File data block (0x100,0x102) 1.1054 + #x, *x Multiplexed block (0x101,0x103) 1.1055 + - High tone (inter-block gap) (0x110) 1.1056 + + High tone with dummy byte (0x111) 1.1057 + _ Gap (silence) (0x112) 1.1058 + B Change of baud rate (0x113) 1.1059 + ! Position marker (0x120) 1.1060 + D Disc information (0x200) 1.1061 + d Standard disc side (0x201) 1.1062 + dx Multiplexed disc side (0x202) 1.1063 + R Standard machine ROM (0x300) 1.1064 + Rx Multiplexed machine ROM (0x301) 1.1065 + 6 6502 standard state (0x400) 1.1066 + U Electron ULA state (0x401) 1.1067 + W WD1770 state (0x402) 1.1068 + m Standard memory data (0x410) 1.1069 + mx Multiplexed memory data (0x410) 1.1070 + 1.1071 + E Emulator identification string (0xff00) 1.1072 + ? Unknown (unsupported chunk) 1.1073 + """ 1.1074 + 1.1075 + chunks_symbols = { 1.1076 + 0x0: 'O ', # Originator 1.1077 + 0x1: 'I ', # Instructions/manual 1.1078 + 0x2: 'C ', # Author credits 1.1079 + 0x3: 'S ', # Inlay scan 1.1080 + 0x5: 'M ', # Target machine info 1.1081 + 0x6: 'X ', # Multiplexing information 1.1082 + 0x7: 'P ', # Extra palette 1.1083 + 0x100: '# ', # Block information (implicit start/stop bit) 1.1084 + 0x101: '#x', # Multiplexed (as 0x100) 1.1085 + 0x102: '* ', # Generic block information 1.1086 + 0x103: '*x', # Multiplexed generic block (as 0x102) 1.1087 + 0x110: '- ', # High pitched tone 1.1088 + 0x111: '+ ', # High pitched tone with dummy byte 1.1089 + 0x112: '_ ', # Gap (silence) 1.1090 + 0x113: 'B ', # Change of baud rate 1.1091 + 0x120: '! ', # Position marker 1.1092 + 0x200: 'D ', # Disc information 1.1093 + 0x201: 'd ', # Standard disc side 1.1094 + 0x202: 'dx', # Multiplexed disc side 1.1095 + 0x300: 'R ', # Standard machine ROM 1.1096 + 0x301: 'Rx', # Multiplexed machine ROM 1.1097 + 0x400: '6 ', # 6502 standard state 1.1098 + 0x401: 'U ', # Electron ULA state 1.1099 + 0x402: 'W ', # WD1770 state 1.1100 + 0x410: 'm ', # Standard memory data 1.1101 + 0x411: 'mx', # Multiplexed memory data 1.1102 + 0xff00: 'E ' # Emulator identification string 1.1103 + } 1.1104 + 1.1105 + if len(self.chunks) == 0: 1.1106 + print 'No chunks' 1.1107 + return 1.1108 + 1.1109 + # Display chunks 1.1110 + print 'Chunks:' 1.1111 + 1.1112 + n = 0 1.1113 + 1.1114 + for c in self.chunks: 1.1115 + 1.1116 + if n % 16 == 0: 1.1117 + sys.stdout.write(string.rjust('%i: '% n, 8)) 1.1118 + 1.1119 + if chunks_symbols.has_key(c[0]): 1.1120 + sys.stdout.write(chunks_symbols[c[0]]) 1.1121 + else: 1.1122 + # Unknown 1.1123 + sys.stdout.write('? ') 1.1124 + 1.1125 + if n % 16 == 15: 1.1126 + sys.stdout.write('\n') 1.1127 + 1.1128 + n = n + 1 1.1129 + 1.1130 + print
