diff --git a/components/fatfs/fatfsgen.py b/components/fatfs/fatfsgen.py index 40b029229f..38fd973eff 100755 --- a/components/fatfs/fatfsgen.py +++ b/components/fatfs/fatfsgen.py @@ -42,37 +42,36 @@ class FATFS: assert (root_entry_count * BYTES_PER_DIRECTORY_ENTRY) % sector_size == 0 assert ((root_entry_count * BYTES_PER_DIRECTORY_ENTRY) // sector_size) % 2 == 0 - root_dir_sectors_cnt = (root_entry_count * BYTES_PER_DIRECTORY_ENTRY) // sector_size + root_dir_sectors_cnt: int = (root_entry_count * BYTES_PER_DIRECTORY_ENTRY) // sector_size - self.state = FATFSState(entry_size=entry_size, - sector_size=sector_size, - explicit_fat_type=explicit_fat_type, - reserved_sectors_cnt=reserved_sectors_cnt, - root_dir_sectors_cnt=root_dir_sectors_cnt, - size=size, - file_sys_type=file_sys_type, - num_heads=num_heads, - fat_tables_cnt=fat_tables_cnt, - sectors_per_fat=sectors_per_fat, - sectors_per_cluster=sectors_per_cluster, - media_type=media_type, - hidden_sectors=hidden_sectors, - sec_per_track=sec_per_track, - long_names_enabled=long_names_enabled, - volume_label=volume_label, - oem_name=oem_name) - binary_image = bytearray( + self.state: FATFSState = FATFSState(entry_size=entry_size, + sector_size=sector_size, + explicit_fat_type=explicit_fat_type, + reserved_sectors_cnt=reserved_sectors_cnt, + root_dir_sectors_cnt=root_dir_sectors_cnt, + size=size, + file_sys_type=file_sys_type, + num_heads=num_heads, + fat_tables_cnt=fat_tables_cnt, + sectors_per_fat=sectors_per_fat, + sectors_per_cluster=sectors_per_cluster, + media_type=media_type, + hidden_sectors=hidden_sectors, + sec_per_track=sec_per_track, + long_names_enabled=long_names_enabled, + volume_label=volume_label, + oem_name=oem_name) + binary_image: bytes = bytearray( read_filesystem(binary_image_path) if binary_image_path else self.create_empty_fatfs()) self.state.binary_image = binary_image - self.fat = FAT(fatfs_state=self.state, - reserved_sectors_cnt=self.state.reserved_sectors_cnt) + self.fat: FAT = FAT(fatfs_state=self.state, reserved_sectors_cnt=self.state.reserved_sectors_cnt) - self.root_directory = Directory(name='A', # the name is not important, must be string - size=self.state.root_dir_sectors_cnt * self.state.sector_size, - fat=self.fat, - cluster=self.fat.clusters[1], - fatfs_state=self.state) + self.root_directory: Directory = Directory(name='A', # the name is not important, must be string + size=self.state.root_dir_sectors_cnt * self.state.sector_size, + fat=self.fat, + cluster=self.fat.clusters[1], + fatfs_state=self.state) self.root_directory.init_directory() def create_file(self, name: str, extension: str = '', path_from_root: Optional[List[str]] = None) -> None: @@ -171,7 +170,8 @@ def main() -> None: sectors_per_cluster=args.sectors_per_cluster, size=args.partition_size, root_entry_count=args.root_entry_count, - explicit_fat_type=args.fat_type) + explicit_fat_type=args.fat_type, + long_names_enabled=args.long_name_support) fatfs.generate(args.input_directory) fatfs.write_filesystem(args.output_file) diff --git a/components/fatfs/fatfsgen_utils/cluster.py b/components/fatfs/fatfsgen_utils/cluster.py index 0f6aa82122..6121fde924 100644 --- a/components/fatfs/fatfsgen_utils/cluster.py +++ b/components/fatfs/fatfsgen_utils/cluster.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 -from typing import Optional +from typing import Dict, Optional from construct import Int16ul @@ -14,20 +14,20 @@ class Cluster: """ class Cluster handles values in FAT table and allocates sectors in data region. """ - RESERVED_BLOCK_ID = 0 - ROOT_BLOCK_ID = 1 - ALLOCATED_BLOCK_FAT12 = 0xFFF - ALLOCATED_BLOCK_FAT16 = 0xFFFF + RESERVED_BLOCK_ID: int = 0 + ROOT_BLOCK_ID: int = 1 + ALLOCATED_BLOCK_FAT12: int = 0xFFF + ALLOCATED_BLOCK_FAT16: int = 0xFFFF ALLOCATED_BLOCK_SWITCH = {FAT12: ALLOCATED_BLOCK_FAT12, FAT16: ALLOCATED_BLOCK_FAT16} - INITIAL_BLOCK_SWITCH = {FAT12: 0xFF8, FAT16: 0xFFF8} + INITIAL_BLOCK_SWITCH: Dict[int, int] = {FAT12: 0xFF8, FAT16: 0xFFF8} def __init__(self, cluster_id: int, fatfs_state: FATFSState, is_empty: bool = True) -> None: - self.id = cluster_id - self.fatfs_state = fatfs_state + self.id: int = cluster_id + self.fatfs_state: FATFSState = fatfs_state self._next_cluster = None # type: Optional[Cluster] if self.id == Cluster.RESERVED_BLOCK_ID: @@ -35,7 +35,7 @@ class Cluster: self.set_in_fat(self.INITIAL_BLOCK_SWITCH[self.fatfs_state.fatfs_type]) return - self.cluster_data_address = self._compute_cluster_data_address() + self.cluster_data_address: int = self._compute_cluster_data_address() self.is_empty = is_empty assert self.cluster_data_address or self.is_empty diff --git a/components/fatfs/fatfsgen_utils/entry.py b/components/fatfs/fatfsgen_utils/entry.py index 7436030ba2..df71f38a42 100644 --- a/components/fatfs/fatfsgen_utils/entry.py +++ b/components/fatfs/fatfsgen_utils/entry.py @@ -1,71 +1,105 @@ -# SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Optional +from typing import Any, List, Optional from construct import Const, Int8ul, Int16ul, Int32ul, PaddedString, Struct from .exceptions import LowerCaseException, TooLongNameException from .fatfs_state import FATFSState -from .utils import is_valid_fatfs_name, pad_string +from .utils import MAX_EXT_SIZE, MAX_NAME_SIZE, is_valid_fatfs_name, pad_string class Entry: """ The class Entry represents entry of the directory. """ - ATTR_READ_ONLY = 0x01 - ATTR_HIDDEN = 0x02 - ATTR_SYSTEM = 0x04 - ATTR_VOLUME_ID = 0x08 - ATTR_DIRECTORY = 0x10 - ATTR_ARCHIVE = 0x20 - MAX_NAME_SIZE_S = 8 - MAX_EXT_SIZE_S = 3 + ATTR_READ_ONLY: int = 0x01 + ATTR_HIDDEN: int = 0x02 + ATTR_SYSTEM: int = 0x04 + ATTR_VOLUME_ID: int = 0x08 + ATTR_DIRECTORY: int = 0x10 + ATTR_ARCHIVE: int = 0x20 + ATTR_LONG_NAME: int = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID + + # indexes in the entry structure and sizes in bytes, not in characters (encoded using 2 bytes for lfn) + LDIR_Name1_IDX: int = 1 + LDIR_Name1_SIZE: int = 5 + LDIR_Name2_IDX: int = 14 + LDIR_Name2_SIZE: int = 6 + LDIR_Name3_IDX: int = 28 + LDIR_Name3_SIZE: int = 2 + + # one entry can hold 13 characters with size 2 bytes distributed in three regions of the 32 bytes entry + CHARS_PER_ENTRY: int = LDIR_Name1_SIZE + LDIR_Name2_SIZE + LDIR_Name3_SIZE + + SHORT_ENTRY: int = -1 + SHORT_ENTRY_LN: int = 0 ENTRY_FORMAT_SHORT_NAME = Struct( - 'DIR_Name' / PaddedString(MAX_NAME_SIZE_S, 'utf-8'), - 'DIR_Name_ext' / PaddedString(MAX_EXT_SIZE_S, 'utf-8'), + 'DIR_Name' / PaddedString(MAX_NAME_SIZE, 'utf-8'), + 'DIR_Name_ext' / PaddedString(MAX_EXT_SIZE, 'utf-8'), 'DIR_Attr' / Int8ul, 'DIR_NTRes' / Const(b'\x00'), 'DIR_CrtTimeTenth' / Const(b'\x00'), - 'DIR_CrtTime' / Const(b'\x01\x00'), + 'DIR_CrtTime' / Const(b'\x00\x00'), 'DIR_CrtDate' / Const(b'\x21\x00'), 'DIR_LstAccDate' / Const(b'\x00\x00'), 'DIR_FstClusHI' / Const(b'\x00\x00'), - 'DIR_WrtTime' / Const(b'\x01\x00'), - 'DIR_WrtDate' / Const(b'\x01\x00'), + 'DIR_WrtTime' / Const(b'\x00\x00'), + 'DIR_WrtDate' / Const(b'\x21\x00'), 'DIR_FstClusLO' / Int16ul, 'DIR_FileSize' / Int32ul, ) - # IDF-4044 - ENTRY_FORMAT_LONG_NAME = Struct() - def __init__(self, entry_id: int, parent_dir_entries_address: int, fatfs_state: FATFSState) -> None: - self.fatfs_state = fatfs_state - self.id = entry_id - self.entry_address = parent_dir_entries_address + self.id * self.fatfs_state.entry_size - self._is_alias = False - self._is_empty = True + self.fatfs_state: FATFSState = fatfs_state + self.id: int = entry_id + self.entry_address: int = parent_dir_entries_address + self.id * self.fatfs_state.entry_size + self._is_alias: bool = False + self._is_empty: bool = True @property def is_empty(self) -> bool: return self._is_empty - def _parse_entry(self, entry_bytearray: Optional[bytearray]) -> dict: - if self.fatfs_state.long_names_enabled: - return Entry.ENTRY_FORMAT_LONG_NAME.parse(entry_bytearray) # type: ignore + @staticmethod + def _parse_entry(entry_bytearray: Optional[bytearray]) -> dict: return Entry.ENTRY_FORMAT_SHORT_NAME.parse(entry_bytearray) # type: ignore - def _build_entry(self, **kwargs) -> Any: # type: ignore - if self.fatfs_state.long_names_enabled: - return Entry.ENTRY_FORMAT_LONG_NAME.build(dict(**kwargs)) + @staticmethod + def _build_entry(**kwargs) -> Any: # type: ignore return Entry.ENTRY_FORMAT_SHORT_NAME.build(dict(**kwargs)) + @staticmethod + def _build_entry_long(names: List[bytes], checksum: int, order: int, is_last: bool, entity_type: int) -> bytes: + """ + Long entry starts with 1 bytes of the order, if the entry is the last in the chain it is or-masked with 0x40, + otherwise is without change (or masked with 0x00). The following example shows 3 entries: + first two (0x2000-0x2040) are long in the reverse order and the last one (0x2040-0x2060) is short. + The entries define file name "thisisverylongfilenama.txt". + + 00002000: 42 67 00 66 00 69 00 6C 00 65 00 0F 00 43 6E 00 Bg.f.i.l.e...Cn. + 00002010: 61 00 6D 00 61 00 2E 00 74 00 00 00 78 00 74 00 a.m.a...t...x.t. + 00002020: 01 74 00 68 00 69 00 73 00 69 00 0F 00 43 73 00 .t.h.i.s.i...Cs. + 00002030: 76 00 65 00 72 00 79 00 6C 00 00 00 6F 00 6E 00 v.e.r.y.l...o.n. + 00002040: 54 48 49 53 49 53 7E 31 54 58 54 20 00 00 00 00 THISIS~1TXT..... + 00002050: 21 00 00 00 00 00 00 00 21 00 02 00 15 00 00 00 !.......!....... + """ + order |= (0x40 if is_last else 0x00) + long_entry: bytes = (Int8ul.build(order) + # order of the long name entry (possibly masked with 0x40) + names[0] + # first 5 characters (10 bytes) of the name part + Int8ul.build(entity_type) + # one byte entity type ATTR_LONG_NAME + Int8ul.build(0) + # one byte of zeros + Int8ul.build(checksum) + # lfn_checksum defined in utils.py + names[1] + # next 6 characters (12 bytes) of the name part + Int16ul.build(0) + # 2 bytes of zeros + names[2]) # last 2 characters (4 bytes) of the name part + return long_entry + @property def entry_bytes(self) -> Any: return self.fatfs_state.binary_image[self.entry_address: self.entry_address + self.fatfs_state.entry_size] @@ -75,51 +109,70 @@ class Entry: self.fatfs_state.binary_image[self.entry_address: self.entry_address + self.fatfs_state.entry_size] = value def _clean_entry(self) -> None: - self.entry_bytes = self.fatfs_state.entry_size * b'\x00' + self.entry_bytes: bytes = self.fatfs_state.entry_size * b'\x00' def allocate_entry(self, first_cluster_id: int, entity_name: str, entity_type: int, entity_extension: str = '', - size: int = 0) -> None: + size: int = 0, + lfn_order: int = SHORT_ENTRY, + lfn_names: Optional[List[bytes]] = None, + lfn_checksum_: int = 0, + lfn_is_last: bool = False) -> None: """ :param first_cluster_id: id of the first data cluster for given entry :param entity_name: name recorded in the entry :param entity_extension: extension recorded in the entry :param size: size of the content of the file :param entity_type: type of the entity (file [0x20] or directory [0x10]) + :param lfn_order: if long names support is enabled, defines order in long names entries sequence (-1 for short) + :param lfn_names: if the entry is dedicated for long names the lfn_names contains + LDIR_Name1, LDIR_Name2 and LDIR_Name3 in this order + :param lfn_checksum_: use only for long file names, checksum calculated lfn_checksum function + :param lfn_is_last: determines if the long file name entry is holds last part of the name, + thus its address is first in the physical order :returns: None :raises LowerCaseException: In case when long_names_enabled is set to False and filename exceeds 8 chars for name or 3 chars for extension the exception is raised """ - if not ((is_valid_fatfs_name(entity_name) and - is_valid_fatfs_name(entity_extension)) or - self.fatfs_state.long_names_enabled): - raise LowerCaseException('Lower case is not supported because long name support is not enabled!') + valid_full_name: bool = is_valid_fatfs_name(entity_name) and is_valid_fatfs_name(entity_extension) + if not (valid_full_name or lfn_order >= 0): + raise LowerCaseException('Lower case is not supported in short name entry, use upper case.') # clean entry before allocation self._clean_entry() self._is_empty = False - object_name = entity_name.upper() - object_extension = entity_extension.upper() - # implementation of long names support will be part of IDF-4044 - exceeds_short_name = len(object_name) > Entry.MAX_NAME_SIZE_S or len(object_extension) > Entry.MAX_EXT_SIZE_S + object_name = entity_name.upper() if not self.fatfs_state.long_names_enabled else entity_name + object_extension = entity_extension.upper() if not self.fatfs_state.long_names_enabled else entity_extension + + exceeds_short_name: bool = len(object_name) > MAX_NAME_SIZE or len(object_extension) > MAX_EXT_SIZE if not self.fatfs_state.long_names_enabled and exceeds_short_name: raise TooLongNameException( - 'Maximal length of the object name is 8 characters and 3 characters for extension!') + 'Maximal length of the object name is {} characters and {} characters for extension!'.format( + MAX_NAME_SIZE, MAX_EXT_SIZE + )) start_address = self.entry_address end_address = start_address + self.fatfs_state.entry_size - self.fatfs_state.binary_image[start_address: end_address] = self._build_entry( - DIR_Name=pad_string(object_name, size=Entry.MAX_NAME_SIZE_S), - DIR_Name_ext=pad_string(object_extension, size=Entry.MAX_EXT_SIZE_S), - DIR_Attr=entity_type, - DIR_FstClusLO=first_cluster_id, - DIR_FileSize=size - ) + if lfn_order in (self.SHORT_ENTRY, self.SHORT_ENTRY_LN): + self.fatfs_state.binary_image[start_address: end_address] = self._build_entry( + DIR_Name=pad_string(object_name, size=MAX_NAME_SIZE), + DIR_Name_ext=pad_string(object_extension, size=MAX_EXT_SIZE), + DIR_Attr=entity_type, + DIR_FstClusLO=first_cluster_id, + DIR_FileSize=size + ) + else: + assert lfn_names is not None + self.fatfs_state.binary_image[start_address: end_address] = self._build_entry_long(lfn_names, + lfn_checksum_, + lfn_order, + lfn_is_last, + self.ATTR_LONG_NAME) def update_content_size(self, content_size: int) -> None: parsed_entry = self._parse_entry(self.entry_bytes) diff --git a/components/fatfs/fatfsgen_utils/fs_object.py b/components/fatfs/fatfsgen_utils/fs_object.py index 8f78a8a17c..8ce3f9274d 100644 --- a/components/fatfs/fatfsgen_utils/fs_object.py +++ b/components/fatfs/fatfsgen_utils/fs_object.py @@ -2,30 +2,34 @@ # SPDX-License-Identifier: Apache-2.0 import os -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from .entry import Entry from .exceptions import FatalError, WriteDirectoryException from .fat import FAT, Cluster from .fatfs_state import FATFSState -from .utils import required_clusters_count, split_content_into_sectors, split_to_name_and_extension +from .long_filename_utils import (build_lfn_full_name, build_lfn_unique_entry_name_order, + get_required_lfn_entries_count, split_name_to_lfn_entries, + split_name_to_lfn_entry_blocks) +from .utils import (MAX_EXT_SIZE, MAX_NAME_SIZE, build_lfn_short_entry_name, lfn_checksum, required_clusters_count, + split_content_into_sectors, split_to_name_and_extension) class File: """ The class File provides API to write into the files. It represents file in the FS. """ - ATTR_ARCHIVE = 0x20 - ENTITY_TYPE = ATTR_ARCHIVE + ATTR_ARCHIVE: int = 0x20 + ENTITY_TYPE: int = ATTR_ARCHIVE def __init__(self, name: str, fat: FAT, fatfs_state: FATFSState, entry: Entry, extension: str = '') -> None: - self.name = name - self.extension = extension - self.fatfs_state = fatfs_state - self.fat = fat - self.size = 0 - self._first_cluster = None - self._entry = entry + self.name: str = name + self.extension: str = extension + self.fatfs_state: FATFSState = fatfs_state + self.fat: FAT = fat + self.size: int = 0 + self._first_cluster: Optional[Cluster] = None + self._entry: Entry = entry @property def entry(self) -> Entry: @@ -51,7 +55,7 @@ class File: if current_cluster is None: raise FatalError('No free space left!') - address = current_cluster.cluster_data_address + address: int = current_cluster.cluster_data_address self.fatfs_state.binary_image[address: address + len(content_part)] = content_as_list current_cluster = current_cluster.next_cluster @@ -61,9 +65,12 @@ class Directory: The Directory class provides API to add files and directories into the directory and to find the file according to path and write it. """ - ATTR_DIRECTORY = 0x10 - ATTR_ARCHIVE = 0x20 - ENTITY_TYPE = ATTR_DIRECTORY + ATTR_DIRECTORY: int = 0x10 + ATTR_ARCHIVE: int = 0x20 + ENTITY_TYPE: int = ATTR_DIRECTORY + + CURRENT_DIRECTORY = '.' + PARENT_DIRECTORY = '..' def __init__(self, name, @@ -75,20 +82,20 @@ class Directory: extension='', parent=None): # type: (str, FAT, FATFSState, Optional[Entry], Cluster, Optional[int], str, Directory) -> None - self.name = name - self.fatfs_state = fatfs_state - self.extension = extension + self.name: str = name + self.fatfs_state: FATFSState = fatfs_state + self.extension: str = extension - self.fat = fat - self.size = size or self.fatfs_state.sector_size + self.fat: FAT = fat + self.size: int = size or self.fatfs_state.sector_size # if directory is root its parent is itself self.parent: Directory = parent or self - self._first_cluster = cluster + self._first_cluster: Cluster = cluster # entries will be initialized after the cluster allocation self.entries: List[Entry] = [] - self.entities = [] # type: ignore + self.entities: List[Union[File, Directory]] = [] # type: ignore self._entry = entry # currently not in use (will use later for e.g. modification time, etc.) @property @@ -106,7 +113,7 @@ class Directory: def name_equals(self, name: str, extension: str) -> bool: return self.name == name and self.extension == extension - def create_entries(self, cluster: Cluster) -> list: + def create_entries(self, cluster: Cluster) -> List[Entry]: return [Entry(entry_id=i, parent_dir_entries_address=cluster.cluster_data_address, fatfs_state=self.fatfs_state) @@ -114,19 +121,20 @@ class Directory: def init_directory(self) -> None: self.entries = self.create_entries(self._first_cluster) + + # the root directory doesn't contain link to itself nor the parent if not self.is_root: - # the root directory doesn't contain link to itself nor the parent - free_entry1 = self.find_free_entry() or self.chain_directory() - free_entry1.allocate_entry(first_cluster_id=self.first_cluster.id, - entity_name='.', - entity_extension='', - entity_type=self.ENTITY_TYPE) + self_dir_reference: Entry = self.find_free_entry() or self.chain_directory() + self_dir_reference.allocate_entry(first_cluster_id=self.first_cluster.id, + entity_name=self.CURRENT_DIRECTORY, + entity_extension='', + entity_type=self.ENTITY_TYPE) self.first_cluster = self._first_cluster - free_entry2 = self.find_free_entry() or self.chain_directory() - free_entry2.allocate_entry(first_cluster_id=self.parent.first_cluster.id, - entity_name='..', - entity_extension='', - entity_type=self.parent.ENTITY_TYPE) + parent_dir_reference: Entry = self.find_free_entry() or self.chain_directory() + parent_dir_reference.allocate_entry(first_cluster_id=self.parent.first_cluster.id, + entity_name=self.PARENT_DIRECTORY, + entity_extension='', + entity_type=self.parent.ENTITY_TYPE) self.parent.first_cluster = self.parent.first_cluster def lookup_entity(self, object_name: str, extension: str): # type: ignore @@ -151,21 +159,57 @@ class Directory: return None def _extend_directory(self) -> None: - current = self.first_cluster + current: Cluster = self.first_cluster while current.next_cluster is not None: current = current.next_cluster - new_cluster = self.fat.find_free_cluster() + new_cluster: Cluster = self.fat.find_free_cluster() current.set_in_fat(new_cluster.id) current.next_cluster = new_cluster self.entries += self.create_entries(new_cluster) def chain_directory(self) -> Entry: self._extend_directory() - free_entry = self.find_free_entry() + free_entry: Entry = self.find_free_entry() if free_entry is None: raise FatalError('No more space left!') return free_entry + @staticmethod + def allocate_long_name_object(free_entry, + name, + extension, + target_dir, + free_cluster, + entity_type): + # type: (Entry, str, str, Directory, Cluster, int) -> Tuple[Cluster, Entry, Directory] + lfn_full_name: str = build_lfn_full_name(name, extension) + lfn_unique_entry_order: int = build_lfn_unique_entry_name_order(target_dir.entities, name) + lfn_short_entry_name: str = build_lfn_short_entry_name(name, extension, lfn_unique_entry_order) + checksum: int = lfn_checksum(lfn_short_entry_name) + entries_count: int = get_required_lfn_entries_count(lfn_full_name) + + # entries in long file name entries chain starts with the last entry + split_names_reversed = reversed(list(enumerate(split_name_to_lfn_entries(lfn_full_name, entries_count)))) + for i, name_split_to_entry in split_names_reversed: + order: int = i + 1 + lfn_names: List[bytes] = list( + map(lambda x: x.lower(), split_name_to_lfn_entry_blocks(name_split_to_entry))) # type: ignore + free_entry.allocate_entry(first_cluster_id=free_cluster.id, + entity_name=name, + entity_extension=extension, + entity_type=entity_type, + lfn_order=order, + lfn_names=lfn_names, + lfn_checksum_=checksum, + lfn_is_last=order == entries_count) + free_entry = target_dir.find_free_entry() or target_dir.chain_directory() + free_entry.allocate_entry(first_cluster_id=free_cluster.id, + entity_name=lfn_short_entry_name[:MAX_NAME_SIZE], + entity_extension=lfn_short_entry_name[MAX_NAME_SIZE:], + entity_type=entity_type, + lfn_order=Entry.SHORT_ENTRY_LN) + return free_cluster, free_entry, target_dir + def allocate_object(self, name, entity_type, @@ -177,14 +221,23 @@ class Directory: and allocates cluster (both the record in FAT and cluster in the data region) and entry in the specified directory """ - free_cluster = self.fat.find_free_cluster() - target_dir = self if not path_from_root else self.recursive_search(path_from_root, self) - free_entry = target_dir.find_free_entry() or target_dir.chain_directory() - free_entry.allocate_entry(first_cluster_id=free_cluster.id, - entity_name=name, - entity_extension=extension, - entity_type=entity_type) - return free_cluster, free_entry, target_dir + free_cluster: Cluster = self.fat.find_free_cluster() + target_dir: Directory = self if not path_from_root else self.recursive_search(path_from_root, self) + free_entry: Entry = target_dir.find_free_entry() or target_dir.chain_directory() + + name_fits_short_struct: bool = len(name) <= MAX_NAME_SIZE and len(extension) <= MAX_EXT_SIZE + if not self.fatfs_state.long_names_enabled or name_fits_short_struct: + free_entry.allocate_entry(first_cluster_id=free_cluster.id, + entity_name=name, + entity_extension=extension, + entity_type=entity_type) + return free_cluster, free_entry, target_dir + return self.allocate_long_name_object(free_entry=free_entry, + name=name, + extension=extension, + target_dir=target_dir, + free_cluster=free_cluster, + entity_type=entity_type) def new_file(self, name: str, extension: str, path_from_root: Optional[List[str]]) -> None: free_cluster, free_entry, target_dir = self.allocate_object(name=name, @@ -192,7 +245,11 @@ class Directory: entity_type=Directory.ATTR_ARCHIVE, path_from_root=path_from_root) - file = File(name, fat=self.fat, extension=extension, fatfs_state=self.fatfs_state, entry=free_entry) + file: File = File(name=name, + fat=self.fat, + extension=extension, + fatfs_state=self.fatfs_state, + entry=free_entry) file.first_cluster = free_cluster target_dir.entities.append(file) @@ -202,7 +259,11 @@ class Directory: entity_type=Directory.ATTR_DIRECTORY, path_from_root=path_from_root) - directory = Directory(name=name, fat=self.fat, parent=parent, fatfs_state=self.fatfs_state, entry=free_entry) + directory: Directory = Directory(name=name, + fat=self.fat, + parent=parent, + fatfs_state=self.fatfs_state, + entry=free_entry) directory.first_cluster = free_cluster directory.init_directory() target_dir.entities.append(directory) @@ -216,9 +277,9 @@ class Directory: :returns: None :raises WriteDirectoryException: raised is the target object for writing is a directory """ - entity_to_write = self.recursive_search(path, self) + entity_to_write: Entry = self.recursive_search(path, self) if isinstance(entity_to_write, File): - clusters_cnt = required_clusters_count(cluster_size=self.fatfs_state.sector_size, content=content) + clusters_cnt: int = required_clusters_count(cluster_size=self.fatfs_state.sector_size, content=content) self.fat.allocate_chain(entity_to_write.first_cluster, clusters_cnt) entity_to_write.write(content) else: diff --git a/components/fatfs/fatfsgen_utils/long_filename_utils.py b/components/fatfs/fatfsgen_utils/long_filename_utils.py new file mode 100644 index 0000000000..0b2f782a4c --- /dev/null +++ b/components/fatfs/fatfsgen_utils/long_filename_utils.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +from typing import List + +from .entry import Entry +from .utils import convert_to_utf16_and_pad + +# File name with long filenames support can be as long as memory allows. It is split into entries +# holding 13 characters of the filename, thus the number of required entries is ceil(len(long_name) / 13). +# This is computed using `get_required_lfn_entries_count`. +# For creating long name entries we need to split the name by 13 characters using `split_name_to_lfn_entries` +# and in every entry into three blocks with sizes 5, 6 and 2 characters using `split_name_to_lfn_entry`. + + +def get_required_lfn_entries_count(lfn_full_name: str) -> int: + """ + Compute the number of entries required to store the long name. + One long filename entry can hold 13 characters with size 2 bytes. + + E.g. "thisisverylongfilenama.txt" with length of 26 needs 2 lfn entries, + but "thisisverylongfilenamax.txt" with 27 characters needs 3 lfn entries. + """ + entries_count: int = (len(lfn_full_name) + Entry.CHARS_PER_ENTRY - 1) // Entry.CHARS_PER_ENTRY + return entries_count + + +def split_name_to_lfn_entries(name: str, entries: int) -> List[str]: + """ + If the filename is longer than 8 (name) + 3 (extension) characters, + generator uses long name structure and splits the name into suitable amount of blocks. + + E.g. 'thisisverylongfilenama.txt' would be split to ['THISISVERYLON', 'GFILENAMA.TXT'], + in case of 'thisisverylongfilenamax.txt' - ['THISISVERYLON', 'GFILENAMAX.TX', 'T'] + """ + return [name[i * Entry.CHARS_PER_ENTRY:(i + 1) * Entry.CHARS_PER_ENTRY] for i in range(entries)] + + +def split_name_to_lfn_entry_blocks(name: str) -> List[bytes]: + """ + Filename is divided into three blocks in every long file name entry. Sizes of the blocks are defined + by LDIR_Name1_SIZE, LDIR_Name2_SIZE and LDIR_Name3_SIZE, thus every block contains LDIR_Name{X}_SIZE * 2 bytes. + + If the filename ends in one of the blocks, it is terminated by zero encoded to two bytes (0x0000). Other unused + characters are set to 0xFFFF. + E.g.: + 'GFILENAMA.TXT' -> [b'G\x00F\x00I\x00L\x00E\x00', b'N\x00A\x00M\x00A\x00.\x00T\x00', b'X\x00T\x00']; + 'T' -> [b'T\x00\x00\x00\xff\xff\xff\xff\xff\xff', b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', b'\xff\xff\xff\xff'] + + Notice that since every character is coded using 2 bytes be must add 0x00 to ASCII symbols ('G' -> 'G\x00', etc.), + since character 'T' ends in the first block, we must add '\x00\x00' after 'T\x00'. + """ + max_entry_size: int = Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE + Entry.LDIR_Name2_SIZE + assert len(name) <= max_entry_size + return [ + convert_to_utf16_and_pad(content=name[:Entry.LDIR_Name1_SIZE], + expected_size=Entry.LDIR_Name1_SIZE), + convert_to_utf16_and_pad(content=name[Entry.LDIR_Name1_SIZE:Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE], + expected_size=Entry.LDIR_Name2_SIZE), + convert_to_utf16_and_pad(content=name[Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE:], + expected_size=Entry.LDIR_Name3_SIZE) + ] + + +def build_lfn_unique_entry_name_order(entities: list, lfn_entry_name: str) -> int: + """ + The short entry contains only the first 6 characters of the file name, + and we have to distinguish it from other names within the directory starting with the same 6 characters. + To make it unique, we add its order in relation to other names such that lfn_entry_name[:6] == other[:6]. + The order is specified by the character, starting with '1'. + + E.g. the file in directory 'thisisverylongfilenama.txt' will be named 'THISIS~1TXT' in its short entry. + If we add another file 'thisisverylongfilenamax.txt' its name in the short entry will be 'THISIS~2TXT'. + """ + preceding_entries: int = 0 + for entity in entities: + if entity.name[:6] == lfn_entry_name[:6]: + preceding_entries += 1 + return preceding_entries + ord('1') + + +def build_lfn_full_name(name: str, extension: str) -> str: + """ + The extension is optional, and the long filename entry explicitly specifies it, + on the opposite as for short file names. + """ + return f'{name}.{extension}' if len(extension) > 0 else name diff --git a/components/fatfs/fatfsgen_utils/utils.py b/components/fatfs/fatfsgen_utils/utils.py index 27774b4a9d..d547ffa440 100644 --- a/components/fatfs/fatfsgen_utils/utils.py +++ b/components/fatfs/fatfsgen_utils/utils.py @@ -14,7 +14,13 @@ FAT16_MAX_CLUSTERS: int = 65525 FAT12: int = 12 FAT16: int = 16 FAT32: int = 32 -BYTES_PER_DIRECTORY_ENTRY = 32 +BYTES_PER_DIRECTORY_ENTRY: int = 32 +UINT32_MAX: int = (1 << 32) - 1 +MAX_NAME_SIZE: int = 8 +MAX_EXT_SIZE: int = 3 + +# long names are encoded to two bytes in utf-16 +LONG_NAMES_ENCODING: str = 'utf-16' def crc32(input_values: List[int], crc: int) -> int: @@ -36,7 +42,7 @@ def get_non_data_sectors_cnt(reserved_sectors_cnt: int, sectors_per_fat_cnt: int def get_fatfs_type(clusters_count: int) -> int: if clusters_count < FAT12_MAX_CLUSTERS: return FAT12 - if clusters_count < FAT16_MAX_CLUSTERS: + if clusters_count <= FAT16_MAX_CLUSTERS: return FAT16 return FAT32 @@ -55,6 +61,35 @@ def pad_string(content: str, size: Optional[int] = None, pad: int = 0x20) -> str return content.ljust(size or len(content), chr(pad))[:size] +def build_lfn_short_entry_name(name: str, extension: str, order: int) -> str: + return '{}{}'.format(pad_string(content=name[:MAX_NAME_SIZE - 2] + '~' + chr(order), size=MAX_NAME_SIZE), + pad_string(extension[:MAX_EXT_SIZE], size=MAX_EXT_SIZE)) + + +def lfn_checksum(short_entry_name: str) -> int: + """ + Function defined by FAT specification. Computes checksum out of name in the short file name entry. + """ + checksum_result = 0 + for i in range(MAX_NAME_SIZE + MAX_EXT_SIZE): + # operation is a right rotation on 8 bits (Python equivalent for unsigned char in C) + checksum_result = (0x80 if checksum_result & 1 else 0x00) + (checksum_result >> 1) + ord(short_entry_name[i]) + checksum_result &= 0xff + return checksum_result + + +def convert_to_utf16_and_pad(content: str, + expected_size: int, + pad: bytes = b'\xff', + terminator: bytes = b'\x00\x00') -> bytes: + # we need to get rid of the Byte order mark 0xfeff or 0xfffe, fatfs does not use it + bom_utf16: bytes = b'\xfe\xff' + encoded_content_utf16: bytes = content.encode(LONG_NAMES_ENCODING)[len(bom_utf16):] + terminated_encoded_content_utf16: bytes = (encoded_content_utf16 + terminator) if (2 * expected_size > len( + encoded_content_utf16) > 0) else encoded_content_utf16 + return terminated_encoded_content_utf16.ljust(2 * expected_size, pad) + + def split_to_name_and_extension(full_name: str) -> Tuple[str, str]: name, extension = os.path.splitext(full_name) return name, extension.replace('.', '') @@ -65,7 +100,7 @@ def is_valid_fatfs_name(string: str) -> bool: def split_by_half_byte_12_bit_little_endian(value: int) -> Tuple[int, int, int]: - value_as_bytes = Int16ul.build(value) + value_as_bytes: bytes = Int16ul.build(value) return value_as_bytes[0] & 0x0f, value_as_bytes[0] >> 4, value_as_bytes[1] & 0x0f @@ -91,7 +126,7 @@ def clean_second_half_byte(bytes_array: bytearray, address: int) -> None: def split_content_into_sectors(content: bytes, sector_size: int) -> List[bytes]: result = [] - clusters_cnt = required_clusters_count(cluster_size=sector_size, content=content) + clusters_cnt: int = required_clusters_count(cluster_size=sector_size, content=content) for i in range(clusters_cnt): result.append(content[sector_size * i:(i + 1) * sector_size]) @@ -99,8 +134,7 @@ def split_content_into_sectors(content: bytes, sector_size: int) -> List[bytes]: def get_args_for_partition_generator(desc: str) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description=desc) + parser: argparse.ArgumentParser = argparse.ArgumentParser(description=desc) parser.add_argument('input_directory', help='Path to the directory that will be encoded into fatfs image') parser.add_argument('--output_file', @@ -122,6 +156,9 @@ def get_args_for_partition_generator(desc: str) -> argparse.Namespace: parser.add_argument('--root_entry_count', default=512, help='Number of entries in the root directory') + parser.add_argument('--long_name_support', + action='store_true', + help='Set flag to enable long names support.') parser.add_argument('--fat_type', default=0, type=int, diff --git a/components/fatfs/project_include.cmake b/components/fatfs/project_include.cmake index c86f9bc2bc..9052fa44c6 100644 --- a/components/fatfs/project_include.cmake +++ b/components/fatfs/project_include.cmake @@ -44,6 +44,13 @@ function(fatfs_create_partition_image partition base_dir) set(fatfs_sector_size 4096) endif() + if("${CONFIG_FATFS_LFN_NONE}") + set(fatfs_long_names_option) + elseif("${CONFIG_FATFS_LFN_HEAP}") + set(fatfs_long_names_option --long_name_support) + elseif("${CONFIG_FATFS_LFN_STACK}") + set(fatfs_long_names_option --long_name_support) + endif() if("${CONFIG_FATFS_AUTO_TYPE}") set(fatfs_explicit_type 0) @@ -63,13 +70,13 @@ function(fatfs_create_partition_image partition base_dir) # contents of the base dir changing. add_custom_target(fatfs_${partition}_bin ALL COMMAND ${fatfsgen_py} ${base_dir_full_path} + ${fatfs_long_names_option} --partition_size ${size} --output_file ${image_file} --sector_size "${fatfs_sector_size}" --sectors_per_cluster "${sectors_per_cluster}" --fat_type "${fatfs_explicit_type}" ) - set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${image_file}) diff --git a/components/fatfs/test_fatfsgen/test_fatfsgen.py b/components/fatfs/test_fatfsgen/test_fatfsgen.py index e606b57f11..751d3029c2 100755 --- a/components/fatfs/test_fatfsgen/test_fatfsgen.py +++ b/components/fatfs/test_fatfsgen/test_fatfsgen.py @@ -6,25 +6,32 @@ import os import shutil import sys import unittest +from subprocess import STDOUT, check_output from test_utils import CFG, fill_sector, generate_test_dir_1, generate_test_dir_2 sys.path.append(os.path.join(os.path.dirname(__file__), '..')) import fatfsgen # noqa E402 # pylint: disable=C0413 +from fatfsgen_utils.exceptions import InconsistentFATAttributes # noqa E402 # pylint: disable=C0413 from fatfsgen_utils.exceptions import TooLongNameException # noqa E402 # pylint: disable=C0413 from fatfsgen_utils.exceptions import WriteDirectoryException # noqa E402 # pylint: disable=C0413 from fatfsgen_utils.exceptions import LowerCaseException, NoFreeClusterException # noqa E402 # pylint: disable=C0413 -from fatfsgen_utils.utils import read_filesystem # noqa E402 # pylint: disable=C0413 +from fatfsgen_utils.utils import FAT12, read_filesystem # noqa E402 # pylint: disable=C0413 class FatFSGen(unittest.TestCase): def setUp(self) -> None: os.makedirs('output_data') + os.makedirs('test_dir') generate_test_dir_1() generate_test_dir_2() def tearDown(self) -> None: shutil.rmtree('output_data') + shutil.rmtree('test_dir') + + if os.path.exists('fatfs_image.img'): + os.remove('fatfs_image.img') def test_empty_file_sn_fat12(self) -> None: fatfs = fatfsgen.FATFS() @@ -79,7 +86,7 @@ class FatFSGen(unittest.TestCase): self.assertEqual( file_system[0x1000:0x1010], b'\xf8\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') - self.assertEqual(file_system[0x6040:0x6050], b'WRITEF TXT\x20\x00\x00\x01\x00') + self.assertEqual(file_system[0x6040:0x6050], b'WRITEF TXT\x20\x00\x00\x00\x00') self.assertEqual(file_system[0x605a:0x6060], b'\x03\x00\x0b\x00\x00\x00') self.assertEqual(file_system[0x7000:0x700b], b'testcontent') # check file content @@ -129,8 +136,8 @@ class FatFSGen(unittest.TestCase): self.assertEqual(file_system[0x1000: 0x10d0], b'\xf8\xff\xff\x82\xf0\xff' + 192 * b'\xff' + 10 * b'\x00') self.assertEqual(file_system[0x85000:0x85005], b'later') - self.assertEqual(file_system[0x86000:0x86010], b'A126 \x00\x00\x01\x00') - self.assertEqual(file_system[0x86020:0x86030], b'A127 \x00\x00\x01\x00') + self.assertEqual(file_system[0x86000:0x86010], b'A126 \x00\x00\x00\x00') + self.assertEqual(file_system[0x86020:0x86030], b'A127 \x00\x00\x00\x00') def test_write_to_folder_in_folder_sn_fat12(self) -> None: fatfs = fatfsgen.FATFS() @@ -209,13 +216,13 @@ class FatFSGen(unittest.TestCase): fatfs.write_filesystem(CFG['output_file']) file_system = read_filesystem(CFG['output_file']) - self.assertEqual(file_system[0x2000:0x2010], b'TESTFOLD \x10\x00\x00\x01\x00') - self.assertEqual(file_system[0x2010:0x2020], b'!\x00\x00\x00\x00\x00\x01\x00\x01\x00\x02\x00\x00\x00\x00\x00') - self.assertEqual(file_system[0x6040:0x6050], b'TESTFOLD \x10\x00\x00\x01\x00') - self.assertEqual(file_system[0x6040:0x6050], b'TESTFOLD \x10\x00\x00\x01\x00') + self.assertEqual(file_system[0x2000:0x2010], b'TESTFOLD \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x2010:0x2020], b'!\x00\x00\x00\x00\x00\x00\x00\x21\x00\x02\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6040:0x6050], b'TESTFOLD \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6040:0x6050], b'TESTFOLD \x10\x00\x00\x00\x00') - self.assertEqual(file_system[0x7040:0x7050], b'TESTFOLD \x10\x00\x00\x01\x00') - self.assertEqual(file_system[0x8040:0x8050], b'WRITEF TXT \x00\x00\x01\x00') + self.assertEqual(file_system[0x7040:0x7050], b'TESTFOLD \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x8040:0x8050], b'WRITEF TXT \x00\x00\x00\x00') self.assertEqual(file_system[0x9000:0x9010], b'later\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') def test_e2e_deep_folder_into_image(self) -> None: @@ -223,9 +230,9 @@ class FatFSGen(unittest.TestCase): fatfs.generate(CFG['test_dir']) fatfs.write_filesystem(CFG['output_file']) file_system = read_filesystem(CFG['output_file']) - self.assertEqual(file_system[0x6060:0x6070], b'TESTFIL2 \x00\x00\x01\x00') - self.assertEqual(file_system[0x6070:0x6080], b'!\x00\x00\x00\x00\x00\x01\x00\x01\x00\x05\x00\x0b\x00\x00\x00') - self.assertEqual(file_system[0x7040:0x7050], b'LASTFILE \x00\x00\x01\x00') + self.assertEqual(file_system[0x6060:0x6070], b'TESTFIL2 \x00\x00\x00\x00') + self.assertEqual(file_system[0x6070:0x6080], b'!\x00\x00\x00\x00\x00\x00\x00\x21\x00\x05\x00\x0b\x00\x00\x00') + self.assertEqual(file_system[0x7040:0x7050], b'LASTFILE \x00\x00\x00\x00') self.assertEqual(file_system[0x8000:0x8010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00') self.assertEqual(file_system[0x9000:0x9010], b'thisistest\n\x00\x00\x00\x00\x00') self.assertEqual(file_system[0xa000:0xa010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') @@ -236,10 +243,10 @@ class FatFSGen(unittest.TestCase): fatfs.write_filesystem(CFG['output_file']) file_system = read_filesystem(CFG['output_file']) - self.assertEqual(file_system[0x2020:0x2030], b'TESTFILE \x00\x00\x01\x00') - self.assertEqual(file_system[0x6060:0x6070], b'TESTFIL2 \x00\x00\x01\x00') - self.assertEqual(file_system[0x7000:0x7010], b'. \x10\x00\x00\x01\x00') - self.assertEqual(file_system[0x7040:0x7050], b'LASTFILETXT \x00\x00\x01\x00') + self.assertEqual(file_system[0x2020:0x2030], b'TESTFILE \x00\x00\x00\x00') + self.assertEqual(file_system[0x6060:0x6070], b'TESTFIL2 \x00\x00\x00\x00') + self.assertEqual(file_system[0x7000:0x7010], b'. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x7040:0x7050], b'LASTFILETXT \x00\x00\x00\x00') self.assertEqual(file_system[0x8000:0x8010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00') self.assertEqual(file_system[0x9000:0x9010], b'thisistest\n\x00\x00\x00\x00\x00') self.assertEqual(file_system[0xa000:0xa010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') @@ -279,8 +286,192 @@ class FatFSGen(unittest.TestCase): self.assertEqual(file_system[0x1000: 0x1110], b'\xf8\xff\xff\xff\x82\x00' + 258 * b'\xff' + 8 * b'\x00') self.assertEqual(file_system[0x85000:0x85005], b'later') - self.assertEqual(file_system[0x86000:0x86010], b'A126 \x00\x00\x01\x00') - self.assertEqual(file_system[0x86020:0x86030], b'A127 \x00\x00\x01\x00') + self.assertEqual(file_system[0x86000:0x86010], b'A126 \x00\x00\x00\x00') + self.assertEqual(file_system[0x86020:0x86030], b'A127 \x00\x00\x00\x00') + + def test_empty_lfn_short_name(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_file('HELLO', extension='TXT') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2019], b'HELLO TXT \x00\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x00!') + + def test_lfn_short_name(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_file('HELLO', extension='TXT') + fatfs.write_content(path_from_root=['HELLO.TXT'], content=b'this is a test') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'HELLO TXT \x00\x00\x00\x00') + self.assertEqual(file_system[0x2010: 0x2020], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x0e\x00\x00\x00') + self.assertEqual(file_system[0x6000: 0x6010], b'this is a test\x00\x00') + + def test_lfn_empty_name(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_file('HELLOHELLOHELLO', extension='TXT') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'Bl\x00o\x00.\x00t\x00x\x00\x0f\x00\xb3t\x00') + self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\xb3h\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'HELLOH~1TXT \x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + + def test_lfn_plain_name(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_file('HELLOHELLOHELLO', extension='TXT') + fatfs.write_content(path_from_root=['HELLOHELLOHELLO.TXT'], content=b'this is a test') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'Bl\x00o\x00.\x00t\x00x\x00\x0f\x00\xb3t\x00') + self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\xb3h\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'HELLOH~1TXT \x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x0e\x00\x00\x00') + self.assertEqual(file_system[0x6000: 0x6010], b'this is a test\x00\x00') + + def test_lfn_plain_name_no_ext(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_file('HELLOHELLOHELLO') + fatfs.write_content(path_from_root=['HELLOHELLOHELLO'], content=b'this is a test') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'Bl\x00o\x00\x00\x00\xff\xff\xff\xff\x0f\x00V\xff\xff') + self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00Vh\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'HELLOH~1 \x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x0e\x00\x00\x00') + self.assertEqual(file_system[0x6000: 0x6010], b'this is a test\x00\x00') + + def test_lfn_short_entry_exception(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + self.assertRaises(LowerCaseException, fatfs.create_file, 'hello', extension='txt') + + def test_lfn_nested_empty(self) -> None: + fatfs = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_directory('VERYLONGTESTFOLD') + fatfs.create_file('HELLO', extension='TXT', path_from_root=['VERYLONGTESTFOLD']) + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\xa6\xff\xff') + self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\xa6o\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'VERYLO~1 \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + + self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6012: 0x6020], b'\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6030: 0x6040], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6040: 0x6050], b'HELLO TXT \x00\x00\x00\x00') + self.assertEqual(file_system[0x6050: 0x6060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x03\x00\x00\x00\x00\x00') + + def test_lfn_nested_long_empty(self) -> None: + fatfs: fatfsgen.FATFS = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_directory('verylongtestfold') + fatfs.create_file('hellohellohello', extension='txt', path_from_root=['verylongtestfold']) + fatfs.write_filesystem(CFG['output_file']) + + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\x10\xff\xff') + self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\x10o\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'verylo~1 \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + + self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6012: 0x6020], b'\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6030: 0x6040], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6040: 0x6050], b'Bl\x00o\x00.\x00t\x00x\x00\x0f\x00\xd5t\x00') + self.assertEqual(file_system[0x6050: 0x6060], + b'\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + + def test_lfn_nested_text(self) -> None: + fatfs: fatfsgen.FATFS = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_directory('VERYLONGTESTFOLD') + fatfs.create_file('HELLO', extension='TXT', path_from_root=['VERYLONGTESTFOLD']) + fatfs.write_content(path_from_root=['VERYLONGTESTFOLD', 'HELLO.TXT'], content=b'this is a test') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\xa6\xff\xff') + self.assertEqual(file_system[0x2012: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\xa6o\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'VERYLO~1 \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + + self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6012: 0x6020], b'\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6030: 0x6040], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6040: 0x6050], b'HELLO TXT \x00\x00\x00\x00') + self.assertEqual(file_system[0x6050: 0x6060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x03\x00\x0e\x00\x00\x00') + + self.assertEqual(file_system[0x7000: 0x7010], b'this is a test\x00\x00') + + def test_boundary_clusters12(self) -> None: + output: bytes = check_output(['python', '../fatfsgen.py', '--partition_size', '16756736', 'test_dir'], + stderr=STDOUT) + self.assertEqual( + output, + b'WARNING: It is not recommended to create FATFS with bounding count of clusters: 4085 or 65525\n') + + def test_boundary_clusters16(self) -> None: + output: bytes = check_output(['python', '../fatfsgen.py', '--partition_size', '268415097', 'test_dir'], + stderr=STDOUT) + self.assertEqual( + output, + b'WARNING: It is not recommended to create FATFS with bounding count of clusters: 4085 or 65525\n') + + def test_boundary_clusters_fat32(self) -> None: + self.assertRaises(NotImplementedError, fatfsgen.FATFS, size=268419193) + + def test_inconsistent_fat12(self) -> None: + self.assertRaises(InconsistentFATAttributes, fatfsgen.FATFS, size=268411001, explicit_fat_type=FAT12) + + def test_lfn_increasing(self) -> None: + fatfs: fatfsgen.FATFS = fatfsgen.FATFS(long_names_enabled=True) + fatfs.create_directory('VERYLONGTESTFOLD') + fatfs.create_file('HELLOHELLOHELLOOOOOOO', extension='TXT', path_from_root=['VERYLONGTESTFOLD']) + fatfs.create_file('HELLOHELLOHELLOOOOOOB', extension='TXT', path_from_root=['VERYLONGTESTFOLD']) + fatfs.write_content(path_from_root=['VERYLONGTESTFOLD', 'HELLOHELLOHELLOOOOOOO.TXT'], + content=b'this is a test A') + fatfs.write_content(path_from_root=['VERYLONGTESTFOLD', 'HELLOHELLOHELLOOOOOOB.TXT'], + content=b'this is a test B') + fatfs.write_filesystem(CFG['output_file']) + file_system = read_filesystem(CFG['output_file']) + + self.assertEqual(file_system[0x2000: 0x2010], b'Bo\x00l\x00d\x00\x00\x00\xff\xff\x0f\x00\xa6\xff\xff') + self.assertEqual(file_system[0x2011: 0x2020], b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff') + self.assertEqual(file_system[0x2020: 0x2030], b'\x01v\x00e\x00r\x00y\x00l\x00\x0f\x00\xa6o\x00') + self.assertEqual(file_system[0x2030: 0x2040], b'n\x00g\x00t\x00e\x00s\x00\x00\x00t\x00f\x00') + self.assertEqual(file_system[0x2040: 0x2050], b'VERYLO~1 \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x2050: 0x2060], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + + self.assertEqual(file_system[0x6000: 0x6010], b'. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6011: 0x6020], b'\x00\x00\x00\x00\x00\x00\x00!\x00\x02\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6020: 0x6030], b'.. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x6030: 0x6040], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x01\x00\x00\x00\x00\x00') + self.assertEqual(file_system[0x6040: 0x6050], b'Bl\x00o\x00o\x00o\x00o\x00\x0f\x00\xb3o\x00') + self.assertEqual(file_system[0x6050: 0x6060], b'o\x00o\x00.\x00t\x00x\x00\x00\x00t\x00\x00\x00') + + self.assertEqual(file_system[0x6050: 0x6060], b'o\x00o\x00.\x00t\x00x\x00\x00\x00t\x00\x00\x00') + self.assertEqual(file_system[0x6060: 0x6070], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\xb3h\x00') + self.assertEqual(file_system[0x6070: 0x6080], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00') + self.assertEqual(file_system[0x6080: 0x6090], b'HELLOH~1TXT \x00\x00\x00\x00') + self.assertEqual(file_system[0x6090: 0x60a0], b'!\x00\x00\x00\x00\x00\x00\x00!\x00\x03\x00\x10\x00\x00\x00') + self.assertEqual(file_system[0x60a0: 0x60b0], b'Bl\x00o\x00o\x00o\x00o\x00\x0f\x00\x93o\x00') + + self.assertEqual(file_system[0x60b0: 0x60c0], b'o\x00b\x00.\x00t\x00x\x00\x00\x00t\x00\x00\x00') + self.assertEqual(file_system[0x60c0: 0x60d0], b'\x01h\x00e\x00l\x00l\x00o\x00\x0f\x00\x93h\x00') + self.assertEqual(file_system[0x60d0: 0x60e0], b'e\x00l\x00l\x00o\x00h\x00\x00\x00e\x00l\x00') + self.assertEqual(file_system[0x60e0: 0x60f0], b'HELLOH~2TXT \x00\x00\x00\x00') if __name__ == '__main__': diff --git a/components/fatfs/test_fatfsgen/test_utils.py b/components/fatfs/test_fatfsgen/test_utils.py index e455862423..5fac082753 100644 --- a/components/fatfs/test_fatfsgen/test_utils.py +++ b/components/fatfs/test_fatfsgen/test_utils.py @@ -39,6 +39,6 @@ def generate_test_dir_2() -> None: file.write('ahoj\n') -def fill_sector(fatfs: fatfsgen.FATFS) -> None: +def fill_sector(fatfs: fatfsgen.FATFS, file_prefix: str = 'A') -> None: for i in range(CFG['sector_size'] // CFG['entry_size']): - fatfs.create_file(f'A{str(i).upper()}', path_from_root=['TESTFOLD']) + fatfs.create_file(f'{file_prefix}{str(i).upper()}', path_from_root=['TESTFOLD']) diff --git a/components/fatfs/test_fatfsgen/test_wl_fatfsgen.py b/components/fatfs/test_fatfsgen/test_wl_fatfsgen.py index a5c64be669..f1659b74ce 100755 --- a/components/fatfs/test_fatfsgen/test_wl_fatfsgen.py +++ b/components/fatfs/test_fatfsgen/test_wl_fatfsgen.py @@ -116,10 +116,10 @@ class WLFatFSGen(unittest.TestCase): with open(CFG['output_file'], 'rb') as fs_file: file_system = bytearray(fs_file.read()) - self.assertEqual(file_system[0x3020:0x3030], b'TESTFILE \x00\x00\x01\x00') - self.assertEqual(file_system[0x7060:0x7070], b'TESTFIL2 \x00\x00\x01\x00') - self.assertEqual(file_system[0x8000:0x8010], b'. \x10\x00\x00\x01\x00') - self.assertEqual(file_system[0x8040:0x8050], b'LASTFILETXT \x00\x00\x01\x00') + self.assertEqual(file_system[0x3020:0x3030], b'TESTFILE \x00\x00\x00\x00') + self.assertEqual(file_system[0x7060:0x7070], b'TESTFIL2 \x00\x00\x00\x00') + self.assertEqual(file_system[0x8000:0x8010], b'. \x10\x00\x00\x00\x00') + self.assertEqual(file_system[0x8040:0x8050], b'LASTFILETXT \x00\x00\x00\x00') self.assertEqual(file_system[0x9000:0x9010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00') self.assertEqual(file_system[0xa000:0xa010], b'thisistest\n\x00\x00\x00\x00\x00') self.assertEqual(file_system[0xb000:0xb010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') @@ -133,9 +133,9 @@ class WLFatFSGen(unittest.TestCase): with open(CFG['output_file'], 'rb') as fs_file: file_system = bytearray(fs_file.read()) - self.assertEqual(file_system[0x7060:0x7070], b'TESTFIL2 \x00\x00\x01\x00') - self.assertEqual(file_system[0x7070:0x7080], b'!\x00\x00\x00\x00\x00\x01\x00\x01\x00\x05\x00\x0b\x00\x00\x00') - self.assertEqual(file_system[0x8040:0x8050], b'LASTFILE \x00\x00\x01\x00') + self.assertEqual(file_system[0x7060:0x7070], b'TESTFIL2 \x00\x00\x00\x00') + self.assertEqual(file_system[0x7070:0x7080], b'!\x00\x00\x00\x00\x00\x00\x00\x21\x00\x05\x00\x0b\x00\x00\x00') + self.assertEqual(file_system[0x8040:0x8050], b'LASTFILE \x00\x00\x00\x00') self.assertEqual(file_system[0x9000:0x9010], b'deeptest\n\x00\x00\x00\x00\x00\x00\x00') self.assertEqual(file_system[0xa000:0xa010], b'thisistest\n\x00\x00\x00\x00\x00') self.assertEqual(file_system[0xb000:0xb010], b'ahoj\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') diff --git a/components/fatfs/wl_fatfsgen.py b/components/fatfs/wl_fatfsgen.py index 94dcb2b50f..0f7fdf4765 100755 --- a/components/fatfs/wl_fatfsgen.py +++ b/components/fatfs/wl_fatfsgen.py @@ -7,7 +7,7 @@ from typing import List, Optional from construct import Const, Int32ul, Struct from fatfsgen import FATFS from fatfsgen_utils.exceptions import WLNotInitialized -from fatfsgen_utils.utils import crc32, generate_4bytes_random, get_args_for_partition_generator +from fatfsgen_utils.utils import UINT32_MAX, crc32, generate_4bytes_random, get_args_for_partition_generator class WLFATFS: @@ -18,7 +18,6 @@ class WLFATFS: WL_STATE_RECORD_SIZE = 16 WL_STATE_HEADER_SIZE = 64 WL_STATE_COPY_COUNT = 2 - UINT32_MAX = 4294967295 WL_SECTOR_SIZE = 0x1000 WL_STATE_T_DATA = Struct( @@ -136,7 +135,7 @@ class WLFATFS: ) ) - crc = crc32(list(wl_config_data), WLFATFS.UINT32_MAX) + crc = crc32(list(wl_config_data), UINT32_MAX) wl_config_crc = Int32ul.build(crc) # adding three 4 byte zeros to align the structure @@ -157,7 +156,7 @@ class WLFATFS: device_id=self._device_id or generate_4bytes_random(), ) ) - crc = crc32(list(wl_state_data), WLFATFS.UINT32_MAX) + crc = crc32(list(wl_state_data), UINT32_MAX) wl_state_crc = Int32ul.build(crc) wl_state = wl_state_data + wl_state_crc self.fatfs_binary_image += WLFATFS.WL_STATE_COPY_COUNT * ( @@ -194,7 +193,8 @@ if __name__ == '__main__': sectors_per_cluster=args.sectors_per_cluster, size=args.partition_size, root_entry_count=args.root_entry_count, - explicit_fat_type=args.fat_type) + explicit_fat_type=args.fat_type, + long_names_enabled=args.long_name_support) wl_fatfs.wl_generate(args.input_directory) wl_fatfs.init_wl() diff --git a/docs/en/api-reference/storage/fatfs.rst b/docs/en/api-reference/storage/fatfs.rst index c9a300114c..a9fd89fa9b 100644 --- a/docs/en/api-reference/storage/fatfs.rst +++ b/docs/en/api-reference/storage/fatfs.rst @@ -95,8 +95,7 @@ The tool is used to create filesystem images on a host and populate it with cont The script is based on the partition generator (:component_file:`fatfsgen.py`) and except for generating partition also initializes wear levelling. -Current implementation supports short file names, FAT12 and FAT16. Long file names support is the subject of the future work. - +The latest version supports both short and long file names, FAT12 and FAT16. The long file names are limited to 255 characters, and can contain multiple period (".") characters within the filename and additional characters "+", ",", ";", "=", "[" and also "]". The long files name characters are encoded using utf-16, on the contrary to short file names encoded using utf-8. Build system integration with FATFS partition generator ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/zh_CN/api-reference/storage/fatfs.rst b/docs/zh_CN/api-reference/storage/fatfs.rst index 47e6b06695..ae873c3821 100644 --- a/docs/zh_CN/api-reference/storage/fatfs.rst +++ b/docs/zh_CN/api-reference/storage/fatfs.rst @@ -95,7 +95,7 @@ FatFs 分区生成器 该脚本是建立在分区生成器的基础上 (:component_file:`fatfsgen.py`),目前除了可以生成分区外,也可以初始化磨损均衡。 -当前支持短文件名、FAT12 和 FAT16。未来计划实现对长文件名的支持。 +目前最新版本支持短文件名、长文件名、FAT12 和 FAT16。长文件名的上线是 255 个字符,文件名中可以包含多个 "." 字符以及其他字符如 "+"、","、";"、"="、"[" and also "]" 等。长文件名字符采用 utf-16 编码而短文件名采用 utf-8 编码。 构建系统中使用 FatFs 分区生成器 diff --git a/examples/storage/fatfsgen/fatfs_long_name_image/hellolongname.txt b/examples/storage/fatfsgen/fatfs_long_name_image/hellolongname.txt new file mode 100644 index 0000000000..45caad2725 --- /dev/null +++ b/examples/storage/fatfsgen/fatfs_long_name_image/hellolongname.txt @@ -0,0 +1 @@ +This is generated on the host; long name it has diff --git a/examples/storage/fatfsgen/fatfs_long_name_image/sublongnames/testlongfilenames.txt b/examples/storage/fatfsgen/fatfs_long_name_image/sublongnames/testlongfilenames.txt new file mode 100644 index 0000000000..e8c8c6d51f --- /dev/null +++ b/examples/storage/fatfsgen/fatfs_long_name_image/sublongnames/testlongfilenames.txt @@ -0,0 +1 @@ +this is test; long name it has diff --git a/examples/storage/fatfsgen/fatfsgen_example_test.py b/examples/storage/fatfsgen/fatfsgen_example_test.py index f3495f4265..b6a995d2f6 100644 --- a/examples/storage/fatfsgen/fatfsgen_example_test.py +++ b/examples/storage/fatfsgen/fatfsgen_example_test.py @@ -1,5 +1,5 @@ -# SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD -# SPDX-License-Identifier: CC0 +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 from typing import Optional import ttfw_idf @@ -31,6 +31,30 @@ def test_examples_fatfsgen(env: ttfw_idf.TinyFW.Env, _: Optional[list]) -> None: timeout=20) env.close_dut(dut.name) + dut = env.get_dut('fatfsgen', 'examples/storage/fatfsgen', app_config_name='test_read_write_partition_gen_ln') + dut.start_app() + dut.expect_all('example: Mounting FAT filesystem', + 'example: Opening file', + 'example: File written', + 'example: Reading file', + 'example: Read from file: \'This is written by the device\'', + 'example: Reading file', + 'example: Read from file: \'This is generated on the host; long name it has\'', + 'example: Unmounting FAT filesystem', + 'example: Done', + timeout=20) + env.close_dut(dut.name) + + dut = env.get_dut('fatfsgen', 'examples/storage/fatfsgen', app_config_name='test_read_only_partition_gen_ln') + dut.start_app() + dut.expect_all('example: Mounting FAT filesystem', + 'example: Reading file', + 'example: Read from file: \'this is test; long name it has\'', + 'example: Unmounting FAT filesystem', + 'example: Done', + timeout=20) + env.close_dut(dut.name) + if __name__ == '__main__': test_examples_fatfsgen() diff --git a/examples/storage/fatfsgen/main/CMakeLists.txt b/examples/storage/fatfsgen/main/CMakeLists.txt index f44254cf8d..3f9669aae4 100644 --- a/examples/storage/fatfsgen/main/CMakeLists.txt +++ b/examples/storage/fatfsgen/main/CMakeLists.txt @@ -9,8 +9,14 @@ idf_component_register(SRCS "fatfsgen_example_main.c" # the generated image will be raw without wear levelling support. # Otherwise it will support wear levelling and thus enable read-write mounting of the image in the device. -if(CONFIG_EXAMPLE_FATFS_MODE_READ_ONLY) - fatfs_create_rawflash_image(storage ../fatfs_image FLASH_IN_PROJECT) +if(CONFIG_FATFS_LFN_NONE) + set(image ../fatfs_image) else() - fatfs_create_spiflash_image(storage ../fatfs_image FLASH_IN_PROJECT) + set(image ../fatfs_long_name_image) +endif() + +if(CONFIG_EXAMPLE_FATFS_MODE_READ_ONLY) + fatfs_create_rawflash_image(storage ${image} FLASH_IN_PROJECT) +else() + fatfs_create_spiflash_image(storage ${image} FLASH_IN_PROJECT) endif() diff --git a/examples/storage/fatfsgen/main/fatfsgen_example_main.c b/examples/storage/fatfsgen/main/fatfsgen_example_main.c index ed2d44180e..a6d6dae8b9 100644 --- a/examples/storage/fatfsgen/main/fatfsgen_example_main.c +++ b/examples/storage/fatfsgen/main/fatfsgen_example_main.c @@ -1,7 +1,7 @@ /* - * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD * - * SPDX-License-Identifier: CC0 + * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ #include @@ -18,6 +18,12 @@ #define EXAMPLE_FATFS_MODE_READ_ONLY false #endif +#if CONFIG_FATFS_LFN_NONE +#define EXAMPLE_FATFS_LONG_NAMES false +#else +#define EXAMPLE_FATFS_LONG_NAMES true +#endif + static const char *TAG = "example"; @@ -50,10 +56,17 @@ void app_main(void) } char line[128]; + char *device_filename; + if (EXAMPLE_FATFS_LONG_NAMES){ + device_filename = "/spiflash/innerbutverylongname.txt"; + } else { + device_filename = "/spiflash/inner.txt"; + } + if (!EXAMPLE_FATFS_MODE_READ_ONLY){ // Open file for reading ESP_LOGI(TAG, "Opening file"); - FILE *f = fopen("/spiflash/inner.txt", "wb"); + FILE *f = fopen(device_filename, "wb"); if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for writing"); return; @@ -64,7 +77,7 @@ void app_main(void) // Open file for reading ESP_LOGI(TAG, "Reading file"); - f = fopen("/spiflash/inner.txt", "rb"); + f = fopen(device_filename, "rb"); if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for reading"); return; @@ -82,10 +95,21 @@ void app_main(void) FILE *f; char *pos; ESP_LOGI(TAG, "Reading file"); + char *host_filename1; + char *host_filename2; + + if (EXAMPLE_FATFS_LONG_NAMES){ + host_filename1 = "/spiflash/sublongnames/testlongfilenames.txt"; + host_filename2 = "/spiflash/hellolongname.txt"; + } else{ + host_filename1 = "/spiflash/sub/test.txt"; + host_filename2 = "/spiflash/hello.txt"; + } + if (EXAMPLE_FATFS_MODE_READ_ONLY){ - f = fopen("/spiflash/sub/test.txt", "rb"); + f = fopen(host_filename1, "rb"); } else { - f = fopen("/spiflash/hello.txt", "rb"); + f = fopen(host_filename2, "rb"); } if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for reading"); diff --git a/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen b/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen index a5c12f9896..9642cc9005 100644 --- a/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen +++ b/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen @@ -1 +1,4 @@ CONFIG_EXAMPLE_FATFS_MODE_READ_ONLY=y +CONFIG_FATFS_LFN_HEAP=n +CONFIG_FATFS_LFN_STACK=n +CONFIG_FATFS_LFN_NONE=y diff --git a/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen_ln b/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen_ln new file mode 100644 index 0000000000..5122126dcb --- /dev/null +++ b/examples/storage/fatfsgen/sdkconfig.ci.test_read_only_partition_gen_ln @@ -0,0 +1,4 @@ +CONFIG_EXAMPLE_FATFS_MODE_READ_ONLY=y +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_LFN_NONE=n +CONFIG_FATFS_LFN_STACK=n diff --git a/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen b/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen index a995ed5a51..c9c707f7bb 100644 --- a/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen +++ b/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen @@ -1 +1,4 @@ CONFIG_EXAMPLE_FATFS_MODE_READ_ONLY=n +CONFIG_FATFS_LFN_HEAP=n +CONFIG_FATFS_LFN_STACK=n +CONFIG_FATFS_LFN_NONE=y diff --git a/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen_ln b/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen_ln new file mode 100644 index 0000000000..33a0ccfad3 --- /dev/null +++ b/examples/storage/fatfsgen/sdkconfig.ci.test_read_write_partition_gen_ln @@ -0,0 +1,4 @@ +CONFIG_EXAMPLE_FATFS_MODE_READ_ONLY=n +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_LFN_NONE=n +CONFIG_FATFS_LFN_STACK=n