From 898d4a937d013719da53c6cf93d84094d46bee3e Mon Sep 17 00:00:00 2001 From: manuel Date: Fri, 16 Oct 2026 08:38:13 +0000 Subject: kernel/filesystem: Add tests for FAT and more tests for filesystem FAT32 --- .../fat32/file_allocation_table.tests.cpp | 202 +++++++++++++++++++++ .../kernel/filesystems/fat32/filesystem.tests.cpp | 121 ++++++++++-- kernel/kernel/test_support/filesystems/fat32.cpp | 73 ++++++++ kernel/kernel/test_support/filesystems/fat32.hpp | 22 +++ 4 files changed, 399 insertions(+), 19 deletions(-) create mode 100644 kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp create mode 100644 kernel/kernel/test_support/filesystems/fat32.cpp create mode 100644 kernel/kernel/test_support/filesystems/fat32.hpp diff --git a/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp b/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp new file mode 100644 index 00000000..bcc85a51 --- /dev/null +++ b/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +using namespace kstd::units_literals; + +// NOLINTBEGIN(readability-magic-numbers) + +SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_fixture, "Fat32 cluster chains with real image", + "[filesystem][fat32][fat][img]") +{ + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "fat32.img"; + + GIVEN("a mounted fat32 filesystem from a real image") + { + REQUIRE(std::filesystem::exists(image_path)); + REQUIRE_NOTHROW(setup_modules_from_img({"test_img_module"}, {image_path})); + + auto boot_device = kernel::devices::storage::determine_boot_device(); + REQUIRE(boot_device); + + auto dev_inode = kstd::make_shared(boot_device); + auto fs = kstd::make_shared(); + auto mount = fs->mount(dev_inode); + REQUIRE(mount); + auto driver_state = static_pointer_cast(mount->second); + REQUIRE(driver_state); + + THEN("the root directory occupies a single cluster") + { + auto const chain = kernel::filesystems::fat32::cluster_chain(driver_state->boot_sector.root_cluster, *driver_state); + REQUIRE(chain); + REQUIRE(*chain == kstd::vector{2}); + } + + THEN("the root directory cluster is located right after the file allocation tables") + { + REQUIRE(kernel::filesystems::fat32::cluster_offset(2, *driver_state) == (32 + 2 * 536) * 512_B); + } + + THEN("the entries of the root directory, HELLO.TXT and an unused cluster are classified") + { + using kernel::filesystems::fat32::fat_entry_type; + using kernel::filesystems::fat32::read_fat_entry; + + REQUIRE(read_fat_entry(2, *driver_state)->type == fat_entry_type::end_of_chain); + REQUIRE(read_fat_entry(3, *driver_state)->type == fat_entry_type::end_of_chain); + REQUIRE(read_fat_entry(4, *driver_state)->type == fat_entry_type::free); + } + + THEN("following a free cluster fails") + { + REQUIRE(kernel::filesystems::fat32::next_cluster(100, *driver_state).error() == + kernel::filesystems::fat32::errc::corrupted_cluster_chain); + } + } +} + +SCENARIO("Fat32 cluster chain traversal", "[filesystem][fat32][fat]") +{ + auto const block_size = 512_B; + + GIVEN("a mounted minimal fat32 layout") + { + auto device = kstd::make_shared("mock", block_size, 18 * block_size); + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device); + + auto dev_inode = kstd::make_shared(device); + auto fs = kstd::make_shared(); + auto mount = fs->mount(dev_inode); + REQUIRE(mount); + auto driver_state = static_pointer_cast(mount->second); + REQUIRE(driver_state); + + using kernel::filesystems::fat32::cluster_chain; + using kernel::filesystems::fat32::errc; + using kernel::filesystems::fat32::next_cluster; + using kernel::tests::filesystems::fat32::write_fat_entry; + + THEN("only clusters of the data region are valid") + { + REQUIRE_FALSE(kernel::filesystems::fat32::is_valid_cluster(0, *driver_state)); + REQUIRE_FALSE(kernel::filesystems::fat32::is_valid_cluster(1, *driver_state)); + REQUIRE(kernel::filesystems::fat32::is_valid_cluster(2, *driver_state)); + REQUIRE(kernel::filesystems::fat32::is_valid_cluster(17, *driver_state)); + REQUIRE_FALSE(kernel::filesystems::fat32::is_valid_cluster(18, *driver_state)); + } + + THEN("cluster offsets start at the data region") + { + REQUIRE(kernel::filesystems::fat32::cluster_offset(2, *driver_state) == 1024_B); + REQUIRE(kernel::filesystems::fat32::cluster_offset(3, *driver_state) == 1536_B); + } + + THEN("entries are classified by their value") + { + using kernel::filesystems::fat32::fat_entry_type; + using kernel::filesystems::fat32::read_fat_entry; + + write_fat_entry(*device, 3, 0); + write_fat_entry(*device, 4, 1); + write_fat_entry(*device, 5, 0xF000'0006); + write_fat_entry(*device, 6, 0x0FFF'FFF7); + write_fat_entry(*device, 7, 0x0FFF'FFF8); + write_fat_entry(*device, 8, 0xFFFF'FFFF); + + REQUIRE(read_fat_entry(3, *driver_state)->type == fat_entry_type::free); + REQUIRE(read_fat_entry(4, *driver_state)->type == fat_entry_type::reserved); + REQUIRE(read_fat_entry(5, *driver_state)->type == fat_entry_type::allocated); + REQUIRE(read_fat_entry(5, *driver_state)->value == 6); + REQUIRE(read_fat_entry(6, *driver_state)->type == fat_entry_type::bad_cluster); + REQUIRE(read_fat_entry(7, *driver_state)->type == fat_entry_type::end_of_chain); + REQUIRE(read_fat_entry(8, *driver_state)->type == fat_entry_type::end_of_chain); + REQUIRE(read_fat_entry(8, *driver_state)->value == 0x0FFF'FFFF); + REQUIRE(read_fat_entry(18, *driver_state).error() == errc::invalid_cluster); + } + + THEN("a multi cluster chain is followed to its end") + { + write_fat_entry(*device, 3, 4); + write_fat_entry(*device, 4, 5); + write_fat_entry(*device, 5, 0x0FFF'FFF8); + + auto const after_first = next_cluster(3, *driver_state); + REQUIRE(after_first); + REQUIRE(*after_first == std::optional{4}); + + auto const after_last = next_cluster(5, *driver_state); + REQUIRE(after_last); + REQUIRE_FALSE(after_last->has_value()); + + REQUIRE(cluster_chain(3, *driver_state) == kstd::vector{3, 4, 5}); + } + + THEN("the reserved upper four bits of an entry are ignored") + { + write_fat_entry(*device, 6, 0xF000'0007); + write_fat_entry(*device, 7, 0xFFFF'FFFF); + + REQUIRE(cluster_chain(6, *driver_state) == kstd::vector{6, 7}); + } + + THEN("a chain cannot start outside the data region") + { + REQUIRE(next_cluster(0, *driver_state).error() == errc::invalid_cluster); + REQUIRE(next_cluster(1, *driver_state).error() == errc::invalid_cluster); + REQUIRE(next_cluster(18, *driver_state).error() == errc::invalid_cluster); + REQUIRE(cluster_chain(18, *driver_state).error() == errc::invalid_cluster); + } + + THEN("a chain running into a free cluster is corrupted") + { + write_fat_entry(*device, 8, 9); + write_fat_entry(*device, 9, 0); + + REQUIRE(cluster_chain(8, *driver_state).error() == errc::corrupted_cluster_chain); + } + + THEN("a chain running into a reserved or bad cluster is corrupted") + { + write_fat_entry(*device, 10, 1); + write_fat_entry(*device, 11, 0x0FFF'FFF7); + + REQUIRE(next_cluster(10, *driver_state).error() == errc::corrupted_cluster_chain); + REQUIRE(next_cluster(11, *driver_state).error() == errc::corrupted_cluster_chain); + } + + THEN("a chain pointing outside the data region is corrupted") + { + write_fat_entry(*device, 12, 100); + + REQUIRE(next_cluster(12, *driver_state).error() == errc::corrupted_cluster_chain); + } + + THEN("a cyclic chain is detected") + { + write_fat_entry(*device, 13, 14); + write_fat_entry(*device, 14, 13); + + REQUIRE(cluster_chain(13, *driver_state).error() == errc::corrupted_cluster_chain); + } + } +} + +// NOLINTEND(readability-magic-numbers) diff --git a/kernel/kernel/filesystems/fat32/filesystem.tests.cpp b/kernel/kernel/filesystems/fat32/filesystem.tests.cpp index 8e4bc8c1..b2f4af62 100644 --- a/kernel/kernel/filesystems/fat32/filesystem.tests.cpp +++ b/kernel/kernel/filesystems/fat32/filesystem.tests.cpp @@ -1,14 +1,16 @@ #include #include +#include #include +#include #include +#include #include #include +#include #include -#include - #include #include #include @@ -20,39 +22,118 @@ using namespace kstd::units_literals; -SCENARIO("fat32 filesystem mount and lookup", "[filesystem][fat32][filesystem]") +// NOLINTBEGIN(readability-magic-numbers) + +SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_fixture, "Fat32 filesystem mount with real image", + "[filesystem][fat32][filesystem][img]") { - GIVEN("a mounted fat32 filesystem") + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "fat32.img"; + + GIVEN("a mounted fat32 filesystem from a real image") { + REQUIRE(std::filesystem::exists(image_path)); + REQUIRE_NOTHROW(setup_modules_from_img({"test_img_module"}, {image_path})); + + auto boot_device = kernel::devices::storage::determine_boot_device(); + REQUIRE(boot_device); + + auto dev_inode = kstd::make_shared(boot_device); auto fs = kstd::make_shared(); - auto mount = fs->mount(nullptr); + auto mount = fs->mount(dev_inode); REQUIRE(mount); auto [root, driver_data] = *mount; - REQUIRE(root); + auto driver_state = static_pointer_cast(driver_data); - THEN("the filesystem can be mounted successfully") + THEN("the root inode is a directory starting at the root cluster") { - REQUIRE(mount); + REQUIRE(root); + REQUIRE(root->is_directory()); + REQUIRE(static_pointer_cast(root)->first_cluster() == 2); } - THEN("looking up a non-existent directory returns an error") + THEN("the layout is derived from the boot sector") { - auto non_existent_inode_1 = fs->lookup(root, "", driver_data); - REQUIRE(!non_existent_inode_1); - auto non_existent_inode_2 = fs->lookup(root, "nonexistent", driver_data); - REQUIRE(!non_existent_inode_2); + REQUIRE(driver_state); + REQUIRE(driver_state->sector_size == 512_B); + REQUIRE(driver_state->cluster_size == 512_B); + REQUIRE(driver_state->fat_start_sector == 32); + REQUIRE(driver_state->data_start_sector == 32 + 2 * 536); + REQUIRE(driver_state->cluster_count == 69632 - (32 + 2 * 536)); } + } +} + +SCENARIO("Fat32 filesystem mount validates the boot sector", "[filesystem][fat32][filesystem]") +{ + auto const block_size = 512_B; - THEN("looking up with a null parent inode returns an error") + GIVEN("no backing inode") + { + auto fs = kstd::make_shared(); + + THEN("mount fails with invalid_inode") { - auto result = fs->lookup(nullptr, "dev", driver_data); - REQUIRE(!result); + REQUIRE(fs->mount(nullptr).error() == kernel::vfs::errc::invalid_inode); } + } - THEN("create_inode always returns an error") + GIVEN("a block device containing a minimal fat32 layout") + { + auto device = kstd::make_shared("mock", block_size, 18 * block_size); + auto dev_inode = kstd::make_shared(device); + auto fs = kstd::make_shared(); + auto boot_sector = kernel::tests::filesystems::fat32::mock_boot_sector(); + + THEN("the unmodified layout mounts") + { + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode)); + } + + THEN("a sector size that is not a supported power of two is rejected") + { + boot_sector.bytes_per_sector = 0; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_boot_sector); + + boot_sector.bytes_per_sector = 513; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_boot_sector); + } + + THEN("a cluster size that is not a power of two is rejected") + { + boot_sector.sectors_per_cluster = 3; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_boot_sector); + } + + THEN("a missing file allocation table is rejected") + { + boot_sector.number_of_fats = 0; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_boot_sector); + } + + THEN("a data region that does not fit into the volume is rejected") { - auto result = fs->create_inode(root, "new_device", kapi::filesystem::file_type::directory, driver_data); - REQUIRE(!result); + boot_sector.total_sectors_32 = 2; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_boot_sector); + } + + THEN("more clusters than the file allocation table can describe are rejected") + { + boot_sector.total_sectors_32 = 1 + 1 + 127; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_boot_sector); + } + + THEN("a root cluster outside the data region is rejected") + { + boot_sector.root_cluster = 18; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->mount(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_cluster); } } } @@ -97,3 +178,5 @@ SCENARIO("Fat32 filesystem rejects invalid boot signature", "[filesystem][fat32] } } } + +// NOLINTEND(readability-magic-numbers) diff --git a/kernel/kernel/test_support/filesystems/fat32.cpp b/kernel/kernel/test_support/filesystems/fat32.cpp new file mode 100644 index 00000000..4513a53a --- /dev/null +++ b/kernel/kernel/test_support/filesystems/fat32.cpp @@ -0,0 +1,73 @@ +#include + +#include +#include +#include +#include + +#include + +#include +#include + +using namespace kstd::units_literals; + +namespace kernel::tests::filesystems::fat32 +{ + namespace + { + constexpr auto sector_size = 512_B; + constexpr std::uint16_t reserved_sectors = 1; + constexpr std::uint32_t fat_sectors = 1; + constexpr std::uint32_t data_sectors = 16; + + auto write_bytes(kernel::tests::devices::block_device & device, kstd::bytes offset, void const * source, + kstd::bytes size) -> void + { + auto const required_size = offset + size; + if (kstd::bytes{device.data.size()} < required_size) + { + device.data.resize(required_size.value, 0); + } + + std::memcpy(device.data.data() + offset, source, size.value); + } + } // namespace + + auto mock_boot_sector() -> kernel::filesystems::fat32::boot_sector + { + auto boot_sector = kernel::filesystems::fat32::boot_sector{}; + boot_sector.bytes_per_sector = static_cast(sector_size.value); + boot_sector.sectors_per_cluster = 1; + boot_sector.reserved_sector_count = reserved_sectors; + boot_sector.number_of_fats = 1; + boot_sector.total_sectors_32 = reserved_sectors + fat_sectors + data_sectors; + boot_sector.fat_size_32 = fat_sectors; + boot_sector.root_cluster = kernel::filesystems::fat32::constants::first_data_cluster; + boot_sector.boot_signature = kernel::filesystems::fat32::constants::boot_signature; + return boot_sector; + } + + auto setup_mock_fat32_layout(kernel::tests::devices::block_device & device) -> void + { + setup_mock_fat32_layout(device, mock_boot_sector()); + } + + auto setup_mock_fat32_layout(kernel::tests::devices::block_device & device, + kernel::filesystems::fat32::boot_sector const & boot_sector) -> void + { + write_bytes(device, kernel::filesystems::fat32::constants::boot_sector_offset, &boot_sector, + kstd::size_of(boot_sector)); + + write_fat_entry(device, 0, 0x0FFF'FFF8); + write_fat_entry(device, 1, 0x0FFF'FFFF); + write_fat_entry(device, kernel::filesystems::fat32::constants::first_data_cluster, 0x0FFF'FFFF); + } + + auto write_fat_entry(kernel::tests::devices::block_device & device, std::uint32_t cluster, std::uint32_t value) + -> void + { + auto const offset = reserved_sectors * sector_size + cluster * kernel::filesystems::fat32::constants::fat_entry_size; + write_bytes(device, offset, &value, kstd::size_of(value)); + } +} // namespace kernel::tests::filesystems::fat32 diff --git a/kernel/kernel/test_support/filesystems/fat32.hpp b/kernel/kernel/test_support/filesystems/fat32.hpp new file mode 100644 index 00000000..e53974fb --- /dev/null +++ b/kernel/kernel/test_support/filesystems/fat32.hpp @@ -0,0 +1,22 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEMS_FAT32_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEMS_FAT32_HPP + +#include +#include + +#include + +namespace kernel::tests::filesystems::fat32 +{ + //! The boot sector of the mock layout: 512 byte sectors, 1 sector per cluster, 1 reserved sector, a single FAT of + //! 1 sector and 16 data clusters (2 to 17). The root directory starts at cluster 2. + auto mock_boot_sector() -> kernel::filesystems::fat32::boot_sector; + auto setup_mock_fat32_layout(kernel::tests::devices::block_device & device) -> void; + auto setup_mock_fat32_layout(kernel::tests::devices::block_device & device, + kernel::filesystems::fat32::boot_sector const & boot_sector) -> void; + auto write_fat_entry(kernel::tests::devices::block_device & device, std::uint32_t cluster, std::uint32_t value) + -> void; + +} // namespace kernel::tests::filesystems::fat32 + +#endif -- cgit v1.2.3