diff options
| author | manuel <manuel.bott@eps.ch> | 2026-10-29 10:03:17 +0000 |
|---|---|---|
| committer | manuel <manuel.bott@eps.ch> | 2026-10-29 10:03:17 +0000 |
| commit | 32e34486215b46a1a244962998c893640b18a792 (patch) | |
| tree | 8f84558d8cdca24e596b2e6fd22cf258610a8ede | |
| parent | 856ddc4a22eabe038e08120e1b87fb3afb4aa6b8 (diff) | |
| download | kernel-32e34486215b46a1a244962998c893640b18a792.tar.xz kernel-32e34486215b46a1a244962998c893640b18a792.zip | |
kernel/filesystem: Add inode:readfeature/readFiles
| -rw-r--r-- | kernel/kapi/filesystem.tests.cpp | 92 | ||||
| -rw-r--r-- | kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp | 38 | ||||
| -rw-r--r-- | kernel/kernel/filesystems/fat32/filesystem.cpp | 121 | ||||
| -rw-r--r-- | kernel/kernel/filesystems/fat32/filesystem.tests.cpp | 205 | ||||
| -rw-r--r-- | kernel/kernel/filesystems/fat32/inode.cpp | 126 | ||||
| -rw-r--r-- | kernel/kernel/filesystems/fat32/inode.hpp | 22 | ||||
| -rw-r--r-- | kernel/kernel/filesystems/fat32/inode.tests.cpp | 522 | ||||
| -rw-r--r-- | kernel/kernel/main.cpp | 84 | ||||
| -rw-r--r-- | kernel/kernel/test_support/filesystems/fat32.cpp | 27 | ||||
| -rw-r--r-- | kernel/kernel/test_support/filesystems/fat32.hpp | 13 |
10 files changed, 1104 insertions, 146 deletions
diff --git a/kernel/kapi/filesystem.tests.cpp b/kernel/kapi/filesystem.tests.cpp index efcb4fd3..f46c6bb8 100644 --- a/kernel/kapi/filesystem.tests.cpp +++ b/kernel/kapi/filesystem.tests.cpp @@ -12,7 +12,9 @@ #include <cstddef> #include <filesystem> #include <ranges> +#include <string> #include <string_view> +#include <utility> #include <vector> using namespace kstd::units_literals; @@ -339,4 +341,94 @@ SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_vfs_fixture, } } +SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_vfs_fixture, "Kapi filesystem with a mounted fat32 image", + "[kapi][filesystem][fat32]") +{ + auto const ext2_image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + auto const fat32_image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "fat32.img"; + + GIVEN("an ext2 root filesystem with a fat32 image mounted on /information") + { + REQUIRE(std::filesystem::exists(ext2_image_path)); + REQUIRE(std::filesystem::exists(fat32_image_path)); + REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_1", "test_img_module_2"}, + {ext2_image_path, fat32_image_path})); + REQUIRE(kapi::filesystem::mount("/dev/ram1", "/information")); + + auto const read_text = [](std::size_t fd, std::size_t chunk_size) { + auto text = std::string{}; + auto buffer = std::vector<std::byte>(chunk_size); + while (true) + { + auto const bytes_read = kapi::filesystem::read(fd, buffer); + REQUIRE(bytes_read); + if (*bytes_read == 0_B) + { + return text; + } + text.append(std::bit_cast<char *>(buffer.data()), bytes_read->value); + } + }; + + THEN("a file can be opened by its lowercase name, read and closed again") + { + auto const fd = kapi::filesystem::open("/information/hello.txt"); + REQUIRE(fd); + REQUIRE(read_text(*fd, 64) == "Hello World!"); + REQUIRE(kapi::filesystem::close(*fd)); + } + + THEN("a file can be read in small chunks through its file descriptor") + { + auto const fd = kapi::filesystem::open("/information/hello.txt"); + REQUIRE(fd); + REQUIRE(read_text(*fd, 5) == "Hello World!"); + REQUIRE(kapi::filesystem::close(*fd)); + } + + THEN("reading continues at a seeked position") + { + auto const fd = kapi::filesystem::open("/information/hello.txt"); + REQUIRE(fd); + REQUIRE(kapi::filesystem::seek(*fd, 6_B_off, kapi::filesystem::seek_origin::beginning) == 6_B); + REQUIRE(read_text(*fd, 16) == "World!"); + REQUIRE(kapi::filesystem::close(*fd)); + } + + THEN("the status of a file reports its type and size") + { + auto const status = kapi::filesystem::status("/information/hello.txt"); + REQUIRE(status); + REQUIRE(status->mode == std::to_underlying(kapi::filesystem::file_type::regular)); + REQUIRE(status->size == 12_B); + } + + THEN("the mounted root directory can be listed") + { + auto const directory = kapi::filesystem::open("/information"); + REQUIRE(directory); + + auto buffer = std::vector<kapi::filesystem::directory_entry>{10}; + REQUIRE(kapi::filesystem::read_directory(*directory, buffer) == 1); + REQUIRE(buffer[0].name == "HELLO.TXT"); + REQUIRE(kapi::filesystem::read_directory(*directory, buffer) == 0); + } + + THEN("writing to a file fails because the filesystem is read only") + { + auto const fd = kapi::filesystem::open("/information/hello.txt"); + REQUIRE(fd); + + auto const buffer = std::vector<std::byte>{std::byte{'X'}}; + REQUIRE(kapi::filesystem::write(*fd, buffer).error() == kstd::errc::read_only_file_system); + REQUIRE(kapi::filesystem::close(*fd)); + } + + THEN("opening a missing file fails") + { + REQUIRE(kapi::filesystem::open("/information/missing.txt").error() == kstd::errc::no_such_file_or_directory); + } + } +} + // NOLINTEND (readability-magic-numbers)
\ No newline at end of file diff --git a/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp b/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp index 204c5a40..ba2b80ea 100644 --- a/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp +++ b/kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp @@ -34,35 +34,15 @@ namespace { using kernel::filesystems::fat32::directory_entry; using kernel::filesystems::fat32::directory_iterator; + using kernel::tests::filesystems::fat32::make_entry; 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>{}; @@ -204,7 +184,7 @@ SCENARIO("Fat32 directory_iterator on a mock layout", "[filesystem][fat32][readd directory_entry{}, file_entry("HIDDEN TXT"), }; - write_entries(*device, 2, entries); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 2, entries); THEN("only the regular entries before the end marker are visited") { @@ -254,8 +234,8 @@ SCENARIO("Fat32 directory_iterator on a mock layout", "[filesystem][fat32][readd { 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")}); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 3, first_cluster_entries); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 4, std::array{file_entry("LAST TXT")}); write_fat_entry(*device, 3, 4); write_fat_entry(*device, 4, 0x0FFF'FFFF); @@ -287,7 +267,7 @@ SCENARIO("Fat32 directory_iterator on a mock layout", "[filesystem][fat32][readd { auto entries = std::array<directory_entry, entries_per_cluster>{}; std::ranges::fill(entries, file_entry("FILE")); - write_entries(*device, 6, entries); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 6, entries); write_fat_entry(*device, 6, 0x0FFF'FFFF); THEN("iteration ends with the chain") @@ -311,8 +291,8 @@ SCENARIO("Fat32 directory_iterator on a mock layout", "[filesystem][fat32][readd { 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); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 13, entries); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 14, entries); write_fat_entry(*device, 13, 14); write_fat_entry(*device, 14, 13); @@ -333,8 +313,8 @@ SCENARIO("Fat32 directory_iterator on a mock layout", "[filesystem][fat32][readd { 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); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 8, entries); + kernel::tests::filesystems::fat32::write_directory_entries(*device, 9, entries); write_fat_entry(*device, 8, 9); write_fat_entry(*device, 9, 0); diff --git a/kernel/kernel/filesystems/fat32/filesystem.cpp b/kernel/kernel/filesystems/fat32/filesystem.cpp index c33fd676..63058e54 100644 --- a/kernel/kernel/filesystems/fat32/filesystem.cpp +++ b/kernel/kernel/filesystems/fat32/filesystem.cpp @@ -2,6 +2,7 @@ #include <kernel/filesystems/fat32/boot_sector.hpp> #include <kernel/filesystems/fat32/directory_entry.hpp> +#include <kernel/filesystems/fat32/directory_iterator.hpp> #include <kernel/filesystems/fat32/driver_state.hpp> #include <kernel/filesystems/fat32/error.hpp> #include <kernel/filesystems/fat32/file_allocation_table.hpp> @@ -12,15 +13,14 @@ #include <kapi/filesystem.hpp> #include <kstd/memory.hpp> -#include <kstd/print.hpp> +#include <kstd/mutex.hpp> #include <kstd/result.hpp> #include <kstd/span.hpp> #include <kstd/system_error.hpp> #include <kstd/units.hpp> -#include <array> +#include <algorithm> #include <bit> -#include <cstddef> #include <cstdint> #include <optional> #include <string_view> @@ -32,46 +32,16 @@ namespace kernel::filesystems::fat32 //! Hidden implementation details. namespace { - //! Convert a fixed size, space padded character array of the boot sector into a string view. + //! Compare two names, ignoring the case of ASCII letters. //! - //! @param array The array to convert. - //! @return A view of the array contents. - template<std::size_t Size> - [[nodiscard]] constexpr auto to_string_view(std::array<char, Size> const & array) -> std::string_view - { - return {array.data(), array.size()}; - } - - // TODO: Remove later, just for demo - auto print_boot_sector(boot_sector const & boot_sector) -> void - { - kstd::println("[OS:FAT32] jump_boot: {:#04X} {:#04X} {:#04X}", boot_sector.jump_boot[0], boot_sector.jump_boot[1], - boot_sector.jump_boot[2]); - kstd::println("[OS:FAT32] oem_name: '{}'", to_string_view(boot_sector.oem_name)); - kstd::println("[OS:FAT32] bytes_per_sector: {}", boot_sector.bytes_per_sector); - kstd::println("[OS:FAT32] sectors_per_cluster: {}", boot_sector.sectors_per_cluster); - kstd::println("[OS:FAT32] reserved_sector_count: {}", boot_sector.reserved_sector_count); - kstd::println("[OS:FAT32] number_of_fats: {}", boot_sector.number_of_fats); - kstd::println("[OS:FAT32] root_entry_count: {}", boot_sector.root_entry_count); - kstd::println("[OS:FAT32] total_sectors_16: {}", boot_sector.total_sectors_16); - kstd::println("[OS:FAT32] media: {:#04X}", boot_sector.media); - kstd::println("[OS:FAT32] fat_size_16: {}", boot_sector.fat_size_16); - kstd::println("[OS:FAT32] sectors_per_track: {}", boot_sector.sectors_per_track); - kstd::println("[OS:FAT32] number_of_heads: {}", boot_sector.number_of_heads); - kstd::println("[OS:FAT32] hidden_sectors: {}", boot_sector.hidden_sectors); - kstd::println("[OS:FAT32] total_sectors_32: {}", boot_sector.total_sectors_32); - kstd::println("[OS:FAT32] fat_size_32: {}", boot_sector.fat_size_32); - kstd::println("[OS:FAT32] extended_flags: {:#06X}", boot_sector.extended_flags); - kstd::println("[OS:FAT32] filesystem_version: {:#06X}", boot_sector.filesystem_version); - kstd::println("[OS:FAT32] root_cluster: {}", boot_sector.root_cluster); - kstd::println("[OS:FAT32] filesystem_info_sector: {}", boot_sector.filesystem_info_sector); - kstd::println("[OS:FAT32] backup_boot_sector: {}", boot_sector.backup_boot_sector); - kstd::println("[OS:FAT32] drive_number: {:#04X}", boot_sector.drive_number); - kstd::println("[OS:FAT32] extended_boot_signature: {:#04X}", boot_sector.extended_boot_signature); - kstd::println("[OS:FAT32] volume_id: {:#010X}", boot_sector.volume_id); - kstd::println("[OS:FAT32] volume_label: '{}'", to_string_view(boot_sector.volume_label)); - kstd::println("[OS:FAT32] filesystem_type: '{}'", to_string_view(boot_sector.filesystem_type)); - kstd::println("[OS:FAT32] boot_signature: {:#06X}", boot_sector.boot_signature); + //! Short names are stored in upper case, but are matched case-insensitively, e.g. "hello.txt" finds "HELLO.TXT". + [[nodiscard]] constexpr auto equals_ignoring_case(std::string_view lhs, std::string_view rhs) -> bool + { + auto const to_lower = [](char character) { + return character >= 'A' && character <= 'Z' ? static_cast<char>(character - 'A' + 'a') : character; + }; + + return std::ranges::equal(lhs, rhs, {}, to_lower, to_lower); } //! Read the boot sector from a backing inode and check its signature. @@ -139,13 +109,29 @@ namespace kernel::filesystems::fat32 auto filesystem::probe(inode_ptr const & inode) const -> kstd::result<std::uint32_t> { + if (!inode) + { + return kstd::failure(vfs::errc::invalid_inode); + } + auto const boot_sector = read_boot_sector(inode); if (!boot_sector) { return kstd::failure(boot_sector.error()); } - print_boot_sector(*boot_sector); + // The boot signature alone is shared with every MBR, so the layout must describe a valid fat32 volume as well. + auto state = driver_state{}; + state.boot_sector = *boot_sector; + if (auto layout_result = compute_layout(state); !layout_result) + { + return kstd::failure(layout_result.error()); + } + + if (!is_valid_cluster(state.boot_sector.root_cluster, state)) + { + return kstd::failure(errc::invalid_cluster); + } return 0; } @@ -178,12 +164,57 @@ namespace kernel::filesystems::fat32 return kstd::failure(errc::invalid_cluster); } - auto root = kstd::make_shared<inode>(root_cluster, constants::attribute_directory); + auto root = kstd::make_shared<inode>(root_cluster, constants::attribute_directory, kstd::bytes{}, + constants::root_inode_number); return mount_result{root, driver_state}; } - auto filesystem::lookup(inode_ptr const &, std::string_view, driver_data_ptr) const -> kstd::result<inode_ptr> + auto filesystem::lookup(inode_ptr const & parent, std::string_view name, driver_data_ptr driver_data) const + -> kstd::result<inode_ptr> { + auto const directory = static_pointer_cast<inode>(parent); + if (!directory) + { + return kstd::failure(vfs::errc::invalid_inode); + } + + if (!directory->is_directory()) + { + return kstd::failure(vfs::errc::not_a_directory); + } + + auto const state = static_pointer_cast<driver_state>(driver_data); + if (!state) + { + return kstd::failure(vfs::errc::not_mounted); + } + + auto guard = kstd::lock_guard{state->lock}; + + auto it = directory_iterator{directory->first_cluster(), *state}; + for (; it != directory_iterator{}; ++it) + { + auto const entry_name = it->entry.short_name(); + if (!equals_ignoring_case(entry_name, name)) + { + continue; + } + + // A ".." entry pointing to the root directory stores cluster 0 instead of the root cluster. + if (it->entry.first_cluster() == 0 && (it->entry.attributes & constants::attribute_directory) != 0) + { + return kstd::make_shared<inode>(std::uint32_t{state->boot_sector.root_cluster}, constants::attribute_directory, + kstd::bytes{}, constants::root_inode_number); + } + + return kstd::make_shared<inode>(*it); + } + + if (it.error()) + { + return kstd::failure(it.error()); + } + return kstd::failure(vfs::errc::no_such_file_or_directory); } diff --git a/kernel/kernel/filesystems/fat32/filesystem.tests.cpp b/kernel/kernel/filesystems/fat32/filesystem.tests.cpp index b2f4af62..12958e75 100644 --- a/kernel/kernel/filesystems/fat32/filesystem.tests.cpp +++ b/kernel/kernel/filesystems/fat32/filesystem.tests.cpp @@ -1,8 +1,11 @@ #include <kernel/filesystems/fat32/filesystem.hpp> #include <kernel/devices/storage.hpp> +#include <kernel/filesystems/fat32/directory_entry.hpp> +#include <kernel/filesystems/fat32/directory_iterator.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/inode.hpp> #include <kernel/test_support/devices/block_device.hpp> #include <kernel/test_support/filesystems/fat32.hpp> @@ -18,6 +21,7 @@ #include <catch2/catch_test_macros.hpp> +#include <array> #include <filesystem> using namespace kstd::units_literals; @@ -179,4 +183,205 @@ SCENARIO("Fat32 filesystem rejects invalid boot signature", "[filesystem][fat32] } } +SCENARIO("Fat32 filesystem probe validates the layout", "[filesystem][fat32][filesystem]") +{ + auto const block_size = 512_B; + + GIVEN("no backing inode") + { + auto fs = kstd::make_shared<kernel::filesystems::fat32::filesystem>(); + + THEN("probe fails with invalid_inode") + { + REQUIRE(fs->probe(nullptr).error() == kernel::vfs::errc::invalid_inode); + } + } + + GIVEN("a block device") + { + 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("a minimal fat32 layout is accepted") + { + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, boot_sector); + REQUIRE(fs->probe(dev_inode) == 0u); + } + + THEN("a boot sector carrying only the boot signature, like a master boot record, is rejected") + { + auto signature_only = kernel::filesystems::fat32::boot_sector{}; + signature_only.boot_signature = kernel::filesystems::fat32::constants::boot_signature; + kernel::tests::filesystems::fat32::setup_mock_fat32_layout(*device, signature_only); + REQUIRE(fs->probe(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->probe(dev_inode).error() == kernel::filesystems::fat32::errc::invalid_cluster); + } + } +} + +SCENARIO("Fat32 filesystem lookup on a mock layout", "[filesystem][fat32][filesystem][lookup]") +{ + auto const block_size = 512_B; + + GIVEN("a mounted minimal fat32 layout with a file and a subdirectory") + { + using namespace kernel::filesystems::fat32; + using kernel::tests::filesystems::fat32::make_entry; + using kernel::tests::filesystems::fat32::write_directory_entries; + using kernel::tests::filesystems::fat32::write_fat_entry; + + 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); + + write_directory_entries(*device, 2, + std::array{ + make_entry("LABEL", constants::attribute_volume_id), + make_entry("HELLO TXT", constants::attribute_archive, 3, 5), + make_entry("SUBDIR", constants::attribute_directory, 4), + }); + write_fat_entry(*device, 3, 0x0FFF'FFFF); + write_directory_entries(*device, 4, + std::array{ + make_entry(".", constants::attribute_directory, 4), + make_entry("..", constants::attribute_directory, 0), + make_entry("INNER TXT", constants::attribute_archive, 5, 7), + }); + write_fat_entry(*device, 4, 0x0FFF'FFFF); + write_fat_entry(*device, 5, 0x0FFF'FFFF); + + 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 [root, driver_data] = *mount; + root->set_driver_state(driver_data); + + THEN("a file is found by its exact short name") + { + auto const found = fs->lookup(root, "HELLO.TXT", driver_data); + REQUIRE(found); + + auto const file = static_pointer_cast<kernel::filesystems::fat32::inode>(*found); + REQUIRE(file->is_regular()); + REQUIRE(file->first_cluster() == 3); + REQUIRE(file->size() == 5_B); + + auto const status = file->status(); + REQUIRE(status); + REQUIRE(status->inode_number == (cluster_offset(2, static_cast<driver_state const &>(*driver_data)) + 32_B).value); + } + + THEN("names are matched ignoring their case") + { + REQUIRE(fs->lookup(root, "hello.txt", driver_data)); + REQUIRE(fs->lookup(root, "Hello.Txt", driver_data)); + REQUIRE(fs->lookup(root, "subdir", driver_data)); + } + + THEN("a name that only partially matches is not found") + { + REQUIRE(fs->lookup(root, "HELLO", driver_data).error() == kernel::vfs::errc::no_such_file_or_directory); + REQUIRE(fs->lookup(root, "HELLO.TXT2", driver_data).error() == kernel::vfs::errc::no_such_file_or_directory); + REQUIRE(fs->lookup(root, "", driver_data).error() == kernel::vfs::errc::no_such_file_or_directory); + } + + THEN("the volume label is not found") + { + REQUIRE(fs->lookup(root, "LABEL", driver_data).error() == kernel::vfs::errc::no_such_file_or_directory); + } + + THEN("a file in a subdirectory is found through the subdirectory") + { + auto const subdir = fs->lookup(root, "SUBDIR", driver_data); + REQUIRE(subdir); + REQUIRE((*subdir)->is_directory()); + + auto const inner = fs->lookup(*subdir, "inner.txt", driver_data); + REQUIRE(inner); + REQUIRE(static_pointer_cast<kernel::filesystems::fat32::inode>(*inner)->size() == 7_B); + } + + THEN("the dot dot entry of a subdirectory of the root leads to the root directory") + { + auto const subdir = fs->lookup(root, "SUBDIR", driver_data); + REQUIRE(subdir); + + auto const parent = fs->lookup(*subdir, "..", driver_data); + REQUIRE(parent); + REQUIRE((*parent)->is_directory()); + REQUIRE(static_pointer_cast<kernel::filesystems::fat32::inode>(*parent)->first_cluster() == 2); + REQUIRE((*parent)->status()->inode_number == constants::root_inode_number); + } + + THEN("looking up below a file fails") + { + auto const file = fs->lookup(root, "HELLO.TXT", driver_data); + REQUIRE(file); + REQUIRE(fs->lookup(*file, "X", driver_data).error() == kernel::vfs::errc::not_a_directory); + } + + THEN("invalid arguments are rejected") + { + REQUIRE(fs->lookup(nullptr, "HELLO.TXT", driver_data).error() == kernel::vfs::errc::invalid_inode); + REQUIRE(fs->lookup(root, "HELLO.TXT", nullptr).error() == kernel::vfs::errc::not_mounted); + } + + WHEN("the full root directory cluster is marked as free in the file allocation table") + { + auto entries = std::array<directory_entry, 16>{}; + entries.fill(make_entry("FILLER TXT", constants::attribute_archive)); + write_directory_entries(*device, 2, entries); + write_fat_entry(*device, 2, 0); + + THEN("lookup reports the corrupted chain instead of a missing file") + { + REQUIRE(fs->lookup(root, "MISSING", driver_data).error() == errc::corrupted_cluster_chain); + } + } + } +} + +SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_fixture, "Fat32 filesystem lookup with real image", + "[filesystem][fat32][filesystem][lookup][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 [root, driver_data] = *mount; + + THEN("HELLO.TXT is found by its lowercase name with its size") + { + auto const found = fs->lookup(root, "hello.txt", driver_data); + REQUIRE(found); + REQUIRE((*found)->is_regular()); + REQUIRE(static_pointer_cast<kernel::filesystems::fat32::inode>(*found)->first_cluster() == 3); + REQUIRE(static_pointer_cast<kernel::filesystems::fat32::inode>(*found)->size() == 12_B); + } + + THEN("a missing file is not found") + { + REQUIRE(fs->lookup(root, "missing.txt", driver_data).error() == kernel::vfs::errc::no_such_file_or_directory); + } + } +} + // NOLINTEND(readability-magic-numbers) diff --git a/kernel/kernel/filesystems/fat32/inode.cpp b/kernel/kernel/filesystems/fat32/inode.cpp index f6b75b69..0c3d07c5 100644 --- a/kernel/kernel/filesystems/fat32/inode.cpp +++ b/kernel/kernel/filesystems/fat32/inode.cpp @@ -1,33 +1,82 @@ #include <kernel/vfs/inode.hpp> #include <kernel/filesystems/fat32/directory_entry.hpp> +#include <kernel/filesystems/fat32/directory_iterator.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/inode.hpp> +#include <kernel/vfs/error.hpp> #include <kapi/filesystem.hpp> +#include <kstd/memory.hpp> +#include <kstd/mutex.hpp> #include <kstd/result.hpp> #include <kstd/units.hpp> +#include <algorithm> #include <cstddef> #include <cstdint> #include <span> #include <utility> +using namespace kstd::units_literals; + namespace kernel::filesystems::fat32 { - inode::inode(std::uint32_t first_cluster, std::uint8_t attributes) + inode::inode(std::uint32_t first_cluster, std::uint8_t attributes, kstd::bytes size, std::uint64_t inode_number) : m_first_cluster(first_cluster) , m_attributes(attributes) + , m_size(size) + , m_inode_number(inode_number) + {} + + inode::inode(directory_record const & record) + : inode(record.entry.first_cluster(), record.entry.attributes, kstd::bytes{record.entry.file_size}, + record.inode_number()) {} - auto inode::read(std::span<std::byte>, kstd::bytes) const -> kstd::result<kstd::bytes> + auto inode::read(std::span<std::byte> buffer, kstd::bytes offset) const -> kstd::result<kstd::bytes> { - return kstd::bytes{}; + if (is_directory()) + { + return kstd::failure(vfs::errc::is_a_directory); + } + + auto state = static_pointer_cast<fat32::driver_state>(driver_state().lock()); + if (!state || !state->backing_inode) + { + return kstd::failure(vfs::errc::not_mounted); + } + + if (offset >= m_size || buffer.empty()) + { + return 0_B; + } + + // The last cluster of a file is padded up to the cluster size, so the read must be limited to the file size. + auto const requested_size = std::min(kstd::bytes{buffer.size()}, m_size - offset); + + auto guard = kstd::lock_guard{state->lock}; + auto const read_result = read_cluster_chain(m_first_cluster, buffer.first(requested_size.value), offset, *state); + if (!read_result) + { + return kstd::failure(read_result.error()); + } + + // A chain that ends before the file size disagrees with the directory entry, so the volume is inconsistent. + if (*read_result < requested_size) + { + return kstd::failure(errc::corrupted_cluster_chain); + } + + return *read_result; } auto inode::write(std::span<std::byte const>, kstd::bytes) -> kstd::result<kstd::bytes> { - return kstd::bytes{}; + return kstd::failure(vfs::errc::read_only_file_system); } auto inode::is_directory() const -> bool @@ -35,15 +84,80 @@ namespace kernel::filesystems::fat32 return (m_attributes & constants::attribute_directory) != 0; } + auto inode::is_regular() const -> bool + { + return (m_attributes & (constants::attribute_directory | constants::attribute_volume_id)) == 0; + } + + auto inode::status() const -> kstd::result<kapi::filesystem::file_status> + { + auto status = vfs::inode::status(); + if (!status) + { + return status; + } + + status->size = m_size; + status->inode_number = m_inode_number; + status->link_count = 1; + return status; + } + auto inode::read_directory(kapi::filesystem::directory_cursor position, - std::span<kapi::filesystem::directory_entry>) const + std::span<kapi::filesystem::directory_entry> entries) const -> kstd::result<std::pair<std::size_t, kapi::filesystem::directory_cursor>> { - return std::pair{0, position}; + if (!is_directory()) + { + return kstd::failure(vfs::errc::not_a_directory); + } + + auto state = static_pointer_cast<fat32::driver_state>(driver_state().lock()); + if (!state || !state->backing_inode) + { + return kstd::failure(vfs::errc::not_mounted); + } + + if (entries.empty()) + { + return std::pair{0uz, position}; + } + + auto guard = kstd::lock_guard{state->lock}; + + auto count = 0uz; + auto it = directory_iterator{m_first_cluster, kstd::bytes{position.value}, *state}; + for (; it != directory_iterator{}; ++it) + { + auto const is_directory_entry = (it->entry.attributes & constants::attribute_directory) != 0; + entries[count++] = kapi::filesystem::directory_entry{ + .name = it->entry.short_name(), + .type = is_directory_entry ? kapi::filesystem::file_type::directory : kapi::filesystem::file_type::regular, + .inode_number = it->inode_number(), + }; + + // Stop before advancing, so that the offset behind the last returned entry is the position to resume at. + if (count == entries.size()) + { + break; + } + } + + if (it.error()) + { + return kstd::failure(it.error()); + } + + return std::pair{count, kapi::filesystem::directory_cursor{it.offset().value}}; } auto inode::first_cluster() const -> std::uint32_t { return m_first_cluster; } + + auto inode::size() const -> kstd::bytes + { + return m_size; + } } // namespace kernel::filesystems::fat32 diff --git a/kernel/kernel/filesystems/fat32/inode.hpp b/kernel/kernel/filesystems/fat32/inode.hpp index d14f4ec9..82a8547c 100644 --- a/kernel/kernel/filesystems/fat32/inode.hpp +++ b/kernel/kernel/filesystems/fat32/inode.hpp @@ -1,6 +1,7 @@ #ifndef TEACHOS_KERNEL_FILESYSTEMS_FAT32_INODE_HPP #define TEACHOS_KERNEL_FILESYSTEMS_FAT32_INODE_HPP +#include <kernel/filesystems/fat32/directory_iterator.hpp> #include <kernel/vfs/inode.hpp> #include <kapi/filesystem.hpp> @@ -24,7 +25,15 @@ namespace kernel::filesystems::fat32 //! //! @param first_cluster The first cluster of the inode's data. //! @param attributes The attributes of the directory entry describing the inode. - explicit inode(std::uint32_t first_cluster, std::uint8_t attributes); + //! @param size The size of the inode's data in bytes. + //! @param inode_number The inode number reported in the inode's status. + explicit inode(std::uint32_t first_cluster, std::uint8_t attributes, kstd::bytes size = {}, + std::uint64_t inode_number = {}); + + //! Create a fat32 inode for the file described by a directory record. + //! + //! @param record The record describing the inode. + explicit inode(directory_record const & record); [[nodiscard]] auto read(std::span<std::byte> buffer, kstd::bytes offset) const -> kstd::result<kstd::bytes> override; @@ -34,6 +43,10 @@ namespace kernel::filesystems::fat32 [[nodiscard]] auto is_directory() const -> bool override; + [[nodiscard]] auto is_regular() const -> bool override; + + [[nodiscard]] auto status() const -> kstd::result<kapi::filesystem::file_status> override; + [[nodiscard]] auto read_directory(kapi::filesystem::directory_cursor position, std::span<kapi::filesystem::directory_entry> entries) const -> kstd::result<std::pair<std::size_t, kapi::filesystem::directory_cursor>> override; @@ -43,9 +56,16 @@ namespace kernel::filesystems::fat32 //! @return The first cluster number. [[nodiscard]] auto first_cluster() const -> std::uint32_t; + //! Get the size of the inode's data. + //! + //! @return The size in bytes, which is always 0 for directories. + [[nodiscard]] auto size() const -> kstd::bytes; + private: std::uint32_t m_first_cluster{}; std::uint8_t m_attributes{}; + kstd::bytes m_size{}; + std::uint64_t m_inode_number{}; }; } // namespace kernel::filesystems::fat32 diff --git a/kernel/kernel/filesystems/fat32/inode.tests.cpp b/kernel/kernel/filesystems/fat32/inode.tests.cpp index 030ee0ac..c5825535 100644 --- a/kernel/kernel/filesystems/fat32/inode.tests.cpp +++ b/kernel/kernel/filesystems/fat32/inode.tests.cpp @@ -1,32 +1,175 @@ #include <kernel/filesystems/fat32/inode.hpp> +#include <kernel/devices/storage.hpp> #include <kernel/filesystems/fat32/directory_entry.hpp> +#include <kernel/filesystems/fat32/directory_iterator.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 <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> #include <kstd/vector.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 <utility> +#include <vector> using namespace kstd::units_literals; -SCENARIO("Fat32 inode read/write", "[filesystem][fat32][inode]") +// NOLINTBEGIN(readability-magic-numbers) + +namespace +{ + constexpr auto cluster_size = 512uz; + + //! The byte stored at a given offset of a test file, chosen so that every offset can be told apart from its + //! neighbors and from the start of other clusters. + auto file_byte(std::size_t offset) -> std::byte + { + return static_cast<std::byte>(offset % 251); + } + + //! Write a test file of a given size into a chain of clusters. + auto write_file(kernel::tests::devices::block_device & device, std::span<std::uint32_t const> chain, std::size_t size) + -> void + { + for (auto index = 0uz; index < chain.size(); ++index) + { + auto data = std::vector<std::byte>(cluster_size, std::byte{0xEE}); + for (auto offset = 0uz; offset < cluster_size && index * cluster_size + offset < size; ++offset) + { + data[offset] = file_byte(index * cluster_size + offset); + } + kernel::tests::filesystems::fat32::write_cluster_data(device, chain[index], data); + + auto const next = index + 1 < chain.size() ? chain[index + 1] : 0x0FFF'FFFF; + kernel::tests::filesystems::fat32::write_fat_entry(device, chain[index], next); + } + } + + auto expected_bytes(std::size_t offset, std::size_t size) -> std::vector<std::byte> + { + auto bytes = std::vector<std::byte>(size); + for (auto index = 0uz; index < size; ++index) + { + bytes[index] = file_byte(offset + index); + } + return bytes; + } +} // namespace + +SCENARIO("Fat32 inode attributes", "[filesystem][fat32][inode]") { - GIVEN("a fat32 inode") + using namespace kernel::filesystems::fat32; + + GIVEN("a fat32 inode with the directory attribute") + { + auto inode = kernel::filesystems::fat32::inode{2, constants::attribute_directory}; + + THEN("it is a directory and keeps its first cluster") + { + REQUIRE(inode.is_directory()); + REQUIRE_FALSE(inode.is_regular()); + REQUIRE(inode.first_cluster() == 2); + } + } + + GIVEN("a fat32 inode without the directory attribute") + { + auto inode = kernel::filesystems::fat32::inode{3, 0}; + + THEN("it is a regular file") + { + REQUIRE_FALSE(inode.is_directory()); + REQUIRE(inode.is_regular()); + } + } + + GIVEN("a fat32 inode with the read only and archive attributes") + { + auto inode = kernel::filesystems::fat32::inode{3, constants::attribute_read_only | constants::attribute_archive}; + + THEN("it is a regular file") + { + REQUIRE(inode.is_regular()); + } + } + + GIVEN("a fat32 inode with the volume id attribute") { - auto inode = kernel::filesystems::fat32::inode{2, 0}; + auto inode = kernel::filesystems::fat32::inode{0, constants::attribute_volume_id}; + + THEN("it is neither a directory nor a regular file") + { + REQUIRE_FALSE(inode.is_directory()); + REQUIRE_FALSE(inode.is_regular()); + } + } + + GIVEN("a fat32 inode created from a directory record") + { + auto record = directory_record{}; + record.entry.attributes = constants::attribute_archive; + record.entry.first_cluster_high = 0x0001; + record.entry.first_cluster_low = 0x0002; + record.entry.file_size = 1234; + record.location = 4096_B; + + auto inode = kernel::filesystems::fat32::inode{record}; + + THEN("it takes its first cluster, size and inode number from the record") + { + REQUIRE(inode.first_cluster() == 0x0001'0002); + REQUIRE(inode.size() == 1234_B); + REQUIRE(inode.is_regular()); + } + + THEN("its status reports a regular file with the record's size and location") + { + auto const status = inode.status(); + REQUIRE(status); + REQUIRE(status->mode == std::to_underlying(kapi::filesystem::file_type::regular)); + REQUIRE(status->size == 1234_B); + REQUIRE(status->inode_number == 4096); + REQUIRE(status->link_count == 1); + } + } +} + +SCENARIO("Fat32 inode read/write without a mount", "[filesystem][fat32][inode]") +{ + GIVEN("a fat32 file inode that does not belong to a mount") + { + auto inode = kernel::filesystems::fat32::inode{3, 0, 10_B}; WHEN("reading from the inode") { kstd::vector<std::byte> buffer(10); auto bytes_read = inode.read(buffer, 0_B); - THEN("no bytes are read") + THEN("the read fails because the inode is not mounted") { - REQUIRE(bytes_read == 0_B); + REQUIRE_FALSE(bytes_read); + REQUIRE(bytes_read.error() == kernel::vfs::errc::not_mounted); } } @@ -35,34 +178,377 @@ SCENARIO("Fat32 inode read/write", "[filesystem][fat32][inode]") kstd::vector<std::byte> buffer(10, std::byte{'x'}); auto bytes_written = inode.write(buffer, 0_B); - THEN("no bytes are written") + THEN("the write fails because the filesystem is read only") { - REQUIRE(bytes_written == 0_B); + REQUIRE_FALSE(bytes_written); + REQUIRE(bytes_written.error() == kernel::vfs::errc::read_only_file_system); } } } } -SCENARIO("Fat32 inode attributes", "[filesystem][fat32][inode]") +SCENARIO("Fat32 inode reads file data on a mock layout", "[filesystem][fat32][inode]") { - GIVEN("a fat32 inode with the directory attribute") + auto const block_size = 512_B; + + GIVEN("a mounted minimal fat32 layout") { - auto inode = kernel::filesystems::fat32::inode{2, kernel::filesystems::fat32::constants::attribute_directory}; + 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); - THEN("it is a directory and keeps its first cluster") + 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); + + using namespace kernel::filesystems::fat32; + + auto const make_file = [&](std::uint32_t first_cluster, kstd::bytes size) { + auto inode = kstd::make_shared<kernel::filesystems::fat32::inode>(first_cluster, constants::attribute_archive, size); + inode->set_driver_state(mount->second); + return inode; + }; + + WHEN("a file fits into a single cluster") { - REQUIRE(inode.is_directory()); - REQUIRE(inode.first_cluster() == 2); + auto const chain = std::array{3u}; + write_file(*device, chain, 100); + auto const file = make_file(3, 100_B); + + THEN("the whole file can be read") + { + auto buffer = std::vector<std::byte>(100); + auto const result = file->read(buffer, 0_B); + REQUIRE(result); + REQUIRE(*result == 100_B); + REQUIRE(buffer == expected_bytes(0, 100)); + } + + THEN("a read is limited to the file size and leaves the rest of the buffer untouched") + { + auto buffer = std::vector<std::byte>(cluster_size, std::byte{0xAB}); + auto const result = file->read(buffer, 0_B); + REQUIRE(result); + REQUIRE(*result == 100_B); + REQUIRE(std::ranges::equal(std::span{buffer}.first(100), expected_bytes(0, 100))); + REQUIRE(std::ranges::all_of(std::span{buffer}.subspan(100), [](auto byte) { return byte == std::byte{0xAB}; })); + } + + THEN("a read starting inside the file returns the remaining bytes") + { + auto buffer = std::vector<std::byte>(cluster_size); + auto const result = file->read(buffer, 60_B); + REQUIRE(result); + REQUIRE(*result == 40_B); + REQUIRE(std::ranges::equal(std::span{buffer}.first(40), expected_bytes(60, 40))); + } + + THEN("a read at or behind the end of the file returns no bytes") + { + auto buffer = std::vector<std::byte>(10, std::byte{0xAB}); + REQUIRE(file->read(buffer, 100_B) == 0_B); + REQUIRE(file->read(buffer, 5000_B) == 0_B); + REQUIRE(std::ranges::all_of(buffer, [](auto byte) { return byte == std::byte{0xAB}; })); + } + + THEN("a read into an empty buffer returns no bytes") + { + REQUIRE(file->read(std::span<std::byte>{}, 0_B) == 0_B); + } + } + + WHEN("a file spans multiple fragmented clusters") + { + auto const chain = std::array{5u, 3u, 9u}; + auto const size = 2 * cluster_size + 200; + write_file(*device, chain, size); + auto const file = make_file(5, kstd::bytes{size}); + + THEN("the whole file can be read in one go") + { + auto buffer = std::vector<std::byte>(size); + auto const result = file->read(buffer, 0_B); + REQUIRE(result); + REQUIRE(*result == kstd::bytes{size}); + REQUIRE(buffer == expected_bytes(0, size)); + } + + THEN("a read crossing a cluster boundary returns the bytes of both clusters") + { + auto buffer = std::vector<std::byte>(100); + auto const result = file->read(buffer, kstd::bytes{cluster_size - 50}); + REQUIRE(result); + REQUIRE(*result == 100_B); + REQUIRE(buffer == expected_bytes(cluster_size - 50, 100)); + } + + THEN("a read starting in the last cluster stops at the end of the file") + { + auto buffer = std::vector<std::byte>(cluster_size); + auto const result = file->read(buffer, kstd::bytes{2 * cluster_size + 150}); + REQUIRE(result); + REQUIRE(*result == 50_B); + REQUIRE(std::ranges::equal(std::span{buffer}.first(50), expected_bytes(2 * cluster_size + 150, 50))); + } + + THEN("the file can be read in small consecutive chunks") + { + auto contents = std::vector<std::byte>{}; + auto buffer = std::vector<std::byte>(77); + auto offset = 0_B; + while (true) + { + auto const result = file->read(buffer, offset); + REQUIRE(result); + if (*result == 0_B) + { + break; + } + contents.insert(contents.end(), buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(result->value)); + offset += *result; + } + REQUIRE(contents == expected_bytes(0, size)); + } + } + + WHEN("a file is empty") + { + auto const file = make_file(0, 0_B); + + THEN("reading returns no bytes") + { + auto buffer = std::vector<std::byte>(10); + REQUIRE(file->read(buffer, 0_B) == 0_B); + } + } + + WHEN("the cluster chain ends before the file size") + { + auto const chain = std::array{3u}; + write_file(*device, chain, cluster_size); + auto const file = make_file(3, kstd::bytes{2 * cluster_size}); + + THEN("reading past the chain reports a corrupted cluster chain") + { + auto buffer = std::vector<std::byte>(2 * cluster_size); + auto const result = file->read(buffer, 0_B); + REQUIRE_FALSE(result); + REQUIRE(result.error() == errc::corrupted_cluster_chain); + } + } + + WHEN("a non empty file has no first cluster") + { + auto const file = make_file(0, 10_B); + + THEN("reading reports an invalid cluster") + { + auto buffer = std::vector<std::byte>(10); + auto const result = file->read(buffer, 0_B); + REQUIRE_FALSE(result); + REQUIRE(result.error() == errc::invalid_cluster); + } + } + + WHEN("reading from the root directory") + { + auto buffer = std::vector<std::byte>(10); + auto const result = mount->first->read(buffer, 0_B); + + THEN("the read fails because it is a directory") + { + REQUIRE_FALSE(result); + REQUIRE(result.error() == kernel::vfs::errc::is_a_directory); + } + } + + THEN("the root directory reports the root inode number") + { + auto const status = mount->first->status(); + REQUIRE(status); + REQUIRE(status->mode == std::to_underlying(kapi::filesystem::file_type::directory)); + REQUIRE(status->inode_number == constants::root_inode_number); } } +} - GIVEN("a fat32 inode without the directory attribute") +SCENARIO("Fat32 inode lists directories on a mock layout", "[filesystem][fat32][inode][readdir]") +{ + auto const block_size = 512_B; + + GIVEN("a mounted minimal fat32 layout with a populated root directory") { - auto inode = kernel::filesystems::fat32::inode{3, 0}; + using namespace kernel::filesystems::fat32; + using kernel::tests::filesystems::fat32::make_entry; + + 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 deleted = make_entry("GONE TXT", constants::attribute_archive); + deleted.name[0] = static_cast<char>(constants::deleted_entry_marker); + + kernel::tests::filesystems::fat32::write_directory_entries( + *device, 2, + std::array{ + make_entry("LABEL", constants::attribute_volume_id), + make_entry("A TXT", constants::attribute_archive, 3, 1), + deleted, + make_entry("B TXT", constants::attribute_archive, 4, 1), + make_entry("DIR", constants::attribute_directory, 5), + }); + + 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 [root, driver_data] = *mount; + root->set_driver_state(driver_data); + + auto const root_location = cluster_offset(2, static_cast<driver_state const &>(*driver_data)); - THEN("it is not a directory") + THEN("all visible entries are listed with their names, types and inode numbers") { - REQUIRE_FALSE(inode.is_directory()); + auto entries = std::vector<kapi::filesystem::directory_entry>(10); + auto const result = root->read_directory(kapi::filesystem::directory_cursor{0}, entries); + REQUIRE(result); + REQUIRE(result->first == 3); + + REQUIRE(entries[0].name == "A.TXT"); + REQUIRE(entries[0].type == kapi::filesystem::file_type::regular); + REQUIRE(entries[0].inode_number == (root_location + 32_B).value); + + REQUIRE(entries[1].name == "B.TXT"); + REQUIRE(entries[1].type == kapi::filesystem::file_type::regular); + REQUIRE(entries[1].inode_number == (root_location + 96_B).value); + + REQUIRE(entries[2].name == "DIR"); + REQUIRE(entries[2].type == kapi::filesystem::file_type::directory); + + AND_THEN("listing again from the returned position yields no more entries") + { + auto const again = root->read_directory(result->second, entries); + REQUIRE(again); + REQUIRE(again->first == 0); + REQUIRE(again->second == result->second); + } + } + + THEN("listing in chunks resumes exactly behind the last returned entry") + { + auto names = std::vector<std::string>{}; + auto entries = std::vector<kapi::filesystem::directory_entry>(2); + auto position = kapi::filesystem::directory_cursor{0}; + + while (true) + { + auto const result = root->read_directory(position, entries); + REQUIRE(result); + REQUIRE(names.size() < 10); // fails loudly on a non-terminating loop rather than hanging the suite + for (auto index = 0uz; index < result->first; ++index) + { + names.emplace_back(entries[index].name.data(), entries[index].name.size()); + } + position = result->second; + if (result->first == 0) + { + break; + } + } + + REQUIRE(names == std::vector<std::string>{"A.TXT", "B.TXT", "DIR"}); + } + + THEN("listing into an empty span keeps the position") + { + auto const result = root->read_directory(kapi::filesystem::directory_cursor{32}, {}); + REQUIRE(result); + REQUIRE(result->first == 0); + REQUIRE(result->second == kapi::filesystem::directory_cursor{32}); + } + + THEN("listing a file fails") + { + auto const file = fs->lookup(root, "A.TXT", driver_data); + REQUIRE(file); + (*file)->set_driver_state(driver_data); + + auto entries = std::vector<kapi::filesystem::directory_entry>(1); + REQUIRE((*file)->read_directory(kapi::filesystem::directory_cursor{0}, entries).error() == + kernel::vfs::errc::not_a_directory); + } + + THEN("listing a directory that does not belong to a mount fails") + { + auto const directory = kernel::filesystems::fat32::inode{2, constants::attribute_directory}; + auto entries = std::vector<kapi::filesystem::directory_entry>(1); + REQUIRE(directory.read_directory(kapi::filesystem::directory_cursor{0}, entries).error() == + kernel::vfs::errc::not_mounted); } } } + +SCENARIO_METHOD(kernel::tests::vfs::storage_boot_module_fixture, "Fat32 inode reads files from a real image", + "[filesystem][fat32][inode][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 [root, driver_data] = *mount; + root->set_driver_state(driver_data); + + auto const open = [&](kernel::vfs::filesystem::inode_ptr const & parent, std::string_view name) { + auto const found = fs->lookup(parent, name, driver_data); + REQUIRE(found); + (*found)->set_driver_state(driver_data); + return *found; + }; + + auto const read_all = [](kernel::vfs::filesystem::inode_ptr const & file) { + auto buffer = std::vector<std::byte>(4096); + auto const result = file->read(buffer, 0_B); + REQUIRE(result); + return std::string{reinterpret_cast<char const *>(buffer.data()), result->value}; + }; + + THEN("HELLO.TXT contains its text") + { + REQUIRE(read_all(open(root, "hello.txt")) == "Hello World!"); + } + + THEN("HELLO.TXT can be read from an offset") + { + auto buffer = std::vector<std::byte>(64); + auto const result = open(root, "hello.txt")->read(buffer, 6_B); + REQUIRE(result); + REQUIRE(std::string_view{reinterpret_cast<char const *>(buffer.data()), result->value} == "World!"); + } + + THEN("the root directory lists HELLO.TXT only") + { + auto entries = std::vector<kapi::filesystem::directory_entry>(10); + auto const result = root->read_directory(kapi::filesystem::directory_cursor{0}, entries); + REQUIRE(result); + + auto names = std::vector<std::string>{}; + for (auto index = 0uz; index < result->first; ++index) + { + names.emplace_back(entries[index].name.data(), entries[index].name.size()); + } + REQUIRE(names == std::vector<std::string>{"HELLO.TXT"}); + REQUIRE(entries[0].type == kapi::filesystem::file_type::regular); + } + } +} + +// NOLINTEND(readability-magic-numbers) diff --git a/kernel/kernel/main.cpp b/kernel/kernel/main.cpp index 13f84cae..4dcb9dcf 100644 --- a/kernel/kernel/main.cpp +++ b/kernel/kernel/main.cpp @@ -1,10 +1,6 @@ #include <kernel/devices/init.hpp> #include <kernel/devices/storage.hpp> #include <kernel/drivers/init.hpp> -// TODO remove fat32 dependencies -#include <kernel/filesystems/fat32/driver_state.hpp> -#include <kernel/filesystems/fat32/file_allocation_table.hpp> -#include <kernel/filesystems/fat32/inode.hpp> #include <kernel/memory.hpp> #include <kernel/vfs.hpp> @@ -25,7 +21,6 @@ #include <kstd/vector.hpp> #include <cstddef> -#include <cstdint> #include <ranges> #include <string_view> @@ -45,63 +40,58 @@ auto run_demo_fat32() -> void kapi::system::panic("[OS:DEM] failed to mount /dev/ram3 on /entrance", result.error()); } - auto entrance = kernel::vfs::vfs::get().open("/entrance"); - if (!entrance) + auto const directory = kapi::filesystem::open("/entrance"); + if (!directory) { - kapi::system::panic("[OS:DEM] failed to open /entrance", entrance.error()); + kapi::system::panic("[OS:DEM] failed to open /entrance", directory.error()); } - auto const root = kstd::static_pointer_cast<kernel::filesystems::fat32::inode>((*entrance)->inode()); - auto const state = kstd::static_pointer_cast<kernel::filesystems::fat32::driver_state>(root->driver_state().lock()); - - kstd::println("[OS:DEM] FAT starts at sector {}, data region starts at sector {}, {} clusters of {} bytes", - state->fat_start_sector, state->data_start_sector, state->cluster_count, state->cluster_size); - - auto const root_chain = kernel::filesystems::fat32::cluster_chain(root->first_cluster(), *state); - if (!root_chain) + auto entries = kstd::vector<kapi::filesystem::directory_entry>{10}; + auto const count = kapi::filesystem::read_directory(*directory, entries); + if (!count) { - kapi::system::panic("[OS:DEM] failed to read the root directory cluster chain", root_chain.error()); + kapi::system::panic("[OS:DEM] failed to list /entrance", count.error()); } - kstd::println("[OS:DEM] root directory starts at cluster {} and spans {} cluster(s):", root->first_cluster(), - root_chain->size()); - for (auto const cluster : *root_chain) + kstd::println("[OS:DEM] ls '/entrance'"); + for (auto const & entry : std::ranges::subrange(entries.cbegin(), entries.cbegin() + *count)) { - kstd::println("[OS:DEM] cluster {} at byte offset {}", cluster, - kernel::filesystems::fat32::cluster_offset(cluster, *state)); + auto const status = kapi::filesystem::status(kstd::format("/entrance/{}", entry.name)); + if (status) + { + kstd::println("\t{:<8} {:<#8o} {:>#8} {}", entry.inode_number, status->mode, status->size, entry.name); + } } - constexpr auto demo_cluster_count = std::uint32_t{4}; - kstd::println("[OS:DEM] reading the FAT entries of the first {} data clusters:", demo_cluster_count); - for (auto cluster = kernel::filesystems::fat32::constants::first_data_cluster; - cluster < kernel::filesystems::fat32::constants::first_data_cluster + demo_cluster_count; ++cluster) - { - auto const entry = kernel::filesystems::fat32::read_fat_entry(cluster, *state); - if (!entry) + auto const cat = [](std::string_view path) { + auto const file = kapi::filesystem::open(path); + if (!file) { - kstd::println("[OS:DEM] cluster {} -> {}", cluster, entry.error()); - continue; + kapi::system::panic("[OS:DEM] failed to open file", file.error()); } - switch (entry->type) + kstd::println("[OS:DEM] cat '{}'", path); + + auto buffer = kstd::vector<std::byte>{256}; + while (true) { - case kernel::filesystems::fat32::fat_entry_type::free: - kstd::println("[OS:DEM] cluster {} -> free", cluster); - break; - case kernel::filesystems::fat32::fat_entry_type::reserved: - kstd::println("[OS:DEM] cluster {} -> reserved", cluster); - break; - case kernel::filesystems::fat32::fat_entry_type::allocated: - kstd::println("[OS:DEM] cluster {} -> next cluster {}", cluster, entry->value); - break; - case kernel::filesystems::fat32::fat_entry_type::bad_cluster: - kstd::println("[OS:DEM] cluster {} -> bad cluster", cluster); - break; - case kernel::filesystems::fat32::fat_entry_type::end_of_chain: - kstd::println("[OS:DEM] cluster {} -> end of chain ({:#010X})", cluster, entry->value); + auto const bytes_read = kapi::filesystem::read(*file, buffer); + if (!bytes_read) + { + kapi::system::panic("[OS:DEM] failed to read file", bytes_read.error()); + } + + if (*bytes_read == 0_B) + { break; + } + + kstd::print("{}", std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read->value}); } - } + kstd::println(""); + }; + + cat("/entrance/hello.txt"); } auto run_demo() -> void diff --git a/kernel/kernel/test_support/filesystems/fat32.cpp b/kernel/kernel/test_support/filesystems/fat32.cpp index 233cc09f..43a2a645 100644 --- a/kernel/kernel/test_support/filesystems/fat32.cpp +++ b/kernel/kernel/test_support/filesystems/fat32.cpp @@ -1,16 +1,20 @@ #include <kernel/test_support/filesystems/fat32.hpp> #include <kernel/filesystems/fat32/boot_sector.hpp> +#include <kernel/filesystems/fat32/directory_entry.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 <algorithm> +#include <array> #include <cstddef> #include <cstdint> #include <cstring> #include <span> +#include <string_view> using namespace kstd::units_literals; @@ -80,4 +84,27 @@ namespace kernel::tests::filesystems::fat32 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()}); } + + auto make_entry(std::string_view name, std::uint8_t attributes, std::uint32_t first_cluster, std::uint32_t file_size) + -> kernel::filesystems::fat32::directory_entry + { + auto entry = kernel::filesystems::fat32::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 write_directory_entries(kernel::tests::devices::block_device & device, std::uint32_t cluster, + std::span<kernel::filesystems::fat32::directory_entry const> entries) -> void + { + constexpr auto entries_per_cluster = sector_size.value / kernel::filesystems::fat32::constants::directory_entry_size.value; + + auto cluster_entries = std::array<kernel::filesystems::fat32::directory_entry, entries_per_cluster>{}; + std::ranges::copy(entries, cluster_entries.begin()); + write_cluster_data(device, cluster, std::as_bytes(std::span{cluster_entries})); + } } // namespace kernel::tests::filesystems::fat32 diff --git a/kernel/kernel/test_support/filesystems/fat32.hpp b/kernel/kernel/test_support/filesystems/fat32.hpp index 1eb28c6f..f27392af 100644 --- a/kernel/kernel/test_support/filesystems/fat32.hpp +++ b/kernel/kernel/test_support/filesystems/fat32.hpp @@ -2,11 +2,13 @@ #define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEMS_FAT32_HPP #include <kernel/filesystems/fat32/boot_sector.hpp> +#include <kernel/filesystems/fat32/directory_entry.hpp> #include <kernel/test_support/devices/block_device.hpp> #include <cstddef> #include <cstdint> #include <span> +#include <string_view> namespace kernel::tests::filesystems::fat32 { @@ -21,6 +23,17 @@ namespace kernel::tests::filesystems::fat32 auto write_cluster_data(kernel::tests::devices::block_device & device, std::uint32_t cluster, std::span<std::byte const> data) -> void; + //! Create a short directory entry. + //! + //! @param name The raw 11 character name, i.e. the space padded base name directly followed by the extension. + auto make_entry(std::string_view name, std::uint8_t attributes, std::uint32_t first_cluster = 0, + std::uint32_t file_size = 0) -> kernel::filesystems::fat32::directory_entry; + + //! Fill a cluster of the mock layout with directory entries, terminated by an end of directory marker if they do not + //! fill the whole cluster. + auto write_directory_entries(kernel::tests::devices::block_device & device, std::uint32_t cluster, + std::span<kernel::filesystems::fat32::directory_entry const> entries) -> void; + } // namespace kernel::tests::filesystems::fat32 #endif |
