Switcher

Annotated UEFfile.py

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