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