From 46437c1e4f77fa725f7f0592d8515eb3c1b36274 Mon Sep 17 00:00:00 2001 From: Craig Thomas Date: Mon, 26 Sep 2022 13:12:18 -0400 Subject: [PATCH] Change file_util so that contents can be saved to disk. Add new skip_to_sequence function for cassettes. Change the way cassettes are read for better error handling. Fix problem with granule allocation, fix README Add different types of pre-ambles to disk functions. Fix pre and post amble reading and writing. Fix various portions of preambles. Fix issues with saving chunks to disk. Add preamble type for ASCII files. Add unit tests for listing multiple files of different types. --- README.md | 22 +- assembler.py | 18 +- cocoasm/virtualfiles/cassette.py | 211 +++++---- cocoasm/virtualfiles/disk.py | 413 +++++++++++------- cocoasm/virtualfiles/virtual_file.py | 10 +- .../virtualfiles/virtual_file_container.py | 21 +- file_util.py | 63 ++- test/virtualfiles/test_cassette.py | 236 ++++------ test/virtualfiles/test_disk.py | 366 ++++++++++++---- 9 files changed, 795 insertions(+), 565 deletions(-) diff --git a/README.md b/README.md index fb43f45..d0a2136 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,9 @@ that are available: * `--print` - prints out the assembled statements * `--symbols` - prints out the symbol table -* `--bin_file` - save assembled contents to a binary file -* `--cas_file` - save assembled contents to a cassette file -* `--dsk_file` - save assembled contents to a virtual disk file +* `--to_bin` - save assembled contents to a binary file +* `--to_cas` - save assembled contents to a cassette file +* `--to_dsk` - save assembled contents to a virtual disk file * `--name` - saves the program with the specified name on a cassette or virtual disk file --- @@ -240,9 +240,9 @@ The columns are as follows: ### Save to Binary File -To save the assembled contents to a binary file, use the `--bin_file` switch: +To save the assembled contents to a binary file, use the `--to_bin` switch: - python3 assembler.py test.asm --bin_file test.bin + python3 assembler.py test.asm --to_bin test.bin The assembled program will be saved to the file `test.bin`. Note that this file may not be useful on its own, as it does not have any meta information about @@ -255,9 +255,9 @@ will not have any effect on the assembled file). ### Save to Cassette File -To save the assembled contents to a cassette file, use the `--cas_file` switch: +To save the assembled contents to a cassette file, use the `--to_cas` switch: - python3 assembler.py test.asm --cas_file test.cas + python3 assembler.py test.asm --to_cas test.cas This will assemble the program and save it to a cassette file called `test.cas`. The source code must include the `NAM` mnemonic to name the program (e.g. @@ -268,7 +268,7 @@ The source code must include the `NAM` mnemonic to name the program (e.g. not be overwritten. If you wish to add the program to `test.cas`, you must specify the `--append` flag during assembly: - python3 assembler.py test.asm --cas_file test.cas --append + python3 assembler.py test.asm --to_cas test.cas --append To load from the cassette file, you must use BASIC's `CLOADM` command as follows: @@ -278,9 +278,9 @@ To load from the cassette file, you must use BASIC's `CLOADM` command as follows ### Save to Disk File -To save the assembled contents to a disk file, use the `--dsk_file` switch: +To save the assembled contents to a disk file, use the `--to_dsk` switch: - python3 assembler.py test.asm --dsk_file test.dsk + python3 assembler.py test.asm --to_dsk test.dsk This will assemble the program and save it to a disk file called `test.dsk`. The source code must include the `NAM` mnemonic to name the program on disk (e.g. @@ -291,7 +291,7 @@ The source code must include the `NAM` mnemonic to name the program on disk (e.g not be updated. If you wish to add the program to `test.dsk`, you must specify the `--append` flag during assembly: - python3 assembler.py test.asm --dsk_file test.dsk --append + python3 assembler.py test.asm --to_dsk test.dsk --append To load from the disk file, you must use Disk Basic's `LOADM` command as follows: diff --git a/assembler.py b/assembler.py index f7d5051..695a92c 100644 --- a/assembler.py +++ b/assembler.py @@ -38,13 +38,13 @@ def parse_arguments(): help="print out the assembled statements when finished" ) parser.add_argument( - "--bin_file", metavar="BIN_FILE", help="stores the assembled program in a binary BIN_FILE" + "--to_bin", metavar="BIN_FILE", help="stores the assembled program in a binary BIN_FILE" ) parser.add_argument( - "--cas_file", metavar="CAS_FILE", help="stores the assembled program in a cassette image CAS_FILE" + "--to_cas", metavar="CAS_FILE", help="stores the assembled program in a cassette image CAS_FILE" ) parser.add_argument( - "--dsk_file", metavar="DSK_FILE", help="stores the assembled program in a disk image DSK_FILE" + "--to_dsk", metavar="DSK_FILE", help="stores the assembled program in a disk image DSK_FILE" ) parser.add_argument( "--name", help="the name of the file to be created on the cassette or disk image" @@ -107,10 +107,10 @@ def main(args): for statement in program.get_statements(): print(statement) - if args.bin_file: + if args.to_bin: try: virtual_file = VirtualFile( - SourceFile(args.bin_file, file_type=SourceFileType.BINARY), + SourceFile(args.to_bin, file_type=SourceFileType.BINARY), VirtualFileType.BINARY ) virtual_file.open_virtual_file() @@ -120,13 +120,13 @@ def main(args): print("Unable to save binary file:") print(error) - if args.cas_file: + if args.to_cas: if not coco_file.name: print("No name for the program specified, not creating cassette file") return try: virtual_file = VirtualFile( - SourceFile(args.cas_file, file_type=SourceFileType.BINARY), + SourceFile(args.to_cas, file_type=SourceFileType.BINARY), VirtualFileType.CASSETTE ) virtual_file.open_virtual_file() @@ -136,13 +136,13 @@ def main(args): print("Unable to save cassette file:") print(error) - if args.dsk_file: + if args.to_dsk: if not coco_file.name: print("No name for the program specified, not creating disk file") return try: virtual_file = VirtualFile( - SourceFile(args.dsk_file, file_type=SourceFileType.BINARY), + SourceFile(args.to_dsk, file_type=SourceFileType.BINARY), VirtualFileType.DISK ) virtual_file.open_virtual_file() diff --git a/cocoasm/virtualfiles/cassette.py b/cocoasm/virtualfiles/cassette.py index c701f0c..367a6bd 100644 --- a/cocoasm/virtualfiles/cassette.py +++ b/cocoasm/virtualfiles/cassette.py @@ -50,8 +50,10 @@ def list_files(self, filenames=None): :return: a list of CoCoFile objects """ files = [] + pointer = 0 + while True: - coco_file = self.read_file() + coco_file, pointer = self.read_file(pointer) if not coco_file: return files @@ -64,160 +66,140 @@ def add_file(self, coco_file): :param coco_file: the CoCoFile object to add """ - self.write_leader() - self.append_header(coco_file, CassetteFileType.OBJECT_FILE, CassetteDataType.BINARY) - self.write_leader() + self.append_blank() + self.append_leader() + self.append_header(coco_file) + self.append_blank() + self.append_leader() self.append_data_blocks(coco_file.data) self.append_eof() - def read_leader(self): - """ - Reads a cassette leader. Should consist of 128 bytes of the value $55. - Raises a ValueError if there is a problem. Returns True if the leader is okay. - """ - if len(self.buffer) < 128: - raise VirtualFileValidationError("Leader on tape is less than 128 bytes") - - for pointer in range(128): - value = NumericValue(self.buffer[pointer]) - if value.hex() != "55": - raise VirtualFileValidationError("[{}] invalid leader byte".format(value.hex())) - - self.buffer = self.buffer[128:] - return True - - def write_leader(self): - """ - Appends a cassette leader of character $55 to the buffer. The leader is - always 128 bytes long consisting of value $55. - """ - for _ in range(128): - self.buffer.append(0x55) - - def validate_sequence(self, sequence): + def skip_to_sequence(self, sequence, start=0): """ - Ensures that the next group of bytes read matches the sequence specified. - Advances the buffer down the object + Returns a pointer to the internal buffer where the start of the specified sequence begins. + Will return -1 if the sequence does not exist. - :param sequence: an array-like list of bytes to read in the sequence - :return: True if the bytes follow the sequence specified, false otherwise + :param sequence: the sequence to search for + :param start: the place in the buffer to start the search + :return: a pointer to the internal buffer where the sequence begins """ - if len(self.buffer) < len(sequence): - raise VirtualFileValidationError("Not enough bytes in buffer to validate sequence") + for pointer in range(start, start + len(self.buffer) - len(sequence) + 1): + if self.buffer[pointer:pointer + len(sequence)] == sequence: + return pointer - for pointer in range(len(sequence)): - byte_read = NumericValue(self.buffer[0]) - if byte_read.int != sequence[pointer]: - return False - self.buffer = self.buffer[1:] - return True + return -1 - def read_coco_file_name(self): + def read_coco_file_name(self, pointer): """ Reads the filename from the tape data. :return: the string representation of the filename """ raw_file_name = [] - for pointer in range(8): - raw_file_name.append(self.buffer[pointer]) - self.buffer = self.buffer[8:] + for name_offset in range(8): + raw_file_name.append(self.buffer[pointer + name_offset]) coco_file_name = bytearray(raw_file_name).decode("utf-8") - return coco_file_name + pointer += 8 + return coco_file_name, pointer - def read_file(self): + def read_file(self, pointer): """ Reads a cassette file, and returns a CoCoFile object with the information for the next file contained on the cassette file. - :return: a CoCoFile with header information + :param pointer: an integer index into the buffer where to start reading from + :return: a CoCoFile with header information, and a pointer to where the file ended """ - # Make sure there is data to read - if len(self.buffer) == 0: - return None - - # Validate and skip over tape leader - self.read_leader() + # Find the next header block + pointer = self.skip_to_sequence([0x55, 0x3C, 0x00], start=pointer) + if pointer == -1: + return None, pointer - # Validate header block - if not self.validate_sequence([0x55, 0x3C, 0x00]): - raise VirtualFileValidationError("Cassette header does not start with $55 $3C $00") - - # Length byte - self.buffer = self.buffer[1:] + # Skip length byte (always $0F) + pointer += 4 # Extract file name - coco_file_name = self.read_coco_file_name() + coco_file_name, pointer = self.read_coco_file_name(pointer) # Get the file type - file_type = NumericValue(self.buffer[0]) - data_type = NumericValue(self.buffer[1]) - gaps = NumericValue(self.buffer[2]) - - load_addr_int = int(self.buffer[3]) - load_addr_int = load_addr_int << 8 - load_addr_int |= int(self.buffer[4]) - load_addr = NumericValue(load_addr_int) - - exec_addr_int = int(self.buffer[5]) - exec_addr_int = exec_addr_int << 8 - exec_addr_int |= int(self.buffer[6]) - exec_addr = NumericValue(exec_addr_int) + file_type = NumericValue(self.buffer[pointer]) + pointer += 1 + extension = "BAS" + if file_type.int == 0x02: + extension = "BIN" + + # Get the data type + data_type = NumericValue(self.buffer[pointer]) + pointer += 1 + + # Get the gap status + gaps = NumericValue(self.buffer[pointer]) + pointer += 1 + + # Get load and exec addresses + load_addr = self.read_word(pointer) + pointer += 2 + exec_addr = self.read_word(pointer) + pointer += 2 # Advance two spaces to move past the header - self.buffer = self.buffer[9:] - - # Skip over 128 byte leader - self.read_leader() + pointer += 2 - data = self.read_blocks() + # Read any data blocks + data, pointer = self.read_blocks(pointer) if not data: - return None + return None, pointer return CoCoFile( name=coco_file_name, + extension=extension, type=file_type, data_type=data_type, gaps=gaps, load_addr=load_addr, exec_addr=exec_addr, data=data - ) + ), pointer - def read_blocks(self): + def read_blocks(self, pointer): """ - Read all of the blocks that make up a file until an EOF block is found, or + Read all the blocks that make up a file until an EOF block is found, or an error occurs. - :return: an array-like object with all of the file data bytes read in order + :return: an array-like object with all the file data bytes read in order """ data = [] while True: - if not self.validate_sequence([0x55, 0x3C]): - raise VirtualFileValidationError("Data or EOF block validation failed") + pointer = self.skip_to_sequence([0x55, 0x3C], start=pointer) + if pointer == -1: + raise VirtualFileValidationError("Data or EOF block not found") + pointer += 2 - block_type = NumericValue(self.buffer[0]) - self.buffer = self.buffer[1:] + # Read the block type + block_type = NumericValue(self.buffer[pointer]) + pointer += 1 + # Check for EOF block type, and if found skip over length, checksum and $55 byte and return if block_type.hex() == "FF": - # Skip over length byte, checksum, and final $55 - self.buffer = self.buffer[3:] - return data + pointer += 3 + return data, pointer + # Check for data block type and consume it elif block_type.hex() == "01": - data_length = NumericValue(self.buffer[0]).int - self.buffer = self.buffer[1:] - for ptr in range(data_length): - data.append(self.buffer[ptr]) - # Skip over block size, and checksum - self.buffer = self.buffer[data_length:] - self.buffer = self.buffer[2:] + data_length = NumericValue(self.buffer[pointer]).int + pointer += 1 + for block_data_pointer in range(data_length): + data.append(self.buffer[pointer + block_data_pointer]) + pointer += data_length + + # Skip over checksum + pointer += 2 else: raise VirtualFileValidationError("Unknown block type found: {}".format(block_type.hex())) - def append_header(self, coco_file, file_type, data_type): + def append_header(self, coco_file): """ The header of a cassette file is 21 bytes long: byte 1 = $55 (fixed value) @@ -234,8 +216,6 @@ def append_header(self, coco_file, file_type, data_type): byte 21 = $55 (fixed value) :param coco_file: the CoCoFile to append to cassette - :param file_type: the CassetteFileType to save as - :param data_type: the CassetteDataType to save as """ # Standard header self.buffer.append(0x55) @@ -246,10 +226,10 @@ def append_header(self, coco_file, file_type, data_type): # Filename and type checksum += self.append_name(coco_file.name) - self.buffer.append(file_type) - self.buffer.append(data_type) - checksum += file_type - checksum += data_type + self.buffer.append(coco_file.type.int) + self.buffer.append(coco_file.data_type.int) + checksum += coco_file.type.int + checksum += coco_file.data_type.int # No gaps in blocks self.buffer.append(0x00) @@ -288,13 +268,14 @@ def append_name(self, name): checksum += 0x20 return checksum - def append_data_blocks(self, raw_bytes): + def append_data_blocks(self, raw_bytes, gaps=False): """ Appends one or more data blocks to the buffer. Will continue to add data blocks to the buffer until the raw_bytes buffer is empty. The buffer is modified in-place. :param raw_bytes: the raw bytes of data to add to the data block + :param gaps: if True, will append blanks and leaders between data blocks """ if len(raw_bytes) == 0: return @@ -326,6 +307,9 @@ def append_data_blocks(self, raw_bytes): checksum += raw_bytes[index] self.buffer.append(checksum & 0xFF) self.buffer.append(0x55) + if gaps: + self.append_blank() + self.append_leader() self.append_data_blocks(raw_bytes[255:]) def append_eof(self): @@ -348,5 +332,20 @@ def append_eof(self): self.buffer.append(0xFF) self.buffer.append(0x55) + def append_leader(self): + """ + Appends a cassette leader of character $55 to the buffer. The leader is + always 128 bytes long consisting of value $55. + """ + for _ in range(128): + self.buffer.append(0x55) + + def append_blank(self): + """ + Appends a blank space of character $00 to the buffer. The blank space is + always 128 bytes long consisting of value $00. + """ + for _ in range(128): + self.buffer.append(0x00) # E N D O F F I L E ####################################################### diff --git a/cocoasm/virtualfiles/disk.py b/cocoasm/virtualfiles/disk.py index ef8660b..68eecc8 100644 --- a/cocoasm/virtualfiles/disk.py +++ b/cocoasm/virtualfiles/disk.py @@ -8,9 +8,12 @@ from typing import NamedTuple +from abc import ABC, abstractmethod +from enum import Enum + from cocoasm.virtualfiles.coco_file import CoCoFile from cocoasm.virtualfiles.virtual_file_container import VirtualFileContainer -from cocoasm.values import Value, NumericValue, NoneValue +from cocoasm.values import NumericValue, NoneValue from cocoasm.virtualfiles.virtual_file_exceptions import VirtualFileValidationError # C L A S S E S ############################################################### @@ -42,23 +45,179 @@ class DirectoryEntry(NamedTuple): first_granule: int = 0x00 -class Preamble(NamedTuple): +class PreambleType(Enum): + ML = 0 + OTHER = 1 + + +class Preamble(ABC): + def __init__(self): + self.data_length = NoneValue() + self.load_addr = NoneValue() + self.length = 0 + + def get_data_length(self): + """ + Returns the length of the actual data. + + :return: the length of the data + """ + return self.data_length.int + + @abstractmethod + def read(self, buffer, pointer): + """ + Given a buffer and a pointer, reads the preamble, and returns a pointer + past the point of the preamble. + """ + + @abstractmethod + def write(self, buffer, pointer): + """ + Given a buffer and a pointer, writes the preamble, and returns a pointer + past the point of the preamble. + + :param buffer: the buffer to write into + :param pointer: the pointer into the buffer to start the write + :return: a pointer to the byte past the preamble + """ + + def is_ml(self): + """ + Returns whether this is an ML preamble or a basic preamble. + + :return: True if it is an ML preamble + """ + return False + + +class MLPreamble(Preamble): """ - The Preamble class is used to store information relating to a binary file - on a disk image. The Preamble only contains the load address and the length - of data for the binary file. + The machine language preamble data for the file. The preamble is a collection of 5 + bytes at the start of a binary file: + + byte 0 - always $00 + byte 1,2 - the data length of the file + byte 3,4 - the load address for the file + """ - load_addr: Value = NoneValue() - data_length: Value = NoneValue() + def __init__(self): + super().__init__() + self.length = 5 + + def read(self, buffer, pointer): + if len(buffer[pointer:]) < self.length: + raise VirtualFileValidationError("Not enough bytes to read preamble") + + if buffer[pointer] != 0x00: + raise VirtualFileValidationError("Invalid ML preamble flag") + + self.data_length = NumericValue((buffer[pointer + 1] << 8) + buffer[pointer + 2]) + self.load_addr = NumericValue((buffer[pointer + 3] << 8) + buffer[pointer + 4]) + + return pointer + self.length + + def write(self, buffer, pointer): + if len(buffer[pointer:]) < self.length: + raise VirtualFileValidationError("Not enough bytes to write preamble") + buffer[pointer] = 0x00 + buffer[pointer + 1] = self.data_length.high_byte() + buffer[pointer + 2] = self.data_length.low_byte() + buffer[pointer + 3] = self.load_addr.high_byte() + buffer[pointer + 4] = self.load_addr.low_byte() + return pointer + self.length + + def is_ml(self): + return True + + +class ASCIIPreamble(Preamble): + """ + ASCII encoded files in TRS-DOS do not have a preamble or a postamble. + """ + def __init__(self): + super().__init__() + self.length = 0 + + def read(self, buffer, pointer): + return pointer + + def write(self, buffer, pointer): + return pointer + + +class BasicPreamble(Preamble): + """ + All BASIC files under TRS-DOS are considered to have just a preamble that + contains the data length. The preamble is a collection of 3 bytes at the + start of a file: + + byte 0 - always $FF + byte 1,2 - the data length of the file -class Postamble(NamedTuple): + """ + def __init__(self): + super().__init__() + self.length = 3 + + def read(self, buffer, pointer): + if len(buffer[pointer:]) < self.length: + raise VirtualFileValidationError("Not enough bytes to read preamble") + + if buffer[pointer] != 0xFF: + raise VirtualFileValidationError("Invalid basic preamble flag") + + self.data_length = NumericValue((buffer[pointer + 1] << 8) + buffer[pointer + 2]) + + return pointer + self.length + + def write(self, buffer, pointer): + if len(buffer[pointer:]) < self.length: + raise VirtualFileValidationError("Not enough bytes to write preamble") + + buffer[pointer] = 0xFF + buffer[pointer + 1] = self.data_length.high_byte() + buffer[pointer + 2] = self.data_length.low_byte() + return pointer + self.length + + +class Postamble(object): """ The Postamble class is used to store information relating t a binary file on a disk image. The Postamble is stored at the end of a binary file and contains the exec address for the binary. """ - exec_addr: Value = NoneValue() + def __init__(self): + self.exec_addr = NoneValue() + self.length = 5 + + def read(self, buffer, pointer): + if len(buffer[pointer:]) < self.length: + raise VirtualFileValidationError("Not enough bytes to read postamble") + + if buffer[pointer] != 0xFF: + raise VirtualFileValidationError("Invalid postamble byte 0 not 0xFF, got {}".format(hex(buffer[pointer]))) + + if buffer[pointer + 1] != 0x00: + raise VirtualFileValidationError("Invalid postamble byte 1 not 0x00, got {}".format(hex(buffer[pointer+1]))) + + if buffer[pointer + 2] != 0x00: + raise VirtualFileValidationError("Invalid postamble byte 2 not 0x00, got {}".format(hex(buffer[pointer+2]))) + + self.exec_addr = NumericValue((buffer[pointer + 3] << 8) + buffer[pointer + 4]) + return pointer + self.length + + def write(self, buffer, pointer): + if len(buffer[pointer:]) < self.length: + raise VirtualFileValidationError("Not enough bytes to write postamble") + + buffer[pointer] = 0xFF + buffer[pointer + 1] = 0 + buffer[pointer + 2] = 0 + buffer[pointer + 3] = self.exec_addr.high_byte() + buffer[pointer + 4] = self.exec_addr.low_byte() + return pointer + self.length class DiskFile(VirtualFileContainer): @@ -66,7 +225,9 @@ def __init__(self, buffer=None, granule_fill_order=None): super().__init__(buffer=buffer) if buffer is None: self.buffer = [0xFF] * DiskConstants.IMAGE_SIZE - self.granule_fill_order = DiskConstants.GRANULE_FILL_ORDER if not granule_fill_order else granule_fill_order + self.granule_fill_order = DiskConstants.GRANULE_FILL_ORDER + if granule_fill_order: + self.granule_fill_order = granule_fill_order def read_sequence(self, pointer, length, decode=False): """ @@ -86,22 +247,6 @@ def read_sequence(self, pointer, length, decode=False): sequence.append(self.buffer[file_name_pointer]) return bytearray(sequence).decode("utf-8") if decode else sequence - def read_word(self, pointer): - """ - Reads a 16-bit value from the buffer starting at the specified - pointer offset. - - :param pointer: the offset into the buffer to read from - :return: the NumericValue read - """ - if len(self.buffer) < 2 or (len(self.buffer[pointer:]) < 2): - raise VirtualFileValidationError("Unable to read word - insufficient bytes in buffer") - - word_int = int(self.buffer[pointer]) - word_int = word_int << 8 - word_int |= int(self.buffer[pointer + 1]) - return NumericValue(word_int) - def validate_sequence(self, pointer, sequence): """ Ensures that the next group of bytes read matches the sequence specified. @@ -121,6 +266,8 @@ def validate_sequence(self, pointer, sequence): return True def list_files(self, filenames=None): + if len(self.buffer) < DiskConstants.IMAGE_SIZE: + raise VirtualFileValidationError("Disk image size is not {:,d} bytes long".format(DiskConstants.IMAGE_SIZE)) files = [] # Read the File Allocation Table @@ -147,28 +294,32 @@ def list_files(self, filenames=None): pointer += 18 # Set up data definitions - has_preamble = False - load_addr = NoneValue() exec_addr = NoneValue() # If the file is binary, read the preamble and postamble, otherwise don't if file_type.int == 0x02: - preamble = self.read_preamble(starting_granule.int) - has_preamble = True - data_length = preamble.data_length.int - load_addr = preamble.load_addr + preamble = MLPreamble() + elif data_type.int == 0xFF: + preamble = ASCIIPreamble() else: + preamble = BasicPreamble() + + preamble.read(self.buffer, self.seek_granule(starting_granule.int)) + + data_length = preamble.data_length.int + if data_length == 0: data_length = self.calculate_file_length(starting_granule.int, fat, bytes_in_last_sector.int) file_data, post_pointer = self.read_data( starting_granule.int, fat, - has_preamble=has_preamble, + preamble=preamble, data_length=data_length, ) - if has_preamble: - postamble = self.read_postamble(post_pointer) + if preamble.is_ml(): + postamble = Postamble() + postamble.read(self.buffer, post_pointer) exec_addr = postamble.exec_addr coco_file = CoCoFile( @@ -176,7 +327,7 @@ def list_files(self, filenames=None): extension=extension, type=file_type, data_type=data_type, - load_addr=load_addr, + load_addr=preamble.load_addr, exec_addr=exec_addr, data=file_data, ignore_gaps=True @@ -263,21 +414,24 @@ def find_empty_granule(self): for granule_number in self.granule_fill_order: if not self.granule_in_use(granule_number): return granule_number - return -1 + + raise VirtualFileValidationError("no free granules available for allocation") @staticmethod - def calculate_granules_needed(file_data): + def calculate_granules_needed(file_data, preamble, postamble): """ Given an array that contains the actual file data to store, calculates how many - granules are needed on disk to store the file. The routine assumes that a 5-byte - preamble and 5-byte postamble will be appended to the file. A 0-byte file will - always take 1 granule on disk. + granules are needed on disk to store the file. A 0-byte file will always take + 1 granule on disk. :param file_data: the array-like structure containing the file data + :param preamble: the preamble data + :param postamble: the postamble data if it exists, else None :return: a count of the number of granules needed for the file """ - pre_post_amble_len = DiskConstants.PREAMBLE_LEN + DiskConstants.POSTAMBLE_LEN - return int((len(file_data) + pre_post_amble_len) / DiskConstants.HALF_TRACK_LEN) + 1 + additional_len = preamble.length + additional_len += postamble.length if postamble else 0 + return int((len(file_data) + additional_len) / DiskConstants.HALF_TRACK_LEN) + 1 @staticmethod def calculate_sectors_needed(data_len): @@ -292,18 +446,23 @@ def calculate_sectors_needed(data_len): return int(data_len / DiskConstants.BYTES_PER_SECTOR) + 1 @staticmethod - def calculate_last_sector_bytes_used(file_data): + def calculate_last_sector_bytes_used(file_data, preamble, postamble): """ Given an array structure that contains the data to be saved to the virtual disk, calculates how many bytes will be used in the last sector of the last granule for the file. :param file_data: an array-like structure with file data in it + :param preamble: the preamble data + :param postamble: the postamble data if it exists, else None :return: the number of bytes stored in the last sector of the last granule """ - granules_needed = DiskFile.calculate_granules_needed(file_data) + granules_needed = DiskFile.calculate_granules_needed(file_data, preamble, postamble) + + additional_len = preamble.length + additional_len += postamble.length if postamble else 0 - file_data_len = len(file_data) + DiskConstants.PREAMBLE_LEN + DiskConstants.POSTAMBLE_LEN + file_data_len = len(file_data) + additional_len full_granule_len = (granules_needed - 1) * DiskConstants.HALF_TRACK_LEN file_data_len -= full_granule_len @@ -314,18 +473,22 @@ def calculate_last_sector_bytes_used(file_data): return file_data_len @staticmethod - def calculate_last_granules_sectors_used(file_data): + def calculate_last_granules_sectors_used(file_data, preamble, postamble): """ Given an array-like structure that contains data to be saved to the virtual disk, calculates how many sectors are used in the last granule. Note that this function adds the pre- and post-amble bytes to the number of bytes used to store the file. :param file_data: an array-like structure with file data in it + :param preamble: the preamble data + :param postamble: the postamble data if it exists, else None :return: the number of sectors used in the last granule of the file """ - granules_needed = DiskFile.calculate_granules_needed(file_data) + granules_needed = DiskFile.calculate_granules_needed(file_data, preamble, postamble) - file_data_len = len(file_data) + DiskConstants.PREAMBLE_LEN + DiskConstants.POSTAMBLE_LEN + additional_len = preamble.length + additional_len += postamble.length if postamble else 0 + file_data_len = len(file_data) + additional_len full_granule_len = (granules_needed - 1) * DiskConstants.HALF_TRACK_LEN file_data_len -= full_granule_len @@ -345,52 +508,12 @@ def seek_granule(granule): granule_offset += DiskConstants.HALF_TRACK_LEN * 2 return granule_offset - def read_preamble(self, starting_granule): - """ - Reads the preamble data for the file. The preamble is a collection of 5 - bytes at the start of a binary file: - - byte 0 - always $00 - byte 1,2 - the data length of the file - byte 3,4 - the load address for the file - - :param starting_granule: the granule number that contains the preamble - :return: a populated Preamble object - """ - pointer = self.seek_granule(starting_granule) - if not self.validate_sequence(pointer, [0x00]): - raise VirtualFileValidationError("Invalid preamble flag") - - return Preamble( - data_length=self.read_word(pointer + 1), - load_addr=self.read_word(pointer + 3), - ) - - def read_postamble(self, pointer): - """ - Reads the postamble of a binary file. The postamble is a collection of - 5 bytes as follows: - - byte 0 - always $FF - byte 1,2 - always $00, $00 - byte 3,4 - the exec address of the binary file - - :param pointer: a pointer to the postamble data - :return: a populated Postamble object - """ - if not self.validate_sequence(pointer, [0xFF, 0x00, 0x00]): - raise VirtualFileValidationError("Invalid postamble flags") - - return Postamble( - exec_addr=self.read_word(pointer + 3), - ) - - def read_data(self, starting_granule, fat, has_preamble=False, data_length=0): + def read_data(self, starting_granule, fat, preamble, data_length=0): """ Reads a collection of data from a disk image. :param starting_granule: the starting granule for the file - :param has_preamble: whether there is a preamble to be read + :param preamble: the preamble type to read :param data_length: the length of data to read :param fat: the File Allocation Table data for the disk :return: the raw data from the specified file and the pointer to the end of the file @@ -403,9 +526,9 @@ def read_data(self, starting_granule, fat, has_preamble=False, data_length=0): raise VirtualFileValidationError("Unable to read data - insufficient bytes in buffer") # Skip over preamble if it exists - if has_preamble: - pointer += DiskConstants.PREAMBLE_LEN - chunk_size -= DiskConstants.PREAMBLE_LEN + if preamble: + pointer += preamble.length + chunk_size -= preamble.length # Check to see if we are reading more than one granule if data_length > chunk_size: @@ -414,7 +537,7 @@ def read_data(self, starting_granule, fat, has_preamble=False, data_length=0): data_length -= 1 pointer += 1 next_granule = fat[starting_granule] - granule_data, pointer = self.read_data(next_granule, fat, data_length=data_length, has_preamble=False) + granule_data, pointer = self.read_data(next_granule, fat, None, data_length=data_length) file_data.extend(granule_data) else: for _ in range(data_length): @@ -459,51 +582,6 @@ def write_dir_entry(self, directory_entry_number, coco_file, first_granule, last self.buffer[pointer] = 0x00 pointer += 1 - def write_preamble(self, preamble, granule): - """ - Given preamble data, writes the preamble to the start of the specified granule. - - :param preamble: the preamble data to write - :param granule: the granule number to write to - """ - pointer = self.seek_granule(granule) - - # First byte of preamble is $00 - self.buffer[pointer] = 0x00 - pointer += 1 - - self.buffer[pointer] = preamble.data_length.high_byte() - pointer += 1 - self.buffer[pointer] = preamble.data_length.low_byte() - pointer += 1 - - self.buffer[pointer] = preamble.load_addr.high_byte() - pointer += 1 - self.buffer[pointer] = preamble.load_addr.low_byte() - - def write_postamble(self, postamble, pointer): - """ - Given postamble data, writes the postamble format into the buffer at the - specified pointer location. - - :param postamble: the postamble data to write - :param pointer: a pointer into the buffer where to write the postamble data - """ - if not postamble: - return pointer - - # First three bytes of postamble is always $FF $00 $00 - self.buffer[pointer] = 0xFF - pointer += 1 - self.buffer[pointer] = 0x00 - pointer += 1 - self.buffer[pointer] = 0x00 - pointer += 1 - - self.buffer[pointer] = postamble.exec_addr.high_byte() - pointer += 1 - self.buffer[pointer] = postamble.exec_addr.low_byte() - def write_bytes_to_buffer(self, pointer, data_to_write): """ Given a pointer into the disk buffer, and an array-like object of bytes to write @@ -566,19 +644,19 @@ def write_to_granules(self, file_data, allocated_granules, preamble, postamble, skip_bytes = 0 if first_granule and preamble: - self.write_preamble(preamble, granule) - pointer += DiskConstants.PREAMBLE_LEN - skip_bytes += DiskConstants.PREAMBLE_LEN + pointer = preamble.write(self.buffer, pointer) + skip_bytes += preamble.length if len(file_data) < (DiskConstants.HALF_TRACK_LEN - skip_bytes): pointer = self.write_bytes_to_buffer(pointer, file_data) - self.write_postamble(postamble, pointer) + if postamble: + postamble.write(self.buffer, pointer) else: - self.write_bytes_to_buffer(pointer, file_data[:DiskConstants.HALF_TRACK_LEN]) + self.write_bytes_to_buffer(pointer, file_data[:DiskConstants.HALF_TRACK_LEN - skip_bytes]) self.write_to_granules( - file_data[DiskConstants.HALF_TRACK_LEN:], + file_data[DiskConstants.HALF_TRACK_LEN - skip_bytes:], allocated_granules, - preamble, + None, postamble, first_granule=False ) @@ -591,18 +669,28 @@ def add_file(self, coco_file): :param coco_file: the CoCoFile object to write """ - granules_needed = self.calculate_granules_needed(coco_file.data) + if coco_file.type.int == 0x02: + preamble = MLPreamble() + preamble.data_length = NumericValue(len(coco_file.data)) + preamble.load_addr = coco_file.load_addr + postamble = Postamble() + postamble.exec_addr = coco_file.exec_addr + elif coco_file.data_type.int == 0xFF: + preamble = ASCIIPreamble() + postamble = None + else: + preamble = BasicPreamble() + preamble.data_length = NumericValue(len(coco_file.data)) + postamble = None + + granules_needed = self.calculate_granules_needed(coco_file.data, preamble, postamble) # Check to see if there are enough granules for allocation allocated_granules = [] - for granule in range(DiskConstants.TOTAL_GRANULES): - if not self.granule_in_use(granule): - allocated_granules.append(granule) - if len(allocated_granules) == granules_needed: - break - - if len(allocated_granules) != granules_needed: - raise VirtualFileValidationError("Not enough free granules to save file") + while len(allocated_granules) < granules_needed: + granule = self.find_empty_granule() + allocated_granules.append(granule) + self.buffer[DiskConstants.FAT_OFFSET + granule] = 0x99 # Check to see if there is a free directory entry to save the file directory_entry = self.find_empty_directory_entry() @@ -610,21 +698,12 @@ def add_file(self, coco_file): raise VirtualFileValidationError("No free directory entry to save file") # Calculate the number of bytes used in the last sector, and the number of sectors in the last granule - last_sector_bytes_used = self.calculate_last_sector_bytes_used(coco_file.data) - last_granule_sectors_used = self.calculate_last_granules_sectors_used(coco_file.data) + last_sector_bytes_used = self.calculate_last_sector_bytes_used(coco_file.data, preamble, postamble) + last_granule_sectors_used = self.calculate_last_granules_sectors_used(coco_file.data, preamble, postamble) # Write out the directory entry self.write_dir_entry(directory_entry, coco_file, allocated_granules[0], last_sector_bytes_used) - # Generate pre- and post-amble as required - preamble = Preamble( - load_addr=coco_file.load_addr, - data_length=NumericValue(len(coco_file.data)) - ) - postamble = Postamble( - exec_addr=coco_file.exec_addr - ) - # Write the granule data to disk self.write_to_granules(coco_file.data, allocated_granules, preamble, postamble) diff --git a/cocoasm/virtualfiles/virtual_file.py b/cocoasm/virtualfiles/virtual_file.py index 479ce2d..90d97e8 100644 --- a/cocoasm/virtualfiles/virtual_file.py +++ b/cocoasm/virtualfiles/virtual_file.py @@ -43,15 +43,15 @@ def __init__(self, source_file=None, virtual_file_type=None): def get_coco_files(self): try: - cassette_file = CassetteFile(buffer=self.source_file.get_buffer()) - return cassette_file.list_files(), VirtualFileType.CASSETTE + disk_file = DiskFile(buffer=self.source_file.get_buffer()) + return disk_file.list_files(), VirtualFileType.DISK except VirtualFileValidationError: pass try: - disk_file = DiskFile(buffer=self.source_file.get_buffer()) - return disk_file.list_files(), VirtualFileType.DISK - except VirtualFileValidationError: + cassette_file = CassetteFile(buffer=self.source_file.get_buffer()) + return cassette_file.list_files(), VirtualFileType.CASSETTE + except VirtualFileValidationError as error: pass return [], VirtualFileType.BINARY diff --git a/cocoasm/virtualfiles/virtual_file_container.py b/cocoasm/virtualfiles/virtual_file_container.py index 3b3841b..6ed6128 100644 --- a/cocoasm/virtualfiles/virtual_file_container.py +++ b/cocoasm/virtualfiles/virtual_file_container.py @@ -8,7 +8,9 @@ import copy -from abc import ABC, abstractmethod +from abc import abstractmethod +from cocoasm.virtualfiles.virtual_file_exceptions import VirtualFileValidationError +from cocoasm.values import NumericValue # C L A S S E S ############################################################### @@ -55,11 +57,26 @@ def add_file(self, coco_file): @abstractmethod def list_files(self, filenames=None): """ - Lists all of the CoCoFile objects within the container. + Lists all the CoCoFile objects within the container. :param filenames: the list of CoCoFiles to list (if they exist) :return: a list of all the CoCoFile objects in the container """ + def read_word(self, pointer): + """ + Reads a 16-bit value from the buffer starting at the specified + pointer offset. + + :param pointer: the offset into the buffer to read from + :return: the NumericValue read + """ + if len(self.buffer) < 2 or (len(self.buffer[pointer:]) < 2): + raise VirtualFileValidationError("Unable to read word - insufficient bytes in buffer") + + word_int = int(self.buffer[pointer]) + word_int = word_int << 8 + word_int |= int(self.buffer[pointer + 1]) + return NumericValue(word_int) # E N D O F F I L E ####################################################### diff --git a/file_util.py b/file_util.py index c46015a..5d45149 100644 --- a/file_util.py +++ b/file_util.py @@ -33,10 +33,13 @@ def parse_arguments(): "--list", action="store_true", help="list all of the files on the specified host file" ) parser.add_argument( - "--to_bin", action="store_true", help="extracts all the files from the host file, and saves them as BIN files" + "--to_bin", metavar="BIN_FILE", help="extracts all the files from the host file, and saves them as BIN files" ) parser.add_argument( - "--to_cas", action="store_true", help="extracts all the files from the host file, and saves them as CAS files" + "--to_cas", metavar="CAS_FILE", help="extracts all the files from the host file, and saves it to a CAS file" + ) + parser.add_argument( + "--to_dsk", metavar="DSK_FILE", help="extracts all the files from the host file, and saves it to a DSK file" ) parser.add_argument( "--files", nargs="+", type=str, help="list of file names to extract" @@ -65,37 +68,55 @@ def main(args): sys.exit(0) if args.to_cas: + target_virtual_file = VirtualFile( + SourceFile(args.to_cas, file_type=SourceFileType.BINARY), + virtual_file_type=VirtualFileType.CASSETTE + ) + target_virtual_file.open_virtual_file() for number, file in enumerate(virtual_file.list_files()): filename = file.name.strip().replace("\0", "") if files_to_include is None or filename in files_to_include: - cas_file_name = "{}.cas".format(filename) print("-- File #{} [{}] --".format(number + 1, filename)) - target_virtual_file = VirtualFile( - SourceFile(cas_file_name, file_type=SourceFileType.BINARY), - virtual_file_type=VirtualFileType.CASSETTE - ) - target_virtual_file.open_virtual_file() target_virtual_file.add_coco_file(file) - target_virtual_file.save_virtual_file(append_mode=args.append) - print("Saved as {}".format(cas_file_name)) - - if args.to_bin: + target_virtual_file.save_virtual_file(append_mode=args.append) + print("Saved to {}".format(args.to_cas)) + + if args.to_dsk: + target_virtual_file = VirtualFile( + SourceFile(args.to_dsk, file_type=SourceFileType.BINARY), + virtual_file_type=VirtualFileType.DISK + ) + target_virtual_file.open_virtual_file() for number, file in enumerate(virtual_file.list_files()): filename = file.name.strip().replace("\0", "") if files_to_include is None or filename in files_to_include: - bin_file_name = "{}.bin".format(filename) print("-- File #{} [{}] --".format(number + 1, filename)) - target_virtual_file = VirtualFile( - SourceFile(bin_file_name, file_type=SourceFileType.BINARY), - virtual_file_type=VirtualFileType.BINARY - ) - target_virtual_file.open_virtual_file() target_virtual_file.add_coco_file(file) - target_virtual_file.save_virtual_file(append_mode=args.append) - print("Saved as {}".format(bin_file_name)) + target_virtual_file.save_virtual_file(append_mode=args.append) + print("Saved to {}".format(args.to_dsk)) + + if args.to_bin: + target_virtual_file = VirtualFile( + SourceFile(args.to_bin, file_type=SourceFileType.BINARY), + virtual_file_type=VirtualFileType.BINARY + ) + target_virtual_file.open_virtual_file() + files = virtual_file.list_files() + if len(files) > 1: + print("More than one file exists in virtual container, not saving") + sys.exit(1) + + file = files[0] + filename = file.name.strip().replace("\0", "") + if files_to_include is None or filename in files_to_include: + print("-- File #1 [{}] --".format(filename)) + target_virtual_file.add_coco_file(file) + target_virtual_file.save_virtual_file(append_mode=args.append) + print("Saved to {}".format(args.to_bin)) except Exception as error: - raise Exception(error) + print(error) + sys.exit(1) # M A I N ##################################################################### diff --git a/test/virtualfiles/test_cassette.py b/test/virtualfiles/test_cassette.py index 65bab74..a8d708e 100644 --- a/test/virtualfiles/test_cassette.py +++ b/test/virtualfiles/test_cassette.py @@ -42,7 +42,7 @@ def test_append_leader_correct(self): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55] cassette_file = CassetteFile() - cassette_file.write_leader() + cassette_file.append_leader() self.assertEqual(expected, cassette_file.get_buffer()) def test_append_name_full_length_correct(self): @@ -75,11 +75,13 @@ def test_append_header_correct(self): 0x34, 0x56, 0x78, 0x84, 0x55] coco_file = CoCoFile( name=name, + type=NumericValue(0x02), + data_type=NumericValue(0xFF), load_addr=NumericValue(0x1234), exec_addr=NumericValue(0x5678) ) cassette_file = CassetteFile() - cassette_file.append_header(coco_file, CassetteFileType.OBJECT_FILE, CassetteDataType.ASCII) + cassette_file.append_header(coco_file) self.assertEqual(expected, cassette_file.get_buffer()) def test_append_data_blocks_appends_nothing_when_raw_empty(self): @@ -140,6 +142,14 @@ def test_save_file_works_correct(self): cassette_file = CassetteFile() raw_bytes = [0x02] expected = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -149,7 +159,16 @@ def test_save_file_works_correct(self): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, - 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x34, 0x56, 0x78, 0x85, 0x55, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -162,6 +181,7 @@ def test_save_file_works_correct(self): ] coco_file = CoCoFile( name="testfile", + type=NumericValue(0x02), load_addr=NumericValue(0x1234), exec_addr=NumericValue(0x5678), data=raw_bytes @@ -169,66 +189,6 @@ def test_save_file_works_correct(self): cassette_file.add_file(coco_file) self.assertEqual(expected, cassette_file.get_buffer()) - def test_read_leader_works_correctly(self): - buffer = [ - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x3C, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, - 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x01, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, - 0xFF, 0x55 - ] - cassette_file = CassetteFile(buffer=buffer) - self.assertTrue(cassette_file.read_leader()) - - def test_read_leader_raises_when_leader_too_short(self): - buffer = [ - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 - ] - cassette_file = CassetteFile(buffer=buffer) - with self.assertRaises(VirtualFileValidationError): - cassette_file.read_leader() - - def test_read_leader_raises_with_bad_leader_byte(self): - buffer = [ - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x3C, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, - 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x01, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, - 0xFF, 0x55 - ] - cassette_file = CassetteFile(buffer=buffer) - with self.assertRaises(VirtualFileValidationError): - cassette_file.read_leader() - def test_read_coco_filename_works_correctly(self): buffer = [ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -252,65 +212,9 @@ def test_read_coco_filename_works_correctly(self): 0xFF, 0x55 ] cassette_file = CassetteFile(buffer=buffer) - cassette_file.buffer = cassette_file.buffer[132:] - result = cassette_file.read_coco_file_name() + result, pointer = cassette_file.read_coco_file_name(132) self.assertEqual("testfile", result) - - def test_read_test_sequence_returns_true_on_correct_sequence(self): - buffer = [ - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x3C, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, - 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x01, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, - 0xFF, 0x55 - ] - cassette_file = CassetteFile(buffer=buffer) - cassette_file.buffer = cassette_file.buffer[128:] - self.assertTrue(cassette_file.validate_sequence([0x55, 0x3C, 0x00])) - - def test_read_test_sequence_returns_false_on_incorrect_sequence(self): - buffer = [ - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x3C, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, - 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x01, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, - 0xFF, 0x55 - ] - cassette_file = CassetteFile(buffer=buffer) - self.assertFalse(cassette_file.validate_sequence([0x55, 0x3C, 0x00])) - - def test_read_test_sequence_raises_when_buffer_too_short(self): - cassette_file = CassetteFile() - with self.assertRaises(VirtualFileValidationError): - cassette_file.validate_sequence([0x55, 0x3C, 0x00]) + self.assertEqual(140, pointer) def test_read_file_works_correctly(self): raw_bytes = [0x02] @@ -342,39 +246,13 @@ def test_read_file_works_correctly(self): exec_addr=NumericValue(0x5678), data=raw_bytes ) - result_file = cassette_file.read_file() + result_file, _ = cassette_file.read_file(0) self.assertEqual(coco_file.name, result_file.name) self.assertEqual(coco_file.load_addr.int, result_file.load_addr.int) self.assertEqual(coco_file.exec_addr.int, result_file.exec_addr.int) self.assertEqual([0x02], result_file.data) - def test_read_file_raises_on_bad_header_validation(self): - buffer = [ - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0xFF, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, - 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x01, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, - 0xFF, 0x55 - ] - cassette_file = CassetteFile(buffer=buffer) - with self.assertRaises(VirtualFileValidationError): - cassette_file.read_file() - - def test_read_file_raises_on_bad_data_block_validation(self): + def test_read_file_raises_bad_block_type(self): buffer = [ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -393,14 +271,14 @@ def test_read_file_raises_on_bad_data_block_validation(self): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0xFF, 0x01, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x99, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, 0xFF, 0x55 ] cassette_file = CassetteFile(buffer=buffer) with self.assertRaises(VirtualFileValidationError): - cassette_file.read_file() + cassette_file.read_file(0) - def test_read_file_raises_bad_block_type(self): + def test_read_blocks_raises_when_no_data_or_eof_block(self): buffer = [ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -410,7 +288,7 @@ def test_read_file_raises_bad_block_type(self): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x3C, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, + 0x55, 0x3D, 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x66, 0x69, 0x6C, 0x65, 0x02, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x85, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -419,12 +297,12 @@ def test_read_file_raises_bad_block_type(self): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, - 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x99, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3D, 0x99, 0x01, 0x02, 0x04, 0x55, 0x55, 0x3D, 0xFF, 0x00, 0xFF, 0x55 ] cassette_file = CassetteFile(buffer=buffer) with self.assertRaises(VirtualFileValidationError): - cassette_file.read_file() + cassette_file.read_blocks(0) def test_read_file_empty_when_no_data(self): buffer = [ @@ -449,7 +327,7 @@ def test_read_file_empty_when_no_data(self): 0xFF, 0x55 ] cassette_file = CassetteFile(buffer=buffer) - result_file = cassette_file.read_file() + result_file, _ = cassette_file.read_file(0) self.assertIsNone(result_file) def test_extract_multiple_files_works_correctly(self): @@ -561,7 +439,7 @@ def test_extract_multiple_files_with_filename_filter_works_correctly(self): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x3C, 0x01, 0x01, 0xFF, 0x04, 0x55, 0x55, 0x3C, 0xFF, 0x00, 0xFF, 0x55 ] - cassette_file = CassetteFile(buffer=bytearray(buffer)) + cassette_file = CassetteFile(buffer=buffer) coco_files = [ CoCoFile( name="testfile", @@ -585,6 +463,50 @@ def test_extract_multiple_files_with_filename_filter_works_correctly(self): self.assertEqual(coco_files[1].exec_addr.int, result_files[0].exec_addr.int) self.assertEqual(coco_files[1].data, result_files[0].data) + def test_skip_to_sequence_correct_full_sequence_match(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]) + self.assertEqual(0, cassette_file.skip_to_sequence([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) + + def test_skip_to_sequence_correct_subsequence_sequence_match(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]) + self.assertEqual(1, cassette_file.skip_to_sequence([2, 3, 4, 5, 6, 7, 8, 9, 10])) + + def test_skip_to_sequence_correct_subsequence_near_end(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]) + self.assertEqual(8, cassette_file.skip_to_sequence([9, 10])) + + def test_skip_to_sequence_correct_single_end_element(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]) + self.assertEqual(9, cassette_file.skip_to_sequence([10])) + + def test_skip_to_sequence_correct_single_end_element_position_at_end(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]) + self.assertEqual(9, cassette_file.skip_to_sequence([10], start=9)) + + def test_skip_to_sequence_no_match(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]) + self.assertEqual(-1, cassette_file.skip_to_sequence([11, 12, 13, 14])) + + def test_skip_to_sequence_returns_negative_one_when_buffer_too_small(self): + cassette_file = CassetteFile(buffer=[ + 1, 2, 3 + ]) + result = cassette_file.skip_to_sequence([11, 12, 13, 14]) + self.assertEqual(-1, result) + + # M A I N ##################################################################### diff --git a/test/virtualfiles/test_disk.py b/test/virtualfiles/test_disk.py index 675febc..edb6acb 100644 --- a/test/virtualfiles/test_disk.py +++ b/test/virtualfiles/test_disk.py @@ -11,7 +11,7 @@ from mock import patch from cocoasm.values import NumericValue -from cocoasm.virtualfiles.disk import DiskFile, DiskConstants, Preamble, Postamble +from cocoasm.virtualfiles.disk import DiskFile, DiskConstants, ASCIIPreamble, Postamble, MLPreamble, BasicPreamble from cocoasm.virtualfiles.coco_file import CoCoFile from cocoasm.virtualfiles.virtual_file_exceptions import VirtualFileValidationError @@ -86,61 +86,36 @@ def test_seek_granule_more_than_33_correct(self): pointer = disk_file.seek_granule(34) self.assertEqual(82944, pointer) - def test_read_preamble_raises_on_empty_buffer(self): - disk_file = DiskFile() - with self.assertRaises(VirtualFileValidationError): - disk_file.read_preamble(0) - - def test_read_preamble_raises_on_bad_preamble(self): - disk_file = DiskFile(buffer=[0xDE, 0xAD, 0xBE, 0xEF]) - with self.assertRaises(VirtualFileValidationError): - disk_file.read_preamble(0) - - def test_read_preamble_raises_when_buffer_too_small(self): - disk_file = DiskFile(buffer=[0xDE, 0xAD]) - with self.assertRaises(VirtualFileValidationError): - disk_file.read_preamble(0) - - def test_read_preamble_works_correct(self): - disk_file = DiskFile(buffer=[0x00, 0xDE, 0xAD, 0xBE, 0xEF]) - preamble = disk_file.read_preamble(0) - self.assertEqual("DEAD", preamble.data_length.hex()) - self.assertEqual("BEEF", preamble.load_addr.hex()) - - def test_read_postamble_raises_on_empty_buffer(self): - disk_file = DiskFile() - with self.assertRaises(VirtualFileValidationError): - disk_file.read_postamble(0) - - def test_read_postamble_raises_on_bad_postamble(self): - disk_file = DiskFile(buffer=[0xDE, 0xAD, 0xBE, 0xEF]) - with self.assertRaises(VirtualFileValidationError): - disk_file.read_postamble(0) - - def test_read_postamble_correct(self): - disk_file = DiskFile(buffer=[0xFF, 0x00, 0x00, 0xDE, 0xAD]) - postamble = disk_file.read_postamble(0) - self.assertEqual("DEAD", postamble.exec_addr.hex()) - def test_read_data_raises_on_empty_buffer(self): disk_file = DiskFile(buffer=[]) + preamble = MLPreamble() with self.assertRaises(VirtualFileValidationError): - disk_file.read_data(0, [], data_length=4) + disk_file.read_data(0, [], preamble, data_length=4) def test_read_data_small_chunk_size_correct(self): buffer = [0xDE, 0xAD, 0xBE, 0xEF] disk_file = DiskFile(buffer=buffer) - data, pointer = disk_file.read_data(0, [], data_length=4) + data, pointer = disk_file.read_data(0, [], None, data_length=4) self.assertEqual(buffer, data) self.assertEqual(4, pointer) def test_read_data_small_chunk_size_skips_preamble(self): buffer = [0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x01] disk_file = DiskFile(buffer=buffer) - data, pointer = disk_file.read_data(0, [], has_preamble=True, data_length=1) + preamble = MLPreamble() + data, pointer = disk_file.read_data(0, [], preamble, data_length=1) self.assertEqual([0x01], data) self.assertEqual(6, pointer) + def test_read_data_multi_chunk_correct(self): + buffer = [0x00, 0xDE, 0xAD, 0xBE, 0xEF] + buffer.extend([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99] * 300) + disk_file = DiskFile(buffer=buffer) + preamble = MLPreamble() + data, pointer = disk_file.read_data(0, [0x01], preamble, data_length=3000) + self.assertEqual(buffer[5:], data) + self.assertEqual(3005, pointer) + def test_directory_entry_in_use_raises_on_negative_entry(self): disk_file = DiskFile() with self.assertRaises(VirtualFileValidationError): @@ -193,9 +168,10 @@ def test_granule_in_use_correct_when_in_use(self): for granule in range(0, 67): self.assertTrue(disk_file.granule_in_use(granule)) - def test_find_empty_granule_returns_negative_when_no_granules_free(self): + def test_find_empty_granule_raises_when_no_granules_free(self): disk_file = DiskFile(buffer=[0xDE] * 161280) - self.assertEqual(-1, disk_file.find_empty_granule()) + with self.assertRaises(VirtualFileValidationError): + disk_file.find_empty_granule() def test_find_empty_granule_returns_correct_when_granules_free(self): disk_file = DiskFile(buffer=[0xFF] * 161280) @@ -211,16 +187,28 @@ def test_find_empty_granule_raises_when_fill_order_too_small(self): disk_file.find_empty_granule() def test_granules_needed_empty_file(self): - self.assertEqual(1, DiskFile.calculate_granules_needed([])) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(1, DiskFile.calculate_granules_needed([], preamble, None)) def test_granules_needed_single_byte_file(self): - self.assertEqual(1, DiskFile.calculate_granules_needed([])) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(1, DiskFile.calculate_granules_needed([], preamble, None)) def test_granules_needed_single_granule(self): - self.assertEqual(1, DiskFile.calculate_granules_needed([0x00] * (DiskConstants.HALF_TRACK_LEN - 11))) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(1, DiskFile.calculate_granules_needed([0x00] * (DiskConstants.HALF_TRACK_LEN - 11), preamble, None)) def test_granules_needed_two_granules(self): - self.assertEqual(2, DiskFile.calculate_granules_needed([0x00] * DiskConstants.HALF_TRACK_LEN)) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(2, DiskFile.calculate_granules_needed([0x00] * DiskConstants.HALF_TRACK_LEN, preamble, None)) def test_sectors_needed_zero_byte_file_correct(self): self.assertEqual(1, DiskFile.calculate_sectors_needed(0)) @@ -232,10 +220,16 @@ def test_sectors_needed_two_sectors(self): self.assertEqual(2, DiskFile.calculate_sectors_needed(DiskConstants.BYTES_PER_SECTOR)) def test_calculate_last_sector_bytes_used_zero_length_file_correct(self): - self.assertEqual(10, DiskFile.calculate_last_sector_bytes_used([])) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(5, DiskFile.calculate_last_sector_bytes_used([], preamble, None)) def test_calculate_last_sector_bytes_used_large_file_correct(self): - self.assertEqual(10, DiskFile.calculate_last_sector_bytes_used([0x00] * 2560)) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(10, DiskFile.calculate_last_sector_bytes_used([0x00] * 2565, preamble, None)) @patch("cocoasm.virtualfiles.disk.DiskConstants.DIR_OFFSET", 0) def test_write_dir_entry(self): @@ -270,36 +264,22 @@ def test_write_dir_entry_space_fills_name(self): ) def test_calculate_last_granules_sectors_used_empty_file_data(self): - self.assertEqual(1, DiskFile.calculate_last_granules_sectors_used([])) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(1, DiskFile.calculate_last_granules_sectors_used([], preamble, None)) def test_calculate_last_granules_sectors_used_less_than_one_sector(self): - self.assertEqual(1, DiskFile.calculate_last_granules_sectors_used([0x00] * 245)) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(1, DiskFile.calculate_last_granules_sectors_used([0x00] * 245, preamble, None)) def test_calculate_last_granules_sectors_used_more_than_one_sector(self): - self.assertEqual(2, DiskFile.calculate_last_granules_sectors_used([0x00] * 500)) - - def test_write_preamble_works_correctly(self): - preamble = Preamble( - load_addr=NumericValue("$DEAD"), - data_length=NumericValue("$BEEF"), - ) - disk_file = DiskFile(buffer=[0xAA] * 5) - disk_file.write_preamble(preamble, 0) - self.assertEqual([0x00, 0xBE, 0xEF, 0xDE, 0xAD], disk_file.get_buffer()) - - def test_write_postamble_works_correctly(self): - postamble = Postamble( - exec_addr=NumericValue("$DEAD") - ) - disk_file = DiskFile(buffer=[0xAA] * 5) - disk_file.write_postamble(postamble, 0) - self.assertEqual([0xFF, 0x00, 0x00, 0xDE, 0xAD], disk_file.get_buffer()) - - def test_write_postamble_does_nothing_if_no_postamble(self): - postamble = None - disk_file = DiskFile(buffer=[0xAA] * 5) - disk_file.write_postamble(postamble, 0) - self.assertEqual([0xAA, 0xAA, 0xAA, 0xAA, 0xAA], disk_file.get_buffer()) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + self.assertEqual(2, DiskFile.calculate_last_granules_sectors_used([0x00] * 500, preamble, None)) def test_write_bytes_to_buffer_empty_bytes_does_nothing(self): disk_file = DiskFile(buffer=[0xAA] * 5) @@ -345,6 +325,11 @@ def test_to_fat_single_granule_correct(self): , disk_file.get_buffer() ) + def test_list_files_raises_on_incorrect_size(self): + disk_file = DiskFile(buffer=[0xFF] * 256) + with self.assertRaises(VirtualFileValidationError): + disk_file.list_files() + def test_list_files_no_entries_returns_empty_list(self): disk_file = DiskFile(buffer=[0xFF] * 161280) self.assertEqual([], disk_file.list_files()) @@ -377,8 +362,10 @@ def test_list_files_multiple_entries_correct(self): DiskConstants.DIR_OFFSET, [0x54, 0x45, 0x53, 0x54, 0x20, 0x20, 0x20, 0x20, 0x42, 0x49, 0x4E, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x53, 0x45, 0x43, 0x4F, 0x4E, 0x44, 0x20, 0x20, 0x42, 0x41, 0x53, 0x00, 0xFF, 0x01, 0x00, 0x0C, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + 0x53, 0x45, 0x43, 0x4F, 0x4E, 0x44, 0x20, 0x20, 0x42, 0x41, 0x53, 0x00, 0x00, 0x01, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x41, 0x53, 0x43, 0x49, 0x49, 0x20, 0x20, 0x20, 0x41, 0x53, 0x43, 0x00, 0xFF, 0x02, 0x00, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,] ) disk_file.write_bytes_to_buffer( DiskFile.seek_granule(0), @@ -386,14 +373,18 @@ def test_list_files_multiple_entries_correct(self): ) disk_file.write_bytes_to_buffer( DiskFile.seek_granule(1), - [0x00, 0x00, 0x02, 0xCA, 0xFE, 0xBB, 0xBB, 0xFF, 0x00, 0x00, 0xBA, 0xBE] + [0xFF, 0x00, 0x09, 0xCA, 0xFE, 0xBB, 0xBB, 0xFF, 0x00, 0x00, 0xBA, 0xBE] + ) + disk_file.write_bytes_to_buffer( + DiskFile.seek_granule(2), + [0xFF, 0x00, 0x09, 0xCA, 0xFE, 0xBB, 0xBB, 0xFF, 0x00, 0x00, 0xBA, 0xBE] ) disk_file.write_bytes_to_buffer( DiskConstants.FAT_OFFSET, - [0x00, 0xC1] + [0xC1, 0xC1, 0xC1] ) coco_files = disk_file.list_files() - self.assertEqual(2, len(coco_files)) + self.assertEqual(3, len(coco_files)) coco_file = coco_files[0] self.assertEqual("TEST", coco_file.name) self.assertEqual("BIN", coco_file.extension) @@ -407,10 +398,19 @@ def test_list_files_multiple_entries_correct(self): self.assertEqual("SECOND", coco_file.name) self.assertEqual("BAS", coco_file.extension) self.assertEqual(0x00, coco_file.type.int) + self.assertEqual(0x00, coco_file.data_type.int) + self.assertEqual("", coco_file.load_addr.hex()) + self.assertEqual("", coco_file.exec_addr.hex()) + self.assertEqual([0xCA, 0xFE, 0xBB, 0xBB, 0xFF, 0x00, 0x00, 0xBA, 0xBE], coco_file.data) + + coco_file = coco_files[2] + self.assertEqual("ASCII", coco_file.name) + self.assertEqual("ASC", coco_file.extension) + self.assertEqual(0x00, coco_file.type.int) self.assertEqual(0xFF, coco_file.data_type.int) self.assertEqual("", coco_file.load_addr.hex()) self.assertEqual("", coco_file.exec_addr.hex()) - self.assertEqual([0x00, 0x00, 0x02, 0xCA, 0xFE, 0xBB, 0xBB, 0xFF, 0x00, 0x00, 0xBA, 0xBE], coco_file.data) + self.assertEqual([0xFF, 0x00, 0x09, 0xCA, 0xFE, 0xBB, 0xBB, 0xFF, 0x00, 0x00, 0xBA, 0xBE], coco_file.data) def test_write_to_granules_does_nothing_on_empty_allocated_granules(self): disk_file = DiskFile(buffer=[0xFF] * 161280) @@ -426,13 +426,11 @@ def test_write_to_granules_correct_no_preamble(self): def test_write_to_granules_correct_with_preamble(self): disk_file = DiskFile(buffer=[0xFF] * 161280) - preamble = Preamble( - load_addr=NumericValue("$DEAD"), - data_length=NumericValue("$BEEF"), - ) - postamble = Postamble( - exec_addr=NumericValue("$CAFE") - ) + preamble = MLPreamble() + preamble.load_addr = NumericValue("$DEAD") + preamble.data_length = NumericValue("$BEEF") + postamble = Postamble() + postamble.exec_addr = NumericValue("$CAFE") disk_file.write_to_granules([0x00] * 234, [0], preamble, postamble) expected = [0x00, 0xBE, 0xEF, 0xDE, 0xAD] expected.extend([0x00] * 234) @@ -500,6 +498,200 @@ def test_calculate_file_length_mutiple_granules_multiple_sectors_correct(self): result = DiskFile.calculate_file_length(0, [0x01, 0x02, 0xC2], 256) self.assertEqual(4608 + 512, result) + +class TestMLPreamble(unittest.TestCase): + """ + A test class for the MLPreamble class. + """ + def setUp(self): + """ + Common setup routines needed for all unit tests. + """ + + def test_ml_preamble_true_is_ml(self): + preamble = MLPreamble() + self.assertTrue(preamble.is_ml()) + + def test_read_ml_preamble_raises_on_empty_buffer(self): + preamble = MLPreamble() + with self.assertRaises(VirtualFileValidationError): + preamble.read([], 0) + + def test_read_ml_preamble_raises_on_bad_preamble(self): + preamble = MLPreamble() + with self.assertRaises(VirtualFileValidationError): + preamble.read([0xDE, 0xAD, 0xBE, 0xEF, 0x00], 0) + + def test_read_ml_preamble_raises_when_buffer_too_small(self): + preamble = MLPreamble() + with self.assertRaises(VirtualFileValidationError): + preamble.read([0xDE, 0xAD], 0) + + def test_read_ml_preamble_works_correct(self): + preamble = MLPreamble() + pointer = preamble.read([0x00, 0xDE, 0xAD, 0xBE, 0xEF], 0) + self.assertEqual("DEAD", preamble.data_length.hex()) + self.assertEqual("BEEF", preamble.load_addr.hex()) + self.assertEqual(5, pointer) + + def test_write_ml_preamble_fails_when_not_enough_space(self): + preamble = MLPreamble() + buffer = [1, 2, 3] + with self.assertRaises(VirtualFileValidationError): + preamble.write(buffer, 0) + + def test_write_ml_preamble_works_correctly(self): + preamble = MLPreamble() + preamble.data_length = NumericValue(0xDEAD) + preamble.load_addr = NumericValue(0xBEEF) + buffer = [0x00] * 5 + preamble.write(buffer, 0) + self.assertEqual([0x00, 0xDE, 0xAD, 0xBE, 0xEF], buffer) + + def test_write_ml_preamble_get_data_length_works_correctly(self): + preamble = MLPreamble() + preamble.data_length = NumericValue(0xDEAD) + preamble.load_addr = NumericValue(0xBEEF) + buffer = [0x00] * 5 + preamble.write(buffer, 0) + self.assertEqual(0xDEAD, preamble.get_data_length()) + + +class TestBasicPreamble(unittest.TestCase): + """ + A test class for the BasicPreamble class. + """ + def setUp(self): + """ + Common setup routines needed for all unit tests. + """ + + def test_basic_preamble_true_is_ml(self): + preamble = BasicPreamble() + self.assertFalse(preamble.is_ml()) + + def test_read_basic_preamble_raises_on_empty_buffer(self): + preamble = BasicPreamble() + with self.assertRaises(VirtualFileValidationError): + preamble.read([], 0) + + def test_read_basic_preamble_raises_on_bad_preamble(self): + preamble = BasicPreamble() + with self.assertRaises(VirtualFileValidationError): + preamble.read([0xDE, 0xAD, 0xBE, 0xEF, 0x00], 0) + + def test_read_basic_preamble_raises_when_buffer_too_small(self): + preamble = BasicPreamble() + with self.assertRaises(VirtualFileValidationError): + preamble.read([0xDE, 0xAD], 0) + + def test_read_basic_preamble_works_correct(self): + preamble = BasicPreamble() + pointer = preamble.read([0xFF, 0xDE, 0xAD], 0) + self.assertEqual("DEAD", preamble.data_length.hex()) + self.assertEqual(3, pointer) + + def test_write_basic_preamble_fails_when_not_enough_space(self): + preamble = BasicPreamble() + buffer = [1, 2] + with self.assertRaises(VirtualFileValidationError): + preamble.write(buffer, 0) + + def test_write_basic_preamble_works_correctly(self): + preamble = BasicPreamble() + preamble.data_length = NumericValue(0xDEAD) + buffer = [0x00] * 3 + preamble.write(buffer, 0) + self.assertEqual([0xFF, 0xDE, 0xAD], buffer) + + def test_write_basic_preamble_get_data_length_works_correctly(self): + preamble = BasicPreamble() + preamble.data_length = NumericValue(0xDEAD) + buffer = [0x00] * 3 + preamble.write(buffer, 0) + self.assertEqual(0xDEAD, preamble.get_data_length()) + + +class TestASCIIPreamble(unittest.TestCase): + """ + A test class for the BasicPreamble class. + """ + def setUp(self): + """ + Common setup routines needed for all unit tests. + """ + + def test_ascii_preamble_true_is_ml(self): + preamble = ASCIIPreamble() + self.assertFalse(preamble.is_ml()) + + def test_read_ascii_preamble_works_correct(self): + preamble = ASCIIPreamble() + pointer = preamble.read([0xFF, 0xDE, 0xAD], 0) + self.assertEqual(0, preamble.data_length.int) + self.assertEqual(0, pointer) + + def test_write_ascii_preamble_works_correctly(self): + preamble = ASCIIPreamble() + buffer = [0x00] * 3 + preamble.write(buffer, 0) + self.assertEqual([0x00, 0x00, 0x00], buffer) + + +class TestPostamble(unittest.TestCase): + """ + A test class for the BasicPreamble class. + """ + def setUp(self): + """ + Common setup routines needed for all unit tests. + """ + + def test_read_raises_on_first_invalid_byte(self): + postamble = Postamble() + with self.assertRaises(VirtualFileValidationError): + postamble.read([0xAA, 0x00, 0x00, 0xDE, 0xAD], 0) + + def test_read_raises_on_second_invalid_byte(self): + postamble = Postamble() + with self.assertRaises(VirtualFileValidationError): + postamble.read([0xFF, 0xAA, 0x00, 0xDE, 0xAD], 0) + + def test_read_raises_on_third_invalid_byte(self): + postamble = Postamble() + with self.assertRaises(VirtualFileValidationError): + postamble.read([0xFF, 0x00, 0xAA, 0xDE, 0xAD], 0) + + def test_read_returns_correct_pointer(self): + postamble = Postamble() + result = postamble.read([0xFF, 0x00, 0x00, 0xDE, 0xAD], 0) + self.assertEqual(5, result) + self.assertEqual(0xDEAD, postamble.exec_addr.int) + + def test_write_postamble_fails_when_not_enough_space(self): + postamble = Postamble() + buffer = [1, 2] + with self.assertRaises(VirtualFileValidationError): + postamble.write(buffer, 0) + + def test_write_postamble_works_correctly(self): + postamble = Postamble() + postamble.exec_addr = NumericValue(0xDEAD) + buffer = [0x00] * 5 + postamble.write(buffer, 0) + self.assertEqual([0xFF, 0x00, 0x00, 0xDE, 0xAD], buffer) + + def test_read_postamble_raises_on_empty_buffer(self): + postamble = Postamble() + with self.assertRaises(VirtualFileValidationError): + postamble.read([], 0) + + def test_read_postamble_correct(self): + postamble = Postamble() + postamble.read([0xFF, 0x00, 0x00, 0xDE, 0xAD], 0) + self.assertEqual("DEAD", postamble.exec_addr.hex()) + + # M A I N #####################################################################