aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormanuel <manuel.bott@eps.ch>2026-10-16 08:38:13 +0000
committermanuel <manuel.bott@eps.ch>2026-10-16 08:38:13 +0000
commit898d4a937d013719da53c6cf93d84094d46bee3e (patch)
tree52a7c5dfea2cbe34f299ea4b3fef071f9ddec6e7
parent92176bc0615f7ea16d652cd1c6efd10c1be0b594 (diff)
downloadkernel-898d4a937d013719da53c6cf93d84094d46bee3e.tar.xz
kernel-898d4a937d013719da53c6cf93d84094d46bee3e.zip
kernel/filesystem: Add tests for FAT and more tests for filesystem FAT32
-rw-r--r--kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp202
-rw-r--r--kernel/kernel/filesystems/fat32/filesystem.tests.cpp121
-rw-r--r--kernel/kernel/test_support/filesystems/fat32.cpp73
-rw-r--r--kernel/kernel/test_support/filesystems/fat32.hpp22
4 files changed, 399 insertions, 19 deletions
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 <kernel/filesystems/fat32/file_allocation_table.hpp>
+
+#include <kernel/devices/storage.hpp>
+#include <kernel/filesystems/fat32/driver_state.hpp>
+#include <kernel/filesystems/fat32/error.hpp>
+#include <kernel/filesystems/fat32/filesystem.hpp>
+#include <kernel/test_support/devices/block_device.hpp>
+#include <kernel/test_support/filesystems/fat32.hpp>
+#include <kernel/test_support/vfs/storage_boot_module_fixture.hpp>
+#include <kernel/vfs/device_inode.hpp>
+
+#include <kstd/memory.hpp>
+#include <kstd/units.hpp>
+#include <kstd/vector.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <cstdint>
+#include <filesystem>
+#include <optional>
+
+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<kernel::vfs::device_inode>(boot_device);
+ auto fs = kstd::make_shared<kernel::filesystems::fat32::filesystem>();
+ auto mount = fs->mount(dev_inode);
+ REQUIRE(mount);
+ auto driver_state = static_pointer_cast<kernel::filesystems::fat32::driver_state>(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<std::uint32_t>{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<kernel::tests::devices::block_device>("mock", block_size, 18 * block_size);
+ kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device);
+
+ auto dev_inode = kstd::make_shared<kernel::vfs::device_inode>(device);
+ auto fs = kstd::make_shared<kernel::filesystems::fat32::filesystem>();
+ auto mount = fs->mount(dev_inode);
+ REQUIRE(mount);
+ auto driver_state = static_pointer_cast<kernel::filesystems::fat32::driver_state>(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<std::uint32_t>{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<std::uint32_t>{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<std::uint32_t>{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 <kernel/filesystems/fat32/filesystem.hpp>
#include <kernel/devices/storage.hpp>
+#include <kernel/filesystems/fat32/driver_state.hpp>
#include <kernel/filesystems/fat32/error.hpp>
+#include <kernel/filesystems/fat32/inode.hpp>
#include <kernel/test_support/devices/block_device.hpp>
+#include <kernel/test_support/filesystems/fat32.hpp>
#include <kernel/test_support/vfs/storage_boot_module_fixture.hpp>
#include <kernel/vfs/device_inode.hpp>
+#include <kernel/vfs/error.hpp>
#include <kernel/vfs/filesystem.hpp>
-#include <kapi/filesystem.hpp>
-
#include <kstd/memory.hpp>
#include <kstd/print.hpp>
#include <kstd/units.hpp>
@@ -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<kernel::vfs::device_inode>(boot_device);
auto fs = kstd::make_shared<kernel::filesystems::fat32::filesystem>();
- 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<kernel::filesystems::fat32::driver_state>(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<kernel::filesystems::fat32::inode>(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<kernel::filesystems::fat32::filesystem>();
+
+ 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<kernel::tests::devices::block_device>("mock", block_size, 18 * block_size);
+ auto dev_inode = kstd::make_shared<kernel::vfs::device_inode>(device);
+ auto fs = kstd::make_shared<kernel::filesystems::fat32::filesystem>();
+ 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 <kernel/test_support/filesystems/fat32.hpp>
+
+#include <kernel/filesystems/fat32/boot_sector.hpp>
+#include <kernel/filesystems/fat32/file_allocation_table.hpp>
+#include <kernel/filesystems/fat32/filesystem.hpp>
+#include <kernel/test_support/devices/block_device.hpp>
+
+#include <kstd/units.hpp>
+
+#include <cstdint>
+#include <cstring>
+
+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<std::uint16_t>(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 <kernel/filesystems/fat32/boot_sector.hpp>
+#include <kernel/test_support/devices/block_device.hpp>
+
+#include <cstdint>
+
+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