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:
Martin Vychodil
2026-05-07 18:34:49 +08:00
4 changed files with 663 additions and 484 deletions
+156 -118
View File
@@ -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'.
+120 -82
View File
@@ -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