aboutsummaryrefslogtreecommitdiff
path: root/kernel
diff options
context:
space:
mode:
authormanuel <manuel.bott@eps.ch>2026-10-29 10:03:17 +0000
committermanuel <manuel.bott@eps.ch>2026-10-29 10:03:17 +0000
commit32e34486215b46a1a244962998c893640b18a792 (patch)
tree8f84558d8cdca24e596b2e6fd22cf258610a8ede /kernel
parent856ddc4a22eabe038e08120e1b87fb3afb4aa6b8 (diff)
downloadkernel-32e34486215b46a1a244962998c893640b18a792.tar.xz
kernel-32e34486215b46a1a244962998c893640b18a792.zip
kernel/filesystem: Add inode:readfeature/readFiles
Diffstat (limited to 'kernel')
-rw-r--r--kernel/kapi/filesystem.tests.cpp92
-rw-r--r--kernel/kernel/filesystems/fat32/directory_iterator.tests.cpp38
-rw-r--r--kernel/kernel/filesystems/fat32/filesystem.cpp121
-rw-r--r--kernel/kernel/filesystems/fat32/filesystem.tests.cpp205
-rw-r--r--kernel/kernel/filesystems/fat32/inode.cpp126
-rw-r--r--kernel/kernel/filesystems/fat32/inode.hpp22
-rw-r--r--kernel/kernel/filesystems/fat32/inode.tests.cpp522
-rw-r--r--kernel/kernel/main.cpp84
-rw-r--r--kernel/kernel/test_support/filesystems/fat32.cpp27
-rw-r--r--kernel/kernel/test_support/filesystems/fat32.hpp13
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