diff options
| author | manuel <manuel.bott@eps.ch> | 2026-10-22 11:29:53 +0000 |
|---|---|---|
| committer | bbuerge <b.buerge@proton.me> | 2026-10-22 14:31:29 +0000 |
| commit | 856ddc4a22eabe038e08120e1b87fb3afb4aa6b8 (patch) | |
| tree | 711fe865f6aa944f142a8a71f31c4a38b0f60a37 | |
| parent | 1eb33a7446a441d053c6ad7b24d3050e770cfff7 (diff) | |
| download | kernel-feature/readDirectoryEntries.tar.xz kernel-feature/readDirectoryEntries.zip | |
kernel/filesystem: Tests for directory_iterator and more for file allocation tablefeature/readDirectoryEntries
4 files changed, 528 insertions, 0 deletions
diff --git a/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp b/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp new file mode 100644 index 00000000..204c5a40 --- /dev/null +++ b/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp @@ -0,0 +1,354 @@ +#include <kernel/filesystems/fat32/directory_iterator.hpp> + +#include <kernel/devices/storage.hpp> +#include <kernel/filesystems/fat32/directory_entry.hpp> +#include <kernel/filesystems/fat32/driver_state.hpp> +#include <kernel/filesystems/fat32/error.hpp> +#include <kernel/filesystems/fat32/file_allocation_table.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 <catch2/catch_test_macros.hpp> + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <span> +#include <string> +#include <string_view> +#include <vector> + +using namespace kstd::units_literals; + +// NOLINTBEGIN(readability-magic-numbers) + +namespace +{ + using kernel::filesystems::fat32::directory_entry; + using kernel::filesystems::fat32::directory_iterator; + + constexpr auto entries_per_cluster = 16uz; + + auto make_entry(std::string_view name, std::uint8_t attributes, std::uint32_t first_cluster = 0, + std::uint32_t file_size = 0) -> directory_entry + { + auto entry = directory_entry{}; + std::ranges::fill(entry.name, ' '); + std::ranges::copy(name.substr(0, entry.name.size()), entry.name.begin()); + entry.attributes = attributes; + entry.first_cluster_high = static_cast<std::uint16_t>(first_cluster >> 16); + entry.first_cluster_low = static_cast<std::uint16_t>(first_cluster & 0xFFFF); + entry.file_size = file_size; + return entry; + } + + auto file_entry(std::string_view name) -> directory_entry + { + return make_entry(name, kernel::filesystems::fat32::constants::attribute_archive); + } + + auto write_entries(kernel::tests::devices::block_device & device, std::uint32_t cluster, + std::span<directory_entry const> entries) -> void + { + auto cluster_entries = std::array<directory_entry, entries_per_cluster>{}; + std::ranges::copy(entries, cluster_entries.begin()); + kernel::tests::filesystems::fat32::write_cluster_data(device, cluster, std::as_bytes(std::span{cluster_entries})); + } + + auto collect_names(directory_iterator it) -> std::vector<std::string> + { + auto names = std::vector<std::string>{}; + for (; it != directory_iterator{}; ++it) + { + REQUIRE(names.size() < 64); // fails loudly on a non-terminating loop rather than hanging the suite + auto const name = it->entry.short_name(); + names.emplace_back(name.data(), name.size()); + } + return names; + } +} // namespace + +SCENARIO("Fat32 short names are formatted for display", "[filesystem][fat32][readdir]") +{ + using namespace kernel::filesystems::fat32; + + THEN("padding is removed and the extension is separated by a dot") + { + REQUIRE(make_entry("HELLO TXT", 0).short_name() == "HELLO.TXT"); + REQUIRE(make_entry("ABCDEFGHIJK", 0).short_name() == "ABCDEFGH.IJK"); + } + + THEN("a name without extension has no dot") + { + REQUIRE(make_entry("README", 0).short_name() == "README"); + } + + THEN("the dot entries keep their names") + { + REQUIRE(make_entry(".", constants::attribute_directory).short_name() == "."); + REQUIRE(make_entry("..", constants::attribute_directory).short_name() == ".."); + } + + THEN("the lowercase flags are honored per component") + { + auto entry = make_entry("HELLO TXT", 0); + + entry.nt_reserved = constants::lowercase_base_name; + REQUIRE(entry.short_name() == "hello.TXT"); + + entry.nt_reserved = constants::lowercase_extension; + REQUIRE(entry.short_name() == "HELLO.txt"); + + entry.nt_reserved = constants::lowercase_base_name | constants::lowercase_extension; + REQUIRE(entry.short_name() == "hello.txt"); + } + + THEN("an escaped leading 0xE5 is restored") + { + auto entry = make_entry("XAB", 0); + entry.name[0] = static_cast<char>(constants::escaped_deleted_entry_marker); + auto const name = entry.short_name(); + REQUIRE(static_cast<std::uint8_t>(name[0]) == constants::deleted_entry_marker); + REQUIRE(std::string_view{name.data() + 1, name.size() - 1} == "AB"); + } + + THEN("the first cluster combines both halves") + { + REQUIRE(make_entry("A", 0, 0x0012'3456).first_cluster() == 0x0012'3456); + } +} + +SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_fixture, "Fat32 directory_iterator walks a real directory", + "[filesystem][fat32][readdir][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); + + auto const root_cluster = driver_state->boot_sector.root_cluster; + + THEN("the volume label is skipped and only HELLO.TXT is found") + { + REQUIRE(collect_names(directory_iterator{root_cluster, *driver_state}) == std::vector<std::string>{"HELLO.TXT"}); + } + + THEN("the record describes HELLO.TXT and where it is stored") + { + auto it = directory_iterator{root_cluster, *driver_state}; + REQUIRE(it != directory_iterator{}); + REQUIRE(it->entry.first_cluster() == 3); + REQUIRE(it->entry.file_size == 12); + REQUIRE(it->offset == 32_B); + REQUIRE(it->location == kernel::filesystems::fat32::cluster_offset(root_cluster, *driver_state) + 32_B); + REQUIRE(it->inode_number() == it->location.value); + REQUIRE(it.offset() == 64_B); + + ++it; + REQUIRE(it == directory_iterator{}); + REQUIRE_FALSE(it.error()); + } + } +} + +SCENARIO("Fat32 directory_iterator on a mock layout", "[filesystem][fat32][readdir]") +{ + 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 namespace kernel::filesystems::fat32; + using kernel::tests::filesystems::fat32::write_fat_entry; + + WHEN("the directory contains deleted, long name and volume label entries") + { + auto deleted = file_entry("GONE TXT"); + deleted.name[0] = static_cast<char>(constants::deleted_entry_marker); + + auto const entries = std::array{ + make_entry("LABEL", constants::attribute_volume_id), + make_entry("Ah\0e\0l\0l\0o", constants::attribute_long_name), + file_entry("HELLO TXT"), + deleted, + make_entry("SUBDIR", constants::attribute_directory, 5), + directory_entry{}, + file_entry("HIDDEN TXT"), + }; + write_entries(*device, 2, entries); + + THEN("only the regular entries before the end marker are visited") + { + REQUIRE(collect_names(directory_iterator{2, *driver_state}) == + std::vector<std::string>{"HELLO.TXT", "SUBDIR"}); + } + + THEN("the records carry the offsets of the visited entries") + { + auto it = directory_iterator{2, *driver_state}; + REQUIRE(it->offset == 64_B); + REQUIRE(it->location == 1024_B + 64_B); + ++it; + REQUIRE(it->offset == 128_B); + REQUIRE(it->entry.first_cluster() == 5); + } + + THEN("iteration can be resumed behind a visited entry") + { + auto it = directory_iterator{2, *driver_state}; + REQUIRE(collect_names(directory_iterator{2, it.offset(), *driver_state}) == + std::vector<std::string>{"SUBDIR"}); + } + + THEN("an offset inside an entry is rounded down to its start") + { + REQUIRE(directory_iterator{2, 70_B, *driver_state}->offset == 64_B); + } + + THEN("copies of an iterator advance independently") + { + auto first = directory_iterator{2, *driver_state}; + auto second = first; + ++first; + REQUIRE(second->entry.short_name() == "HELLO.TXT"); + REQUIRE(first->entry.short_name() == "SUBDIR"); + REQUIRE(first != second); + ++second; + REQUIRE(first == second); + } + } + + WHEN("the directory spans two clusters") + { + auto first_cluster_entries = std::array<directory_entry, entries_per_cluster>{}; + for (auto i = 0uz; i < first_cluster_entries.size(); ++i) + { + first_cluster_entries[i] = file_entry(std::string{"FILE"} + static_cast<char>('A' + i)); + } + write_entries(*device, 3, first_cluster_entries); + write_entries(*device, 4, std::array{file_entry("LAST TXT")}); + write_fat_entry(*device, 3, 4); + write_fat_entry(*device, 4, 0x0FFF'FFFF); + + THEN("the entries of both clusters are visited in order") + { + auto const names = collect_names(directory_iterator{3, *driver_state}); + REQUIRE(names.size() == 17); + REQUIRE(names.front() == "FILEA"); + REQUIRE(names.back() == "LAST.TXT"); + } + + THEN("the entries of the second cluster are located in it") + { + auto it = directory_iterator{3, 512_B, *driver_state}; + REQUIRE(it->offset == 512_B); + REQUIRE(it->location == cluster_offset(4, *driver_state)); + REQUIRE(it->entry.short_name() == "LAST.TXT"); + } + + THEN("an offset behind the chain yields no entries and no error") + { + auto it = directory_iterator{3, 1024_B, *driver_state}; + REQUIRE(it == directory_iterator{}); + REQUIRE_FALSE(it.error()); + } + } + + WHEN("a full directory has no end marker") + { + auto entries = std::array<directory_entry, entries_per_cluster>{}; + std::ranges::fill(entries, file_entry("FILE")); + write_entries(*device, 6, entries); + write_fat_entry(*device, 6, 0x0FFF'FFFF); + + THEN("iteration ends with the chain") + { + auto it = directory_iterator{6, *driver_state}; + REQUIRE(collect_names(it).size() == entries_per_cluster); + std::ranges::advance(it, entries_per_cluster); + REQUIRE(it == directory_iterator{}); + REQUIRE_FALSE(it.error()); + } + } + + THEN("a directory cannot start outside the data region") + { + auto it = directory_iterator{0, *driver_state}; + REQUIRE(it == directory_iterator{}); + REQUIRE(it.error() == errc::invalid_cluster); + } + + WHEN("the directory's chain is cyclic") + { + auto entries = std::array<directory_entry, entries_per_cluster>{}; + std::ranges::fill(entries, file_entry("LOOP")); + write_entries(*device, 13, entries); + write_entries(*device, 14, entries); + write_fat_entry(*device, 13, 14); + write_fat_entry(*device, 14, 13); + + THEN("iteration ends with a corrupted chain error") + { + auto it = directory_iterator{13, *driver_state}; + auto count = 0uz; + for (; it != directory_iterator{} && count < 1000; ++it) + { + ++count; + } + REQUIRE(count < 1000); + REQUIRE(it.error() == errc::corrupted_cluster_chain); + } + } + + WHEN("the directory's chain runs into a free cluster") + { + auto entries = std::array<directory_entry, entries_per_cluster>{}; + std::ranges::fill(entries, file_entry("FILE")); + write_entries(*device, 8, entries); + write_entries(*device, 9, entries); + write_fat_entry(*device, 8, 9); + write_fat_entry(*device, 9, 0); + + THEN("iteration ends with a corrupted chain error once the free cluster is followed") + { + auto it = directory_iterator{8, *driver_state}; + while (it != directory_iterator{}) + { + ++it; + } + REQUIRE(it.error() == errc::corrupted_cluster_chain); + } + } + } +} + +// NOLINTEND(readability-magic-numbers) diff --git a/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp b/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp index bcc85a51..1b79d2bf 100644 --- a/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp +++ b/kernel/kernel/filesystems/fat32/file_allocation_table.tests.cpp @@ -1,6 +1,7 @@ #include <kernel/filesystems/fat32/file_allocation_table.hpp> #include <kernel/devices/storage.hpp> +#include <kernel/filesystems/fat32/directory_entry.hpp> #include <kernel/filesystems/fat32/driver_state.hpp> #include <kernel/filesystems/fat32/error.hpp> #include <kernel/filesystems/fat32/filesystem.hpp> @@ -10,14 +11,20 @@ #include <kernel/vfs/device_inode.hpp> #include <kstd/memory.hpp> +#include <kstd/span.hpp> #include <kstd/units.hpp> #include <kstd/vector.hpp> #include <catch2/catch_test_macros.hpp> +#include <algorithm> +#include <array> +#include <cstddef> #include <cstdint> #include <filesystem> #include <optional> +#include <span> +#include <string_view> using namespace kstd::units_literals; @@ -70,6 +77,40 @@ SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_fixture, "Fat32 cluster REQUIRE(kernel::filesystems::fat32::next_cluster(100, *driver_state).error() == kernel::filesystems::fat32::errc::corrupted_cluster_chain); } + + THEN("the root directory entries can be read from its cluster chain") + { + using namespace kernel::filesystems::fat32; + + auto entries = std::array<directory_entry, 3>{}; + auto const buffer = std::as_writable_bytes(std::span{entries}); + auto const read_result = read_cluster_chain(driver_state->boot_sector.root_cluster, buffer, 0_B, *driver_state); + REQUIRE(read_result); + REQUIRE(*read_result == kstd::bytes{buffer.size()}); + + auto const & volume_label = entries[0]; + REQUIRE(std::string_view{volume_label.name.data(), volume_label.name.size()} == "TESTFAT "); + REQUIRE(volume_label.attributes == constants::attribute_volume_id); + + auto const & hello = entries[1]; + REQUIRE(std::string_view{hello.name.data(), hello.name.size()} == "HELLO TXT"); + REQUIRE(hello.attributes == constants::attribute_archive); + REQUIRE(hello.first_cluster_high == 0); + REQUIRE(hello.first_cluster_low == 3); + REQUIRE(hello.file_size == 12); + + REQUIRE(static_cast<std::uint8_t>(entries[2].name[0]) == constants::end_of_directory_marker); + } + + THEN("the contents of HELLO.TXT can be read from its cluster chain") + { + auto contents = std::array<char, 12>{}; + auto const read_result = kernel::filesystems::fat32::read_cluster_chain( + 3, std::as_writable_bytes(std::span{contents}), 0_B, *driver_state); + REQUIRE(read_result); + REQUIRE(*read_result == 12_B); + REQUIRE(std::string_view{contents.data(), contents.size()} == "Hello World!"); + } } } @@ -199,4 +240,123 @@ SCENARIO("Fat32 cluster chain traversal", "[filesystem][fat32][fat]") } } +SCENARIO("Fat32 cluster chain reading", "[filesystem][fat32][fat]") +{ + auto const block_size = 512_B; + + GIVEN("a mounted minimal fat32 layout with a three cluster chain") + { + 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); + + using kernel::filesystems::fat32::errc; + using kernel::filesystems::fat32::read_cluster_chain; + using kernel::tests::filesystems::fat32::write_cluster_data; + using kernel::tests::filesystems::fat32::write_fat_entry; + + auto cluster_data = std::array<std::byte, 512>{}; + for (auto const & [cluster, fill] : {std::pair{3u, 'A'}, std::pair{4u, 'B'}, std::pair{5u, 'C'}}) + { + std::ranges::fill(cluster_data, static_cast<std::byte>(fill)); + write_cluster_data(*device, cluster, cluster_data); + } + + write_fat_entry(*device, 3, 4); + write_fat_entry(*device, 4, 5); + write_fat_entry(*device, 5, 0x0FFF'FFF8); + + 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); + + THEN("the whole chain is read in order") + { + auto buffer = std::array<std::byte, 3 * 512>{}; + auto const read_result = read_cluster_chain(3, buffer, 0_B, *driver_state); + REQUIRE(read_result); + REQUIRE(*read_result == 1536_B); + REQUIRE(buffer[0] == std::byte{'A'}); + REQUIRE(buffer[511] == std::byte{'A'}); + REQUIRE(buffer[512] == std::byte{'B'}); + REQUIRE(buffer[1024] == std::byte{'C'}); + REQUIRE(buffer[1535] == std::byte{'C'}); + } + + THEN("a read spanning a cluster boundary continues in the next cluster") + { + auto buffer = std::array<std::byte, 24>{}; + auto const read_result = read_cluster_chain(3, buffer, 500_B, *driver_state); + REQUIRE(read_result); + REQUIRE(*read_result == 24_B); + REQUIRE(std::ranges::count(buffer, std::byte{'A'}) == 12); + REQUIRE(std::ranges::count(buffer, std::byte{'B'}) == 12); + REQUIRE(buffer[11] == std::byte{'A'}); + REQUIRE(buffer[12] == std::byte{'B'}); + } + + THEN("an offset beyond the first cluster skips the preceding clusters") + { + auto buffer = std::array<std::byte, 10>{}; + auto const read_result = read_cluster_chain(3, buffer, 1100_B, *driver_state); + REQUIRE(read_result); + REQUIRE(*read_result == 10_B); + REQUIRE(std::ranges::count(buffer, std::byte{'C'}) == 10); + } + + THEN("a read is truncated at the end of the chain") + { + auto buffer = std::array<std::byte, 4 * 512>{}; + auto const read_result = read_cluster_chain(3, buffer, 0_B, *driver_state); + REQUIRE(read_result); + REQUIRE(*read_result == 1536_B); + + auto const tail_result = read_cluster_chain(3, buffer, 1500_B, *driver_state); + REQUIRE(tail_result); + REQUIRE(*tail_result == 36_B); + } + + THEN("reading at or past the end of the chain reads nothing") + { + auto buffer = std::array<std::byte, 16>{}; + REQUIRE(read_cluster_chain(3, buffer, 1536_B, *driver_state) == 0_B); + REQUIRE(read_cluster_chain(3, buffer, 5000_B, *driver_state) == 0_B); + } + + THEN("an empty buffer reads nothing") + { + REQUIRE(read_cluster_chain(3, std::span<std::byte>{}, 0_B, *driver_state) == 0_B); + } + + THEN("a chain cannot start outside the data region") + { + auto buffer = std::array<std::byte, 16>{}; + REQUIRE(read_cluster_chain(0, buffer, 0_B, *driver_state).error() == errc::invalid_cluster); + REQUIRE(read_cluster_chain(18, buffer, 0_B, *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); + + auto buffer = std::array<std::byte, 3 * 512>{}; + REQUIRE(read_cluster_chain(8, buffer, 0_B, *driver_state).error() == errc::corrupted_cluster_chain); + } + + THEN("a cyclic chain is detected while reading and while skipping") + { + write_fat_entry(*device, 13, 14); + write_fat_entry(*device, 14, 13); + + auto buffer = std::array<std::byte, 17 * 512>{}; + REQUIRE(read_cluster_chain(13, buffer, 0_B, *driver_state).error() == errc::corrupted_cluster_chain); + REQUIRE(read_cluster_chain(13, std::span{buffer}.first(16), 20 * 512_B, *driver_state).error() == + errc::corrupted_cluster_chain); + } + } +} + // NOLINTEND(readability-magic-numbers) diff --git a/kernel/kernel/test_support/filesystems/fat32.cpp b/kernel/kernel/test_support/filesystems/fat32.cpp index 4513a53a..233cc09f 100644 --- a/kernel/kernel/test_support/filesystems/fat32.cpp +++ b/kernel/kernel/test_support/filesystems/fat32.cpp @@ -7,8 +7,10 @@ #include <kstd/units.hpp> +#include <cstddef> #include <cstdint> #include <cstring> +#include <span> using namespace kstd::units_literals; @@ -70,4 +72,12 @@ namespace kernel::tests::filesystems::fat32 auto const offset = reserved_sectors * sector_size + cluster * kernel::filesystems::fat32::constants::fat_entry_size; write_bytes(device, offset, &value, kstd::size_of(value)); } + + auto write_cluster_data(kernel::tests::devices::block_device & device, std::uint32_t cluster, + std::span<std::byte const> data) -> void + { + auto const data_start = (reserved_sectors + fat_sectors) * sector_size; + auto const offset = data_start + (cluster - kernel::filesystems::fat32::constants::first_data_cluster) * sector_size; + write_bytes(device, offset, data.data(), kstd::bytes{data.size()}); + } } // namespace kernel::tests::filesystems::fat32 diff --git a/kernel/kernel/test_support/filesystems/fat32.hpp b/kernel/kernel/test_support/filesystems/fat32.hpp index e53974fb..1eb28c6f 100644 --- a/kernel/kernel/test_support/filesystems/fat32.hpp +++ b/kernel/kernel/test_support/filesystems/fat32.hpp @@ -4,7 +4,9 @@ #include <kernel/filesystems/fat32/boot_sector.hpp> #include <kernel/test_support/devices/block_device.hpp> +#include <cstddef> #include <cstdint> +#include <span> namespace kernel::tests::filesystems::fat32 { @@ -16,6 +18,8 @@ namespace kernel::tests::filesystems::fat32 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; + auto write_cluster_data(kernel::tests::devices::block_device & device, std::uint32_t cluster, + std::span<std::byte const> data) -> void; } // namespace kernel::tests::filesystems::fat32 |
