diff options
| author | Marcel Braun <marcel.braun@ost.ch> | 2026-04-12 19:15:38 +0200 |
|---|---|---|
| committer | Marcel Braun <marcel.braun@ost.ch> | 2026-04-12 19:15:38 +0200 |
| commit | 4d2a1d028f8ba28b655026b93124e71a12562619 (patch) | |
| tree | f49deef4dd3e8728fd1000b04c0908966f37663f /kernel/src/filesystem | |
| parent | 21fd1281cf19572e202d583689b99c33ec68da50 (diff) | |
| parent | cb7edbe6d4454ee5b217b522f62f4a7b92475a32 (diff) | |
| download | kernel-4d2a1d028f8ba28b655026b93124e71a12562619.tar.xz kernel-4d2a1d028f8ba28b655026b93124e71a12562619.zip | |
Merge branch 'ext2' into 'develop-BA-FS26'
ext2 and tests
See merge request teachos/kernel!22
Diffstat (limited to 'kernel/src/filesystem')
25 files changed, 1835 insertions, 95 deletions
diff --git a/kernel/src/filesystem/dentry.cpp b/kernel/src/filesystem/dentry.cpp index 2f99e91..6591011 100644 --- a/kernel/src/filesystem/dentry.cpp +++ b/kernel/src/filesystem/dentry.cpp @@ -12,10 +12,10 @@ namespace kernel::filesystem { - dentry::dentry(kstd::shared_ptr<dentry> const & parent, kstd::shared_ptr<inode> const & node, std::string_view name) + dentry::dentry(kstd::shared_ptr<dentry> const & parent, kstd::shared_ptr<inode> const & inode, std::string_view name) : m_name(name) , m_parent(parent) - , m_inode(node) + , m_inode(inode) { if (!m_inode) { @@ -33,6 +33,11 @@ namespace kernel::filesystem return m_parent; } + auto dentry::get_name() const -> std::string_view + { + return m_name.view(); + } + auto dentry::add_child(kstd::shared_ptr<dentry> const & child) -> void { m_children.push_back(child); diff --git a/kernel/src/filesystem/dentry.tests.cpp b/kernel/src/filesystem/dentry.tests.cpp new file mode 100644 index 0000000..a6620d3 --- /dev/null +++ b/kernel/src/filesystem/dentry.tests.cpp @@ -0,0 +1,135 @@ +#include "kernel/filesystem/dentry.hpp" + +#include "kernel/test_support/cpu.hpp" +#include "kernel/test_support/filesystem/inode.hpp" + +#include <kstd/memory> +#include <kstd/print> + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Dentry construction", "[filesystem][dentry]") +{ + GIVEN("A parent dentry and inode") + { + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto parent_dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, inode); + + WHEN("constructing a dentry") + { + auto child_dentry = kernel::filesystem::dentry{parent_dentry, inode, "child"}; + + THEN("the dentry has the correct parent, inode, and name") + { + REQUIRE(child_dentry.get_parent() == parent_dentry); + REQUIRE(child_dentry.get_inode() == inode); + REQUIRE(child_dentry.get_name() == "child"); + } + + THEN("no flag is set") + { + REQUIRE_FALSE(child_dentry.has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + } + } + + WHEN("constructing a dentry with an empty name") + { + auto child_dentry = kernel::filesystem::dentry{parent_dentry, inode}; + + THEN("the dentry has the correct parent and inode, and an empty name") + { + REQUIRE(child_dentry.get_parent() == parent_dentry); + REQUIRE(child_dentry.get_inode() == inode); + REQUIRE(child_dentry.get_name().empty()); + } + + THEN("no flag is set") + { + REQUIRE_FALSE(child_dentry.has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + } + } + + WHEN("constructing a dentry with a null parent") + { + auto child_dentry = kernel::filesystem::dentry{nullptr, inode, "child"}; + + THEN("the dentry has a null parent, the correct inode, and the correct name") + { + REQUIRE(child_dentry.get_parent() == nullptr); + REQUIRE(child_dentry.get_inode() == inode); + REQUIRE(child_dentry.get_name() == "child"); + } + + THEN("no flag is set") + { + REQUIRE_FALSE(child_dentry.has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + } + } + + WHEN("constructing a dentry with a null inode") + { + THEN("the system panics") + { + REQUIRE_THROWS_AS((kernel::filesystem::dentry{parent_dentry, nullptr, "child"}), kernel::tests::cpu::halt); + } + } + } +} + +SCENARIO("Dentry child logic", "[filesystem][dentry]") +{ + GIVEN("A parent dentry and inode") + { + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto parent_dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, inode); + + WHEN("adding child dentries") + { + auto child1 = kstd::make_shared<kernel::filesystem::dentry>(parent_dentry, inode, "child1"); + auto child2 = kstd::make_shared<kernel::filesystem::dentry>(parent_dentry, inode, "child2"); + parent_dentry->add_child(child1); + parent_dentry->add_child(child2); + + THEN("the children can be found by name") + { + REQUIRE(parent_dentry->find_child("child1") == child1); + REQUIRE(parent_dentry->find_child("child2") == child2); + } + + THEN("finding a non-existent child returns null") + { + REQUIRE(parent_dentry->find_child("nonexistent") == nullptr); + } + } + } +} + +SCENARIO("Dentry Flag logic", "[filesystem][dentry]") +{ + GIVEN("A dentry") + { + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto dentry = kernel::filesystem::dentry{nullptr, inode, "test"}; + + WHEN("setting a flag") + { + dentry.set_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted); + + THEN("the flag is set") + { + REQUIRE(dentry.has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + } + } + + WHEN("unsetting a flag") + { + dentry.set_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted); + dentry.unset_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted); + + THEN("the flag is unset") + { + REQUIRE_FALSE(dentry.has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + } + } + } +} diff --git a/kernel/src/filesystem/devfs/filesystem.cpp b/kernel/src/filesystem/devfs/filesystem.cpp index 9043ac5..03b4218 100644 --- a/kernel/src/filesystem/devfs/filesystem.cpp +++ b/kernel/src/filesystem/devfs/filesystem.cpp @@ -1,6 +1,7 @@ #include "kernel/filesystem/devfs/filesystem.hpp" #include "kapi/devices/device.hpp" + #include "kernel/devices/storage/management.hpp" #include "kernel/filesystem/devfs/inode.hpp" #include "kernel/filesystem/device_inode.hpp" @@ -13,12 +14,12 @@ namespace kernel::filesystem::devfs { - auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const &) -> int + auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const &) -> operation_result { m_root_inode = kstd::make_shared<inode>(); build_device_inode_table(); - return 0; + return operation_result::success; } auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) diff --git a/kernel/src/filesystem/devfs/filesystem.tests.cpp b/kernel/src/filesystem/devfs/filesystem.tests.cpp new file mode 100644 index 0000000..f8c4764 --- /dev/null +++ b/kernel/src/filesystem/devfs/filesystem.tests.cpp @@ -0,0 +1,72 @@ +#include "kernel/filesystem/devfs/filesystem.hpp" + +#include "kernel/filesystem/filesystem.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_fixture.hpp" + +#include <catch2/catch_test_macros.hpp> + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_fixture, + "Devfs filesystem lookup uses storage management devices", "[filesystem][devfs][filesystem]") +{ + GIVEN("a boot module registry with one module") + { + setup_modules(1); + + auto fs = kernel::filesystem::devfs::filesystem{}; + auto result = fs.mount(nullptr); + + THEN("mount succeeds") + { + REQUIRE(result == kernel::filesystem::filesystem::operation_result::success); + REQUIRE(fs.root_inode() != nullptr); + } + + THEN("lookup on root finds ram0 device inode") + { + auto inode = fs.lookup(fs.root_inode(), "ram0"); + REQUIRE(inode != nullptr); + REQUIRE(inode->is_device()); + } + + THEN("lookup of an unknown device returns null") + { + auto inode = fs.lookup(fs.root_inode(), "ram99"); + REQUIRE(inode == nullptr); + } + + THEN("lookup with wrong parent returns null") + { + auto other_fs = kernel::filesystem::devfs::filesystem{}; + other_fs.mount(nullptr); + + auto inode = fs.lookup(other_fs.root_inode(), "ram0"); + REQUIRE(inode == nullptr); + } + + THEN("lookup with a non-directory parent returns null") + { + auto non_directory_inode = fs.lookup(fs.root_inode(), "ram0"); + REQUIRE(non_directory_inode != nullptr); + REQUIRE(!non_directory_inode->is_directory()); + + auto result = fs.lookup(non_directory_inode, "anything"); + REQUIRE(result == nullptr); + } + } + + GIVEN("a boot module registry with three modules") + { + setup_modules(3, 2048); + + auto fs = kernel::filesystem::devfs::filesystem{}; + auto result = fs.mount(nullptr); + REQUIRE(result == kernel::filesystem::filesystem::operation_result::success); + + THEN("lookup finds all generated RAM devices") + { + REQUIRE(fs.lookup(fs.root_inode(), "ram0") != nullptr); + REQUIRE(fs.lookup(fs.root_inode(), "ram16") != nullptr); + REQUIRE(fs.lookup(fs.root_inode(), "ram32") != nullptr); + } + } +} diff --git a/kernel/src/filesystem/devfs/inode.tests.cpp b/kernel/src/filesystem/devfs/inode.tests.cpp new file mode 100644 index 0000000..50e34a7 --- /dev/null +++ b/kernel/src/filesystem/devfs/inode.tests.cpp @@ -0,0 +1,54 @@ +#include "kernel/filesystem/devfs/inode.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <cstdint> + +SCENARIO("Devfs inode creation", "[filesystem][devfs][inode]") +{ + GIVEN("a devfs inode") + { + auto inode = kernel::filesystem::devfs::inode{}; + + THEN("the inode has the correct kind") + { + REQUIRE(inode.is_directory()); + REQUIRE_FALSE(inode.is_device()); + REQUIRE_FALSE(inode.is_regular()); + } + } +} + +SCENARIO("Devfs inode read/write", "[filesystem][devfs][inode]") +{ + GIVEN("a devfs inode") + { + auto inode = kernel::filesystem::devfs::inode{}; + + WHEN("attempting to read from the devfs inode") + { + kstd::vector<uint8_t> buffer(512); + auto bytes_read = inode.read(buffer.data(), 0, buffer.size()); + + THEN("no bytes are read") + { + REQUIRE(bytes_read == 0); + } + } + + WHEN("attempting to write to the devfs inode") + { + kstd::vector<uint8_t> buffer(512); + auto bytes_written = inode.write(buffer.data(), 0, buffer.size()); + + THEN("no bytes are written") + { + REQUIRE(bytes_written == 0); + } + } + } +} diff --git a/kernel/src/filesystem/device_inode.cpp b/kernel/src/filesystem/device_inode.cpp index af8cecc..397a0fd 100644 --- a/kernel/src/filesystem/device_inode.cpp +++ b/kernel/src/filesystem/device_inode.cpp @@ -1,14 +1,12 @@ #include "kernel/filesystem/device_inode.hpp" +#include "kapi/devices/device.hpp" #include "kapi/system.hpp" #include "kernel/devices/block_device_utils.hpp" -#include "kapi/devices/device.hpp" #include "kernel/filesystem/inode.hpp" -#include <kstd/cstring> #include <kstd/memory> -#include <kstd/vector> #include <cstddef> diff --git a/kernel/src/filesystem/device_inode.tests.cpp b/kernel/src/filesystem/device_inode.tests.cpp new file mode 100644 index 0000000..4e31812 --- /dev/null +++ b/kernel/src/filesystem/device_inode.tests.cpp @@ -0,0 +1,108 @@ +#include "kernel/filesystem/device_inode.hpp" + +#include "kernel/test_support/cpu.hpp" +#include "kernel/test_support/devices/block_device.hpp" +#include "kernel/test_support/devices/character_device.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <cstddef> +#include <cstdint> + +SCENARIO("Device inode construction", "[filesystem][device_inode]") +{ + GIVEN("a block device") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "test_block_device", 512, 3 * 512); + + WHEN("constructing a device inode with the block device") + { + auto inode = kernel::filesystem::device_inode{device}; + + THEN("the device inode has the correct device") + { + REQUIRE(inode.device() == device); + } + + THEN("the device inode has the correct kind") + { + REQUIRE(inode.is_device()); + REQUIRE_FALSE(inode.is_directory()); + REQUIRE_FALSE(inode.is_regular()); + } + } + + WHEN("constructing a device inode with a null device") + { + THEN("the constructor panics") + { + REQUIRE_THROWS_AS((kernel::filesystem::device_inode{nullptr}), kernel::tests::cpu::halt); + } + } + } +} + +SCENARIO("Device inode read/write", "[filesystem][device_inode]") +{ + GIVEN("a block device and a device inode for that device") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "test_block_device", 512, 3 * 512); + auto inode = kernel::filesystem::device_inode{device}; + + WHEN("writing to the device inode") + { + kstd::vector<uint8_t> write_buffer(1024); + for (size_t i = 0; i < write_buffer.size(); ++i) + { + write_buffer[i] = static_cast<uint8_t>(i % 256); + } + + auto bytes_written = inode.write(write_buffer.data(), 256, write_buffer.size()); + + THEN("the correct number of bytes is written") + { + REQUIRE(bytes_written == 1024); + } + + THEN("the data written matches the data read back from the device inode") + { + kstd::vector<uint8_t> read_buffer(1024); + auto bytes_read = inode.read(read_buffer.data(), 256, read_buffer.size()); + + REQUIRE(bytes_read == write_buffer.size()); + REQUIRE(read_buffer == write_buffer); + } + } + } +} + +SCENARIO("Device inode read/write with a non-block device", "[filesystem][device_inode]") +{ + GIVEN("a non-block device and a device inode for that device") + { + auto device = kstd::make_shared<kernel::tests::devices::character_device>(0, 0, "test_character_device"); + auto inode = kernel::filesystem::device_inode{device}; + + WHEN("reading from the device inode") + { + kstd::vector<uint8_t> read_buffer(512); + THEN("the system panics") + { + REQUIRE_THROWS_AS(inode.read(read_buffer.data(), 0, read_buffer.size()), kernel::tests::cpu::halt); + } + } + + WHEN("writing to the device inode") + { + kstd::vector<uint8_t> write_buffer(512); + THEN("the system panics") + { + REQUIRE_THROWS_AS(inode.write(write_buffer.data(), 0, write_buffer.size()), kernel::tests::cpu::halt); + } + } + } +} diff --git a/kernel/src/filesystem/ext2/filesystem.cpp b/kernel/src/filesystem/ext2/filesystem.cpp index eb9edc4..0ad5c97 100644 --- a/kernel/src/filesystem/ext2/filesystem.cpp +++ b/kernel/src/filesystem/ext2/filesystem.cpp @@ -1,13 +1,17 @@ #include "kernel/filesystem/ext2/filesystem.hpp" -#include "kernel/devices/block_device_utils.hpp" #include "kapi/devices/device.hpp" + +#include "kernel/devices/block_device_utils.hpp" +#include "kernel/filesystem/ext2/block_group_descriptor.hpp" #include "kernel/filesystem/ext2/inode.hpp" +#include "kernel/filesystem/ext2/linked_directory_entry.hpp" #include "kernel/filesystem/ext2/superblock.hpp" #include "kernel/filesystem/filesystem.hpp" #include "kernel/filesystem/inode.hpp" #include <kstd/memory> +#include <kstd/vector> #include <cstddef> #include <cstdint> @@ -17,56 +21,211 @@ namespace kernel::filesystem::ext2 { namespace { - // constexpr size_t SUPERBLOCK_OFFSET = 1024; - // constexpr uint16_t EXT2_MAGIC = 0xEF53; - - // // Mode bits - // constexpr uint16_t S_IFMT = 0xF000; - // constexpr uint16_t S_IFREG = 0x8000; - // constexpr uint16_t S_IFDIR = 0x4000; - - // auto S_ISREG(uint16_t mode) -> bool - // { - // return (mode & S_IFMT) == S_IFREG; - // } - // auto S_ISDIR(uint16_t mode) -> bool - // { - // return (mode & S_IFMT) == S_IFDIR; - // } - - // auto get_block_size(superblock const & superblock) -> size_t - // { - // return 1024U << superblock.log_block_size; - // } - - // auto get_inode_size(superblock const & superblock) -> size_t - // { - // return superblock.rev_level == 0 ? 128 : superblock.inode_size; - // } + auto S_ISREG(uint16_t mode) -> bool + { + return (mode & constants::mode_mask) == constants::mode_regular; + } + + auto S_ISDIR(uint16_t mode) -> bool + { + return (mode & constants::mode_mask) == constants::mode_directory; + } } // namespace - auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const & device) -> int + auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const & device) -> operation_result { - kernel::filesystem::filesystem::mount(device); // TODO BA-FS26 error handling? - // TODO BA-FS26 load proper root inode from ext2 metadata - // m_root_inode = inode{inode_kind::directory}; - - // TODO BA-FS26 implement - m_root_inode = kstd::make_shared<inode>(); - // devices::block_device_utils::read(device, nullptr, 0, 0); // TODO BA-FS26 just for testing - return 0; + kernel::filesystem::filesystem::mount(device); + + kernel::devices::block_device_utils::read(m_device, &m_superblock, constants::superblock_offset, + sizeof(m_superblock)); + + if (m_superblock.magic != constants::magic_number) + { + return operation_result::invalid_magic_number; + } + + auto const block_size = get_block_size(); + auto const blocks_per_group = m_superblock.blocks_per_group; + auto const num_block_groups = (m_superblock.blocks_count + blocks_per_group - 1) / blocks_per_group; + + m_block_group_descriptors = kstd::vector<block_group_descriptor>(num_block_groups); + + auto const block_group_descriptor_table_offset = block_size == 1024 ? 2 * block_size : block_size; + kernel::devices::block_device_utils::read(m_device, m_block_group_descriptors.data(), + block_group_descriptor_table_offset, + num_block_groups * sizeof(block_group_descriptor)); + + m_root_inode = read_inode(constants::root_inode_number); + + if (!m_root_inode || !m_root_inode->is_directory()) + { + return operation_result::invalid_root_inode; + } + return operation_result::success; } - auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const & /*parent*/, std::string_view name) + auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) -> kstd::shared_ptr<kernel::filesystem::inode> { - // TODO BA-FS26 implement ext2 directory traversal and inode loading - if (name == "dev") + if (!parent || !parent->is_directory()) + { + return nullptr; + } + + auto * ext2_parent = static_cast<inode *>(parent.get()); + if (!ext2_parent) + { + return nullptr; + } + + auto const block_size = get_block_size(); + auto const & inode_data = ext2_parent->m_data; + kstd::vector<uint8_t> buffer(block_size); + + for (uint32_t i = 0; i < get_inode_block_count(inode_data); ++i) + { + auto const global_block_number = map_inode_block_index_to_global_block_number(i, inode_data); + auto const block_offset = global_block_number * block_size; + kernel::devices::block_device_utils::read(m_device, buffer.data(), block_offset, block_size); + + auto const * entry = reinterpret_cast<linked_directory_entry const *>(buffer.data()); + auto bytes_read = 0uz; + + while (bytes_read < block_size && entry->inode != 0) + { + auto const entry_name = std::string_view{entry->name.data(), entry->name_len}; + if (entry_name == name) + { + return read_inode(entry->inode); + } + + bytes_read += entry->rec_len; + entry = reinterpret_cast<linked_directory_entry const *>(buffer.data() + bytes_read); + } + } + + return nullptr; + } + + auto filesystem::read_inode(uint32_t inode_number) -> kstd::shared_ptr<inode> + { + auto const block_size = get_block_size(); + auto const inodes_per_group = m_superblock.inodes_per_group; + auto const block_group_index = (inode_number - 1) / inodes_per_group; + auto const inode_index_within_group = (inode_number - 1) % inodes_per_group; + + if (block_group_index >= m_block_group_descriptors.size()) { - // TODO BA-FS26 just for testing return nullptr; } - return kstd::make_shared<inode>(); + auto const & block_group_descriptor = m_block_group_descriptors.at(block_group_index); + auto const inode_table_start_block = block_group_descriptor.inode_table; + auto const inode_table_offset = static_cast<size_t>(inode_table_start_block) * block_size; + auto const inode_offset = inode_table_offset + inode_index_within_group * get_inode_size(); + + auto new_inode = kstd::make_shared<inode>(this); + kernel::devices::block_device_utils::read(m_device, &new_inode->m_data, inode_offset, sizeof(inode_data)); + + // TODO BA-FS26 improve inode_kind really needed? or just map it to the mode bits? + if (S_ISREG(new_inode->m_data.mode)) + { + new_inode->m_kind = inode::inode_kind::regular; + } + else if (S_ISDIR(new_inode->m_data.mode)) + { + new_inode->m_kind = inode::inode_kind::directory; + } + else + { + // TODO BA-FS26 really correct?? + return nullptr; + } + + return new_inode; + } + + auto filesystem::map_inode_block_index_to_global_block_number(uint32_t inode_block_index, inode_data data) -> uint32_t + { + if (inode_block_index < constants::direct_block_count) + { + return data.block.at(inode_block_index); + } + inode_block_index -= constants::direct_block_count; + + auto const block_size = get_block_size(); + auto const numbers_per_block = block_size / sizeof(uint32_t); + + auto const block_numbers_per_singly_indirect_block = numbers_per_block; + auto const block_numbers_per_doubly_indirect_block = numbers_per_block * block_numbers_per_singly_indirect_block; + auto const block_numbers_per_triply_indirect_block = numbers_per_block * block_numbers_per_doubly_indirect_block; + + if (inode_block_index < block_numbers_per_singly_indirect_block) + { + auto const singly_indirect_block_number = data.block.at(constants::singly_indirect_block_index); + return read_block_number_at_index(singly_indirect_block_number, inode_block_index); + } + inode_block_index -= block_numbers_per_singly_indirect_block; + + if (inode_block_index < block_numbers_per_doubly_indirect_block) + { + auto const doubly_indirect_block_number = data.block.at(constants::doubly_indirect_block_index); + auto const singly_indirect_block_index_in_doubly_indirect_block = + inode_block_index / block_numbers_per_singly_indirect_block; + auto const singly_indirect_block_number = read_block_number_at_index( + doubly_indirect_block_number, singly_indirect_block_index_in_doubly_indirect_block); + + auto const block_index_in_singly_indirect_block = inode_block_index % block_numbers_per_singly_indirect_block; + return read_block_number_at_index(singly_indirect_block_number, block_index_in_singly_indirect_block); + } + inode_block_index -= block_numbers_per_doubly_indirect_block; + + if (inode_block_index < block_numbers_per_triply_indirect_block) + { + auto const triply_indirect_block_number = data.block.at(constants::triply_indirect_block_index); + auto const doubly_indirect_block_index_in_triply_indirect_block = + inode_block_index / block_numbers_per_doubly_indirect_block; + auto const doubly_indirect_block_number = read_block_number_at_index( + triply_indirect_block_number, doubly_indirect_block_index_in_triply_indirect_block); + + auto const remaining_block_numbers = inode_block_index % block_numbers_per_doubly_indirect_block; + + auto const singly_indirect_block_index_in_doubly_indirect_block = + remaining_block_numbers / block_numbers_per_singly_indirect_block; + auto const singly_indirect_block_number = read_block_number_at_index( + doubly_indirect_block_number, singly_indirect_block_index_in_doubly_indirect_block); + + auto const block_index_in_singly_indirect_block = + remaining_block_numbers % block_numbers_per_singly_indirect_block; + return read_block_number_at_index(singly_indirect_block_number, block_index_in_singly_indirect_block); + } + + return 0; // TODO BA-FS26 really correct?? + } + + auto filesystem::read_block_number_at_index(uint32_t block_number, uint32_t index) -> uint32_t + { + uint32_t block_number_buffer = 0; + + auto const block_start_offset = block_number * get_block_size(); + auto const number_start_address = block_start_offset + index * sizeof(uint32_t); + kernel::devices::block_device_utils::read(m_device, &block_number_buffer, number_start_address, sizeof(uint32_t)); + + return block_number_buffer; + } + + auto filesystem::get_block_size() -> size_t + { + return constants::base_block_size << m_superblock.log_block_size; + } + + auto filesystem::get_inode_size() -> size_t + { + return m_superblock.rev_level == 0 ? 128 : m_superblock.inode_size; + } + + auto filesystem::get_inode_block_count(inode_data const & data) -> uint32_t + { + return data.blocks / (2 << m_superblock.log_block_size); } } // namespace kernel::filesystem::ext2 diff --git a/kernel/src/filesystem/ext2/filesystem.tests.cpp b/kernel/src/filesystem/ext2/filesystem.tests.cpp new file mode 100644 index 0000000..b13ebf3 --- /dev/null +++ b/kernel/src/filesystem/ext2/filesystem.tests.cpp @@ -0,0 +1,132 @@ +#include "kernel/filesystem/ext2/filesystem.hpp" + +#include "kernel/devices/storage/management.hpp" +#include "kernel/filesystem/ext2/inode.hpp" +#include "kernel/filesystem/filesystem.hpp" +#include "kernel/test_support/devices/block_device.hpp" +#include "kernel/test_support/filesystem/ext2.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_fixture.hpp" + +#include <kstd/memory> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <array> +#include <cstdint> +#include <filesystem> + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_fixture, + "Ext2 filesystem mount and lookup with real image", "[filesystem][ext2][filesystem][img]") +{ + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + + GIVEN("a mounted ext2 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::management::get().determine_boot_device(); + REQUIRE(boot_device != nullptr); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(boot_device) == kernel::filesystem::filesystem::operation_result::success); + + THEN("the root inode is available and is a directory") + { + REQUIRE(fs.root_inode() != nullptr); + REQUIRE(fs.root_inode()->is_directory()); + } + + THEN("lookup resolves known entries from the image") + { + auto information = fs.lookup(fs.root_inode(), "information"); + REQUIRE(information != nullptr); + REQUIRE(information->is_directory()); + + auto info_1 = fs.lookup(information, "info_1.txt"); + REQUIRE(info_1 != nullptr); + REQUIRE(info_1->is_regular()); + } + + THEN("lookup returns null for invalid inputs") + { + REQUIRE(fs.lookup(nullptr, "information") == nullptr); + + auto information = fs.lookup(fs.root_inode(), "information"); + REQUIRE(information != nullptr); + auto info_1 = fs.lookup(information, "info_1.txt"); + REQUIRE(info_1 != nullptr); + + REQUIRE(fs.lookup(info_1, "anything") == nullptr); + REQUIRE(fs.lookup(fs.root_inode(), "does_not_exist") == nullptr); + } + } +} + +SCENARIO("Ext2 filesystem rejects invalid magic", "[filesystem][ext2][filesystem]") +{ + auto const block_size = 1024; + GIVEN("a block device that does not contain an ext2 superblock") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 2 * block_size); + REQUIRE(device != nullptr); + + auto fs = kernel::filesystem::ext2::filesystem{}; + + THEN("mount fails with invalid_magic_number") + { + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::invalid_magic_number); + } + } +} + +SCENARIO("Ext2 block mapping includes direct and all indirect levels", "[filesystem][ext2][filesystem]") +{ + auto const block_size = 1024; + + GIVEN("a minimally valid ext2 layout with configured indirect block tables") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 128 * block_size); + REQUIRE(device != nullptr); + + kernel::tests::filesystem::ext2::setup_mock_ext2_layout(*device); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::success); + + auto inode_data = kernel::filesystem::ext2::inode_data{}; + inode_data.block[0] = 7; + + inode_data.block[12] = 30; + kernel::tests::filesystem::ext2::write_u32(*device, 30 * block_size, 31); + + inode_data.block[13] = 40; + kernel::tests::filesystem::ext2::write_u32(*device, 40 * block_size, 41); + kernel::tests::filesystem::ext2::write_u32(*device, 41 * block_size, 42); + + inode_data.block[14] = 50; + kernel::tests::filesystem::ext2::write_u32(*device, 50 * block_size, 51); + kernel::tests::filesystem::ext2::write_u32(*device, 51 * block_size, 52); + kernel::tests::filesystem::ext2::write_u32(*device, 52 * block_size, 53); + + auto const numbers_per_block = static_cast<uint32_t>(block_size / sizeof(uint32_t)); + auto const singly_start = static_cast<uint32_t>(kernel::filesystem::ext2::constants::direct_block_count); + auto const doubly_start = singly_start + numbers_per_block; + auto const triply_start = doubly_start + numbers_per_block * numbers_per_block; + + THEN("mapping resolves direct, singly, doubly and triply indirect indexes") + { + REQUIRE(fs.map_inode_block_index_to_global_block_number(0, inode_data) == 7); + REQUIRE(fs.map_inode_block_index_to_global_block_number(singly_start, inode_data) == 31); + REQUIRE(fs.map_inode_block_index_to_global_block_number(doubly_start, inode_data) == 42); + REQUIRE(fs.map_inode_block_index_to_global_block_number(triply_start, inode_data) == 53); + } + + THEN("mapping returns zero for out-of-range indexes") + { + auto const beyond_triply = triply_start + numbers_per_block * numbers_per_block * numbers_per_block; + REQUIRE(fs.map_inode_block_index_to_global_block_number(beyond_triply, inode_data) == 0); + } + } +} diff --git a/kernel/src/filesystem/ext2/inode.cpp b/kernel/src/filesystem/ext2/inode.cpp index b75969a..a29bb3b 100644 --- a/kernel/src/filesystem/ext2/inode.cpp +++ b/kernel/src/filesystem/ext2/inode.cpp @@ -1,24 +1,59 @@ #include "kernel/filesystem/ext2/inode.hpp" +#include "kapi/system.hpp" + +#include "kernel/devices/block_device_utils.hpp" +#include "kernel/filesystem/ext2/filesystem.hpp" #include "kernel/filesystem/inode.hpp" +#include <algorithm> #include <cstddef> +#include <cstdint> namespace kernel::filesystem::ext2 { - inode::inode() + inode::inode(filesystem * fs) : kernel::filesystem::inode(inode_kind::regular) - {} + , m_filesystem(fs) + { + if (!m_filesystem) + { + kapi::system::panic("[EXT2] ext2::inode constructed with filesystem null pointer"); + } + } - auto inode::read(void * /*buffer*/, size_t /*offset*/, size_t /*size*/) const -> size_t + auto inode::read(void * buffer, size_t offset, size_t size) const -> size_t { - // TODO BA-FS26 implement - return 0; + auto block_index = offset / m_filesystem->get_block_size(); + auto in_block_offset = offset % m_filesystem->get_block_size(); + + auto bytes_read = 0uz; + + while (bytes_read < size) + { + auto const block_number = m_filesystem->map_inode_block_index_to_global_block_number(block_index, m_data); + if (block_number == 0) // TODO BA-FS26 really correct? + { + break; + } + + auto const block_start_offset = block_number * m_filesystem->get_block_size(); + auto const read_offset = block_start_offset + in_block_offset; + auto const bytes_to_read = std::min(size - bytes_read, m_filesystem->get_block_size() - in_block_offset); + + bytes_read += kernel::devices::block_device_utils::read( + m_filesystem->device(), static_cast<uint8_t *>(buffer) + bytes_read, read_offset, bytes_to_read); + + block_index++; + in_block_offset = 0; // After the first block, we always start at the beginning of the block + } + + return bytes_read; } auto inode::write(void const * /*buffer*/, size_t /*offset*/, size_t /*size*/) -> size_t { - // TODO BA-FS26 implement + kapi::system::panic("[EXT2] inode::write is not implemented yet"); return 0; } } // namespace kernel::filesystem::ext2
\ No newline at end of file diff --git a/kernel/src/filesystem/ext2/inode.tests.cpp b/kernel/src/filesystem/ext2/inode.tests.cpp new file mode 100644 index 0000000..795ff10 --- /dev/null +++ b/kernel/src/filesystem/ext2/inode.tests.cpp @@ -0,0 +1,159 @@ +#include "kernel/filesystem/ext2/inode.hpp" + +#include "kernel/devices/storage/management.hpp" +#include "kernel/filesystem/ext2/filesystem.hpp" +#include "kernel/filesystem/filesystem.hpp" +#include "kernel/test_support/cpu.hpp" +#include "kernel/test_support/devices/block_device.hpp" +#include "kernel/test_support/filesystem/ext2.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_fixture.hpp" + +#include <kstd/memory> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <cstddef> +#include <filesystem> +#include <string_view> + +SCENARIO("Ext2 inode initialization and properties", "[filesystem][ext2][inode]") +{ + GIVEN("an ext2 filesystem") + { + auto fs = kernel::filesystem::ext2::filesystem{}; + + THEN("the inode is initialized and has the kind regular") + { + auto inode = kernel::filesystem::ext2::inode{&fs}; + REQUIRE(inode.is_regular()); + REQUIRE(!inode.is_directory()); + REQUIRE(!inode.is_device()); + } + } + + GIVEN("no filesystem (null pointer)") + { + THEN("constructing an inode with a null filesystem pointer panics") + { + REQUIRE_THROWS_AS(kernel::filesystem::ext2::inode{nullptr}, kernel::tests::cpu::halt); + } + } +} + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_fixture, "Ext2 inode reads from real image", + "[filesystem][ext2][inode][img]") +{ + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + + GIVEN("a mounted ext2 filesystem and a regular file inode") + { + REQUIRE(std::filesystem::exists(image_path)); + REQUIRE_NOTHROW(setup_modules_from_img({"test_img_module"}, {image_path})); + + auto boot_device = kernel::devices::storage::management::get().determine_boot_device(); + REQUIRE(boot_device != nullptr); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(boot_device) == kernel::filesystem::filesystem::operation_result::success); + + auto information = fs.lookup(fs.root_inode(), "information"); + REQUIRE(information != nullptr); + auto file = fs.lookup(information, "info_1.txt"); + REQUIRE(file != nullptr); + REQUIRE(file->is_regular()); + + THEN("reading from offset zero returns expected file prefix") + { + auto buffer = kstd::vector<std::byte>(6); + auto const bytes_read = file->read(buffer.data(), 0, buffer.size()); + + REQUIRE(bytes_read == 6); + + auto const text = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read}; + REQUIRE(text == "info_1"); + } + + THEN("reading with an offset returns the expected byte") + { + auto buffer = kstd::vector<std::byte>(1); + auto const bytes_read = file->read(buffer.data(), 5, buffer.size()); + + REQUIRE(bytes_read == 1); + REQUIRE(static_cast<char>(buffer[0]) == '1'); + } + } +} + +SCENARIO("Ext2 inode read stops when block mapping resolves to zero", "[filesystem][ext2][inode]") +{ + auto const block_size = 1024; + GIVEN("an ext2 inode without mapped data blocks") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 64 * block_size); + REQUIRE(device != nullptr); + kernel::tests::filesystem::ext2::setup_mock_ext2_layout(*device); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::success); + + auto inode = kernel::filesystem::ext2::inode{&fs}; + inode.m_data.blocks = 2; + inode.m_data.block[0] = 0; + + auto buffer = kstd::vector<std::byte>(32, std::byte{0xAB}); + + THEN("no bytes are read") + { + auto const bytes_read = inode.read(buffer.data(), 0, buffer.size()); + REQUIRE(bytes_read == 0); + } + } +} + +SCENARIO("Ext2 inode read across block boundaries", "[filesystem][ext2][inode]") +{ + auto const block_size = 1024; + GIVEN("an ext2 inode with two direct blocks and a block size of 1024 bytes") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 64 * block_size); + REQUIRE(device != nullptr); + kernel::tests::filesystem::ext2::setup_mock_ext2_layout(*device); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::success); + + auto inode = kernel::filesystem::ext2::inode{&fs}; + inode.m_data.blocks = 2; + inode.m_data.block[0] = 20; + kernel::tests::filesystem::ext2::write_bytes(*device, 21 * block_size - 6, "Hello ", 6); + inode.m_data.block[1] = 21; + kernel::tests::filesystem::ext2::write_bytes(*device, 21 * block_size, "World!", 6); + + auto buffer = kstd::vector<std::byte>(12, std::byte{0x00}); + + THEN("reading across the block boundary returns the combined content") + { + auto const bytes_read = inode.read(buffer.data(), block_size - 6, buffer.size()); + REQUIRE(bytes_read == 12); + + auto const text = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read}; + REQUIRE(text == "Hello World!"); + } + } +} + +SCENARIO("Ext2 inode write is not implemented", "[filesystem][ext2][inode]") +{ + GIVEN("an ext2 inode") + { + auto fs = kernel::filesystem::ext2::filesystem{}; + auto inode = kernel::filesystem::ext2::inode{&fs}; + + THEN("writing to the inode panics") + { + auto buffer = kstd::vector<std::byte>(32, std::byte{0x00}); + REQUIRE_THROWS_AS(inode.write(buffer.data(), 0, buffer.size()), kernel::tests::cpu::halt); + } + } +} diff --git a/kernel/src/filesystem/file_descriptor_table.cpp b/kernel/src/filesystem/file_descriptor_table.cpp index 287aea2..1c062a1 100644 --- a/kernel/src/filesystem/file_descriptor_table.cpp +++ b/kernel/src/filesystem/file_descriptor_table.cpp @@ -10,13 +10,13 @@ #include <cstddef> #include <optional> -namespace kernel::filesystem +namespace { - namespace - { - constinit auto static global_file_descriptor_table = std::optional<file_descriptor_table>{}; - } // namespace + constinit auto static global_file_descriptor_table = std::optional<kernel::filesystem::file_descriptor_table>{}; +} // namespace +namespace kernel::filesystem +{ auto file_descriptor_table::init() -> void { if (global_file_descriptor_table) @@ -87,4 +87,12 @@ namespace kernel::filesystem m_open_files.at(index) = nullptr; } -} // namespace kernel::filesystem
\ No newline at end of file +} // namespace kernel::filesystem + +namespace kernel::tests::filesystem::file_descriptor_table +{ + auto deinit() -> void + { + global_file_descriptor_table.reset(); + } +} // namespace kernel::tests::filesystem::file_descriptor_table diff --git a/kernel/src/filesystem/file_descriptor_table.tests.cpp b/kernel/src/filesystem/file_descriptor_table.tests.cpp new file mode 100644 index 0000000..5aeadb2 --- /dev/null +++ b/kernel/src/filesystem/file_descriptor_table.tests.cpp @@ -0,0 +1,113 @@ +#include "kernel/filesystem/file_descriptor_table.hpp" + +#include "kernel/filesystem/open_file_description.hpp" +#include "kernel/test_support/filesystem/inode.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("File descriptor table add/get file", "[filesystem][file_descriptor_table]") +{ + GIVEN("a file descriptor table and an open file description") + { + auto & table = kernel::filesystem::file_descriptor_table::get(); + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto file_description_1 = kstd::make_shared<kernel::filesystem::open_file_description>(inode); + auto file_description_2 = kstd::make_shared<kernel::filesystem::open_file_description>(inode); + + WHEN("adding the open file description to the file descriptor table") + { + auto fd_1 = table.add_file(file_description_1); + auto fd_2 = table.add_file(file_description_2); + auto fd_3 = table.add_file(file_description_2); + + THEN("a valid file descriptor is returned") + { + REQUIRE(fd_1 == 0); + REQUIRE(fd_2 == 1); + REQUIRE(fd_3 == 2); + } + + THEN("the file description can be retrieved using the returned file descriptor") + { + auto retrieved_description = table.get_file(fd_1); + REQUIRE(retrieved_description == file_description_1); + } + } + } + + GIVEN("a invalid open file description") + { + auto & table = kernel::filesystem::file_descriptor_table::get(); + + THEN("adding a null file description returns an error code") + { + auto fd = table.add_file(nullptr); + REQUIRE(fd == -1); + } + + THEN("retrieving a file description with a negative file descriptor returns a null pointer") + { + auto retrieved_description = table.get_file(-1); + REQUIRE(retrieved_description == nullptr); + } + + THEN("retrieving a file description with an out-of-bounds file descriptor returns a null pointer") + { + auto retrieved_description = table.get_file(1000); + REQUIRE(retrieved_description == nullptr); + } + } +} + +SCENARIO("File descriptor table remove file", "[filesystem][file_descriptor_table]") +{ + GIVEN("a file descriptor table with an open file description") + { + auto & table = kernel::filesystem::file_descriptor_table::get(); + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto file_description = kstd::make_shared<kernel::filesystem::open_file_description>(inode); + auto fd = table.add_file(file_description); + + WHEN("removing the file description using the file descriptor") + { + table.remove_file(fd); + + THEN("the file description can no longer be retrieved using the file descriptor") + { + auto retrieved_description = table.get_file(fd); + REQUIRE(retrieved_description == nullptr); + } + } + + WHEN("removing a file description the other file descriptor keep the same index") + { + auto fd2 = table.add_file(file_description); + table.remove_file(fd); + + THEN("the second file description can still be retrieved using its file descriptor") + { + auto retrieved_description = table.get_file(fd2); + REQUIRE(retrieved_description == file_description); + } + } + } + + GIVEN("an invalid file descriptor") + { + auto & table = kernel::filesystem::file_descriptor_table::get(); + + THEN("removing a file with a negative file descriptor does nothing") + { + REQUIRE_NOTHROW(table.remove_file(-1)); + } + + THEN("removing a file with an out-of-bounds file descriptor does nothing") + { + REQUIRE_NOTHROW(table.remove_file(1000)); + } + } +}
\ No newline at end of file diff --git a/kernel/src/filesystem/filesystem.cpp b/kernel/src/filesystem/filesystem.cpp index 0ac9cf8..d8b04eb 100644 --- a/kernel/src/filesystem/filesystem.cpp +++ b/kernel/src/filesystem/filesystem.cpp @@ -1,24 +1,62 @@ #include "kernel/filesystem/filesystem.hpp" #include "kapi/devices/device.hpp" +#include "kapi/system.hpp" + +#include "kernel/filesystem/ext2/filesystem.hpp" #include "kernel/filesystem/inode.hpp" #include <kstd/memory> +#include <array> + namespace kernel::filesystem { - auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const & device) -> int + namespace + { + constexpr auto static filesystem_factories = std::array{ + []() { return kstd::make_shared<ext2::filesystem>(); }, + }; + } // namespace + + auto filesystem::probe_and_mount(kstd::shared_ptr<kapi::devices::device> const & device) + -> kstd::shared_ptr<filesystem> + { + if (!device) + { + kapi::system::panic("[FILESYSTEM] cannot mount filesystem: device is null."); + } + + for (auto & factory : filesystem_factories) + { + auto fs = factory(); + if (fs->mount(device) == operation_result::success) + { + return fs; + } + } + + return nullptr; + } + + auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const & device) -> operation_result { if (!device) { - return -1; // TODO BA-FS26 panic or errorcode? + kapi::system::panic("[FILESYSTEM] cannot mount filesystem: device is null."); } + m_device = device; - return 0; + return operation_result::success; } auto filesystem::root_inode() const -> kstd::shared_ptr<inode> const & { return m_root_inode; } + + auto filesystem::device() const -> kstd::shared_ptr<kapi::devices::device> const & + { + return m_device; + } } // namespace kernel::filesystem
\ No newline at end of file diff --git a/kernel/src/filesystem/mount.cpp b/kernel/src/filesystem/mount.cpp index f9e709c..d165385 100644 --- a/kernel/src/filesystem/mount.cpp +++ b/kernel/src/filesystem/mount.cpp @@ -13,11 +13,13 @@ namespace kernel::filesystem { mount::mount(kstd::shared_ptr<dentry> const & mount_dentry, kstd::shared_ptr<dentry> const & root_dentry, - kstd::shared_ptr<filesystem> const & fs, std::string_view mount_path) + kstd::shared_ptr<filesystem> const & fs, std::string_view mount_path, + kstd::shared_ptr<mount> const & parent_mount) : m_mount_path(mount_path) , m_mount_dentry(mount_dentry) , m_root_dentry(root_dentry) , m_filesystem(fs) + , m_parent_mount(parent_mount) { if (!m_filesystem) { @@ -25,7 +27,7 @@ namespace kernel::filesystem } } - auto mount::get_mount_dentry() const -> kstd::shared_ptr<dentry> + auto mount::get_mount_dentry() const -> kstd::shared_ptr<dentry> const & { return m_mount_dentry; } @@ -44,4 +46,9 @@ namespace kernel::filesystem { return m_mount_path.view(); } + + auto mount::get_parent_mount() const -> kstd::shared_ptr<mount> const & + { + return m_parent_mount; + } } // namespace kernel::filesystem
\ No newline at end of file diff --git a/kernel/src/filesystem/mount.tests.cpp b/kernel/src/filesystem/mount.tests.cpp new file mode 100644 index 0000000..4c4393a --- /dev/null +++ b/kernel/src/filesystem/mount.tests.cpp @@ -0,0 +1,49 @@ +#include "kernel/filesystem/mount.hpp" + +#include "kernel/filesystem/dentry.hpp" +#include "kernel/test_support/cpu.hpp" +#include "kernel/test_support/filesystem/filesystem.hpp" +#include "kernel/test_support/filesystem/inode.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Mount construction", "[filesystem][mount]") +{ + GIVEN("a filesystem and a root dentry") + { + auto fs = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode, "/"); + + WHEN("constructing a mount with the filesystem and root dentry") + { + auto mount = kernel::filesystem::mount{root_dentry, root_dentry, fs, "/", nullptr}; + + THEN("the mount has the correct filesystem, root dentry, mount dentry, and mount path") + { + REQUIRE(mount.get_filesystem() == fs); + REQUIRE(mount.root_dentry() == root_dentry); + REQUIRE(mount.get_mount_dentry() == root_dentry); + REQUIRE(mount.get_mount_path() == "/"); + } + + THEN("the mount has no parent mount") + { + REQUIRE(mount.get_parent_mount() == nullptr); + } + } + + WHEN("constructing a mount with a null filesystem") + { + THEN("the constructor panics") + { + REQUIRE_THROWS_AS((kernel::filesystem::mount{root_dentry, root_dentry, nullptr, "/", nullptr}), + kernel::tests::cpu::halt); + } + } + } +} diff --git a/kernel/src/filesystem/mount_table.cpp b/kernel/src/filesystem/mount_table.cpp index 737434e..3b1dee3 100644 --- a/kernel/src/filesystem/mount_table.cpp +++ b/kernel/src/filesystem/mount_table.cpp @@ -1,17 +1,83 @@ #include "kernel/filesystem/mount_table.hpp" +#include "kernel/filesystem/dentry.hpp" #include "kernel/filesystem/mount.hpp" #include <kstd/memory> +#include <kstd/vector> +#include <algorithm> #include <cstddef> +#include <ranges> #include <string_view> namespace kernel::filesystem { - void mount_table::add_mount(kstd::shared_ptr<mount> mount) + namespace + { + auto is_descendant_of(kstd::shared_ptr<mount> const & candidate, kstd::shared_ptr<mount> const & ancestor) -> bool + { + for (auto current = candidate; current; current = current->get_parent_mount()) + { + if (current == ancestor) + { + return true; + } + } + + return false; + } + + auto is_strict_prefix(std::string_view prefix, std::string_view path) -> bool + { + return prefix != "/" && path.starts_with(prefix) && path.size() > prefix.size() && path[prefix.size()] == '/'; + } + + auto is_visible_mount(kstd::shared_ptr<mount> const & candidate, + kstd::vector<kstd::shared_ptr<mount>> const & mounts) -> bool + { + return std::ranges::none_of(mounts, [&](auto const & other) { + return other != candidate && is_strict_prefix(other->get_mount_path(), candidate->get_mount_path()) && + !is_descendant_of(candidate, other); + }); + } + } // namespace + + auto mount_table::has_child_mounts(kstd::shared_ptr<mount> const & parent_mount) const -> bool + { + return std::ranges::any_of( + m_mounts, [&parent_mount](auto const & mount) { return mount->get_parent_mount() == parent_mount; }); + } + + void mount_table::add_mount(kstd::shared_ptr<mount> const & mount) { m_mounts.push_back(mount); + if (auto mount_dentry = mount->get_mount_dentry()) + { + mount_dentry->set_flag(dentry::dentry_flags::dcache_mounted); + } + } + + auto mount_table::remove_mount(std::string_view path) -> operation_result + { + auto mount_it = std::ranges::find_if(std::ranges::reverse_view(m_mounts), [&](auto const & mount) { + return mount->get_mount_path() == path && is_visible_mount(mount, m_mounts); + }); + + if (mount_it == std::ranges::reverse_view(m_mounts).end()) + { + return operation_result::mount_not_found; + } + + auto const & mount = *mount_it; + if (has_child_mounts(mount)) + { + return operation_result::has_child_mounts; + } + + mount->get_mount_dentry()->unset_flag(dentry::dentry_flags::dcache_mounted); + m_mounts.erase(std::ranges::find(m_mounts, mount)); + return operation_result::removed; } auto mount_table::find_longest_prefix_mount(std::string_view path) const -> kstd::shared_ptr<mount> @@ -25,8 +91,9 @@ namespace kernel::filesystem // /a/b/c should match /a/b but not /a/bb or /a/b/c/d, / should match everything bool is_prefix = path.starts_with(mp) && (mp == "/" || path.size() == mp.size() || path[mp.size()] == '/'); + bool visible = is_visible_mount(mount, m_mounts); - if (is_prefix && mp.size() >= best_len) + if (is_prefix && visible && mp.size() >= best_len) { mount_with_longest_prefix = mount; best_len = mp.size(); diff --git a/kernel/src/filesystem/mount_table.tests.cpp b/kernel/src/filesystem/mount_table.tests.cpp new file mode 100644 index 0000000..439fe97 --- /dev/null +++ b/kernel/src/filesystem/mount_table.tests.cpp @@ -0,0 +1,163 @@ +#include "kernel/filesystem/mount_table.hpp" + +#include "kernel/filesystem/dentry.hpp" +#include "kernel/filesystem/mount.hpp" +#include "kernel/test_support/filesystem/filesystem.hpp" +#include "kernel/test_support/filesystem/inode.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <string_view> + +SCENARIO("Mount table construction", "[filesystem][mount_table]") +{ + GIVEN("an empty mount table") + { + kernel::filesystem::mount_table table; + + THEN("finding any mount returns null") + { + REQUIRE(table.find_longest_prefix_mount("/") == nullptr); + REQUIRE(table.find_longest_prefix_mount("/any/path") == nullptr); + } + + THEN("removing any mount returns mount_not_found") + { + REQUIRE(table.remove_mount("/") == kernel::filesystem::mount_table::operation_result::mount_not_found); + REQUIRE(table.remove_mount("/any/path") == kernel::filesystem::mount_table::operation_result::mount_not_found); + } + } +} + +SCENARIO("Adding, finding and removing mounts in the mount table", "[filesystem][mount_table]") +{ + GIVEN("a mount table and some mounts") + { + kernel::filesystem::mount_table table; + + auto fs1 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode1 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode1, "/"); + auto mount1 = kstd::make_shared<kernel::filesystem::mount>(root_dentry1, root_dentry1, fs1, "/", nullptr); + + auto fs2 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode2 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode2, "/"); + auto mount2 = kstd::make_shared<kernel::filesystem::mount>(root_dentry1, root_dentry2, fs2, "/mnt", nullptr); + + table.add_mount(mount1); + table.add_mount(mount2); + + THEN("dentry flags are set correctly for mounted dentries") + { + REQUIRE(root_dentry1->has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + REQUIRE(!root_dentry2->has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + } + + THEN("finding mounts by path returns the correct mount") + { + REQUIRE(table.find_longest_prefix_mount("/") == mount1); + REQUIRE(table.find_longest_prefix_mount("/file") == mount1); + REQUIRE(table.find_longest_prefix_mount("/mnt") == mount2); + REQUIRE(table.find_longest_prefix_mount("/mnt/file") == mount2); + REQUIRE(table.find_longest_prefix_mount("/other") == mount1); + } + + THEN("removing a mount that has no child mounts succeeds") + { + REQUIRE(table.remove_mount("/mnt") == kernel::filesystem::mount_table::operation_result::removed); + REQUIRE(!root_dentry2->has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + REQUIRE(table.find_longest_prefix_mount("/mnt") == mount1); + } + + THEN("removing a mount that does not exist returns mount_not_found") + { + REQUIRE(table.remove_mount("/nonexistent") == kernel::filesystem::mount_table::operation_result::mount_not_found); + } + } + + GIVEN("multiple mounts with the same path") + { + kernel::filesystem::mount_table table; + + auto fs1 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode1 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode1, "/"); + auto mount1 = kstd::make_shared<kernel::filesystem::mount>(root_dentry1, root_dentry1, fs1, "/", nullptr); + + auto fs2 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode2 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode2, "/"); + auto mount2 = kstd::make_shared<kernel::filesystem::mount>(root_dentry1, root_dentry2, fs2, "/", mount1); + + table.add_mount(mount1); + table.add_mount(mount2); + + THEN("finding mounts by path returns the correct mount based on longest prefix") + { + REQUIRE(table.find_longest_prefix_mount("/") == mount2); + REQUIRE(table.find_longest_prefix_mount("/file") == mount2); + REQUIRE(table.find_longest_prefix_mount("/mnt") == mount2); + REQUIRE(table.find_longest_prefix_mount("/mnt/file") == mount2); + REQUIRE(table.find_longest_prefix_mount("/other") == mount2); + } + + THEN("removing the topmost mount with the same path succeeds") + { + REQUIRE(table.remove_mount("/") == kernel::filesystem::mount_table::operation_result::removed); + REQUIRE(!root_dentry2->has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + REQUIRE(table.find_longest_prefix_mount("/") == mount1); + } + } + + GIVEN("a mount with child mounts") + { + kernel::filesystem::mount_table table; + + auto fs1 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode1 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode1, "/"); + auto mount1 = kstd::make_shared<kernel::filesystem::mount>(root_dentry1, root_dentry1, fs1, "/", nullptr); + + auto fs2 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode2 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode2, "/"); + auto mount2 = kstd::make_shared<kernel::filesystem::mount>(root_dentry1, root_dentry2, fs2, "/mnt", mount1); + + auto fs3 = kstd::make_shared<kernel::tests::filesystem::filesystem>(); + auto root_inode3 = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto root_dentry3 = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode3, "/"); + auto mount3 = kstd::make_shared<kernel::filesystem::mount>(root_dentry2, root_dentry3, fs3, "/mnt/submnt", mount2); + + table.add_mount(mount1); + table.add_mount(mount2); + table.add_mount(mount3); + + THEN("finding mounts by path returns the correct mount based on longest prefix") + { + REQUIRE(table.find_longest_prefix_mount("/") == mount1); + REQUIRE(table.find_longest_prefix_mount("/file") == mount1); + REQUIRE(table.find_longest_prefix_mount("/mnt") == mount2); + REQUIRE(table.find_longest_prefix_mount("/mnt/file") == mount2); + REQUIRE(table.find_longest_prefix_mount("/mnt/submnt") == mount3); + REQUIRE(table.find_longest_prefix_mount("/other") == mount1); + } + + THEN("removing a mount with child mounts returns has_child_mounts") + { + REQUIRE(table.remove_mount("/") == kernel::filesystem::mount_table::operation_result::has_child_mounts); + REQUIRE(table.remove_mount("/mnt") == kernel::filesystem::mount_table::operation_result::has_child_mounts); + } + + THEN("removing a leaf mount succeeds") + { + REQUIRE(table.remove_mount("/mnt/submnt") == kernel::filesystem::mount_table::operation_result::removed); + REQUIRE(!root_dentry3->has_flag(kernel::filesystem::dentry::dentry_flags::dcache_mounted)); + REQUIRE(table.find_longest_prefix_mount("/mnt/submnt") == mount2); + } + } +} diff --git a/kernel/src/filesystem/open_file_description.cpp b/kernel/src/filesystem/open_file_description.cpp index 8c04225..f049a34 100644 --- a/kernel/src/filesystem/open_file_description.cpp +++ b/kernel/src/filesystem/open_file_description.cpp @@ -32,4 +32,10 @@ namespace kernel::filesystem m_offset += written_bytes; return written_bytes; } + + auto open_file_description::offset() const -> size_t + { + return m_offset; + } + } // namespace kernel::filesystem
\ No newline at end of file diff --git a/kernel/src/filesystem/open_file_description.tests.cpp b/kernel/src/filesystem/open_file_description.tests.cpp new file mode 100644 index 0000000..db8eb49 --- /dev/null +++ b/kernel/src/filesystem/open_file_description.tests.cpp @@ -0,0 +1,114 @@ +#include "kernel/filesystem/open_file_description.hpp" + +#include "kernel/filesystem/vfs.hpp" +#include "kernel/test_support/filesystem/inode.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <cstddef> +#include <filesystem> +#include <string_view> + +SCENARIO("Open file description construction", "[filesystem][open_file_description]") +{ + GIVEN("an inode and an open file description for that inode") + { + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto file_description = kernel::filesystem::open_file_description{inode}; + + THEN("the initial offset is zero") + { + REQUIRE(file_description.offset() == 0); + } + } +} + +SCENARIO("Open file description read/write offset management", "[filesystem][open_file_description]") +{ + GIVEN("an inode that tracks read/write calls and an open file description for that inode") + { + auto inode = kstd::make_shared<kernel::tests::filesystem::inode>(); + auto file_description = kernel::filesystem::open_file_description{inode}; + + THEN("the offset is updated correctly after reads") + { + REQUIRE(file_description.read(nullptr, 100) == 100); + REQUIRE(file_description.offset() == 100); + REQUIRE(file_description.read(nullptr, 50) == 50); + REQUIRE(file_description.offset() == 150); + } + + THEN("the offset is updated correctly after writes") + { + REQUIRE(file_description.write(nullptr, 200) == 200); + REQUIRE(file_description.offset() == 200); + REQUIRE(file_description.write(nullptr, 25) == 25); + REQUIRE(file_description.offset() == 225); + } + + THEN("reads and writes both update the same offset") + { + REQUIRE(file_description.read(nullptr, 10) == 10); + REQUIRE(file_description.offset() == 10); + REQUIRE(file_description.write(nullptr, 20) == 20); + REQUIRE(file_description.offset() == 30); + REQUIRE(file_description.read(nullptr, 5) == 5); + REQUIRE(file_description.offset() == 35); + REQUIRE(file_description.write(nullptr, 15) == 15); + REQUIRE(file_description.offset() == 50); + } + } +} + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_vfs_fixture, + "Open file description read with real image", "[filesystem][open_file_description][img]") +{ + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + + GIVEN("an open file description for a file in a real image") + { + REQUIRE(std::filesystem::exists(image_path)); + REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module"}, {image_path})); + + auto & vfs = kernel::filesystem::vfs::get(); + auto ofd = vfs.open("/information/info_1.txt"); + REQUIRE(ofd != nullptr); + + THEN("the file can be read and the offset is updated") + { + kstd::vector<std::byte> buffer(32); + auto bytes_read = ofd->read(buffer.data(), buffer.size()); + REQUIRE(bytes_read == 32); + REQUIRE(ofd->offset() == 32); + + std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)}; + auto const content_end = buffer_as_str.find('\0'); + REQUIRE(buffer_as_str.substr(0, content_end) == "info_1\n"); + + for (auto i = content_end; i < buffer_as_str.size(); ++i) + { + REQUIRE(buffer_as_str[i] == '\0'); + } + } + + THEN("the file can be read multiple times") + { + kstd::vector<std::byte> buffer(4); + auto bytes_read_1 = ofd->read(buffer.data(), buffer.size() / 2); + REQUIRE(bytes_read_1 == buffer.size() / 2); + REQUIRE(ofd->offset() == buffer.size() / 2); + + auto bytes_read_2 = ofd->read(buffer.data() + buffer.size() / 2, buffer.size() / 2); + REQUIRE(bytes_read_2 == buffer.size() / 2); + REQUIRE(ofd->offset() == buffer.size()); + + std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), bytes_read_1 + bytes_read_2}; + REQUIRE(buffer_as_str == "info"); + } + } +} diff --git a/kernel/src/filesystem/rootfs/filesystem.cpp b/kernel/src/filesystem/rootfs/filesystem.cpp index 37bf588..dffef99 100644 --- a/kernel/src/filesystem/rootfs/filesystem.cpp +++ b/kernel/src/filesystem/rootfs/filesystem.cpp @@ -1,6 +1,7 @@ #include "kernel/filesystem/rootfs/filesystem.hpp" #include "kapi/devices/device.hpp" + #include "kernel/filesystem/inode.hpp" #include "kernel/filesystem/rootfs/inode.hpp" @@ -10,13 +11,13 @@ namespace kernel::filesystem::rootfs { - auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const &) -> int + auto filesystem::mount(kstd::shared_ptr<kapi::devices::device> const &) -> operation_result { auto rfs_inode = kstd::make_shared<inode>(); rfs_inode->add_child("dev"); m_root_inode = rfs_inode; - return 0; + return operation_result::success; } auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) diff --git a/kernel/src/filesystem/rootfs/filesystem.tests.cpp b/kernel/src/filesystem/rootfs/filesystem.tests.cpp new file mode 100644 index 0000000..b013f78 --- /dev/null +++ b/kernel/src/filesystem/rootfs/filesystem.tests.cpp @@ -0,0 +1,45 @@ +#include "kernel/filesystem/rootfs/filesystem.hpp" + +#include "kernel/filesystem/filesystem.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Rootfs filesystem mount and lookup", "[filesystem][rootfs][filesystem]") +{ + GIVEN("a mounted rootfs filesystem") + { + auto fs = kernel::filesystem::rootfs::filesystem{}; + auto result = fs.mount(nullptr); + + THEN("the filesystem can be mounted successfully") + { + REQUIRE(result == kernel::filesystem::filesystem::operation_result::success); + REQUIRE(fs.root_inode() != nullptr); + } + + THEN("looking up the 'dev' directory returns a valid inode") + { + auto dev_inode = fs.lookup(fs.root_inode(), "dev"); + REQUIRE(dev_inode != nullptr); + REQUIRE(dev_inode->is_directory()); + } + + THEN("looking up a non-existent directory returns null") + { + auto non_existent_inode_1 = fs.lookup(fs.root_inode(), ""); + REQUIRE(non_existent_inode_1 == nullptr); + auto non_existent_inode_2 = fs.lookup(fs.root_inode(), "nonexistent"); + REQUIRE(non_existent_inode_2 == nullptr); + } + + THEN("looking up with a null parent inode returns null") + { + auto result = fs.lookup(nullptr, "dev"); + REQUIRE(result == nullptr); + } + } +} diff --git a/kernel/src/filesystem/rootfs/inode.tests.cpp b/kernel/src/filesystem/rootfs/inode.tests.cpp new file mode 100644 index 0000000..a0c5938 --- /dev/null +++ b/kernel/src/filesystem/rootfs/inode.tests.cpp @@ -0,0 +1,83 @@ +#include "kernel/filesystem/rootfs/inode.hpp" + +#include <kstd/memory> +#include <kstd/print> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +SCENARIO("Rootfs inode creation", "[filesystem][rootfs][inode]") +{ + GIVEN("a rootfs inode") + { + auto inode = kernel::filesystem::rootfs::inode{}; + + THEN("the inode has the correct kind") + { + REQUIRE(inode.is_directory()); + REQUIRE_FALSE(inode.is_device()); + REQUIRE_FALSE(inode.is_regular()); + } + + THEN("the inode has no children") + { + REQUIRE(inode.lookup_child("child") == nullptr); + } + } +} + +SCENARIO("Rootfs inode child management", "[filesystem][rootfs][inode]") +{ + GIVEN("a rootfs inode") + { + auto inode = kernel::filesystem::rootfs::inode{}; + + WHEN("adding a child inode") + { + inode.add_child("child"); + inode.add_child("another child"); + + THEN("the child can be looked up by name") + { + auto child_inode = inode.lookup_child("child"); + REQUIRE(child_inode != nullptr); + REQUIRE(child_inode->is_directory()); + } + + THEN("looking up a non-existent child returns null") + { + REQUIRE(inode.lookup_child("nonexistent") == nullptr); + } + } + } +} + +SCENARIO("Rootfs inode read/write", "[filesystem][rootfs][inode]") +{ + GIVEN("a rootfs inode") + { + auto inode = kernel::filesystem::rootfs::inode{}; + + WHEN("reading from the inode") + { + kstd::vector<char> buffer(10); + auto bytes_read = inode.read(buffer.data(), 0, buffer.size()); + + THEN("no bytes are read") + { + REQUIRE(bytes_read == 0); + } + } + + WHEN("writing to the inode") + { + kstd::vector<char> buffer(10, 'x'); + auto bytes_written = inode.write(buffer.data(), 0, buffer.size()); + + THEN("no bytes are written") + { + REQUIRE(bytes_written == 0); + } + } + } +} diff --git a/kernel/src/filesystem/vfs.cpp b/kernel/src/filesystem/vfs.cpp index 06214d2..67d1af2 100644 --- a/kernel/src/filesystem/vfs.cpp +++ b/kernel/src/filesystem/vfs.cpp @@ -5,9 +5,9 @@ #include "kernel/devices/storage/management.hpp" #include "kernel/filesystem/dentry.hpp" #include "kernel/filesystem/devfs/filesystem.hpp" -#include "kernel/filesystem/ext2/filesystem.hpp" #include "kernel/filesystem/filesystem.hpp" #include "kernel/filesystem/mount.hpp" +#include "kernel/filesystem/mount_table.hpp" #include "kernel/filesystem/open_file_description.hpp" #include "kernel/filesystem/rootfs/filesystem.hpp" @@ -17,13 +17,13 @@ #include <ranges> #include <string_view> -namespace kernel::filesystem +namespace { - namespace - { - constinit auto static active_vfs = std::optional<vfs>{}; - } // namespace + constinit auto static active_vfs = std::optional<kernel::filesystem::vfs>{}; +} // namespace +namespace kernel::filesystem +{ auto vfs::init() -> void { if (active_vfs) @@ -41,15 +41,16 @@ namespace kernel::filesystem root_fs->mount(nullptr); auto root_fs_root_dentry = kstd::make_shared<dentry>(nullptr, root_fs->root_inode()); - m_mount_table.add_mount(kstd::make_shared<mount>(nullptr, root_fs_root_dentry, root_fs, "")); + m_mount_table.add_mount(kstd::make_shared<mount>(nullptr, root_fs_root_dentry, root_fs, "", nullptr)); auto storage_mgmt = devices::storage::management::get(); if (auto boot_device = storage_mgmt.determine_boot_device()) { - // TODO BA-FS26 detect fs type from boot device and load corresponding fs, for now just assume ext2 - auto boot_root_fs = kstd::make_shared<ext2::filesystem>(); - boot_root_fs->mount(boot_device); - do_mount_internal("/", root_fs_root_dentry, boot_root_fs); + auto boot_root_fs = kernel::filesystem::filesystem::probe_and_mount(boot_device); + if (boot_root_fs) + { + do_mount_internal("/", root_fs_root_dentry, boot_root_fs); + } } auto device_fs = kstd::make_shared<devfs::filesystem>(); @@ -77,41 +78,55 @@ namespace kernel::filesystem return nullptr; } - auto vfs::do_mount(std::string_view path, kstd::shared_ptr<filesystem> const & filesystem) -> int + auto vfs::do_mount(std::string_view path, kstd::shared_ptr<filesystem> const & filesystem) -> operation_result { if (!filesystem) { - return -1; // TODO BA-FS26 panic or errorcode? + return operation_result::filesystem_null; } - if (path.empty() || path.front() != '/') + if (path.empty() || path.front() != '/' || (path.size() > 1 && path.back() == '/')) { - return -1; // TODO BA-FS26 panic or errorcode? + return operation_result::invalid_path; } - // TODO BA-FS26 better path validation - if ((path.size() > 1 && path.back() == '/')) + if (auto mount_point_dentry = resolve_path(path)) { - return -1; // TODO BA-FS26 panic or errorcode? + do_mount_internal(path, mount_point_dentry, filesystem); + return operation_result::success; } - if (auto mount_point_dentry = resolve_path(path)) + return operation_result::mount_point_not_found; + } + + auto vfs::unmount(std::string_view path) -> operation_result + { + if (path.empty() || path.front() != '/' || (path.size() > 1 && path.back() == '/')) { - do_mount_internal(path, mount_point_dentry, filesystem); - return 0; + return operation_result::invalid_path; + } + + auto remove_result = m_mount_table.remove_mount(path); + if (remove_result == mount_table::operation_result::removed) + { + return operation_result::success; } - return -1; + if (remove_result == mount_table::operation_result::has_child_mounts) + { + return operation_result::unmount_failed; + } + + return operation_result::mount_point_not_found; } auto vfs::do_mount_internal(std::string_view path, kstd::shared_ptr<dentry> const & mount_point_dentry, kstd::shared_ptr<filesystem> const & fs) -> void { - // TODO BA-FS26 check if mount point is already mounted and handle it (unmount old fs, fail, etc.) + auto parent_mount = m_mount_table.find_longest_prefix_mount(path); auto new_fs_root = kstd::make_shared<dentry>(mount_point_dentry, fs->root_inode()); - auto new_mount = kstd::make_shared<mount>(mount_point_dentry, new_fs_root, fs, path); + auto new_mount = kstd::make_shared<mount>(mount_point_dentry, new_fs_root, fs, path, parent_mount); m_mount_table.add_mount(new_mount); - mount_point_dentry->set_flag(dentry::dentry_flags::dcache_mounted); } auto vfs::resolve_path(std::string_view path) -> kstd::shared_ptr<dentry> @@ -160,5 +175,12 @@ namespace kernel::filesystem return current_dentry; } +} // namespace kernel::filesystem -} // namespace kernel::filesystem
\ No newline at end of file +namespace kernel::tests::filesystem::vfs +{ + auto deinit() -> void + { + active_vfs.reset(); + } +} // namespace kernel::tests::filesystem::vfs diff --git a/kernel/src/filesystem/vfs.tests.cpp b/kernel/src/filesystem/vfs.tests.cpp new file mode 100644 index 0000000..f363041 --- /dev/null +++ b/kernel/src/filesystem/vfs.tests.cpp @@ -0,0 +1,166 @@ +#include "kernel/filesystem/vfs.hpp" + +#include "kernel/devices/storage/management.hpp" +#include "kernel/filesystem/filesystem.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp" + +#include <catch2/catch_test_macros.hpp> + +#include <filesystem> + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_vfs_fixture, "VFS with dummy modules", + "[filesystem][vfs]") +{ + GIVEN("an initialized boot module registry with multiple modules") + { + REQUIRE_NOTHROW(setup_modules_and_init_vfs(5)); + + THEN("vfs initializes and provides /dev mount") + { + auto & vfs = kernel::filesystem::vfs::get(); + auto dev = vfs.open("/dev"); + + REQUIRE(dev != nullptr); + } + + THEN("vfs initializes root filesystem with boot device if boot module is present") + { + auto & vfs = kernel::filesystem::vfs::get(); + auto root_file = vfs.open("/"); + + REQUIRE(root_file != nullptr); + } + } +} + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_vfs_fixture, "VFS with file backed image", + "[filesystem][vfs][img]") +{ + auto const image_path_1 = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + auto const image_path_2 = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_2KB_fs.img"; + auto const image_path_3 = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_4KB_fs.img"; + + GIVEN("a real image file") + { + REQUIRE(std::filesystem::exists(image_path_1)); + REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module"}, {image_path_1})); + + THEN("vfs initializes and provides expected mount points") + { + auto & vfs = kernel::filesystem::vfs::get(); + auto root = vfs.open("/"); + auto dev = vfs.open("/dev"); + auto information = vfs.open("/information/info_1.txt"); + + REQUIRE(root != nullptr); + REQUIRE(dev != nullptr); + REQUIRE(information != nullptr); + } + } + + GIVEN("three real image files") + { + REQUIRE(std::filesystem::exists(image_path_1)); + REQUIRE(std::filesystem::exists(image_path_2)); + REQUIRE(std::filesystem::exists(image_path_3)); + REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_1", "test_img_module_2", "test_img_module_3"}, + {image_path_1, image_path_2, image_path_3})); + + auto & vfs = kernel::filesystem::vfs::get(); + auto storage_mgmt = kernel::devices::storage::management::get(); + auto device_1 = storage_mgmt.device_by_major_minor(1, 16); + auto fs_1 = kernel::filesystem::filesystem::probe_and_mount(device_1); + auto device_2 = storage_mgmt.device_by_major_minor(1, 32); + auto fs_2 = kernel::filesystem::filesystem::probe_and_mount(device_2); + + THEN("vfs initializes first module as root") + { + auto & vfs = kernel::filesystem::vfs::get(); + auto info1 = vfs.open("/information/info_1.txt"); + auto info2 = vfs.open("/information/info_2.txt"); + + REQUIRE(info1 != nullptr); + REQUIRE(info2 != nullptr); + } + + THEN("second image can be mounted, data retrieved and unmounted again") + { + REQUIRE(vfs.do_mount("/information", fs_1) == kernel::filesystem::vfs::operation_result::success); + + auto mounted_monkey_1 = vfs.open("/information/monkey_house/monkey_1.txt"); + REQUIRE(mounted_monkey_1 != nullptr); + + REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::success); + auto unmounted_monkey_1 = vfs.open("/information/monkey_house/monkey_1.txt"); + REQUIRE(unmounted_monkey_1 == nullptr); + + auto info_1 = vfs.open("/information/info_1.txt"); + REQUIRE(info_1 != nullptr); + } + + THEN("third image can be mounted in a mounted file system, unmount only if no child mount exists") + { + REQUIRE(vfs.do_mount("/information", fs_1) == kernel::filesystem::vfs::operation_result::success); + REQUIRE(vfs.do_mount("/information/monkey_house/infrastructure", fs_2) == + kernel::filesystem::vfs::operation_result::success); + + auto mounted_monkey_1 = vfs.open("/information/monkey_house/monkey_1.txt"); + auto mounted_fish1 = vfs.open("/information/monkey_house/infrastructure/enclosures/aquarium/tank_1/fish_1.txt"); + + REQUIRE(mounted_monkey_1 != nullptr); + REQUIRE(mounted_fish1 != nullptr); + + REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::unmount_failed); + REQUIRE(vfs.unmount("/information/monkey_house/infrastructure") == + kernel::filesystem::vfs::operation_result::success); + REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::success); + } + + THEN("images can be stacked mounted and correct file system is unmounted again") + { + REQUIRE(vfs.do_mount("/information", fs_1) == kernel::filesystem::vfs::operation_result::success); + REQUIRE(vfs.do_mount("/information", fs_2) == kernel::filesystem::vfs::operation_result::success); + + auto mounted_tickets = vfs.open("/information/entrance/tickets.txt"); + REQUIRE(mounted_tickets != nullptr); + + REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::success); + mounted_tickets = vfs.open("/information/entrance/tickets.txt"); + REQUIRE(mounted_tickets == nullptr); + + auto mounted_monkey = vfs.open("/information/monkey_house/monkey_1.txt"); + REQUIRE(mounted_monkey != nullptr); + } + + THEN("mount with null file system fails") + { + REQUIRE(vfs.do_mount("/information", nullptr) == kernel::filesystem::vfs::operation_result::filesystem_null); + } + + THEN("mount with invalid path fails") + { + REQUIRE(vfs.do_mount("", fs_1) == kernel::filesystem::vfs::operation_result::invalid_path); + REQUIRE(vfs.do_mount("information", fs_1) == kernel::filesystem::vfs::operation_result::invalid_path); + REQUIRE(vfs.do_mount("/information/", fs_1) == kernel::filesystem::vfs::operation_result::invalid_path); + } + + THEN("mount with non-existent mount point fails") + { + REQUIRE(vfs.do_mount("/information/nonexistent", fs_1) == + kernel::filesystem::vfs::operation_result::mount_point_not_found); + } + + THEN("unmount with invalid path fails") + { + REQUIRE(vfs.unmount("") == kernel::filesystem::vfs::operation_result::invalid_path); + REQUIRE(vfs.unmount("information") == kernel::filesystem::vfs::operation_result::invalid_path); + REQUIRE(vfs.unmount("/information/") == kernel::filesystem::vfs::operation_result::invalid_path); + } + + THEN("unmounting non-existent mount point returns expected error code") + { + REQUIRE(vfs.unmount("/information/nonexistent") == + kernel::filesystem::vfs::operation_result::mount_point_not_found); + } + } +} |
