2022-01-02 19:02:16 -05:00
|
|
|
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
2021-09-21 18:32:54 -04:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2021-09-21 18:32:54 -04:00
|
|
|
import argparse
|
|
|
|
import binascii
|
2021-09-21 18:32:54 -04:00
|
|
|
import os
|
2021-09-21 18:32:54 -04:00
|
|
|
import uuid
|
2022-03-17 13:02:16 -04:00
|
|
|
from datetime import datetime
|
2021-09-21 18:32:54 -04:00
|
|
|
from typing import List, Optional, Tuple
|
2021-09-21 18:32:54 -04:00
|
|
|
|
2022-03-17 13:02:16 -04:00
|
|
|
from construct import BitsInteger, BitStruct, Int16ul
|
2021-09-21 18:32:54 -04:00
|
|
|
|
2021-12-06 12:17:20 -05:00
|
|
|
FAT12_MAX_CLUSTERS: int = 4085
|
|
|
|
FAT16_MAX_CLUSTERS: int = 65525
|
|
|
|
FAT12: int = 12
|
|
|
|
FAT16: int = 16
|
|
|
|
FAT32: int = 32
|
2022-02-09 10:09:09 -05:00
|
|
|
BYTES_PER_DIRECTORY_ENTRY: int = 32
|
|
|
|
UINT32_MAX: int = (1 << 32) - 1
|
|
|
|
MAX_NAME_SIZE: int = 8
|
|
|
|
MAX_EXT_SIZE: int = 3
|
2022-03-17 13:02:16 -04:00
|
|
|
DATETIME = Tuple[int, int, int]
|
|
|
|
FATFS_INCEPTION: datetime = datetime(1980, 1, 1, 0, 0, 0, 0)
|
2022-02-09 10:09:09 -05:00
|
|
|
|
|
|
|
# long names are encoded to two bytes in utf-16
|
|
|
|
LONG_NAMES_ENCODING: str = 'utf-16'
|
2022-03-17 13:02:16 -04:00
|
|
|
SHORT_NAMES_ENCODING: str = 'utf-8'
|
2021-12-06 12:17:20 -05:00
|
|
|
|
2021-09-21 18:32:54 -04:00
|
|
|
|
2021-09-21 18:32:54 -04:00
|
|
|
def crc32(input_values: List[int], crc: int) -> int:
|
|
|
|
"""
|
|
|
|
Name Polynomial Reversed? Init-value XOR-out
|
|
|
|
crc32 0x104C11DB7 True 4294967295 (UINT32_MAX) 0xFFFFFFFF
|
|
|
|
"""
|
|
|
|
return binascii.crc32(bytearray(input_values), crc)
|
|
|
|
|
|
|
|
|
2021-12-06 12:17:20 -05:00
|
|
|
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, root_dir_sectors_cnt: int) -> int:
|
|
|
|
return reserved_sectors_cnt + sectors_per_fat_cnt + root_dir_sectors_cnt
|
|
|
|
|
|
|
|
|
|
|
|
def get_fatfs_type(clusters_count: int) -> int:
|
|
|
|
if clusters_count < FAT12_MAX_CLUSTERS:
|
|
|
|
return FAT12
|
2022-02-09 10:09:09 -05:00
|
|
|
if clusters_count <= FAT16_MAX_CLUSTERS:
|
2021-12-06 12:17:20 -05:00
|
|
|
return FAT16
|
|
|
|
return FAT32
|
|
|
|
|
|
|
|
|
2022-01-02 19:02:16 -05:00
|
|
|
def required_clusters_count(cluster_size: int, content: bytes) -> int:
|
2021-09-21 18:32:54 -04:00
|
|
|
# compute number of required clusters for file text
|
|
|
|
return (len(content) + cluster_size - 1) // cluster_size
|
|
|
|
|
|
|
|
|
2021-09-21 18:32:54 -04:00
|
|
|
def generate_4bytes_random() -> int:
|
|
|
|
return uuid.uuid4().int & 0xFFFFFFFF
|
|
|
|
|
|
|
|
|
|
|
|
def pad_string(content: str, size: Optional[int] = None, pad: int = 0x20) -> str:
|
2021-09-21 18:32:54 -04:00
|
|
|
# cut string if longer and fill with pad character if shorter than size
|
|
|
|
return content.ljust(size or len(content), chr(pad))[:size]
|
|
|
|
|
|
|
|
|
2022-02-09 10:09:09 -05:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2021-09-21 18:32:54 -04:00
|
|
|
def split_to_name_and_extension(full_name: str) -> Tuple[str, str]:
|
2021-09-21 18:32:54 -04:00
|
|
|
name, extension = os.path.splitext(full_name)
|
|
|
|
return name, extension.replace('.', '')
|
|
|
|
|
|
|
|
|
|
|
|
def is_valid_fatfs_name(string: str) -> bool:
|
|
|
|
return string == string.upper()
|
|
|
|
|
|
|
|
|
2022-03-17 13:02:16 -04:00
|
|
|
def split_by_half_byte_12_bit_little_endian(value: int) -> DATETIME:
|
2022-02-09 10:09:09 -05:00
|
|
|
value_as_bytes: bytes = Int16ul.build(value)
|
2021-09-21 18:32:54 -04:00
|
|
|
return value_as_bytes[0] & 0x0f, value_as_bytes[0] >> 4, value_as_bytes[1] & 0x0f
|
|
|
|
|
|
|
|
|
|
|
|
def build_byte(first_half: int, second_half: int) -> int:
|
|
|
|
return (first_half << 4) | second_half
|
|
|
|
|
|
|
|
|
|
|
|
def clean_first_half_byte(bytes_array: bytearray, address: int) -> None:
|
|
|
|
"""
|
|
|
|
the function sets to zero first four bits of the byte.
|
|
|
|
E.g. 10111100 -> 10110000
|
|
|
|
"""
|
|
|
|
bytes_array[address] &= 0xf0
|
|
|
|
|
|
|
|
|
|
|
|
def clean_second_half_byte(bytes_array: bytearray, address: int) -> None:
|
|
|
|
"""
|
|
|
|
the function sets to zero last four bits of the byte.
|
|
|
|
E.g. 10111100 -> 00001100
|
|
|
|
"""
|
|
|
|
bytes_array[address] &= 0x0f
|
|
|
|
|
|
|
|
|
2022-01-02 19:02:16 -05:00
|
|
|
def split_content_into_sectors(content: bytes, sector_size: int) -> List[bytes]:
|
2021-09-21 18:32:54 -04:00
|
|
|
result = []
|
2022-02-09 10:09:09 -05:00
|
|
|
clusters_cnt: int = required_clusters_count(cluster_size=sector_size, content=content)
|
2021-09-21 18:32:54 -04:00
|
|
|
|
|
|
|
for i in range(clusters_cnt):
|
|
|
|
result.append(content[sector_size * i:(i + 1) * sector_size])
|
|
|
|
return result
|
2021-09-21 18:32:54 -04:00
|
|
|
|
|
|
|
|
|
|
|
def get_args_for_partition_generator(desc: str) -> argparse.Namespace:
|
2022-02-09 10:09:09 -05:00
|
|
|
parser: argparse.ArgumentParser = argparse.ArgumentParser(description=desc)
|
2021-09-21 18:32:54 -04:00
|
|
|
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=1024 * 1024,
|
|
|
|
help='Size of the partition in bytes')
|
|
|
|
parser.add_argument('--sector_size',
|
|
|
|
default=4096,
|
2021-12-06 12:17:20 -05:00
|
|
|
type=int,
|
|
|
|
choices=[512, 1024, 2048, 4096],
|
2021-09-21 18:32:54 -04:00
|
|
|
help='Size of the partition in bytes')
|
2021-12-06 12:17:20 -05:00
|
|
|
parser.add_argument('--sectors_per_cluster',
|
|
|
|
default=1,
|
|
|
|
type=int,
|
|
|
|
choices=[1, 2, 4, 8, 16, 32, 64, 128],
|
|
|
|
help='Number of sectors per cluster')
|
|
|
|
parser.add_argument('--root_entry_count',
|
|
|
|
default=512,
|
|
|
|
help='Number of entries in the root directory')
|
2022-02-09 10:09:09 -05:00
|
|
|
parser.add_argument('--long_name_support',
|
|
|
|
action='store_true',
|
|
|
|
help='Set flag to enable long names support.')
|
2022-03-17 13:02:16 -04:00
|
|
|
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')
|
2021-12-06 12:17:20 -05:00
|
|
|
parser.add_argument('--fat_type',
|
|
|
|
default=0,
|
|
|
|
type=int,
|
|
|
|
choices=[12, 16, 0],
|
|
|
|
help="""
|
|
|
|
Type of fat. Select 12 for fat12, 16 for fat16. Don't set, or set to 0 for automatic
|
|
|
|
calculation using cluster size and partition size.
|
|
|
|
""")
|
|
|
|
|
2021-09-21 18:32:54 -04:00
|
|
|
args = parser.parse_args()
|
2021-12-06 12:17:20 -05:00
|
|
|
if args.fat_type == 0:
|
|
|
|
args.fat_type = None
|
|
|
|
args.partition_size = int(str(args.partition_size), 0)
|
2021-09-21 18:32:54 -04:00
|
|
|
if not os.path.isdir(args.input_directory):
|
|
|
|
raise NotADirectoryError(f'The target directory `{args.input_directory}` does not exist!')
|
|
|
|
return args
|
2021-12-06 12:17:20 -05:00
|
|
|
|
|
|
|
|
|
|
|
def read_filesystem(path: str) -> bytearray:
|
|
|
|
with open(path, 'rb') as fs_file:
|
|
|
|
return bytearray(fs_file.read())
|
2022-03-17 13:02:16 -04:00
|
|
|
|
|
|
|
|
|
|
|
DATE_ENTRY = BitStruct(
|
|
|
|
'year' / BitsInteger(7),
|
|
|
|
'month' / BitsInteger(4),
|
|
|
|
'day' / BitsInteger(5))
|
|
|
|
|
|
|
|
TIME_ENTRY = BitStruct(
|
|
|
|
'hour' / BitsInteger(5),
|
|
|
|
'minute' / BitsInteger(6),
|
|
|
|
'second' / BitsInteger(5),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def build_date_entry(year: int, mon: int, mday: int) -> int:
|
|
|
|
"""
|
|
|
|
:param year: denotes year starting from 1980 (0 ~ 1980, 1 ~ 1981, etc), valid values are 1980 + 0..127 inclusive
|
|
|
|
thus theoretically 1980 - 2107
|
|
|
|
:param mon: denotes number of month of year in common order (1 ~ January, 2 ~ February, etc.),
|
|
|
|
valid values: 1..12 inclusive
|
|
|
|
:param mday: denotes number of day in month, valid values are 1..31 inclusive
|
|
|
|
|
|
|
|
:returns: 16 bit integer number (7 bits for year, 4 bits for month and 5 bits for day of the month)
|
|
|
|
"""
|
|
|
|
assert year in range(1980, 2107)
|
|
|
|
assert mon in range(1, 13)
|
|
|
|
assert mday in range(1, 32)
|
|
|
|
return int.from_bytes(DATE_ENTRY.build(dict(year=year - 1980, month=mon, day=mday)), 'big')
|
|
|
|
|
|
|
|
|
|
|
|
def build_time_entry(hour: int, minute: int, sec: int) -> int:
|
|
|
|
"""
|
|
|
|
:param hour: denotes number of hour, valid values are 0..23 inclusive
|
|
|
|
:param minute: denotes minutes, valid range 0..59 inclusive
|
|
|
|
:param sec: denotes seconds with granularity 2 seconds (e.g. 1 ~ 2, 29 ~ 58), valid range 0..29 inclusive
|
|
|
|
|
|
|
|
:returns: 16 bit integer number (5 bits for hour, 6 bits for minute and 5 bits for second)
|
|
|
|
"""
|
|
|
|
assert hour in range(0, 23)
|
|
|
|
assert minute in range(0, 60)
|
|
|
|
assert sec in range(0, 60)
|
|
|
|
return int.from_bytes(TIME_ENTRY.build(dict(hour=hour, minute=minute, second=sec // 2)), 'big')
|