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