def test_make_8dot3_name_unicode(): """Test that make_8dot3_filename generates valid 8dot3 filenames.""" fde = mock.MagicMock() fde.get_entries.return_value = ([], [], []) sfn = EightDotThree() lfn = sfn.make_8dot3_name("Ъци.Ъци", fde) assert "_._" == lfn assert sfn.is_8dot3_conform(lfn)
def test_make_8dot3_name_cut_ext(): """Test that make_8dot3_filename generates valid 8dot3 filenames.""" fde = mock.MagicMock() fde.get_entries.return_value = ([], [], []) sfn = EightDotThree() lfn = sfn.make_8dot3_name("This is a long filename.TeXT", fde) assert "THISIS.TEX" == lfn assert sfn.is_8dot3_conform(lfn)
def test_checksum_calculation_referenceimpl_random(): """Test that the checksum calculation works, following reference impl.""" base = ''.join(random.choices(string.ascii_letters + string.digits, k=8)).upper() ext = ''.join(random.choices(string.ascii_letters + string.digits, k=3)).upper() sfn = EightDotThree() sfn.set_str_name(f"{base}.{ext}") assert sfn.checksum() == calculate_checksum_referenceimpl(sfn.name)
def test_make_8dot3_name_collision(): """Test that make_8dot3_filename generates valid 8dot3 filenames.""" fde = mock.MagicMock() fde_sub = mock.MagicMock() fde_sub.get_short_name.side_effect = [ "THISIS.TXT", "THISIS~1.TXT", "THISIS~2.TXT" ] fde.get_entries.return_value = ([fde_sub, fde_sub], [fde_sub], []) sfn = EightDotThree() lfn = sfn.make_8dot3_name("This is a long filename.txt", fde) assert "THISIS~3.TXT" == lfn assert sfn.is_8dot3_conform(lfn)
def test_make_8dot3_name(): """Test that make_8dot3_filename generates valid 8dot3 filenames.""" fde = mock.MagicMock() fde.get_entries.return_value = ([], [], []) sfn = EightDotThree() n = sfn.make_8dot3_name("This is a long filename.txt", fde) sfn.set_str_name(n) assert "THISIS.TXT" == n assert sfn.is_8dot3_conform(sfn.get_unpadded_filename())
def parse_root_dir(self): """Parse root directory entry.""" root_dir_sfn = EightDotThree() root_dir_sfn.set_str_name("") dir_attr = FATDirectoryEntry.ATTR_DIRECTORY self.root_dir = FATDirectoryEntry(DIR_Name=root_dir_sfn, DIR_Attr=dir_attr, DIR_NTRes=0, DIR_CrtTimeTenth=0, DIR_CrtTime=0, DIR_CrtDate=0, DIR_LstAccessDate=0, DIR_FstClusHI=0, DIR_WrtTime=0, DIR_WrtDate=0, DIR_FstClusLO=0, DIR_FileSize=0, encoding=self.encoding) if self.fat_type in [self.FAT_TYPE_FAT12, self.FAT_TYPE_FAT16]: self._fat12_parse_root_dir() else: self._fat32_parse_root_dir()
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 makedir(self, path: str, permissions: Permissions = None, recreate: bool = False): """Create directory on filesystem. :param path: Path of new directory on filesystem :param permissions: Currently not implemented :param recreate: Ignore if directory already exists """ path = normpath(path) base = split(path)[0] dirname = split(path)[1] # Plausability checks try: self.opendir(base) except DirectoryExpected: raise ResourceNotFound(path) base = self._get_dir_entry(base) try: self._get_dir_entry(path) except ResourceNotFound: pass else: if not recreate: raise DirectoryExists(path) else: # TODO: Update mtime return SubFS(self, path) parent_is_root = base == self.fs.root_dir # Determine 8DOT3 file name + LFN short_name = EightDotThree() n = short_name.make_8dot3_name(dirname, base) short_name.set_str_name(n) dt = DosDateTime.now(tz=self.tz) newdir = FATDirectoryEntry(DIR_Name=short_name, DIR_Attr=FATDirectoryEntry.ATTR_DIRECTORY, DIR_NTRes=0, DIR_CrtTimeTenth=0, DIR_CrtTime=dt.serialize_time(), DIR_CrtDate=dt.serialize_date(), DIR_LstAccessDate=dt.serialize_date(), DIR_FstClusHI=0x00, DIR_WrtTime=dt.serialize_time(), DIR_WrtDate=dt.serialize_date(), DIR_FstClusLO=0x00, DIR_FileSize=0, encoding=self.fs.encoding) # Create LFN entry if required _sfn = short_name.get_unpadded_filename() if _sfn != dirname.upper() or (_sfn != dirname and self.preserve_case): lfn_entry = make_lfn_entry(dirname, short_name) newdir.set_lfn_entry(lfn_entry) # Create . and .. directory entries first_cluster = self.fs.allocate_bytes( FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE * 2, erase=True)[0] newdir.set_cluster(first_cluster) dot_sn = EightDotThree() dot_sn.set_byte_name(b". ") dot = FATDirectoryEntry(DIR_Name=dot_sn, DIR_Attr=FATDirectoryEntry.ATTR_DIRECTORY, DIR_NTRes=newdir.ntres, DIR_CrtTimeTenth=newdir.crttimetenth, DIR_CrtTime=newdir.crttime, DIR_CrtDate=newdir.crtdate, DIR_LstAccessDate=newdir.lstaccessdate, DIR_FstClusHI=newdir.fstclushi, DIR_WrtTime=newdir.wrttime, DIR_WrtDate=newdir.wrtdate, DIR_FstClusLO=newdir.fstcluslo, DIR_FileSize=newdir.filesize, encoding=self.fs.encoding) dotdot_sn = EightDotThree() dotdot_sn.set_byte_name(b".. ") base_fstclushi = base.fstclushi if not parent_is_root else 0x0 base_fstcluslo = base.fstcluslo if not parent_is_root else 0x0 dotdot = FATDirectoryEntry(DIR_Name=dotdot_sn, DIR_Attr=FATDirectoryEntry.ATTR_DIRECTORY, DIR_NTRes=base.ntres, DIR_CrtTimeTenth=base.crttimetenth, DIR_CrtTime=base.crttime, DIR_CrtDate=base.crtdate, DIR_LstAccessDate=base.lstaccessdate, DIR_FstClusHI=base_fstclushi, DIR_WrtTime=base.wrttime, DIR_WrtDate=base.wrtdate, DIR_FstClusLO=base_fstcluslo, DIR_FileSize=base.filesize, encoding=self.fs.encoding) newdir.add_subdirectory(dot) newdir.add_subdirectory(dotdot) # Write new directory contents self.fs.update_directory_entry(newdir) # Write parent directory base.add_subdirectory(newdir) self.fs.update_directory_entry(base) # Flush FAT(s) to disk self.fs.flush_fat() return SubFS(self, path)
def create(self, path: str, wipe: bool = False) -> bool: """Create a new file. :param path: Path of new file on filesystem :param wipe: Overwrite existing file contents """ basename = "/".join(path.split("/")[:-1]) dirname = path.split("/")[-1] # Plausability checks try: self.opendir(basename) except DirectoryExpected: raise ResourceNotFound(path) base = self._get_dir_entry(basename) try: dentry = self._get_dir_entry(path) except ResourceNotFound: pass else: if not wipe: return False else: # Clean up existing file contents # TODO: touch {a,m}time dentry.set_size(0) old_cluster = dentry.get_cluster() dentry.set_cluster(0) self.fs.free_cluster_chain(old_cluster) return True # Determine 8DOT3 file name + LFN short_name = EightDotThree() n = short_name.make_8dot3_name(dirname, base) short_name.set_str_name(n) dt = DosDateTime.now(tz=self.tz) newdir = FATDirectoryEntry(DIR_Name=short_name, DIR_Attr=0, DIR_NTRes=0, DIR_CrtTimeTenth=0, DIR_CrtTime=dt.serialize_time(), DIR_CrtDate=dt.serialize_date(), DIR_LstAccessDate=dt.serialize_date(), DIR_FstClusHI=0x00, DIR_WrtTime=dt.serialize_time(), DIR_WrtDate=dt.serialize_date(), DIR_FstClusLO=0x00, DIR_FileSize=0, encoding=self.fs.encoding) # Create LFN entry if required _sfn = short_name.get_unpadded_filename() if _sfn != dirname.upper() or (_sfn != dirname and self.preserve_case): lfn_entry = make_lfn_entry(dirname, short_name) newdir.set_lfn_entry(lfn_entry) # Write reference to parent directory base.add_subdirectory(newdir) self.fs.update_directory_entry(base) # Flush FAT(s) to disk self.fs.flush_fat() return True
def test_checksum_calculation_referenceimpl(): """Test that the checksum calculation works, following reference impl.""" sfn = EightDotThree() sfn.set_str_name("FILENAME.TXT") assert sfn.checksum() == calculate_checksum_referenceimpl(sfn.name)
def test_checksum_calculation_precalculated(): """Test that the checksum calculation works, with a precalculated value.""" sfn = EightDotThree() sfn.set_str_name("FILENAME.TXT") assert sfn.checksum() == 58
def test_bytename_set_not_a_directory_free_entry(): """Test that 0x00 is detect as the last entry of the directory cluster.""" sfn = EightDotThree() with pytest.raises(NotADirectoryError) as ex: sfn.set_byte_name(b'\xE5 ') assert ex.free_type == ex.FREE_ENTRY
def test_is_8dot3_conform_false(): """Test that non-8.3 file names are correctly detected.""" assert not EightDotThree.is_8dot3_conform("This is a Long file.txt")
def test_is_8dot3_conform_true(): """Test that 8.3 file names are correctly detected.""" assert EightDotThree.is_8dot3_conform("TESTFILE.TXT")
def test_bytename_set_invalid_length(): """Test that a byte name can only be set with the correct length.""" sfn = EightDotThree() for n in [b'', b'1234567890', b'123456789011']: with pytest.raises(ValueError): sfn.set_byte_name(n)
def parse_dir_entries_in_address( self, address: int = 0, max_address: int = 0, tmp_lfn_entry: FATLongDirectoryEntry = None): """Parse directory entries in address range.""" if tmp_lfn_entry is None: tmp_lfn_entry = FATLongDirectoryEntry() dir_hdr_size = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE if max_address == 0: max_address = FATDirectoryEntry.FAT_DIRECTORY_HEADER_SIZE dir_entries = [] for hdr_addr in range(address, max_address, dir_hdr_size): # Parse each entry dir_hdr = self.__parse_dir_entry(hdr_addr) dir_sn = EightDotThree(encoding=self.encoding) dir_first_byte = dir_hdr["DIR_Name"][0] try: dir_sn.set_byte_name(dir_hdr["DIR_Name"]) except NotAFatEntryException as ex: # Not a directory of any kind, invalidate temporary LFN entries tmp_lfn_entry = FATLongDirectoryEntry() if ex.free_type == ex.FREE_ENTRY: # Empty directory entry, continue elif ex.free_type == ex.LAST_ENTRY: # Last directory entry, do not parse any further break else: dir_hdr["DIR_Name"] = dir_sn # Long File Names if FATLongDirectoryEntry.is_lfn_entry(dir_first_byte, dir_hdr["DIR_Attr"]): self.parse_lfn_entry(tmp_lfn_entry, hdr_addr) continue # Normal directory entries if not tmp_lfn_entry.is_lfn_entry_complete(): # Ignore incomplete LFN entries altogether tmp_lfn_entry = None dir_entry = FATDirectoryEntry(encoding=self.encoding, lfn_entry=tmp_lfn_entry, **dir_hdr) dir_entries += [dir_entry] if dir_entry.is_directory() and not dir_entry.is_special(): # Iterate all subdirectories except for dot and dotdot cluster = dir_entry.get_cluster() subdirs = self.parse_dir_entries_in_cluster_chain(cluster) for d in subdirs: dir_entry.add_subdirectory(d) # Reset temporary LFN entry tmp_lfn_entry = FATLongDirectoryEntry() return dir_entries, tmp_lfn_entry