mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
fix(fatfs): fix a bug in SFN generation from LFN and rewrite to match C algorithm
Rewrite build_lfn_short_entry_name() and add _gen_numname_suffix() helper to match the gen_numname() algorithm in ff.c. This fixes: - chr(order) producing raw binary instead of ASCII digits - Collision for order >= 10 when str(order) makes the name exceed 8 chars - Hex suffix with dynamic stem shortening (matching C implementation) - CRC16-CCITT hash for seq > 5 to reduce collision probability Also fix LDIR_Name2_SIZE typo in long_filename_utils.py (should be LDIR_Name3_SIZE), which made the assertion guard too permissive. Add ShortFilenameGenerationTestCase with 9 unit tests covering single-digit, multi-digit, hash-based, and collision-free generation scenarios.
This commit is contained in:
@@ -208,7 +208,7 @@ class Directory:
|
||||
# type: (Entry, str, str, Directory, int, int, DATETIME, 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)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ 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],
|
||||
@@ -68,10 +68,11 @@ def split_name_to_lfn_entry_blocks(name: str) -> List[bytes]:
|
||||
|
||||
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
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user