diff --git a/Kernel/FileSystem/FATFS/Definitions.h b/Kernel/FileSystem/FATFS/Definitions.h index 938fc064b3..a4d9675fcf 100644 --- a/Kernel/FileSystem/FATFS/Definitions.h +++ b/Kernel/FileSystem/FATFS/Definitions.h @@ -9,27 +9,60 @@ #include #include #include +#include namespace Kernel { -struct [[gnu::packed]] FAT32BootRecord { +// This structure represents the DOS 3.31 BIOS Partition Block. +// While DOS 3.31 predates FAT verions 12/16/32 (the versions supported by this driver), +// the fields in this block are common with the DOS 4 and DOS 7 BIOS Parameter blocks. +// This structure will be followed by an "Extended BIOS Partition Block" (EBPB). +// +// The DOS 4 EBPB is *typically* used by FAT 12/16 file systems, while the DOS 7 EBPB +// is *typically* used by FAT 32. _However_, any combination is possible, as the FAT +// version is only determined by the number of clusters. +// +// Note that the DOS 4 and DOS 7 EBPB extensions are incompatible with each other +// (contain fields in different orders and of different lenghts) and do not contain +// an explicit indication to differentiate them. +// This driver uses heuristics to identify the EBPB version (based on the signature bytes +// and sector counts). +// FIXME: Consider also using the MBR parition type field in the future. +struct [[gnu::packed]] DOS3BIOSParameterBlock { u8 boot_jump[3]; char oem_identifier[8]; - u16 bytes_per_sector; + u16 bytes_per_sector; // Offset 0x0B -- beginning of DOS 3.31 BPB. u8 sectors_per_cluster; u16 reserved_sector_count; u8 fat_count; u16 root_directory_entry_count; - u16 unused1; + u16 sector_count_16bit; u8 media_descriptor_type; - u16 unused2; + u16 sectors_per_fat_16bit; u16 sectors_per_track; u16 head_count; u32 hidden_sector_count; - u32 sector_count; - u32 sectors_per_fat; + u32 sector_count_32bit; // 0x020 -- end of DOS 3.31 BPB. +}; +// 11 is the boot jump/OEM identifier prefix prior to the official BPB. +static_assert(sizeof(DOS3BIOSParameterBlock) == 11 + 25); + +struct [[gnu::packed]] DOS4BIOSParameterBlock { + // Begins at sector offset 0x024. + u8 drive_number; // 0x024 + u8 flags; + u8 signature; + u32 volume_id; + char volume_label_string[11]; + char file_system_type[8]; +}; +static_assert(sizeof(DOS4BIOSParameterBlock) == 26); + +struct [[gnu::packed]] DOS7BIOSParameterBlock { + // Begins at sector offset 0x024. + u32 sectors_per_fat_32bit; // 0x024 u16 flags; - u16 fat_version; + u16 fat_version; // Expected value 0x2b2a. u32 root_directory_cluster; u16 fs_info_sector; u16 backup_boot_sector; @@ -39,9 +72,22 @@ struct [[gnu::packed]] FAT32BootRecord { u8 signature; u32 volume_id; char volume_label_string[11]; - char system_identifier_string[8]; + char file_system_type[8]; +}; +static_assert(sizeof(DOS7BIOSParameterBlock) == 54); + +enum DOSBIOSParameterBlockVersion { + DOS_BPB_UNKNOWN, + DOS_BPB_3, // Version 3.4. + DOS_BPB_4, // Version 4.0 + DOS_BPB_7 // Version 7.0 +}; + +enum class FATVersion { + FAT12, + FAT16, + FAT32, }; -static_assert(sizeof(FAT32BootRecord) == 90); enum class FATAttributes : u8 { ReadOnly = 0x01, diff --git a/Kernel/FileSystem/FATFS/FileSystem.cpp b/Kernel/FileSystem/FATFS/FileSystem.cpp index 70fc4b91f7..5db4c61bc9 100644 --- a/Kernel/FileSystem/FATFS/FileSystem.cpp +++ b/Kernel/FileSystem/FATFS/FileSystem.cpp @@ -10,6 +10,85 @@ namespace Kernel { +DOSBIOSParameterBlockVersion DOSBIOSParameterBlock::bpb_version() const +{ + bool dos3_valid = m_dos4_block->signature == 0x28; + bool dos4_valid = m_dos4_block->signature == 0x29; + bool dos7_valid = m_dos7_block->signature == 0x28 || m_dos7_block->signature == 0x29; + // A DOS 7 EBPB should _never_ contain the values 0x28 or 0x29 at + // the offset associated with `m_dos4_block->signature` + // (aka `m_dos7_block->sectors_per_fat_32bit`) due to the maximum number of + // clusters ensuring the number of sectors per fat will not exceed 0x200000. + // As a result, it should be safe to determine BPB version through the + // signature fields by checking the DOS 4 signature offset prior to the DOS 7 one. + // + // With a DOS 3 or DOS 4 EBPB, the DOS 7 signature offset references uninitialized + // space. While unlikely to be set to a valid signature value, it is not implausible. + // We warn the user here, but because it does not represent an invalid FS configuration, + // do not error. + if ((dos3_valid || dos4_valid) && dos7_valid) + dbgln("FATFS: DOS 4 and DOS 7 EBPB signatures detected, EBPB/FAT version detection may be incorrect."); + + if (dos3_valid) + return DOS_BPB_3; + else if (dos4_valid) + return DOS_BPB_4; + else if (dos7_valid) + return DOS_BPB_7; + else + return DOS_BPB_UNKNOWN; +} + +DOS3BIOSParameterBlock const* DOSBIOSParameterBlock::common_bpb() const +{ + return m_common_block; +} + +DOS4BIOSParameterBlock const* DOSBIOSParameterBlock::dos4_bpb() const +{ + // Only return parameter block if signature indicates this portion of the block is filled out. + if (m_dos4_block->signature == 0x28 || m_dos4_block->signature == 0x29) + return m_dos4_block; + else + return nullptr; +} + +DOS7BIOSParameterBlock const* DOSBIOSParameterBlock::dos7_bpb() const +{ + // Only return parameter block if signature indicates this portion of the block is filled out. + if (m_dos7_block->signature == 0x28 || m_dos7_block->signature == 0x29) + return m_dos7_block; + else + return nullptr; +} + +u16 DOSBIOSParameterBlock::sectors_per_fat() const +{ + return common_bpb()->sectors_per_fat_16bit != 0 ? common_bpb()->sectors_per_fat_16bit : m_dos7_block->sectors_per_fat_32bit; +} + +u32 DOSBIOSParameterBlock::sector_count() const +{ + if (common_bpb()->sector_count_16bit != 0) { + // The `16bit` field is only used on partitions smaller than 32 MB, + // and never for FAT32. + // It is set to `0` when the 32 bit field contains the sector count. + return common_bpb()->sector_count_16bit; + } else { + return common_bpb()->sector_count_32bit; + // FIXME: If this is 0 for a FAT32 EBPB with a signature of 0x29, + // read 0x052, which is a 64-bit wide sector count. + } +} + +u8 DOSBIOSParameterBlock::signature() const +{ + if (bpb_version() == DOS_BPB_3 || bpb_version() == DOS_BPB_4) + return m_dos4_block->signature; + else + return m_dos7_block->signature; +} + ErrorOr> FATFS::try_create(OpenFileDescription& file_description, ReadonlyBytes) { return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) FATFS(file_description))); @@ -34,46 +113,126 @@ ErrorOr FATFS::initialize_while_locked() m_boot_record = TRY(KBuffer::try_create_with_size("FATFS: Boot Record"sv, m_device_block_size)); auto boot_record_buffer = UserOrKernelBuffer::for_kernel_buffer(m_boot_record->data()); TRY(raw_read(0, boot_record_buffer)); + m_parameter_block = TRY(adopt_nonnull_own_or_enomem(new (nothrow) DOSBIOSParameterBlock(m_boot_record))); + + // Alias for extended BPB. + DOSBIOSParameterBlock& ebpb = *m_parameter_block; + // Alias for block of common parameters in BPB. + DOS3BIOSParameterBlock const* block = ebpb.common_bpb(); if constexpr (FAT_DEBUG) { - dbgln("FATFS: oem_identifier: {}", boot_record()->oem_identifier); - dbgln("FATFS: bytes_per_sector: {}", boot_record()->bytes_per_sector); - dbgln("FATFS: sectors_per_cluster: {}", boot_record()->sectors_per_cluster); - dbgln("FATFS: reserved_sector_count: {}", boot_record()->reserved_sector_count); - dbgln("FATFS: fat_count: {}", boot_record()->fat_count); - dbgln("FATFS: root_directory_entry_count: {}", boot_record()->root_directory_entry_count); - dbgln("FATFS: media_descriptor_type: {}", boot_record()->media_descriptor_type); - dbgln("FATFS: sectors_per_track: {}", boot_record()->sectors_per_track); - dbgln("FATFS: head_count: {}", boot_record()->head_count); - dbgln("FATFS: hidden_sector_count: {}", boot_record()->hidden_sector_count); - dbgln("FATFS: sector_count: {}", boot_record()->sector_count); - dbgln("FATFS: sectors_per_fat: {}", boot_record()->sectors_per_fat); - dbgln("FATFS: flags: {}", boot_record()->flags); - dbgln("FATFS: fat_version: {}", boot_record()->fat_version); - dbgln("FATFS: root_directory_cluster: {}", boot_record()->root_directory_cluster); - dbgln("FATFS: fs_info_sector: {}", boot_record()->fs_info_sector); - dbgln("FATFS: backup_boot_sector: {}", boot_record()->backup_boot_sector); - dbgln("FATFS: drive_number: {}", boot_record()->drive_number); - dbgln("FATFS: volume_id: {}", boot_record()->volume_id); + dbgln("FATFS: oem_identifier: {}", block->oem_identifier); + dbgln("FATFS: bytes_per_sector: {}", block->bytes_per_sector); + dbgln("FATFS: sectors_per_cluster: {}", block->sectors_per_cluster); + dbgln("FATFS: reserved_sector_count: {}", block->reserved_sector_count); + dbgln("FATFS: fat_count: {}", block->fat_count); + dbgln("FATFS: root_directory_entry_count: {}", block->root_directory_entry_count); + dbgln("FATFS: media_descriptor_type: {}", block->media_descriptor_type); + dbgln("FATFS: sectors_per_track: {}", block->sectors_per_track); + dbgln("FATFS: head_count: {}", block->head_count); + dbgln("FATFS: hidden_sector_count: {}", block->hidden_sector_count); + dbgln("FATFS: sector_count: {}", ebpb.sector_count()); + dbgln("FATFS: sectors_per_fat: {}", ebpb.sectors_per_fat()); + + auto ebpb_version = ebpb.bpb_version(); + if (ebpb_version == DOSBIOSParameterBlockVersion::DOS_BPB_7) { + DOS7BIOSParameterBlock const* dos7_boot_record = ebpb.dos7_bpb(); + dbgln("FATFS: EBPB: DOS 7"); + dbgln("FATFS: flags: {}", dos7_boot_record->flags); + dbgln("FATFS: fat_version: {}", dos7_boot_record->fat_version); + dbgln("FATFS: root_directory_cluster: {}", dos7_boot_record->root_directory_cluster); + dbgln("FATFS: fs_info_sector: {}", dos7_boot_record->fs_info_sector); + dbgln("FATFS: backup_boot_sector: {}", dos7_boot_record->backup_boot_sector); + dbgln("FATFS: drive_number: {}", dos7_boot_record->drive_number); + dbgln("FATFS: volume_id: {}", dos7_boot_record->volume_id); + } else if (ebpb_version == DOSBIOSParameterBlockVersion::DOS_BPB_3 || ebpb_version == DOSBIOSParameterBlockVersion::DOS_BPB_4) { + DOS4BIOSParameterBlock const* dos4_boot_record = ebpb.dos4_bpb(); + if (ebpb_version == DOSBIOSParameterBlockVersion::DOS_BPB_3) { + dbgln("FATFS: EBPB: DOS 3.4"); + } else if (ebpb_version == DOSBIOSParameterBlockVersion::DOS_BPB_4) { + dbgln("FATFS: EBPB: DOS 4"); + } + dbgln("FATFS: drive_number: {}", dos4_boot_record->drive_number); + dbgln("FATFS: flags: {}", dos4_boot_record->flags); + dbgln("FATFS: volume_id: {}", dos4_boot_record->volume_id); + + // volume_label_string and file_system_type are only valid when + // ebpb_version == DOSBIOSParameterBlockVersion::DOS4. + } } - if (boot_record()->signature != signature_1 && boot_record()->signature != signature_2) { + if (ebpb.signature() != signature_1 && ebpb.signature() != signature_2) { dbgln("FATFS: Invalid signature"); return EINVAL; } - m_device_block_size = boot_record()->bytes_per_sector; + DOS3BIOSParameterBlock const* ebpb_block = ebpb.common_bpb(); + // The number of data area sectors is what DOS/Windows used to determine + // if a partition was a FAT12, FAT16, or FAT32 file system. + // From "FAT Type Determination" section of Microsoft FAT Specification + // (fatgen103.doc): + // The FAT type—one of FAT12, FAT16, or FAT32—is determined by the count + // of clusters on the volume and nothing else. + // + // The following calculations are based on the equations provided in this + // section. + + // "RootDirSectors" from MS FAT Specification. This is calculated as: + // Number of bytes occupied by root directory area (0 on FAT32) + // + + // Bytes to fill final sector (ie, round up) + // Converted into sector count (by dividing by bytes per sector). + u32 root_directory_sectors = ((ebpb_block->root_directory_entry_count * sizeof(FATEntry)) + (ebpb_block->bytes_per_sector - 1)) / ebpb_block->bytes_per_sector; + + // "DataSec" from MS FAT Specification. + u32 data_area_sectors = ebpb.sector_count() - ((ebpb_block->reserved_sector_count) + (ebpb_block->fat_count * ebpb.sectors_per_fat()) + root_directory_sectors); + + // CountofClusters from MS FAT Specification. + u32 data_area_clusters = data_area_sectors / ebpb_block->sectors_per_cluster; + + // Cluster thresholds and operators as defined in MS FAT Specification. + if (data_area_clusters < 4085) { + dbgln("FATFS: Detected FAT12 with {} data area clusters", data_area_clusters); + m_fat_version = FATVersion::FAT12; + } else if (data_area_clusters < 65525) { + dbgln("FATFS: Detected FAT16 with {} data area clusters", data_area_clusters); + m_fat_version = FATVersion::FAT16; + } else { + dbgln("FATFS: Assuming FAT32 with {} data area clusters", data_area_clusters); + m_fat_version = FATVersion::FAT32; + } + + m_device_block_size = ebpb_block->bytes_per_sector; set_logical_block_size(m_device_block_size); - u32 root_directory_sectors = ((boot_record()->root_directory_entry_count * sizeof(FATEntry)) + (m_device_block_size - 1)) / m_device_block_size; - m_first_data_sector = boot_record()->reserved_sector_count + (boot_record()->fat_count * boot_record()->sectors_per_fat) + root_directory_sectors; + m_first_data_sector = block->reserved_sector_count + (block->fat_count * ebpb.sectors_per_fat()) + root_directory_sectors; TRY(BlockBasedFileSystem::initialize_while_locked()); FATEntry root_entry {}; - root_entry.first_cluster_low = boot_record()->root_directory_cluster & 0xFFFF; - root_entry.first_cluster_high = boot_record()->root_directory_cluster >> 16; + if (m_fat_version == FATVersion::FAT32) { + // FAT32 stores the root directory within the FAT (at the clusters specified + // in the boot record), as opposed to the root directory area + // (as done by FAT 12/16). + + DOS7BIOSParameterBlock const* boot_record = ebpb.dos7_bpb(); + // Ensure we have a DOS7 BPB (so that we can find the root directory cluster). + if (boot_record == nullptr) { + dbgln("FATFS: Non-DOS7 BPB for FAT32 FS."); + return EINVAL; + } + root_entry.first_cluster_low = boot_record->root_directory_cluster & 0xFFFF; + root_entry.first_cluster_high = boot_record->root_directory_cluster >> 16; + } else { + // FAT12/FAT16. + // Use cluster = 0 as a signal to `first_block_of_cluster()` to look in the + // root directory area for the root entry. + // Clusters 0 and 1 hold special values, and will never be used to store file + // data. + root_entry.first_cluster_low = 0; + root_entry.first_cluster_high = 0; + } root_entry.attributes = FATAttributes::Directory; m_root_inode = TRY(FATInode::create(*this, root_entry)); @@ -86,9 +245,29 @@ Inode& FATFS::root_inode() return *m_root_inode; } -BlockBasedFileSystem::BlockIndex FATFS::first_block_of_cluster(u32 cluster) const +FatBlockSpan FATFS::first_block_of_cluster(u32 cluster) const { - return ((cluster - first_data_cluster) * boot_record()->sectors_per_cluster) + m_first_data_sector; + // For FAT12/16, we use a value of cluster 0 to indicate this is a cluster for the root directory. + // Cluster 0 and cluster 1 hold special values (cluster 0 holds the FAT ID, and cluster 1 + // the "end of chain marker"), neither of which will be present in the table or associated + // with any file. + // "Entries with the Volume Label flag, subdirectory ".." pointing to the FAT12 and FAT16 root, and empty files with size 0 should have first cluster 0." + // --Wikipedia + // + DOSBIOSParameterBlock ebpb(m_boot_record); + DOS3BIOSParameterBlock const* ebpb_block = ebpb.common_bpb(); + if (m_fat_version != FATVersion::FAT32 && cluster == 0) { + // Root directory area follows the FATs after the reserved sectors. + return FatBlockSpan { + ebpb_block->reserved_sector_count + (ebpb_block->fat_count * ebpb.sectors_per_fat()), + (ebpb_block->root_directory_entry_count * sizeof(FATEntry)) / ebpb_block->bytes_per_sector + }; + } else { + return FatBlockSpan { + ((cluster - first_data_cluster) * ebpb_block->sectors_per_cluster) + m_first_data_sector, + ebpb_block->sectors_per_cluster + }; + } } u8 FATFS::internal_file_type_to_directory_entry_type(DirectoryEntryView const& entry) const diff --git a/Kernel/FileSystem/FATFS/FileSystem.h b/Kernel/FileSystem/FATFS/FileSystem.h index 23220e4f8e..a9e681b397 100644 --- a/Kernel/FileSystem/FATFS/FileSystem.h +++ b/Kernel/FileSystem/FATFS/FileSystem.h @@ -17,6 +17,44 @@ namespace Kernel { +class DOSBIOSParameterBlock final { +private: + KBuffer const* const m_boot_record; + const struct DOS4BIOSParameterBlock* m_dos4_block; + const struct DOS7BIOSParameterBlock* m_dos7_block; + const struct DOS3BIOSParameterBlock* m_common_block; + +public: + DOSBIOSParameterBlock(KBuffer const* const boot_record) + : m_boot_record(boot_record) + , m_dos4_block(reinterpret_cast(boot_record->bytes().offset_pointer(0x024))) + , m_dos7_block(reinterpret_cast(boot_record->bytes().offset_pointer(0x024))) + , m_common_block(reinterpret_cast(boot_record->bytes().data())) + { + } + + DOSBIOSParameterBlockVersion bpb_version() const; + + DOS3BIOSParameterBlock const* common_bpb() const; + + DOS4BIOSParameterBlock const* dos4_bpb() const; + + DOS7BIOSParameterBlock const* dos7_bpb() const; + + u16 sectors_per_fat() const; + + u32 sector_count() const; + + u8 signature() const; +}; + +// Represents a block of contiguous sectors to read. This typically represents a +// cluster, but is also used to define areas of the root directory region. +struct FatBlockSpan { + BlockBasedFileSystem::BlockIndex start_block; + size_t number_of_sectors; +}; + class FATFS final : public BlockBasedFileSystem { friend FATInode; @@ -42,13 +80,13 @@ private: static constexpr u32 first_data_cluster = 2; - FAT32BootRecord const* boot_record() const { return reinterpret_cast(m_boot_record->data()); } + FatBlockSpan first_block_of_cluster(u32 cluster) const; - BlockBasedFileSystem::BlockIndex first_block_of_cluster(u32 cluster) const; - - OwnPtr m_boot_record {}; + OwnPtr m_boot_record; + OwnPtr m_parameter_block; RefPtr m_root_inode; u32 m_first_data_sector { 0 }; + FATVersion m_fat_version; }; } diff --git a/Kernel/FileSystem/FATFS/Inode.cpp b/Kernel/FileSystem/FATFS/Inode.cpp index 8f345c3869..306bed48a3 100644 --- a/Kernel/FileSystem/FATFS/Inode.cpp +++ b/Kernel/FileSystem/FATFS/Inode.cpp @@ -58,12 +58,28 @@ ErrorOr> FATInode::compute_block_list() while (cluster < no_more_clusters) { dbgln_if(FAT_DEBUG, "FATFS: Appending cluster {} to inode {}'s cluster chain", cluster, index()); - BlockBasedFileSystem::BlockIndex first_block = fs().first_block_of_cluster(cluster); - for (u8 i = 0; i < fs().boot_record()->sectors_per_cluster; i++) - block_list.append(BlockBasedFileSystem::BlockIndex { first_block.value() + i }); + auto first_block_and_length = fs().first_block_of_cluster(cluster); + for (u8 i = 0; i < first_block_and_length.number_of_sectors; i++) + block_list.append(BlockBasedFileSystem::BlockIndex { first_block_and_length.start_block.value() + i }); + + // Clusters 0 and 1 are reserved in the FAT, and their entries in the FAT will + // not point to another valid cluster in the chain (Cluster 0 typically holds + // the "FAT ID" field with some flags, Cluster 1 should be the end of chain + // marker). + // Internally, we use `cluster == 0` to represent the root directory Inode, + // which is a signal to read the root directory region blocks on FAT12/16 + // file systems. (`fs().first_block_of_cluster` will return the appropriate + // block/sectors to read given cluster == 0). + // Therefore, we read one set of sectors for these invalud cluster numbers, + // and then terminate the loop becuase the FAT entry at `cluster` for these + // values does not represent the next step in the chain (because there is + // nothing else to read). + if (cluster <= 1) { + break; + } u32 fat_offset = cluster * sizeof(u32); - u32 fat_sector_index = fs().boot_record()->reserved_sector_count + (fat_offset / fs().m_device_block_size); + u32 fat_sector_index = fs().m_parameter_block->common_bpb()->reserved_sector_count + (fat_offset / fs().m_device_block_size); u32 entry_offset = fat_offset % fs().m_device_block_size; TRY(fs().raw_read(fat_sector_index, fat_sector_buffer)); @@ -126,6 +142,15 @@ ErrorOr> FATInode::traverse(Function(RefPtrattributes == FATAttributes::LongFileName) { dbgln_if(FAT_DEBUG, "FATFS: Found LFN entry"); TRY(lfn_entries.try_append(*reinterpret_cast(entry))); + } else if ((entry->first_cluster_high << 16 | entry->first_cluster_low) <= 1 && entry->file_size > 0) { + // Because clusters 0 and 1 are reserved, only empty files (size == 0 files) + // should specify these clusters. + // This driver uses a cluster number == 0 to represent the root directory inode + // on FAT12/16 file systems (a signal to look in the root directory region), + // so we ensure that no entries read off the file system have a cluster number + // that would also point to this region. + dbgln_if(FAT_DEBUG, "FATFS: Invalid cluster for entry"); + return EINVAL; } else { dbgln_if(FAT_DEBUG, "FATFS: Found 8.3 entry"); lfn_entries.reverse();