def read_blocks(file): """ Reads all of the data blocks that exist in the file. :param file: the file object to read from :return: an array of bytes read from the file """ data = [] while True: if not CassetteFile.seek_sequence(file, ["55", "3C"]): print("Data or EOF block not found") return [] block_type = Value.create_from_byte(file.read(1)) if block_type.hex() == "FF": file.read(3) return data elif block_type.hex() == "01": data_len = Value.create_from_byte(file.read(1)) for _ in range(data_len.int): data.append(Value.create_from_byte(file.read(1)).int) file.read(2) else: print("Unknown block type found: {}".format(block_type.hex())) return []
def seek_sequence(file, sequence): """ Reads the file until the specified sequence is found. Returns True if the sequence is found. The internal file byte pointer will point to the first byte beyond the sequence. If an EOF occurs, will return False. :param file: the file object to read from :param sequence: the sequence of values to search for :return: True if the sequence is found, False otherwise """ sequence_length = len(sequence) while True: last_values = [] try: for _ in range(sequence_length): last_values.append( Value.create_from_byte(file.read(1)).hex()) except ValueError: return False if last_values == sequence: return True file.seek(-(sequence_length - 1), 1)
def list_files(self, filenames=None): files = [] # Read the File Allocation Table self.host_file.seek(DiskFile.FAT_OFFSET, 0) fat = self.host_file.read(256) # Move through elements in the Directory Table and read them into CoCoFile objects self.host_file.seek(DiskFile.DIR_OFFSET, 0) for file_number in range(0, 72): next_byte = Value.create_from_byte(self.host_file.peek(1)[:1]) if next_byte.hex() == "00" or next_byte.hex() == "FF": self.host_file.seek(32, 1) else: name = "{}".format( self.host_file.read(8).decode("utf-8").replace(" ", "")) extension = "{}".format(self.host_file.read(3).decode("utf-8")) file_type = Value.create_from_byte(self.host_file.read(1)) data_type = Value.create_from_byte(self.host_file.read(1)) starting_granule = Value.create_from_byte( self.host_file.read(1)) current_location = self.host_file.tell() preamble = DiskFile.read_preamble(self.host_file, starting_granule.int) file_data = self.read_data( self.host_file, starting_granule.int, has_preamble=True, data_length=preamble.data_length.int, fat=fat, ) postamble = DiskFile.read_postamble(self.host_file) self.host_file.seek(current_location, 0) coco_file = CoCoFile(name=name, extension=extension, type=file_type, data_type=data_type, load_addr=preamble.load_addr, exec_addr=postamble.exec_addr, data=file_data, ignore_gaps=True) files.append(coco_file) self.host_file.seek(19, 1) return files
def read_preamble(cls, file, 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 file: the file object to modify :param starting_granule: the granule number that contains the preamble :return: a populated Preamble object """ DiskFile.seek_granule(file, starting_granule) preamble_flag = Value.create_from_byte(file.read(1)) if preamble_flag.hex() != "00": raise ValueError("Invalid preamble flag {}".format( preamble_flag.hex())) return Preamble(data_length=Value.create_from_byte(file.read(2)), load_addr=Value.create_from_byte(file.read(2)))
def read_postamble(cls, file): """ 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 file: the file object to modify :return: a populated Postamble object """ postamble_flag = Value.create_from_byte(file.read(1)) if postamble_flag.hex() != "FF": raise ValueError("Invalid first postamble flag {}".format( postamble_flag.hex())) postamble_flag = Value.create_from_byte(file.read(2)) if postamble_flag.hex() != "00": raise ValueError("Invalid second postamble flag {}".format( postamble_flag.hex())) return Postamble(exec_addr=Value.create_from_byte(file.read(2)), )
def read_leader(file): """ Reads a cassette leader. Should consist of 128 bytes of the value $55. Raises a ValueError if there is a problem. :param file: the file object to read from """ for _ in range(128): value = Value.create_from_byte(file.read(1)) if value.hex() != "55": raise ValueError("[{}] invalid leader byte".format( value.hex()))
def read_data(cls, file, starting_granule, has_preamble=False, data_length=0, fat=[]): """ Reads a collection of data from a disk image. :param file: the file object containing data to read from :param starting_granule: the starting granule for the file :param has_preamble: whether there is a preamble to be 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 """ DiskFile.seek_granule(file, starting_granule) file_data = [] chunk_size = DiskFile.HALF_TRACK_LEN # Skip over preamble if it exists if has_preamble: file.read(5) chunk_size -= 5 # Check to see if we are reading more than one granule if data_length > chunk_size: for _ in range(chunk_size): file_data.append(Value.create_from_byte(file.read(1)).int) data_length -= 1 next_granule = fat[starting_granule] file_data.extend( DiskFile.read_data(file, next_granule, data_length=data_length, fat=fat)) else: for _ in range(data_length): file_data.append(Value.create_from_byte(file.read(1)).int) return file_data
def read_file(file): """ Reads a cassette file, and returns a CoCoFile object with the information for the next file contained on the cassette file. :param file: the file object to read from :return: a CoCoFile with header information """ if not CassetteFile.seek_sequence(file, ["55", "3C", "00"]): return None # Skip length byte file.read(1) # Extract file meta-data name = file.read(8).decode("utf-8") file_type = Value.create_from_byte(file.read(1)) data_type = Value.create_from_byte(file.read(1)) gaps = Value.create_from_byte(file.read(1)) load_addr = Value.create_from_byte(file.read(2)) exec_addr = Value.create_from_byte(file.read(2)) # Advance two spaces to move past the header file.read(2) file.read(2) data = CassetteFile.read_blocks(file) if not data: print("Data blocks not found for file {}".format(name)) return None return CoCoFile(name=name, type=file_type, data_type=data_type, gaps=gaps, load_addr=load_addr, exec_addr=exec_addr, data=data)
def test_create_from_byte_works_correctly(self): result = Value.create_from_byte(b"\xDE\xAD") self.assertEqual(result.int, 0xDEAD)
def test_create_from_byte_raises_on_empty_byte(self): with self.assertRaises(ValueTypeError) as context: Value.create_from_byte(b"") self.assertEqual("No byte available for reading", str(context.exception))