def __verify_bpb_header(self): """Verify BPB header for correctness.""" if self.bpb_header["BS_jmpBoot"][0] == 0xEB: if self.bpb_header["BS_jmpBoot"][2] != 0x90: raise PyFATException("Boot code must end with 0x90") elif self.bpb_header["BS_jmpBoot"][0] == 0xE9: pass else: raise PyFATException("Boot code must start with 0xEB or " "0xE9. Is this a FAT partition?") #: 512,1024,2048,4096: As per fatgen103.doc byts_per_sec_range = [2**x for x in range(9, 13)] if self.bpb_header["BPB_BytsPerSec"] not in byts_per_sec_range: raise PyFATException(f"Expected one of {byts_per_sec_range} " f"bytes per sector, got: " f"\'{self.bpb_header['BPB_BytsPerSec']}\'.") #: 1,2,4,8,16,32,64,128: As per fatgen103.doc sec_per_clus_range = [2**x for x in range(8)] if self.bpb_header["BPB_SecPerClus"] not in sec_per_clus_range: raise PyFATException(f"Expected one of {sec_per_clus_range} " f"sectors per cluster, got: " f"\'{self.bpb_header['BPB_SecPerClus']}\'.") bytes_per_cluster = self.bpb_header["BPB_BytsPerSec"] bytes_per_cluster *= self.bpb_header["BPB_SecPerClus"] if bytes_per_cluster > 32768: warnings.warn( "Bytes per cluster should not be more than 32K, " "but got: {}K. Trying to continue " "anyway.".format(bytes_per_cluster // 1024), Warning) if self.bpb_header["BPB_RsvdSecCnt"] == 0: raise PyFATException("Number of reserved sectors must not be 0") if self.bpb_header["BPB_Media"] not in [ 0xf0, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff ]: raise PyFATException("Invalid media type") if self.bpb_header["BPB_NumFATS"] < 1: raise PyFATException("At least one FAT expected, None found.") root_entry_count = self.bpb_header["BPB_RootEntCnt"] * 32 root_entry_count %= self.bpb_header["BPB_BytsPerSec"] if self.bpb_header["BPB_RootEntCnt"] != 0 and root_entry_count != 0: raise PyFATException("Root entry count does not cleanly align with" " bytes per sector!") if self.bpb_header["BPB_TotSec16"] == 0 and \ self.bpb_header["BPB_TotSec32"] == 0: raise PyFATException("16-Bit and 32-Bit total sector count " "value empty.")
def open(self, filename: str, read_only: bool = False): """Open filesystem for usage with PyFat. :param filename: `str`: Name of file to open for usage with PyFat. :param read_only: `bool`: Force read-only mode of filesystem. """ self.is_read_only = read_only if read_only is True: mode = 'rb' else: mode = 'rb+' try: self.__set_fp(open(filename, mode=mode)) except OSError as ex: raise PyFATException(f"Cannot open given file \'{filename}\'.", errno=ex.errno) # Parse BPB & FAT headers of given file self.parse_header() # Parse FAT self._parse_fat() # Parse root directory # TODO: Inefficient to always recursively parse the root dir. # It would make sense to parse it on demand instead. self.parse_root_dir()
def _wrapper(*args, **kwargs): read_only = args[0].is_read_only if read_only is False: return func(*args, **kwargs) else: raise PyFATException("Filesystem has been opened read-only, not " "able to perform a write operation!")
def _wrapper(*args, **kwargs): initialised = args[0].initialised if initialised is True: return func(*args, **kwargs) else: raise PyFATException("Class has not yet been fully initialised, " "please instantiate first.")
def _add_parent(self, cls): """Add parent directory link to current directory entry. raises: PyFATException """ if self._parent is not None: raise PyFATException( "Trying to add multiple parents to current " "directory!", errno=errno.ETOOMANYREFS) if not isinstance(cls, FATDirectoryEntry): raise PyFATException( "Trying to add a non-FAT directory entry " "as parent directory!", errno=errno.EBADE) self._parent = cls
def get_parent_dir(self): """Get the parent directory entry.""" if self._parent is None: raise PyFATException( "Cannot query parent directory of " "root directory", errno=errno.ENOENT) return self._parent
def allocate_bytes(self, size: int, erase: bool = False) -> list: """Try to allocate a cluster (-chain) in FAT for `size` bytes. :param size: `int`: Size in bytes to try to allocate. :param erase: `bool`: If set to true, the newly allocated space is zeroed-out for clean allocation. :returns: List of newly-allocated clusters. """ free_clus = self.FAT_CLUSTER_VALUES[self.fat_type]["FREE_CLUSTER"] min_clus = self.FAT_CLUSTER_VALUES[self.fat_type]["MIN_DATA_CLUSTER"] max_clus = self.FAT_CLUSTER_VALUES[self.fat_type]["MAX_DATA_CLUSTER"] num_clusters = self.calc_num_clusters(size) # Fill list of found free clusters free_clusters = [] for i in range(self.first_free_cluster, len(self.fat)): if min_clus > i or i > max_clus: # Ignore out of bound entries continue if num_clusters == len(free_clusters): # Allocated enough clusters! break if self.fat[i] == free_clus: if i == self.FAT_CLUSTER_VALUES[self.fat_type]["BAD_CLUSTER"]: # Do not allocate a BAD_CLUSTER continue if self.fat_type == self.FAT_TYPE_FAT12 and \ i == self.FAT12_SPECIAL_EOC: # Do not allocate special EOC marker on FAT12 continue free_clusters += [i] else: free_space = len(free_clusters) * self.bytes_per_cluster raise PyFATException( f"Not enough free space to allocate " f"{size} bytes ({free_space} bytes free)", errno=errno.ENOSPC) self.first_free_cluster = i # Allocate cluster chain in FAT eoc_max = self.FAT_CLUSTER_VALUES[self.fat_type]["END_OF_CLUSTER_MAX"] for i, _ in enumerate(free_clusters): try: self.fat[free_clusters[i]] = free_clusters[i + 1] except IndexError: self.fat[free_clusters[i]] = eoc_max if erase is True: with self.__lock: self.__seek(self.get_data_cluster_address(i)) self.__fp.write(b'\0' * self.bytes_per_cluster) return free_clusters
def _verify_is_directory(self): """Verify that current entry is a directory. raises: PyFATException: If current entry is not a directory. """ if not self.is_directory(): raise PyFATException( "Cannot get entries of this entry, as " "it is not a directory.", errno=errno.ENOTDIR)
def _get_fat_size_count(self): """Get BPB_FATsz value.""" if self.bpb_header["BPB_FATSz16"] != 0: return self.bpb_header["BPB_FATSz16"] # Only possible with FAT32 self.__parse_fat32_header() try: return self.fat_header["BPB_FATSz32"] except KeyError: raise PyFATException("Invalid FAT size of 0 detected in header, " "cannot continue")
def write_data_to_cluster(self, data: bytes, cluster: int, extend_cluster: bool = True, erase: bool = False) -> None: """Write given data to cluster. Extends cluster chain if needed. :param data: `bytes`: Data to write to cluster :param cluster: `int`: Cluster to write data to. :param extend_cluster: `bool`: Automatically extend cluster chain if not enough space is available. :param erase: `bool`: Erase cluster contents before writing. This is useful when writing `FATDirectoryEntry` data. """ data_sz = len(data) cluster_sz = 0 last_cluster = None for c in self.get_cluster_chain(cluster): cluster_sz += self.bytes_per_cluster last_cluster = c if cluster_sz >= data_sz: break if data_sz > cluster_sz: if extend_cluster is False: raise PyFATException( "Cannot write data to cluster, " "not enough space available.", errno=errno.ENOSPC) new_chain = self.allocate_bytes(data_sz - cluster_sz, erase=erase)[0] self.fat[last_cluster] = new_chain # Fill rest of data with zeroes if erase is set to True if erase: new_sz = max(1, math.ceil(data_sz / self.bytes_per_cluster)) new_sz *= self.bytes_per_cluster data += b'\0' * (new_sz - data_sz) # Write actual data bytes_written = 0 for c in self.get_cluster_chain(cluster): b = self.get_data_cluster_address(c) t = bytes_written bytes_written += self.bytes_per_cluster self._write_data_to_address(data[t:bytes_written], b) if bytes_written >= len(data): break
def add_lfn_entry(self, LDIR_Ord, LDIR_Name1, LDIR_Attr, LDIR_Type, LDIR_Chksum, LDIR_Name2, LDIR_FstClusLO, LDIR_Name3): """Add LFN entry to this instances chain. :param LDIR_Ord: Ordinance of LFN entry :param LDIR_Name1: First name field of LFN entry :param LDIR_Attr: Attributes of LFN entry :param LDIR_Type: Type of LFN entry :param LDIR_Chksum: Checksum value of following 8dot3 entry :param LDIR_Name2: Second name field of LFN entry :param LDIR_FstClusLO: Cluster address of LFN entry. Always zero. :param LDIR_Name3: Third name field of LFN entry """ # Check if attribute matches if not self.is_lfn_entry(LDIR_Ord, LDIR_Attr): raise NotAnLFNEntryException("Given LFN entry is not a long " "file name entry or attribute " "not set correctly!") # Check if FstClusLO is 0, as required by the spec if LDIR_FstClusLO != 0: raise PyFATException( "Given LFN entry has an invalid first " "cluster ID, don't know what to do.", errno=errno.EFAULT) # Check if item with same index has already been added if LDIR_Ord in self.lfn_entries.keys(): raise PyFATException("Given LFN entry part with index \'{}\'" "has already been added to LFN " "entry list.".format(LDIR_Ord)) mapped_entries = dict( zip(self.FAT_LONG_DIRECTORY_VARS, (LDIR_Ord, LDIR_Name1, LDIR_Attr, LDIR_Type, LDIR_Chksum, LDIR_Name2, LDIR_FstClusLO, LDIR_Name3))) self.lfn_entries[LDIR_Ord] = mapped_entries
def get_cluster_chain(self, first_cluster): """Follow a cluster chain beginning with the first cluster address.""" cluster_vals = self.FAT_CLUSTER_VALUES[self.fat_type] min_data_cluster = cluster_vals["MIN_DATA_CLUSTER"] max_data_cluster = cluster_vals["MAX_DATA_CLUSTER"] eoc_min = cluster_vals["END_OF_CLUSTER_MIN"] eoc_max = cluster_vals["END_OF_CLUSTER_MAX"] i = first_cluster while i <= len(self.fat): if min_data_cluster <= self.fat[i] <= max_data_cluster: # Normal data cluster, follow chain yield i elif self.fat_type == self.FAT_TYPE_FAT12 and \ self.fat[i] == self.FAT12_SPECIAL_EOC: # Special EOC yield i return elif eoc_min <= self.fat[i] <= eoc_max: # End of cluster, end chain yield i return elif self.fat[i] == cluster_vals["BAD_CLUSTER"]: # Bad cluster, cannot follow chain, file broken! raise PyFATException("Bad cluster found in FAT cluster " "chain, cannot access file") elif self.fat[i] == cluster_vals["FREE_CLUSTER"]: # FREE_CLUSTER mark when following a chain is treated an error raise PyFATException("FREE_CLUSTER mark found in FAT cluster " "chain, cannot access file") else: raise PyFATException("Invalid or unknown FAT cluster " "entry found with value " "\'{}\'".format(hex(self.fat[i]))) i = self.fat[i]
def update_directory_entry(self, dir_entry: FATDirectoryEntry) -> None: """Update directory entry on disk. Special handling is required, since the root directory on FAT12/16 is on a fixed location on disk. :param dir_entry: `FATDirectoryEntry`: Directory to write to disk """ is_root_dir = False extend_cluster_chain = True if self.root_dir == dir_entry: if self.fat_type != self.FAT_TYPE_FAT32: # FAT12/16 doesn't have a root directory cluster, # which cannot be enhanced extend_cluster_chain = False is_root_dir = True # Gather all directory entries dir_entries = b'' d, f, s = dir_entry.get_entries() for d in list(itertools.chain(d, f, s)): dir_entries += d.byte_repr() # Write content if not is_root_dir or self.fat_type == self.FAT_TYPE_FAT32: # FAT32 and non-root dir entries can be handled normally self.write_data_to_cluster(dir_entries, dir_entry.get_cluster(), extend_cluster=extend_cluster_chain, erase=True) else: # FAT12/16 does not have a root directory cluster root_dir_addr = self.root_dir_sector * \ self.bpb_header["BPB_BytsPerSec"] root_dir_sz = self.root_dir_sectors * \ self.bpb_header["BPB_BytsPerSec"] if len(dir_entries) > root_dir_sz: raise PyFATException( "Cannot create directory, maximum number " "of root directory entries exhausted!", errno=errno.ENOSPC) # Overwrite empty space as well dir_entries += b'\0' * (root_dir_sz - len(dir_entries)) self._write_data_to_address(dir_entries, root_dir_addr)
def _search_entry(self, name: str): """Find given dir entry by walking current dir. :param name: Name of entry to search for :raises: PyFATException: If entry cannot be found :returns: FATDirectoryEntry: Found entry """ dirs, files, _ = self.get_entries() for entry in dirs + files: try: if entry.get_long_name() == name: return entry except NotAnLFNEntryException: pass if entry.get_short_name() == name: return entry raise PyFATException(f'Cannot find entry {name}', errno=errno.ENOENT)
def remove_dir_entry(self, name): """Remove given dir_entry from dir list. **NOTE:** This will also remove special entries such as ».«, »..« and volume labels! """ d, f, s = self.get_entries() # Iterate all entries for dir_entry in d + f + s: sn = dir_entry.get_short_name() try: ln = dir_entry.get_long_name() except NotAnLFNEntryException: ln = None if name in [sn, ln]: self.__dirs.remove(dir_entry) return raise PyFATException( f"Cannot remove '{name}', no such " f"file or directory!", errno=errno.ENOENT)
def __seek(self, address: int): """Seek to given address with offset.""" if self.__fp is None: raise PyFATException("Cannot seek without a file handle!", errno=errno.ENXIO) self.__fp.seek(address + self.__fp_offset)
def _parse_fat(self): """Parse information in FAT.""" # Read all FATs fat_size = self.bpb_header["BPB_BytsPerSec"] fat_size *= self._get_fat_size_count() # Seek FAT entries first_fat_bytes = self.bpb_header["BPB_RsvdSecCnt"] first_fat_bytes *= self.bpb_header["BPB_BytsPerSec"] fats = [] for i in range(self.bpb_header["BPB_NumFATS"]): with self.__lock: self.__seek(first_fat_bytes + (i * fat_size)) fats += [self.__fp.read(fat_size)] if len(fats) < 1: raise PyFATException("Invalid number of FATs configured, " "cannot continue") elif len(set(fats)) > 1: warnings.warn("One or more FATs differ, filesystem most " "likely corrupted. Using first FAT.") # Parse first FAT self.bytes_per_cluster = self.bpb_header["BPB_BytsPerSec"] * \ self.bpb_header["BPB_SecPerClus"] if len(fats[0]) != self.bpb_header["BPB_BytsPerSec"] * \ self._get_fat_size_count(): raise PyFATException("Invalid length of FAT") # FAT12: 12 bits (1.5 bytes) per FAT entry # FAT16: 16 bits (2 bytes) per FAT entry # FAT32: 32 bits (4 bytes) per FAT entry fat_entry_size = self.fat_type / 8 total_entries = int(fat_size // fat_entry_size) self.fat = [None] * total_entries curr = 0 cluster = 0 incr = self.fat_type / 8 while curr < fat_size: offset = curr + incr if self.fat_type == self.FAT_TYPE_FAT12: fat_nibble = fats[0][int(curr):math.ceil(offset)] fat_nibble = fat_nibble.ljust(2, b"\0") try: self.fat[cluster] = struct.unpack("<H", fat_nibble)[0] except IndexError: # Out of bounds, FAT size is not cleanly divisible by 3 # Do not touch last clusters break if cluster % 2 == 0: # Even: Keep low 12-bits of word self.fat[cluster] &= 0x0FFF else: # Odd: Keep high 12-bits of word self.fat[cluster] >>= 4 if math.ceil(offset) == (fat_size - 1): # Sector boundary case for FAT12 del self.fat[-1] break elif self.fat_type == self.FAT_TYPE_FAT16: self.fat[cluster] = struct.unpack( "<H", fats[0][int(curr):int(offset)])[0] elif self.fat_type == self.FAT_TYPE_FAT32: self.fat[cluster] = struct.unpack( "<L", fats[0][int(curr):int(offset)])[0] # Ignore first four bits, FAT32 clusters are # actually just 28bits long self.fat[cluster] &= 0x0FFFFFFF else: raise PyFATException("Unknown FAT type, cannot continue") curr += incr cluster += 1 if None in self.fat: raise AssertionError("Unknown error during FAT parsing, please " "report this error.")
def parse_header(self): """Parse BPB & FAT headers in opened file.""" with self.__lock: self.__seek(0) boot_sector = self.__fp.read(512) header = struct.unpack(self.bpb_header_layout, boot_sector[:36]) self.bpb_header = dict(zip(self.bpb_header_vars, header)) # Verify BPB headers self.__verify_bpb_header() # Calculate root directory sectors and starting point of root directory root_entries = self.bpb_header["BPB_RootEntCnt"] hdr_size = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE bytes_per_sec = self.bpb_header["BPB_BytsPerSec"] rsvd_secs = self.bpb_header["BPB_RsvdSecCnt"] num_fats = self.bpb_header["BPB_NumFATS"] self.root_dir_sectors = ((root_entries * hdr_size) + (bytes_per_sec - 1)) // bytes_per_sec self.root_dir_sector = rsvd_secs + (self._get_fat_size_count() * num_fats) # Calculate first data sector self.first_data_sector = rsvd_secs + (num_fats * self._get_fat_size_count()) + \ self.root_dir_sectors # Determine FAT type self.fat_type = self.__determine_fat_type() # Parse FAT type specific header if self.fat_type in [self.FAT_TYPE_FAT12, self.FAT_TYPE_FAT16]: self.__parse_fat12_header() elif self.fat_type == self.FAT_TYPE_FAT32: # FAT32, probably - probe for it if self.bpb_header["BPB_FATSz16"] != 0: raise PyFATException( f"Invalid BPB_FATSz16 value of " f"'{self.bpb_header['BPB_FATSz16']}', " f"filesystem corrupt?", errno=errno.EINVAL) self.__parse_fat32_header() else: raise PyFATException( "Unknown FAT filesystem type, " "filesystem corrupt?", errno=errno.EINVAL) # Check signature with self.__lock: self.__seek(510) signature = struct.unpack("<H", self.__fp.read(2))[0] if signature != 0xAA55: raise PyFATException(f"Invalid signature: \'{hex(signature)}\'.") # Initialisation finished self.initialised = True
def make_lfn_entry(dir_name: str, short_name): """Generate a `FATLongDirectoryEntry` instance from directory name. :param dir_name: Long name of directory :param short_name: `EightDotThree` class instance :raises: `PyFATException` if entry name does not require an LFN entry or the name exceeds the FAT limitation of 255 characters """ lfn_entry = FATLongDirectoryEntry() #: Length in bytes of an LFN entry lfn_entry_length = 26 dir_name_str = dir_name dir_name = dir_name.encode(FAT_LFN_ENCODING) dir_name_modulus = len(dir_name) % lfn_entry_length if EightDotThree.is_8dot3_conform(dir_name_str): raise PyFATException( "Directory entry is already 8.3 conform, " "no need to create an LFN entry.", errno=errno.EINVAL) if len(dir_name) > 255: raise PyFATException( "Long file name exceeds 255 " "characters, not supported.", errno=errno.ENAMETOOLONG) checksum = short_name.checksum() if dir_name_modulus != 0: # Null-terminate string if required dir_name += '\0'.encode(FAT_LFN_ENCODING) # Fill the rest with 0xFF if it doesn't fit evenly new_sz = lfn_entry_length - len(dir_name) new_sz %= lfn_entry_length new_sz += len(dir_name) dir_name += b'\xFF' * (new_sz - len(dir_name)) # Generate linked LFN entries lfn_entries = len(dir_name) // lfn_entry_length for i in range(lfn_entries): if i == lfn_entries - 1: lfn_entry_ord = 0x40 | i + 1 else: lfn_entry_ord = i + 1 n = i * lfn_entry_length dirname1 = dir_name[n:n + 10] n += 10 dirname2 = dir_name[n:n + 12] n += 12 dirname3 = dir_name[n:n + 4] lfn_entry.add_lfn_entry(LDIR_Ord=lfn_entry_ord, LDIR_Name1=dirname1, LDIR_Attr=FATDirectoryEntry.ATTR_LONG_NAME, LDIR_Type=0x00, LDIR_Chksum=checksum, LDIR_Name2=dirname2, LDIR_FstClusLO=0, LDIR_Name3=dirname3) return lfn_entry
def __set_fp(self, fp): if isinstance(self.__fp, BufferedReader): raise PyFATException("Cannot overwrite existing file handle, " "create new class instance of PyFAT.") self.__fp = fp