Example #1
0
    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.")
Example #2
0
    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()
Example #3
0
    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!")
Example #4
0
    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.")
Example #5
0
    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
Example #6
0
    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
Example #7
0
    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
Example #8
0
    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)
Example #9
0
    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")
Example #10
0
    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
Example #11
0
    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
Example #12
0
    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]
Example #13
0
    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)
Example #14
0
    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)
Example #15
0
    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)
Example #16
0
 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)
Example #17
0
    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.")
Example #18
0
    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
Example #19
0
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
Example #20
0
 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