mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
Merge branch 'fix/fatfsgen-sfn-ascii-numeric-tail_v6.0' into 'release/v6.0'
fix(fatfs): fix SFN generation from LFN (numeric tail ASCII encoding, switching to CRC hex numbers to mirror FATFS logic, etc.) (v6.0) See merge request espressif/esp-idf!47543
This commit is contained in:
@@ -1,26 +1,38 @@
|
||||
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from .entry import Entry
|
||||
from .exceptions import FatalError, WriteDirectoryException
|
||||
from .fat import FAT, Cluster
|
||||
from .exceptions import FatalError
|
||||
from .exceptions import WriteDirectoryException
|
||||
from .fat import FAT
|
||||
from .fat import Cluster
|
||||
from .fatfs_state import FATFSState
|
||||
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 (DATETIME, INVALID_SFN_CHARS_PATTERN, MAX_EXT_SIZE, MAX_NAME_SIZE, FATDefaults,
|
||||
build_lfn_short_entry_name, build_name, lfn_checksum, required_clusters_count,
|
||||
split_content_into_sectors, split_to_name_and_extension)
|
||||
from .long_filename_utils import build_lfn_full_name
|
||||
from .long_filename_utils import build_lfn_unique_entry_name_order
|
||||
from .long_filename_utils import get_required_lfn_entries_count
|
||||
from .long_filename_utils import split_name_to_lfn_entries
|
||||
from .long_filename_utils import split_name_to_lfn_entry_blocks
|
||||
from .utils import DATETIME
|
||||
from .utils import INVALID_SFN_CHARS_PATTERN
|
||||
from .utils import MAX_EXT_SIZE
|
||||
from .utils import MAX_NAME_SIZE
|
||||
from .utils import FATDefaults
|
||||
from .utils import build_lfn_short_entry_name
|
||||
from .utils import build_name
|
||||
from .utils import lfn_checksum
|
||||
from .utils import required_clusters_count
|
||||
from .utils import split_content_into_sectors
|
||||
from .utils import 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: int = 0x20
|
||||
ENTITY_TYPE: int = ATTR_ARCHIVE
|
||||
|
||||
@@ -30,7 +42,7 @@ class File:
|
||||
self.fatfs_state: FATFSState = fatfs_state
|
||||
self.fat: FAT = fat
|
||||
self.size: int = 0
|
||||
self._first_cluster: Optional[Cluster] = None
|
||||
self._first_cluster: Cluster | None = None
|
||||
self._entry: Entry = entry
|
||||
|
||||
@property
|
||||
@@ -38,7 +50,7 @@ class File:
|
||||
return self._entry
|
||||
|
||||
@property
|
||||
def first_cluster(self) -> Optional[Cluster]:
|
||||
def first_cluster(self) -> Cluster | None:
|
||||
return self._first_cluster
|
||||
|
||||
@first_cluster.setter
|
||||
@@ -59,7 +71,7 @@ class File:
|
||||
raise FatalError('No free space left!')
|
||||
|
||||
address: int = current_cluster.cluster_data_address
|
||||
self.fatfs_state.binary_image[address: address + len(content_part)] = content_as_list
|
||||
self.fatfs_state.binary_image[address : address + len(content_part)] = content_as_list
|
||||
current_cluster = current_cluster.next_cluster
|
||||
|
||||
|
||||
@@ -68,6 +80,7 @@ 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: int = 0x10
|
||||
ATTR_ARCHIVE: int = 0x20
|
||||
ENTITY_TYPE: int = ATTR_DIRECTORY
|
||||
@@ -75,16 +88,17 @@ class Directory:
|
||||
CURRENT_DIRECTORY = '.'
|
||||
PARENT_DIRECTORY = '..'
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
fat,
|
||||
fatfs_state,
|
||||
entry=None,
|
||||
cluster=None,
|
||||
size=None,
|
||||
extension='',
|
||||
parent=None):
|
||||
# type: (str, FAT, FATFSState, Optional[Entry], Cluster, Optional[int], str, Directory) -> None
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
fat: FAT,
|
||||
fatfs_state: FATFSState,
|
||||
entry: Entry | None = None,
|
||||
cluster: Cluster | None = None,
|
||||
size: int | None = None,
|
||||
extension: str = '',
|
||||
parent: 'Directory | None' = None,
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.fatfs_state: FATFSState = fatfs_state
|
||||
self.extension: str = extension
|
||||
@@ -97,8 +111,8 @@ class Directory:
|
||||
self._first_cluster: Cluster = cluster
|
||||
|
||||
# entries will be initialized after the cluster allocation
|
||||
self.entries: List[Entry] = []
|
||||
self.entities: List[Union[File, Directory]] = [] # type: ignore
|
||||
self.entries: list[Entry] = []
|
||||
self.entities: list[File | Directory] = [] # type: ignore
|
||||
self._entry = entry # currently not in use (will use later for e.g. modification time, etc.)
|
||||
|
||||
@property
|
||||
@@ -122,11 +136,11 @@ class Directory:
|
||||
entries_count_: int = self.size // FATDefaults.ENTRY_SIZE
|
||||
return entries_count_
|
||||
|
||||
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)
|
||||
for i in range(self.entries_count)]
|
||||
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)
|
||||
for i in range(self.entries_count)
|
||||
]
|
||||
|
||||
def init_directory(self) -> None:
|
||||
self.entries = self.create_entries(self._first_cluster)
|
||||
@@ -137,10 +151,12 @@ class Directory:
|
||||
# if the directory is not root we initialize the reference to itself and to the parent directory
|
||||
for dir_id, name_ in ((self, self.CURRENT_DIRECTORY), (self.parent, self.PARENT_DIRECTORY)):
|
||||
new_dir_: Entry = self.find_free_entry() or self.chain_directory()
|
||||
new_dir_.allocate_entry(first_cluster_id=dir_id.first_cluster.id,
|
||||
entity_name=name_,
|
||||
entity_extension='',
|
||||
entity_type=dir_id.ENTITY_TYPE)
|
||||
new_dir_.allocate_entry(
|
||||
first_cluster_id=dir_id.first_cluster.id,
|
||||
entity_name=name_,
|
||||
entity_extension='',
|
||||
entity_type=dir_id.ENTITY_TYPE,
|
||||
)
|
||||
|
||||
def lookup_entity(self, object_name: str, extension: str): # type: ignore
|
||||
for entity in self.entities:
|
||||
@@ -149,7 +165,7 @@ class Directory:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _is_end_of_path(path_as_list: List[str]) -> bool:
|
||||
def _is_end_of_path(path_as_list: list[str]) -> bool:
|
||||
"""
|
||||
:param path_as_list: path split into the list
|
||||
|
||||
@@ -168,7 +184,7 @@ class Directory:
|
||||
return next_obj
|
||||
return self.recursive_search(path_as_list[1:], next_obj)
|
||||
|
||||
def find_free_entry(self) -> Optional[Entry]:
|
||||
def find_free_entry(self) -> Entry | None:
|
||||
for entry in self.entries:
|
||||
if entry.is_empty:
|
||||
return entry
|
||||
@@ -197,18 +213,21 @@ class Directory:
|
||||
return free_entry
|
||||
|
||||
@staticmethod
|
||||
def allocate_long_name_object(free_entry,
|
||||
name,
|
||||
extension,
|
||||
target_dir,
|
||||
free_cluster_id,
|
||||
entity_type,
|
||||
date,
|
||||
time):
|
||||
# type: (Entry, str, str, Directory, int, int, DATETIME, DATETIME) -> Entry
|
||||
def allocate_long_name_object(
|
||||
free_entry: Entry,
|
||||
name: str,
|
||||
extension: str,
|
||||
target_dir: 'Directory',
|
||||
free_cluster_id: int,
|
||||
entity_type: int,
|
||||
date: DATETIME,
|
||||
time: DATETIME,
|
||||
) -> Entry:
|
||||
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)
|
||||
lfn_short_entry_name: str = build_lfn_short_entry_name(
|
||||
name, extension, lfn_unique_entry_order, lfn=lfn_full_name
|
||||
)
|
||||
checksum: int = lfn_checksum(lfn_short_entry_name)
|
||||
entries_count: int = get_required_lfn_entries_count(lfn_full_name)
|
||||
|
||||
@@ -216,24 +235,28 @@ class Directory:
|
||||
split_names_reversed = list(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
|
||||
blocks_: List[bytes] = split_name_to_lfn_entry_blocks(name_split_to_entry)
|
||||
lfn_names: List[bytes] = list(map(lambda x: x.lower(), blocks_))
|
||||
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)
|
||||
blocks_: list[bytes] = split_name_to_lfn_entry_blocks(name_split_to_entry)
|
||||
lfn_names: list[bytes] = list(map(lambda x: x.lower(), blocks_))
|
||||
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,
|
||||
date=date,
|
||||
time=time)
|
||||
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,
|
||||
date=date,
|
||||
time=time,
|
||||
)
|
||||
return free_entry
|
||||
|
||||
@staticmethod
|
||||
@@ -243,21 +266,22 @@ class Directory:
|
||||
ret: bool = len(name) <= MAX_NAME_SIZE and len(extension) <= MAX_EXT_SIZE
|
||||
return ret
|
||||
|
||||
def allocate_object(self,
|
||||
name,
|
||||
entity_type,
|
||||
object_timestamp_,
|
||||
path_from_root=None,
|
||||
extension='',
|
||||
is_empty=False):
|
||||
# type: (str, int, datetime, Optional[List[str]], str, bool) -> Tuple[Cluster, Entry, Directory]
|
||||
def allocate_object(
|
||||
self,
|
||||
name: str,
|
||||
entity_type: int,
|
||||
object_timestamp_: datetime,
|
||||
path_from_root: list[str] | None = None,
|
||||
extension: str = '',
|
||||
is_empty: bool = False,
|
||||
) -> tuple[Cluster, Entry, 'Directory']:
|
||||
"""
|
||||
Method finds the target directory in the path
|
||||
and allocates cluster (both the record in FAT and cluster in the data region)
|
||||
and entry in the specified directory
|
||||
"""
|
||||
|
||||
free_cluster: Optional[Cluster] = None
|
||||
free_cluster: Cluster | None = None
|
||||
free_cluster_id = 0x00
|
||||
if not is_empty:
|
||||
free_cluster = self.fat.find_free_cluster()
|
||||
@@ -270,61 +294,74 @@ class Directory:
|
||||
fatfs_time_ = (object_timestamp_.hour, object_timestamp_.minute, object_timestamp_.second)
|
||||
|
||||
if not self.fatfs_state.long_names_enabled or self._is_valid_sfn(name, extension):
|
||||
free_entry.allocate_entry(first_cluster_id=free_cluster_id,
|
||||
entity_name=name,
|
||||
entity_extension=extension,
|
||||
date=fatfs_date_,
|
||||
time=fatfs_time_,
|
||||
fits_short=True,
|
||||
entity_type=entity_type)
|
||||
free_entry.allocate_entry(
|
||||
first_cluster_id=free_cluster_id,
|
||||
entity_name=name,
|
||||
entity_extension=extension,
|
||||
date=fatfs_date_,
|
||||
time=fatfs_time_,
|
||||
fits_short=True,
|
||||
entity_type=entity_type,
|
||||
)
|
||||
return free_cluster, free_entry, target_dir
|
||||
return free_cluster, self.allocate_long_name_object(free_entry=free_entry,
|
||||
name=name,
|
||||
extension=extension,
|
||||
target_dir=target_dir,
|
||||
free_cluster_id=free_cluster_id,
|
||||
entity_type=entity_type,
|
||||
date=fatfs_date_,
|
||||
time=fatfs_time_), target_dir
|
||||
return (
|
||||
free_cluster,
|
||||
self.allocate_long_name_object(
|
||||
free_entry=free_entry,
|
||||
name=name,
|
||||
extension=extension,
|
||||
target_dir=target_dir,
|
||||
free_cluster_id=free_cluster_id,
|
||||
entity_type=entity_type,
|
||||
date=fatfs_date_,
|
||||
time=fatfs_time_,
|
||||
),
|
||||
target_dir,
|
||||
)
|
||||
|
||||
def new_file(self,
|
||||
name: str,
|
||||
extension: str,
|
||||
path_from_root: Optional[List[str]],
|
||||
object_timestamp_: datetime,
|
||||
is_empty: bool) -> None:
|
||||
free_cluster, free_entry, target_dir = self.allocate_object(name=name,
|
||||
extension=extension,
|
||||
entity_type=Directory.ATTR_ARCHIVE,
|
||||
path_from_root=path_from_root,
|
||||
object_timestamp_=object_timestamp_,
|
||||
is_empty=is_empty)
|
||||
def new_file(
|
||||
self,
|
||||
name: str,
|
||||
extension: str,
|
||||
path_from_root: list[str] | None,
|
||||
object_timestamp_: datetime,
|
||||
is_empty: bool,
|
||||
) -> None:
|
||||
free_cluster, free_entry, target_dir = self.allocate_object(
|
||||
name=name,
|
||||
extension=extension,
|
||||
entity_type=Directory.ATTR_ARCHIVE,
|
||||
path_from_root=path_from_root,
|
||||
object_timestamp_=object_timestamp_,
|
||||
is_empty=is_empty,
|
||||
)
|
||||
|
||||
file: File = File(name=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)
|
||||
|
||||
def new_directory(self, name, parent, path_from_root, object_timestamp_):
|
||||
# type: (str, Directory, Optional[List[str]], datetime) -> None
|
||||
free_cluster, free_entry, target_dir = self.allocate_object(name=name,
|
||||
entity_type=Directory.ATTR_DIRECTORY,
|
||||
path_from_root=path_from_root,
|
||||
object_timestamp_=object_timestamp_)
|
||||
def new_directory(
|
||||
self,
|
||||
name: str,
|
||||
parent: 'Directory',
|
||||
path_from_root: list[str] | None,
|
||||
object_timestamp_: datetime,
|
||||
) -> None:
|
||||
free_cluster, free_entry, target_dir = self.allocate_object(
|
||||
name=name,
|
||||
entity_type=Directory.ATTR_DIRECTORY,
|
||||
path_from_root=path_from_root,
|
||||
object_timestamp_=object_timestamp_,
|
||||
)
|
||||
|
||||
directory: 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)
|
||||
|
||||
def write_to_file(self, path: List[str], content: bytes) -> None:
|
||||
def write_to_file(self, path: list[str], content: bytes) -> None:
|
||||
"""
|
||||
Writes to file existing in the directory structure.
|
||||
|
||||
@@ -335,8 +372,9 @@ class Directory:
|
||||
"""
|
||||
entity_to_write: Entry = self.recursive_search(path, self)
|
||||
if isinstance(entity_to_write, File):
|
||||
clusters_cnt: int = required_clusters_count(cluster_size=self.fatfs_state.boot_sector_state.sector_size,
|
||||
content=content)
|
||||
clusters_cnt: int = required_clusters_count(
|
||||
cluster_size=self.fatfs_state.boot_sector_state.sector_size, content=content
|
||||
)
|
||||
self.fat.allocate_chain(entity_to_write.first_cluster, clusters_cnt)
|
||||
entity_to_write.write(content)
|
||||
else:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from typing import List
|
||||
|
||||
from .entry import Entry
|
||||
from .exceptions import NoFreeClusterException
|
||||
from .utils import build_name, convert_to_utf16_and_pad
|
||||
from .utils import build_name
|
||||
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).
|
||||
@@ -27,7 +27,7 @@ def get_required_lfn_entries_count(lfn_full_name: str) -> int:
|
||||
return entries_count
|
||||
|
||||
|
||||
def split_name_to_lfn_entries(name: str, entries: int) -> List[str]:
|
||||
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.
|
||||
@@ -35,10 +35,10 @@ def split_name_to_lfn_entries(name: str, entries: int) -> List[str]:
|
||||
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)]
|
||||
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]:
|
||||
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.
|
||||
@@ -53,25 +53,28 @@ def split_name_to_lfn_entry_blocks(name: str) -> List[bytes]:
|
||||
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
|
||||
max_entry_size: int = Entry.LDIR_Name1_SIZE + Entry.LDIR_Name2_SIZE + Entry.LDIR_Name3_SIZE
|
||||
assert len(name) <= max_entry_size
|
||||
blocks_: List[bytes] = [
|
||||
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)
|
||||
blocks_: list[bytes] = [
|
||||
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
|
||||
),
|
||||
]
|
||||
return blocks_
|
||||
|
||||
|
||||
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 chr(1).
|
||||
The short entry contains only the first characters of the file name plus a '~' suffix
|
||||
with hexadecimal sequence number, matching the gen_numname() algorithm in ff.c.
|
||||
|
||||
For seq <= 5 the suffix is the decimal-looking hex digit (e.g. ~1 .. ~5).
|
||||
For seq > 5 a CRC hash is used instead (handled by build_lfn_short_entry_name).
|
||||
|
||||
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'.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import argparse
|
||||
import binascii
|
||||
@@ -6,9 +6,6 @@ import os
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from construct import BitsInteger
|
||||
from construct import BitStruct
|
||||
@@ -32,7 +29,7 @@ BYTES_PER_DIRECTORY_ENTRY: int = 32
|
||||
UINT32_MAX: int = (1 << 32) - 1
|
||||
MAX_NAME_SIZE: int = 8
|
||||
MAX_EXT_SIZE: int = 3
|
||||
DATETIME = Tuple[int, int, int]
|
||||
DATETIME = tuple[int, int, int]
|
||||
FATFS_INCEPTION_YEAR: int = 1980
|
||||
|
||||
FATFS_INCEPTION: datetime = datetime(FATFS_INCEPTION_YEAR, 1, 1, 0, 0, 0, 0)
|
||||
@@ -53,13 +50,13 @@ SHORT_NAMES_ENCODING: str = 'utf-8'
|
||||
|
||||
# compatible with WL_SECTOR_SIZE
|
||||
# choices for WL are WL_SECTOR_SIZE_512 and WL_SECTOR_SIZE_4096
|
||||
ALLOWED_WL_SECTOR_SIZES: List[int] = [512, 4096]
|
||||
ALLOWED_SECTOR_SIZES: List[int] = [512, 1024, 2048, 4096]
|
||||
ALLOWED_WL_SECTOR_SIZES: list[int] = [512, 4096]
|
||||
ALLOWED_SECTOR_SIZES: list[int] = [512, 1024, 2048, 4096]
|
||||
|
||||
ALLOWED_SECTORS_PER_CLUSTER: List[int] = [1, 2, 4, 8, 16, 32, 64, 128]
|
||||
ALLOWED_SECTORS_PER_CLUSTER: list[int] = [1, 2, 4, 8, 16, 32, 64, 128]
|
||||
|
||||
|
||||
def crc32(input_values: List[int], crc: int) -> int:
|
||||
def crc32(input_values: list[int], crc: int) -> int:
|
||||
"""
|
||||
Name Polynomial Reversed? Init-value XOR-out
|
||||
crc32 0x104C11DB7 True 4294967295 (UINT32_MAX) 0xFFFFFFFF
|
||||
@@ -71,7 +68,9 @@ def number_of_clusters(number_of_sectors: int, sectors_per_cluster: int) -> int:
|
||||
return number_of_sectors // sectors_per_cluster
|
||||
|
||||
|
||||
def get_non_data_sectors_cnt(reserved_sectors_cnt: int, sectors_per_fat_cnt: int, fat_tables_cnt: int, root_dir_sectors_cnt: int) -> int:
|
||||
def get_non_data_sectors_cnt(
|
||||
reserved_sectors_cnt: int, sectors_per_fat_cnt: int, fat_tables_cnt: int, root_dir_sectors_cnt: int
|
||||
) -> int:
|
||||
return reserved_sectors_cnt + sectors_per_fat_cnt * fat_tables_cnt + root_dir_sectors_cnt
|
||||
|
||||
|
||||
@@ -90,7 +89,8 @@ def get_fat_sectors_count(clusters_count: int, sector_size: int) -> int:
|
||||
# number of byte halves
|
||||
cluster_s: int = fatfs_type_ // 4
|
||||
fat_size_bytes: int = (
|
||||
clusters_count * 2 + cluster_s) if fatfs_type_ == FAT16 else (clusters_count * 3 + 1) // 2 + cluster_s
|
||||
(clusters_count * 2 + cluster_s) if fatfs_type_ == FAT16 else (clusters_count * 3 + 1) // 2 + cluster_s
|
||||
)
|
||||
return (fat_size_bytes + sector_size - 1) // sector_size
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ def generate_4bytes_random() -> int:
|
||||
return uuid.uuid4().int & 0xFFFFFFFF
|
||||
|
||||
|
||||
def pad_string(content: str, size: Optional[int] = None, pad: int = PAD_CHAR) -> str:
|
||||
def pad_string(content: str, size: int | None = None, pad: int = PAD_CHAR) -> str:
|
||||
# cut string if longer and fill with pad character if shorter than size
|
||||
return content.ljust(size or len(content), chr(pad))[:size]
|
||||
|
||||
@@ -112,9 +112,45 @@ def right_strip_string(content: str, pad: int = PAD_CHAR) -> str:
|
||||
return content.rstrip(chr(pad))
|
||||
|
||||
|
||||
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 _gen_numname_suffix(seq: int, lfn: str) -> str:
|
||||
"""
|
||||
Generate the numeric tail suffix for a short filename entry, matching
|
||||
the logic of gen_numname() in ff.c.
|
||||
|
||||
For seq > 5, a CRC-based hash is computed from seq and the LFN to reduce
|
||||
collision probability. The suffix is rendered as hexadecimal digits
|
||||
(e.g. '~1', '~A', '~3F2') and always starts with '~'.
|
||||
"""
|
||||
if seq > 5:
|
||||
# Hash path: CRC16-CCITT seeded with seq, fed with LFN characters
|
||||
sreg = seq
|
||||
for ch in lfn:
|
||||
wc = ord(ch)
|
||||
for _ in range(16):
|
||||
sreg = (sreg << 1) + (wc & 1)
|
||||
wc >>= 1
|
||||
if sreg & 0x10000:
|
||||
sreg ^= 0x11021
|
||||
seq = sreg & 0xFFFF
|
||||
|
||||
# Convert seq to uppercase hexadecimal digits (no '0x' prefix)
|
||||
hex_str = format(seq, 'X')
|
||||
return '~' + hex_str
|
||||
|
||||
|
||||
def build_lfn_short_entry_name(name: str, extension: str, order: int, lfn: str = '') -> str:
|
||||
"""
|
||||
Build the 8.3 short entry name for a long filename entry.
|
||||
|
||||
Mirrors gen_numname() from ff.c: the suffix ('~' + hex digits) is built
|
||||
first, then the stem (beginning of the long name) is truncated to fit
|
||||
within MAX_NAME_SIZE (8) characters together with the suffix.
|
||||
"""
|
||||
suffix = _gen_numname_suffix(order, lfn)
|
||||
name_part = name[: MAX_NAME_SIZE - len(suffix)] + suffix
|
||||
padded_name = pad_string(content=name_part, size=MAX_NAME_SIZE)
|
||||
padded_ext = pad_string(extension[:MAX_EXT_SIZE], size=MAX_EXT_SIZE)
|
||||
return f'{padded_name}{padded_ext}'
|
||||
|
||||
|
||||
def lfn_checksum(short_entry_name: str) -> int:
|
||||
@@ -125,20 +161,18 @@ def lfn_checksum(short_entry_name: str) -> int:
|
||||
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
|
||||
checksum_result &= 0xFF
|
||||
return checksum_result
|
||||
|
||||
|
||||
def convert_to_utf16_and_pad(content: str,
|
||||
expected_size: int,
|
||||
pad: bytes = FULL_BYTE) -> bytes:
|
||||
def convert_to_utf16_and_pad(content: str, expected_size: int, pad: bytes = FULL_BYTE) -> 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):]
|
||||
encoded_content_utf16: bytes = content.encode(LONG_NAMES_ENCODING)[len(bom_utf16) :]
|
||||
return encoded_content_utf16.ljust(2 * expected_size, pad)
|
||||
|
||||
|
||||
def split_to_name_and_extension(full_name: str) -> Tuple[str, str]:
|
||||
def split_to_name_and_extension(full_name: str) -> tuple[str, str]:
|
||||
name, extension = os.path.splitext(full_name)
|
||||
return name, extension.replace('.', '')
|
||||
|
||||
@@ -147,9 +181,9 @@ def is_valid_fatfs_name(string: str) -> bool:
|
||||
return string == string.upper()
|
||||
|
||||
|
||||
def split_by_half_byte_12_bit_little_endian(value: int) -> Tuple[int, int, int]:
|
||||
def split_by_half_byte_12_bit_little_endian(value: int) -> tuple[int, int, int]:
|
||||
value_as_bytes: bytes = Int16ul.build(value)
|
||||
return value_as_bytes[0] & 0x0f, value_as_bytes[0] >> 4, value_as_bytes[1] & 0x0f
|
||||
return value_as_bytes[0] & 0x0F, value_as_bytes[0] >> 4, value_as_bytes[1] & 0x0F
|
||||
|
||||
|
||||
def merge_by_half_byte_12_bit_little_endian(v1: int, v2: int, v3: int) -> int:
|
||||
@@ -160,65 +194,73 @@ def build_byte(first_half: int, second_half: int) -> int:
|
||||
return (first_half << 4) | second_half
|
||||
|
||||
|
||||
def split_content_into_sectors(content: bytes, sector_size: int) -> List[bytes]:
|
||||
def split_content_into_sectors(content: bytes, sector_size: int) -> list[bytes]:
|
||||
result = []
|
||||
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])
|
||||
result.append(content[sector_size * i : (i + 1) * sector_size])
|
||||
return result
|
||||
|
||||
|
||||
def get_args_for_partition_generator(desc: str, wl: bool) -> argparse.Namespace:
|
||||
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',
|
||||
default='fatfs_image.img',
|
||||
help='Filename of the generated fatfs image')
|
||||
parser.add_argument('--partition_size',
|
||||
default=FATDefaults.SIZE,
|
||||
help='Size of the partition in bytes.' +
|
||||
('' if wl else ' Use `--partition_size detect` for detecting the minimal partition size.')
|
||||
)
|
||||
parser.add_argument('--sector_size',
|
||||
default=FATDefaults.SECTOR_SIZE,
|
||||
type=int,
|
||||
choices=ALLOWED_WL_SECTOR_SIZES if wl else ALLOWED_SECTOR_SIZES,
|
||||
help='Size of the partition in bytes')
|
||||
parser.add_argument('--sectors_per_cluster',
|
||||
default=1,
|
||||
type=int,
|
||||
choices=ALLOWED_SECTORS_PER_CLUSTER,
|
||||
help='Number of sectors per cluster')
|
||||
parser.add_argument('--root_entry_count',
|
||||
default=FATDefaults.ROOT_ENTRIES_COUNT,
|
||||
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('--use_default_datetime',
|
||||
action='store_true',
|
||||
help='For test purposes. If the flag is set the files are created with '
|
||||
'the default timestamp that is the 1st of January 1980')
|
||||
parser.add_argument('--fat_type',
|
||||
default=0,
|
||||
type=int,
|
||||
choices=[FAT12, FAT16, 0],
|
||||
help="""
|
||||
parser.add_argument('input_directory', help='Path to the directory that will be encoded into fatfs image')
|
||||
parser.add_argument('--output_file', default='fatfs_image.img', help='Filename of the generated fatfs image')
|
||||
parser.add_argument(
|
||||
'--partition_size',
|
||||
default=FATDefaults.SIZE,
|
||||
help='Size of the partition in bytes.'
|
||||
+ ('' if wl else ' Use `--partition_size detect` for detecting the minimal partition size.'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sector_size',
|
||||
default=FATDefaults.SECTOR_SIZE,
|
||||
type=int,
|
||||
choices=ALLOWED_WL_SECTOR_SIZES if wl else ALLOWED_SECTOR_SIZES,
|
||||
help='Size of the partition in bytes',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sectors_per_cluster',
|
||||
default=1,
|
||||
type=int,
|
||||
choices=ALLOWED_SECTORS_PER_CLUSTER,
|
||||
help='Number of sectors per cluster',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--root_entry_count', default=FATDefaults.ROOT_ENTRIES_COUNT, 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(
|
||||
'--use_default_datetime',
|
||||
action='store_true',
|
||||
help='For test purposes. If the flag is set the files are created with '
|
||||
'the default timestamp that is the 1st of January 1980',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fat_type',
|
||||
default=0,
|
||||
type=int,
|
||||
choices=[FAT12, FAT16, 0],
|
||||
help="""
|
||||
Type of the FAT file-system. Select '12' for FAT12, '16' for FAT16.
|
||||
Leave unset or select 0 for automatic file-system type detection.
|
||||
""")
|
||||
parser.add_argument('--fat_count',
|
||||
default=FATDefaults.FAT_TABLES_COUNT,
|
||||
type=int,
|
||||
choices=[1, 2],
|
||||
help='Number of file allocation tables (FATs) in the filesystem.')
|
||||
parser.add_argument('--wl_mode',
|
||||
default=None,
|
||||
type=str,
|
||||
choices=['safe', 'perf'],
|
||||
help='Wear levelling mode to use. Safe or performance. Only for sector size of 512')
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--fat_count',
|
||||
default=FATDefaults.FAT_TABLES_COUNT,
|
||||
type=int,
|
||||
choices=[1, 2],
|
||||
help='Number of file allocation tables (FATs) in the filesystem.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--wl_mode',
|
||||
default=None,
|
||||
type=str,
|
||||
choices=['safe', 'perf'],
|
||||
help='Wear levelling mode to use. Safe or performance. Only for sector size of 512',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.fat_type == 0:
|
||||
@@ -239,10 +281,7 @@ def read_filesystem(path: str) -> bytearray:
|
||||
return bytearray(fs_file.read())
|
||||
|
||||
|
||||
DATE_ENTRY = BitStruct(
|
||||
'year' / BitsInteger(7),
|
||||
'month' / BitsInteger(4),
|
||||
'day' / BitsInteger(5))
|
||||
DATE_ENTRY = BitStruct('year' / BitsInteger(7), 'month' / BitsInteger(4), 'day' / BitsInteger(5))
|
||||
|
||||
TIME_ENTRY = BitStruct(
|
||||
'hour' / BitsInteger(5),
|
||||
@@ -282,9 +321,8 @@ def build_time_entry(hour: int, minute: int, sec: int) -> int:
|
||||
assert hour in range(FATFS_MAX_HOURS)
|
||||
assert minute in range(FATFS_MAX_MINUTES)
|
||||
assert sec in range(FATFS_MAX_SECONDS)
|
||||
return int.from_bytes(TIME_ENTRY.build(
|
||||
dict(hour=hour, minute=minute, second=sec // FATFS_SECONDS_GRANULARITY)),
|
||||
byteorder='big'
|
||||
return int.from_bytes(
|
||||
TIME_ENTRY.build(dict(hour=hour, minute=minute, second=sec // FATFS_SECONDS_GRANULARITY)), byteorder='big'
|
||||
)
|
||||
|
||||
|
||||
@@ -297,14 +335,14 @@ class FATDefaults:
|
||||
SECTOR_SIZE: int = 0x1000
|
||||
HIDDEN_SECTORS: int = 0
|
||||
ENTRY_SIZE: int = 32
|
||||
NUM_HEADS: int = 0xff
|
||||
NUM_HEADS: int = 0xFF
|
||||
OEM_NAME: str = 'MSDOS5.0'
|
||||
SEC_PER_TRACK: int = 0x3f
|
||||
SEC_PER_TRACK: int = 0x3F
|
||||
VOLUME_LABEL: str = 'Espressif'
|
||||
FILE_SYS_TYPE: str = 'FAT'
|
||||
ROOT_ENTRIES_COUNT: int = 512 # number of entries in the root directory, recommended 512
|
||||
MEDIA_TYPE: int = 0xf8
|
||||
SIGNATURE_WORD: bytes = b'\x55\xAA'
|
||||
MEDIA_TYPE: int = 0xF8
|
||||
SIGNATURE_WORD: bytes = b'\x55\xaa'
|
||||
|
||||
# wear levelling defaults
|
||||
VERSION: int = 2
|
||||
|
||||
Reference in New Issue
Block a user