# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 from inspect import getmembers, isroutine from typing import Optional from construct import Const, Int8ul, Int16ul, Int32ul, PaddedString, Struct from .exceptions import InconsistentFATAttributes, NotInitialized from .fatfs_state import BootSectorState from .utils import (ALLOWED_SECTOR_SIZES, ALLOWED_SECTORS_PER_CLUSTER, EMPTY_BYTE, FAT32, FULL_BYTE, SHORT_NAMES_ENCODING, FATDefaults, generate_4bytes_random, pad_string) class BootSector: MAX_VOL_LAB_SIZE = 11 MAX_OEM_NAME_SIZE = 8 MAX_FS_TYPE_SIZE = 8 # the FAT specification defines 512 bytes for the boot sector header BOOT_HEADER_SIZE = 512 BOOT_SECTOR_HEADER = Struct( 'BS_jmpBoot' / Const(b'\xeb\xfe\x90'), 'BS_OEMName' / PaddedString(MAX_OEM_NAME_SIZE, SHORT_NAMES_ENCODING), 'BPB_BytsPerSec' / Int16ul, 'BPB_SecPerClus' / Int8ul, 'BPB_RsvdSecCnt' / Int16ul, 'BPB_NumFATs' / Int8ul, 'BPB_RootEntCnt' / Int16ul, 'BPB_TotSec16' / Int16ul, 'BPB_Media' / Int8ul, 'BPB_FATSz16' / Int16ul, # for FAT32 always zero, for FAT12/FAT16 number of sectors per FAT 'BPB_SecPerTrk' / Int16ul, 'BPB_NumHeads' / Int16ul, 'BPB_HiddSec' / Int32ul, 'BPB_TotSec32' / Int32ul, 'BS_DrvNum' / Const(b'\x80'), 'BS_Reserved1' / Const(EMPTY_BYTE), 'BS_BootSig' / Const(b'\x29'), 'BS_VolID' / Int32ul, 'BS_VolLab' / PaddedString(MAX_VOL_LAB_SIZE, SHORT_NAMES_ENCODING), 'BS_FilSysType' / PaddedString(MAX_FS_TYPE_SIZE, SHORT_NAMES_ENCODING), 'BS_EMPTY' / Const(448 * EMPTY_BYTE), 'Signature_word' / Const(FATDefaults.SIGNATURE_WORD) ) assert BOOT_SECTOR_HEADER.sizeof() == BOOT_HEADER_SIZE def __init__(self, boot_sector_state: Optional[BootSectorState] = None) -> None: self._parsed_header = None self.boot_sector_state: BootSectorState = boot_sector_state def generate_boot_sector(self) -> None: boot_sector_state: BootSectorState = self.boot_sector_state if boot_sector_state is None: raise NotInitialized('The BootSectorState instance is not initialized!') volume_uuid = generate_4bytes_random() pad_header: bytes = (boot_sector_state.sector_size - BootSector.BOOT_HEADER_SIZE) * EMPTY_BYTE data_content: bytes = boot_sector_state.data_sectors * boot_sector_state.sector_size * FULL_BYTE root_dir_content: bytes = boot_sector_state.root_dir_sectors_cnt * boot_sector_state.sector_size * EMPTY_BYTE fat_tables_content: bytes = (boot_sector_state.sectors_per_fat_cnt * boot_sector_state.fat_tables_cnt * boot_sector_state.sector_size * EMPTY_BYTE) self.boot_sector_state.binary_image = ( BootSector.BOOT_SECTOR_HEADER.build( dict(BS_OEMName=pad_string(boot_sector_state.oem_name, size=BootSector.MAX_OEM_NAME_SIZE), BPB_BytsPerSec=boot_sector_state.sector_size, BPB_SecPerClus=boot_sector_state.sectors_per_cluster, BPB_RsvdSecCnt=boot_sector_state.reserved_sectors_cnt, BPB_NumFATs=boot_sector_state.fat_tables_cnt, BPB_RootEntCnt=boot_sector_state.entries_root_count, # if fat type is 12 or 16 BPB_TotSec16 is filled and BPB_TotSec32 is 0x00 and vice versa BPB_TotSec16=0x00 if boot_sector_state.fatfs_type == FAT32 else boot_sector_state.sectors_count, BPB_Media=boot_sector_state.media_type, BPB_FATSz16=boot_sector_state.sectors_per_fat_cnt, BPB_SecPerTrk=boot_sector_state.sec_per_track, BPB_NumHeads=boot_sector_state.num_heads, BPB_HiddSec=boot_sector_state.hidden_sectors, BPB_TotSec32=boot_sector_state.sectors_count if boot_sector_state.fatfs_type == FAT32 else 0x00, BS_VolID=volume_uuid, BS_VolLab=pad_string(boot_sector_state.volume_label, size=BootSector.MAX_VOL_LAB_SIZE), BS_FilSysType=pad_string(boot_sector_state.file_sys_type, size=BootSector.MAX_FS_TYPE_SIZE) ) ) + pad_header + fat_tables_content + root_dir_content + data_content ) def parse_boot_sector(self, binary_data: bytes) -> None: self._parsed_header = BootSector.BOOT_SECTOR_HEADER.parse(binary_data) if self._parsed_header is None: raise NotInitialized('The boot sector header is not parsed successfully!') if self._parsed_header['BPB_TotSec16'] != 0x00: sectors_count_: int = self._parsed_header['BPB_TotSec16'] elif self._parsed_header['BPB_TotSec32'] != 0x00: # uncomment for FAT32 implementation # sectors_count_ = self._parsed_header['BPB_TotSec32'] # possible_fat_types = [FAT32] assert self._parsed_header['BPB_FATSz16'] == 0 raise NotImplementedError('FAT32 not implemented!') else: raise InconsistentFATAttributes('The number of FS sectors cannot be zero!') # in the current code assigning self._parsed_header['BPB_TotSec32'] is not reachable # the option to assign it is kept for possibility to implement FAT32 sectors_per_fat_cnt_ = self._parsed_header['BPB_FATSz16'] or self._parsed_header['BPB_TotSec32'] if self._parsed_header['BPB_BytsPerSec'] not in ALLOWED_SECTOR_SIZES: raise InconsistentFATAttributes(f'The number of bytes ' f"per sector is {self._parsed_header['BPB_BytsPerSec']}! " f'The accepted values are {ALLOWED_SECTOR_SIZES}') if self._parsed_header['BPB_SecPerClus'] not in ALLOWED_SECTORS_PER_CLUSTER: raise InconsistentFATAttributes(f'The number of sectors per cluster ' f"is {self._parsed_header['BPB_SecPerClus']}" f'The accepted values are {ALLOWED_SECTORS_PER_CLUSTER}') total_root_bytes: int = self._parsed_header['BPB_RootEntCnt'] * FATDefaults.ENTRY_SIZE root_dir_sectors_cnt_: int = total_root_bytes // self._parsed_header['BPB_BytsPerSec'] self.boot_sector_state = BootSectorState(oem_name=self._parsed_header['BS_OEMName'], sector_size=self._parsed_header['BPB_BytsPerSec'], sectors_per_cluster=self._parsed_header['BPB_SecPerClus'], reserved_sectors_cnt=self._parsed_header['BPB_RsvdSecCnt'], fat_tables_cnt=self._parsed_header['BPB_NumFATs'], root_dir_sectors_cnt=root_dir_sectors_cnt_, sectors_count=sectors_count_, media_type=self._parsed_header['BPB_Media'], sectors_per_fat_cnt=sectors_per_fat_cnt_, sec_per_track=self._parsed_header['BPB_SecPerTrk'], num_heads=self._parsed_header['BPB_NumHeads'], hidden_sectors=self._parsed_header['BPB_HiddSec'], volume_label=self._parsed_header['BS_VolLab'], file_sys_type=self._parsed_header['BS_FilSysType'], volume_uuid=self._parsed_header['BS_VolID']) self.boot_sector_state.binary_image = binary_data assert self.boot_sector_state.file_sys_type in (f'FAT{self.boot_sector_state.fatfs_type} ', 'FAT ') def __str__(self) -> str: if self._parsed_header is None: return 'Boot sector is not initialized!' res: str = 'Properties of the FATFS:\n' for member in getmembers(self.boot_sector_state, lambda a: not(isroutine(a))): prop_ = getattr(self.boot_sector_state, member[0]) if isinstance(prop_, int) or isinstance(prop_, str) and not member[0].startswith('_'): res += f'{member[0]}: {prop_}\n' return res @property def binary_image(self) -> bytes: if len(self.boot_sector_state.binary_image) == 0: raise NotInitialized('Boot sector is not generated nor initialized!') bin_image_: bytes = self.boot_sector_state.binary_image return bin_image_