aboutsummaryrefslogtreecommitdiff
path: root/kernel/src/filesystem
diff options
context:
space:
mode:
authorLukas Oesch <lukas.oesch@ost.ch>2026-06-10 10:40:46 +0200
committerLukas Oesch <lukas.oesch@ost.ch>2026-06-10 10:40:46 +0200
commit33abd5cf264cb9e34121082105b0bc17b3cf7a36 (patch)
tree36b15d53fea04f4f9d9af817100f7ad013bd9b5c /kernel/src/filesystem
parentd01caf1c4aef3c89c68b9d1cc9fe56445f0860b5 (diff)
parent7e27130c342b7299a1d2188a7192a7f17b5ac2ad (diff)
downloadkernel-33abd5cf264cb9e34121082105b0bc17b3cf7a36.tar.xz
kernel-33abd5cf264cb9e34121082105b0bc17b3cf7a36.zip
Merge branch 'develop-BA-FS26' into 'develop'HEADdevelop
Merge of BA-FS26 branch into develop See merge request teachos/kernel!49
Diffstat (limited to 'kernel/src/filesystem')
-rw-r--r--kernel/src/filesystem/dentry.cpp95
-rw-r--r--kernel/src/filesystem/dentry.tests.cpp150
-rw-r--r--kernel/src/filesystem/devfs/filesystem.cpp81
-rw-r--r--kernel/src/filesystem/devfs/filesystem.tests.cpp72
-rw-r--r--kernel/src/filesystem/devfs/inode.cpp21
-rw-r--r--kernel/src/filesystem/devfs/inode.tests.cpp55
-rw-r--r--kernel/src/filesystem/device_inode.cpp57
-rw-r--r--kernel/src/filesystem/device_inode.tests.cpp109
-rw-r--r--kernel/src/filesystem/ext2/filesystem.cpp247
-rw-r--r--kernel/src/filesystem/ext2/filesystem.tests.cpp139
-rw-r--r--kernel/src/filesystem/ext2/inode.cpp111
-rw-r--r--kernel/src/filesystem/ext2/inode.tests.cpp376
-rw-r--r--kernel/src/filesystem/filesystem.cpp60
-rw-r--r--kernel/src/filesystem/inode.cpp24
-rw-r--r--kernel/src/filesystem/mount.cpp90
-rw-r--r--kernel/src/filesystem/mount.tests.cpp94
-rw-r--r--kernel/src/filesystem/mount_table.cpp79
-rw-r--r--kernel/src/filesystem/mount_table.tests.cpp184
-rw-r--r--kernel/src/filesystem/open_file_descriptor.cpp46
-rw-r--r--kernel/src/filesystem/open_file_descriptor.tests.cpp113
-rw-r--r--kernel/src/filesystem/open_file_table.cpp87
-rw-r--r--kernel/src/filesystem/open_file_table.tests.cpp106
-rw-r--r--kernel/src/filesystem/path.tests.cpp69
-rw-r--r--kernel/src/filesystem/rootfs/filesystem.cpp47
-rw-r--r--kernel/src/filesystem/rootfs/filesystem.tests.cpp38
-rw-r--r--kernel/src/filesystem/rootfs/inode.cpp23
-rw-r--r--kernel/src/filesystem/rootfs/inode.tests.cpp37
-rw-r--r--kernel/src/filesystem/type_registry.cpp72
-rw-r--r--kernel/src/filesystem/type_registry.tests.cpp77
-rw-r--r--kernel/src/filesystem/vfs.cpp299
-rw-r--r--kernel/src/filesystem/vfs.tests.cpp568
31 files changed, 3626 insertions, 0 deletions
diff --git a/kernel/src/filesystem/dentry.cpp b/kernel/src/filesystem/dentry.cpp
new file mode 100644
index 0000000..3d8e01a
--- /dev/null
+++ b/kernel/src/filesystem/dentry.cpp
@@ -0,0 +1,95 @@
+#include <kernel/filesystem/dentry.hpp>
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+
+#include <algorithm>
+#include <cstdint>
+#include <string_view>
+
+namespace kernel::filesystem
+{
+ 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(inode)
+ , m_flags(0)
+ {
+ if (!m_inode)
+ {
+ kapi::system::panic("[FILESYSTEM] dentry constructed with null inode.");
+ }
+ if (m_name.empty())
+ {
+ kapi::system::panic("[FILESYSTEM] dentry constructed with empty name.");
+ }
+ }
+
+ auto dentry::get_inode() const -> kstd::shared_ptr<inode> const &
+ {
+ return m_inode;
+ }
+
+ auto dentry::parent() const -> kstd::shared_ptr<dentry> const &
+ {
+ return m_parent;
+ }
+
+ auto dentry::name() const -> std::string_view
+ {
+ return m_name;
+ }
+
+ auto dentry::absolute_path() const -> kstd::string
+ {
+ kstd::string path = m_name;
+
+ auto parent = m_parent;
+ while (parent)
+ {
+ auto parent_name = parent->m_name;
+ if (parent_name == "/")
+ {
+ path = "/" + path;
+ }
+ else
+ {
+ path = parent_name + "/" + path;
+ }
+
+ parent = parent->m_parent;
+ }
+
+ return path;
+ }
+
+ auto dentry::add_child(kstd::shared_ptr<dentry> const & child) -> void
+ {
+ m_children.push_back(child);
+ }
+
+ auto dentry::find_child(std::string_view name) const -> kstd::shared_ptr<dentry>
+ {
+ auto it = std::ranges::find_if(m_children, [&](auto const & child) { return child->m_name == name; });
+ return (it != m_children.end()) ? *it : nullptr;
+ }
+
+ auto dentry::set_flag(dentry_flags flag) -> void
+ {
+ m_flags |= static_cast<uint32_t>(flag);
+ }
+
+ auto dentry::unset_flag(dentry_flags flag) -> void
+ {
+ m_flags &= ~static_cast<uint32_t>(flag);
+ }
+
+ auto dentry::has_flag(dentry_flags flag) const -> bool
+ {
+ return (m_flags & static_cast<uint32_t>(flag)) != 0;
+ }
+} // namespace kernel::filesystem \ No newline at end of file
diff --git a/kernel/src/filesystem/dentry.tests.cpp b/kernel/src/filesystem/dentry.tests.cpp
new file mode 100644
index 0000000..b7690f5
--- /dev/null
+++ b/kernel/src/filesystem/dentry.tests.cpp
@@ -0,0 +1,150 @@
+#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, "parent");
+
+ 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.parent() == parent_dentry);
+ REQUIRE(child_dentry.get_inode() == inode);
+ REQUIRE(child_dentry.name() == "child");
+ }
+
+ THEN("no flag is set")
+ {
+ REQUIRE_FALSE(child_dentry.has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+ }
+
+ WHEN("constructing a dentry with an empty name")
+ {
+ THEN("the dentry has the correct parent and inode, and an empty name")
+ {
+ REQUIRE_THROWS_AS((kernel::filesystem::dentry{parent_dentry, inode, ""}), kernel::tests::cpu::halt);
+ }
+ }
+
+ 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.parent() == nullptr);
+ REQUIRE(child_dentry.get_inode() == inode);
+ REQUIRE(child_dentry.name() == "child");
+ }
+
+ THEN("no flag is set")
+ {
+ REQUIRE_FALSE(child_dentry.has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+ }
+
+ 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, "parent");
+
+ 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::is_mount_point);
+
+ THEN("the flag is set")
+ {
+ REQUIRE(dentry.has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+ }
+
+ WHEN("unsetting a flag")
+ {
+ dentry.set_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point);
+ dentry.unset_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point);
+
+ THEN("the flag is unset")
+ {
+ REQUIRE_FALSE(dentry.has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+ }
+ }
+}
+
+SCENARIO("Dentry path resolution", "[filesystem][dentry]")
+{
+ GIVEN("A dentry with a parent hierarchy")
+ {
+ auto root_inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto root_dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, root_inode, "/");
+
+ auto home_inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto home_dentry = kstd::make_shared<kernel::filesystem::dentry>(root_dentry, home_inode, "home");
+ root_dentry->add_child(home_dentry);
+
+ auto user_inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto user_dentry = kstd::make_shared<kernel::filesystem::dentry>(home_dentry, user_inode, "user");
+ home_dentry->add_child(user_dentry);
+
+ THEN("the full path is constructed correctly")
+ {
+ REQUIRE(root_dentry->absolute_path() == "/");
+ REQUIRE(home_dentry->absolute_path() == "/home");
+ REQUIRE(user_dentry->absolute_path() == "/home/user");
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/src/filesystem/devfs/filesystem.cpp b/kernel/src/filesystem/devfs/filesystem.cpp
new file mode 100644
index 0000000..ce887ff
--- /dev/null
+++ b/kernel/src/filesystem/devfs/filesystem.cpp
@@ -0,0 +1,81 @@
+#include <kernel/filesystem/devfs/filesystem.hpp>
+
+#include "kernel/filesystem/filesystem.hpp"
+#include <kernel/devices/storage/management.hpp>
+#include <kernel/filesystem/devfs/inode.hpp>
+#include <kernel/filesystem/device_inode.hpp>
+#include <kernel/filesystem/inode.hpp>
+#include <kernel/filesystem/type.hpp>
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/memory>
+
+#include <algorithm>
+#include <string_view>
+
+namespace kernel::filesystem::devfs
+{
+ struct type final : kernel::filesystem::type
+ {
+ [[nodiscard]] auto name() const noexcept -> std::string_view override
+ {
+ return "devfs";
+ }
+
+ [[nodiscard]] auto requires_device() const noexcept -> bool override
+ {
+ return false;
+ }
+
+ [[nodiscard]] auto make_instance() const -> kstd::shared_ptr<kernel::filesystem::filesystem> override
+ {
+ return kstd::make_shared<filesystem>();
+ }
+ };
+
+ [[gnu::used]]
+ constexpr auto registration = type_registration<type>{};
+
+ auto filesystem::mount(kstd::shared_ptr<kernel::filesystem::inode> const &) -> operation_result
+ {
+ m_root_inode = kstd::make_shared<inode>();
+ build_device_inode_table();
+
+ return operation_result::success;
+ }
+
+ auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) const
+ -> kstd::shared_ptr<kernel::filesystem::inode>
+ {
+ if (!parent || !parent->is_directory())
+ {
+ return nullptr;
+ }
+
+ if (parent.get() != m_root_inode.get())
+ {
+ return nullptr;
+ }
+
+ auto it = std::ranges::find_if(m_inodes, [&](auto const & dev_node) {
+ if (auto device_inode_ptr = static_cast<device_inode *>(dev_node.get()))
+ {
+ return device_inode_ptr->device()->name() == name;
+ }
+ return false;
+ });
+ return (it != m_inodes.end()) ? *it : nullptr;
+ }
+
+ auto filesystem::build_device_inode_table() -> void
+ {
+ m_inodes.clear();
+
+ auto storage_mgmt = devices::storage::management::get();
+ std::ranges::for_each(storage_mgmt.all_controllers(), [&](auto const & controller) {
+ std::ranges::for_each(controller->all_devices(),
+ [&](auto const & device) { m_inodes.push_back(kstd::make_shared<device_inode>(device)); });
+ });
+ }
+} // namespace kernel::filesystem::devfs \ No newline at end of file
diff --git a/kernel/src/filesystem/devfs/filesystem.tests.cpp b/kernel/src/filesystem/devfs/filesystem.tests.cpp
new file mode 100644
index 0000000..36cb411
--- /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_FALSE(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.cpp b/kernel/src/filesystem/devfs/inode.cpp
new file mode 100644
index 0000000..7bbfbbe
--- /dev/null
+++ b/kernel/src/filesystem/devfs/inode.cpp
@@ -0,0 +1,21 @@
+#include <kernel/filesystem/devfs/inode.hpp>
+
+#include <cstddef>
+
+namespace kernel::filesystem::devfs
+{
+ auto inode::read(void *, size_t, size_t) const -> size_t
+ {
+ return 0;
+ }
+
+ auto inode::write(void const *, size_t, size_t) -> size_t
+ {
+ return 0;
+ }
+
+ auto inode::is_directory() const -> bool
+ {
+ return true;
+ }
+} // namespace kernel::filesystem::devfs \ No newline at end of file
diff --git a/kernel/src/filesystem/devfs/inode.tests.cpp b/kernel/src/filesystem/devfs/inode.tests.cpp
new file mode 100644
index 0000000..ae26e74
--- /dev/null
+++ b/kernel/src/filesystem/devfs/inode.tests.cpp
@@ -0,0 +1,55 @@
+#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());
+ REQUIRE_FALSE(inode.is_symbolic_link());
+ }
+ }
+}
+
+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
new file mode 100644
index 0000000..81a784c
--- /dev/null
+++ b/kernel/src/filesystem/device_inode.cpp
@@ -0,0 +1,57 @@
+#include <kernel/filesystem/device_inode.hpp>
+
+#include <kernel/devices/block_device_utils.hpp>
+
+#include <kapi/devices/device.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ device_inode::device_inode(kstd::shared_ptr<kapi::devices::device> const & device)
+ : m_device(device)
+ {
+ if (!device)
+ {
+ kapi::system::panic("[FILESYSTEM] device_inode constructed with null device.");
+ }
+ }
+
+ auto device_inode::read(void * buffer, size_t offset, size_t size) const -> size_t
+ {
+ if (m_device->is_block_device())
+ {
+ return devices::block_device_utils::read(m_device, buffer, offset, size);
+ }
+ else
+ {
+ kapi::system::panic("[FILESYSTEM] device_file::read called on non-block device.");
+ }
+ }
+
+ auto device_inode::write(void const * buffer, size_t offset, size_t size) -> size_t
+ {
+ if (m_device->is_block_device())
+ {
+ return devices::block_device_utils::write(m_device, buffer, offset, size);
+ }
+ else
+ {
+ kapi::system::panic("[FILESYSTEM] device_file::write called on non-block device.");
+ }
+ }
+
+ auto device_inode::device() const -> kstd::shared_ptr<kapi::devices::device> const &
+ {
+ return m_device;
+ }
+
+ auto device_inode::is_device() const -> bool
+ {
+ return true;
+ }
+
+} // namespace kernel::filesystem \ No newline at end of file
diff --git a/kernel/src/filesystem/device_inode.tests.cpp b/kernel/src/filesystem/device_inode.tests.cpp
new file mode 100644
index 0000000..025a22a
--- /dev/null
+++ b/kernel/src/filesystem/device_inode.tests.cpp
@@ -0,0 +1,109 @@
+#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());
+ REQUIRE_FALSE(inode.is_symbolic_link());
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..3180a19
--- /dev/null
+++ b/kernel/src/filesystem/ext2/filesystem.cpp
@@ -0,0 +1,247 @@
+#include <kernel/filesystem/ext2/filesystem.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 <kernel/filesystem/type.hpp>
+
+#include <kstd/memory>
+#include <kstd/unikstd.h>
+#include <kstd/vector>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+
+namespace kernel::filesystem::ext2
+{
+
+ struct type final : kernel::filesystem::type
+ {
+ [[nodiscard]] auto name() const noexcept -> std::string_view override
+ {
+ return "ext2";
+ }
+
+ [[nodiscard]] auto requires_device() const noexcept -> bool override
+ {
+ return true;
+ }
+
+ [[nodiscard]] auto make_instance() const -> kstd::shared_ptr<kernel::filesystem::filesystem> override
+ {
+ return kstd::make_shared<filesystem>();
+ }
+ };
+
+ [[gnu::used]]
+ constexpr auto registration = type_registration<type>{};
+
+ auto filesystem::mount(kstd::shared_ptr<kernel::filesystem::inode> const & backing_inode) -> operation_result
+ {
+ kernel::filesystem::filesystem::mount(backing_inode);
+
+ m_backing_inode->read(&m_superblock, constants::superblock_offset, sizeof(m_superblock));
+
+ if (m_superblock.magic != constants::magic_number)
+ {
+ return operation_result::invalid_magic_number;
+ }
+
+ 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);
+
+ m_backing_inode->read(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) const
+ -> kstd::shared_ptr<kernel::filesystem::inode>
+ {
+ if (!parent || !parent->is_directory())
+ {
+ return nullptr;
+ }
+
+ auto * ext2_parent = static_cast<inode *>(parent.get());
+ if (!ext2_parent)
+ {
+ return nullptr;
+ }
+
+ auto const & inode_data = ext2_parent->data();
+ kstd::vector<uint8_t> buffer(block_size());
+
+ for (uint32_t i = 0; i < 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();
+ m_backing_inode->read(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) const -> kstd::shared_ptr<inode>
+ {
+ 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())
+ {
+ return nullptr;
+ }
+
+ 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 * inode_size();
+
+ auto new_inode_data = inode_data{};
+ m_backing_inode->read(&new_inode_data, inode_offset, sizeof(inode_data));
+
+ return kstd::make_shared<inode>(this, new_inode_data);
+ }
+
+ auto filesystem::indirect_levels() const -> std::array<indirect_level, 3>
+ {
+ return {
+ {{constants::singly_indirect_block_index, block_numbers_per_singly_indirect_block()},
+ {constants::doubly_indirect_block_index, block_numbers_per_doubly_indirect_block()},
+ {constants::triply_indirect_block_index, block_numbers_per_triply_indirect_block()}}
+ };
+ }
+
+ auto filesystem::map_inode_block_index_to_global_block_number(size_t inode_block_index, inode_data data) const
+ -> kstd::ssize_t
+ {
+ if (inode_block_index < constants::direct_block_count)
+ {
+ return data.block.at(inode_block_index);
+ }
+
+ inode_block_index -= constants::direct_block_count;
+
+ for (auto const & level : indirect_levels())
+ {
+ if (inode_block_index >= level.capacity)
+ {
+ inode_block_index -= level.capacity;
+ continue;
+ }
+
+ auto block_number = data.block[level.slot_index];
+ if (block_number == 0)
+ {
+ return 0;
+ }
+
+ for (auto stride = level.capacity / block_numbers_per_block();; stride /= block_numbers_per_block())
+ {
+ auto const idx = inode_block_index / stride;
+ inode_block_index %= stride;
+
+ block_number = read_block_number_at_index(block_number, idx);
+ if (block_number == 0)
+ {
+ return 0;
+ }
+
+ if (stride == 1)
+ {
+ break;
+ }
+ }
+
+ return block_number;
+ }
+
+ return -1;
+ }
+
+ auto filesystem::read_block_number_at_index(uint32_t block_number, size_t index) const -> uint32_t
+ {
+ uint32_t block_number_buffer = 0;
+
+ auto const block_start_offset = block_number * block_size();
+ auto const number_start_address = block_start_offset + index * sizeof(uint32_t);
+ m_backing_inode->read(&block_number_buffer, number_start_address, sizeof(uint32_t));
+
+ return block_number_buffer;
+ }
+
+ auto filesystem::block_numbers_per_block() const -> size_t
+ {
+ return block_size() / sizeof(uint32_t);
+ }
+
+ auto filesystem::block_numbers_per_singly_indirect_block() const -> size_t
+ {
+ return block_numbers_per_block();
+ }
+
+ auto filesystem::block_numbers_per_doubly_indirect_block() const -> size_t
+ {
+ return block_numbers_per_singly_indirect_block() * block_numbers_per_block();
+ }
+
+ auto filesystem::block_numbers_per_triply_indirect_block() const -> size_t
+ {
+ return block_numbers_per_doubly_indirect_block() * block_numbers_per_block();
+ }
+
+ auto filesystem::block_size() const -> size_t
+ {
+ return constants::base_block_size << m_superblock.log_block_size;
+ }
+
+ auto filesystem::revision_level() const -> uint32_t
+ {
+ return m_superblock.rev_level;
+ }
+
+ auto filesystem::inode_size() const -> uint16_t
+ {
+ return revision_level() == constants::good_old_revision ? 128 : m_superblock.inode_size;
+ }
+
+ auto filesystem::inode_block_count(inode_data const & data) const -> uint32_t
+ {
+ return data.blocks / (2 << m_superblock.log_block_size);
+ }
+
+ auto filesystem::block_group_descriptor_table_offset() const -> size_t
+ {
+ return block_size() == 1024 ? 2 * block_size() : 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..8341070
--- /dev/null
+++ b/kernel/src/filesystem/ext2/filesystem.tests.cpp
@@ -0,0 +1,139 @@
+#include <kernel/filesystem/ext2/filesystem.hpp>
+
+#include <kernel/devices/storage/management.hpp>
+#include <kernel/filesystem/device_inode.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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(boot_device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+
+ THEN("mount fails with invalid_magic_number")
+ {
+ REQUIRE(fs.mount(dev_inode) == 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == 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) == -1);
+ }
+ }
+}
diff --git a/kernel/src/filesystem/ext2/inode.cpp b/kernel/src/filesystem/ext2/inode.cpp
new file mode 100644
index 0000000..35a32ee
--- /dev/null
+++ b/kernel/src/filesystem/ext2/inode.cpp
@@ -0,0 +1,111 @@
+#include <kernel/filesystem/ext2/inode.hpp>
+
+#include <kernel/filesystem/ext2/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/cstring>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+
+namespace kernel::filesystem::ext2
+{
+ inode::inode(filesystem const * fs, inode_data const & data)
+ : m_filesystem(fs)
+ , m_data(data)
+ {
+ 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 const max_readable = this->size() - offset;
+ auto const requested_size = std::min(size, max_readable);
+
+ if (is_symbolic_link() && this->size() <= sizeof(m_data.block))
+ {
+ auto inline_target = reinterpret_cast<uint8_t const *>(m_data.block.data());
+ kstd::libc::memcpy(static_cast<uint8_t *>(buffer), inline_target + offset, requested_size);
+ return requested_size;
+ }
+
+ auto block_index = offset / m_filesystem->block_size();
+ auto in_block_offset = offset % m_filesystem->block_size();
+
+ auto bytes_read = 0uz;
+
+ while (bytes_read < requested_size)
+ {
+ auto const block_number = m_filesystem->map_inode_block_index_to_global_block_number(block_index, m_data);
+ if (block_number == -1)
+ {
+ break;
+ }
+
+ auto const bytes_to_read = std::min(requested_size - bytes_read, m_filesystem->block_size() - in_block_offset);
+ if (block_number == 0)
+ {
+ kstd::libc::memset(static_cast<uint8_t *>(buffer) + bytes_read, 0, bytes_to_read);
+ bytes_read += bytes_to_read;
+ }
+ else
+ {
+ auto const block_start_offset = block_number * m_filesystem->block_size();
+ auto const read_offset = block_start_offset + in_block_offset;
+
+ bytes_read += m_filesystem->backing_inode()->read(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 *, size_t, size_t) -> size_t
+ {
+ kapi::system::panic("[EXT2] inode::write is not implemented yet");
+ return 0;
+ }
+
+ [[nodiscard]] auto inode::data() const -> inode_data const &
+ {
+ return m_data;
+ }
+
+ auto inode::is_regular() const -> bool
+ {
+ return (m_data.mode & constants::mode_mask) == constants::mode_regular;
+ }
+
+ auto inode::is_directory() const -> bool
+ {
+ return (m_data.mode & constants::mode_mask) == constants::mode_directory;
+ }
+
+ auto inode::is_symbolic_link() const -> bool
+ {
+ return (m_data.mode & constants::mode_mask) == constants::mode_symbolic_link;
+ }
+
+ auto inode::size() const -> uint64_t
+ {
+ uint64_t size = m_data.size;
+
+ if (m_filesystem->revision_level() > constants::good_old_revision && is_regular())
+ {
+ size |= static_cast<uint64_t>(m_data.dir_acl) << 32;
+ }
+
+ return size;
+ }
+
+} // namespace kernel::filesystem::ext2
diff --git a/kernel/src/filesystem/ext2/inode.tests.cpp b/kernel/src/filesystem/ext2/inode.tests.cpp
new file mode 100644
index 0000000..4aecc04
--- /dev/null
+++ b/kernel/src/filesystem/ext2/inode.tests.cpp
@@ -0,0 +1,376 @@
+#include <kernel/filesystem/ext2/inode.hpp>
+
+#include <kernel/devices/storage/management.hpp>
+#include <kernel/filesystem/device_inode.hpp>
+#include <kernel/filesystem/ext2/filesystem.hpp>
+#include <kernel/filesystem/ext2/superblock.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 <algorithm>
+#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{};
+ auto data = kernel::filesystem::ext2::inode_data{};
+
+ THEN("the inode is initialized with regular file mode in data and has the kind regular")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_regular;
+ auto inode = kernel::filesystem::ext2::inode(&fs, data);
+
+ REQUIRE(inode.is_regular());
+ REQUIRE_FALSE(inode.is_directory());
+ REQUIRE_FALSE(inode.is_device());
+ REQUIRE_FALSE(inode.is_symbolic_link());
+ }
+
+ THEN("the inode is initialized with directory mode in data and has the kind directory")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_directory;
+ auto inode = kernel::filesystem::ext2::inode(&fs, data);
+
+ REQUIRE_FALSE(inode.is_regular());
+ REQUIRE(inode.is_directory());
+ REQUIRE_FALSE(inode.is_device());
+ REQUIRE_FALSE(inode.is_symbolic_link());
+ }
+
+ THEN("the inode is initialized with symbolic link mode in data and has the kind symbolic link")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_symbolic_link;
+ auto inode = kernel::filesystem::ext2::inode(&fs, data);
+
+ REQUIRE_FALSE(inode.is_regular());
+ REQUIRE_FALSE(inode.is_directory());
+ REQUIRE_FALSE(inode.is_device());
+ REQUIRE(inode.is_symbolic_link());
+ }
+
+ THEN("the inode is initialized with zero mode in data and has no specific kind")
+ {
+ data.mode = 0;
+ auto inode = kernel::filesystem::ext2::inode(&fs, data);
+
+ REQUIRE_FALSE(inode.is_regular());
+ REQUIRE_FALSE(inode.is_directory());
+ REQUIRE_FALSE(inode.is_device());
+ REQUIRE_FALSE(inode.is_symbolic_link());
+ }
+ }
+
+ 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(boot_device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == 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 handles zeros in block mappings as file holes", "[filesystem][ext2][inode]")
+{
+ auto const block_size = 1024uz;
+ GIVEN("an ext2 inode with only direct 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == kernel::filesystem::filesystem::operation_result::success);
+
+ auto data = kernel::filesystem::ext2::inode_data{};
+ data.block[0] = 30;
+ data.block[1] = 0;
+ data.block[2] = 31;
+ data.size = block_size * 3;
+
+ kernel::tests::filesystem::ext2::write_bytes(*device, 30 * block_size, "Hello", 5);
+ kernel::tests::filesystem::ext2::write_bytes(*device, 31 * block_size, "World!", 6);
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ auto buffer = kstd::vector<std::byte>(data.size, std::byte{0xAB});
+
+ THEN("correct number of bytes are read and holes are returned as zeros")
+ {
+ auto const bytes_read = inode.read(buffer.data(), 0, buffer.size());
+ REQUIRE(bytes_read == data.size);
+
+ auto const text = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read};
+ REQUIRE(text.substr(0, 5) == "Hello");
+ REQUIRE(std::ranges::all_of(text.substr(5, block_size - 5), [](char c) { return c == '\0'; }));
+ REQUIRE(text.substr(2 * block_size, 6) == "World!");
+ REQUIRE(std::ranges::all_of(text.substr(2 * block_size + 6, 3 * block_size), [](char c) { return c == '\0'; }));
+ }
+ }
+
+ GIVEN("an ext2 indode with file holes in singly indirect 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == kernel::filesystem::filesystem::operation_result::success);
+
+ auto data = kernel::filesystem::ext2::inode_data{};
+ data.block[0] = 30;
+ data.block[12] = 31;
+ data.size = block_size * 15;
+
+ kernel::tests::filesystem::ext2::write_u32(*device, 31 * block_size, 50);
+ kernel::tests::filesystem::ext2::write_u32(*device, 31 * block_size + 4, 0);
+ kernel::tests::filesystem::ext2::write_u32(*device, 31 * block_size + 8, 51);
+
+ kernel::tests::filesystem::ext2::write_bytes(*device, 30 * block_size, "Hello", 5);
+ kernel::tests::filesystem::ext2::write_bytes(*device, 50 * block_size, "Blub", 4);
+ kernel::tests::filesystem::ext2::write_bytes(*device, 51 * block_size, "World!", 6);
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ auto buffer = kstd::vector<std::byte>(data.size, std::byte{0xAB});
+
+ THEN("correct number of bytes are read and holes are returned as zeros")
+ {
+ auto const bytes_read = inode.read(buffer.data(), 0, buffer.size());
+ REQUIRE(bytes_read == data.size);
+
+ auto const text = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read};
+ REQUIRE(text.substr(0, 5) == "Hello");
+ REQUIRE(std::ranges::all_of(text.substr(5, 12 * block_size - 5), [](char c) { return c == '\0'; }));
+ REQUIRE(text.substr(12 * block_size, 4) == "Blub");
+ REQUIRE(
+ std::ranges::all_of(text.substr(12 * block_size + 4, 2 * block_size - 4), [](char c) { return c == '\0'; }));
+ REQUIRE(text.substr(14 * block_size, 6) == "World!");
+ REQUIRE(
+ std::ranges::all_of(text.substr(14 * block_size + 6, 1 * block_size - 6), [](char c) { return c == '\0'; }));
+ }
+ }
+
+ GIVEN("an ext2 inode with zero singly indirect block pointer")
+ {
+ 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == kernel::filesystem::filesystem::operation_result::success);
+
+ auto data = kernel::filesystem::ext2::inode_data{};
+ data.block[12] = 0;
+ data.size = block_size * 15;
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ auto buffer = kstd::vector<std::byte>(block_size * 15, std::byte{0xAB});
+
+ THEN("all direct blocks are zero when singly indirect block pointer is zero")
+ {
+ auto const bytes_read = inode.read(buffer.data(), 0, buffer.size());
+ REQUIRE(bytes_read == buffer.size());
+ REQUIRE(std::ranges::all_of(buffer, [](std::byte c) { return c == std::byte{0x00}; }));
+ }
+ }
+}
+
+SCENARIO("Ext2 inode read across block boundaries", "[filesystem][ext2][inode]")
+{
+ auto const block_size = 1024uz;
+ 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 dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == kernel::filesystem::filesystem::operation_result::success);
+
+ auto inode_data = kernel::filesystem::ext2::inode_data{};
+ inode_data.size = block_size * 2;
+ inode_data.block[0] = 20;
+ kernel::tests::filesystem::ext2::write_bytes(*device, 21 * block_size - 6, "Hello ", 6);
+ inode_data.block[1] = 21;
+ kernel::tests::filesystem::ext2::write_bytes(*device, 21 * block_size, "World!", 6);
+ auto inode = kernel::filesystem::ext2::inode{&fs, inode_data};
+
+ 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, kernel::filesystem::ext2::inode_data{}};
+
+ 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);
+ }
+ }
+}
+
+SCENARIO("Ext2 inode get_size() correctly returns size depending on revision level", "[filesystem][ext2][inode]")
+{
+ auto const block_size = 1024uz;
+
+ auto superblock = kernel::filesystem::ext2::superblock{};
+ superblock.magic = kernel::filesystem::ext2::constants::magic_number;
+ superblock.log_block_size = 0;
+ superblock.blocks_count = 64;
+ superblock.blocks_per_group = 64;
+ superblock.inodes_per_group = 32;
+ superblock.inode_size = 128;
+
+ GIVEN("an ext2 inode with good old revision and inode_data.size = 256, inode_data.dir_acl = 32")
+ {
+ superblock.rev_level = 0;
+
+ 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, superblock);
+
+ auto dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == kernel::filesystem::filesystem::operation_result::success);
+
+ auto data = kernel::filesystem::ext2::inode_data{};
+ data.size = 256;
+ data.dir_acl = 32;
+
+ THEN("the inode size is 256 if mode = regular")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_regular;
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ REQUIRE(inode.size() == 256);
+ }
+
+ THEN("the inode size is 256 if mode = directory")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_directory;
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ REQUIRE(inode.size() == 256);
+ }
+ }
+
+ GIVEN("an ext2 inode with good dynamic revision and inode_data.size = 256, inode_data.dir_acl = 32")
+ {
+ superblock.rev_level = 1;
+
+ 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, superblock);
+
+ auto dev_inode = kstd::make_shared<kernel::filesystem::device_inode>(device);
+
+ auto fs = kernel::filesystem::ext2::filesystem{};
+ REQUIRE(fs.mount(dev_inode) == kernel::filesystem::filesystem::operation_result::success);
+
+ auto data = kernel::filesystem::ext2::inode_data{};
+ data.size = 256;
+ data.dir_acl = 32;
+
+ THEN("the inode size is 256 if mode = regular")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_regular;
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ REQUIRE(inode.size() == 0x0000'0020'0000'0100);
+ }
+
+ THEN("the inode size is 256 if mode = directory")
+ {
+ data.mode = kernel::filesystem::ext2::constants::mode_directory;
+
+ auto inode = kernel::filesystem::ext2::inode{&fs, data};
+
+ REQUIRE(inode.size() == 256);
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/src/filesystem/filesystem.cpp b/kernel/src/filesystem/filesystem.cpp
new file mode 100644
index 0000000..24d0e22
--- /dev/null
+++ b/kernel/src/filesystem/filesystem.cpp
@@ -0,0 +1,60 @@
+#include <kernel/filesystem/filesystem.hpp>
+
+#include <kernel/filesystem/ext2/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+
+#include <array>
+
+namespace kernel::filesystem
+{
+ namespace
+ {
+ constexpr auto static filesystem_factories = std::array{
+ []() { return kstd::make_shared<ext2::filesystem>(); },
+ };
+ } // namespace
+
+ auto filesystem::probe_and_mount(kstd::shared_ptr<inode> const & backing_inode) -> kstd::shared_ptr<filesystem>
+ {
+ if (!backing_inode)
+ {
+ kapi::system::panic("[FILESYSTEM] cannot mount filesystem: backing inode is null.");
+ }
+
+ for (auto & factory : filesystem_factories)
+ {
+ auto fs = factory();
+ if (fs->mount(backing_inode) == operation_result::success)
+ {
+ return fs;
+ }
+ }
+
+ return nullptr;
+ }
+
+ auto filesystem::mount(kstd::shared_ptr<inode> const & backing_inode) -> operation_result
+ {
+ if (!backing_inode)
+ {
+ kapi::system::panic("[FILESYSTEM] cannot mount filesystem: backing inode is null.");
+ }
+
+ m_backing_inode = backing_inode;
+ return operation_result::success;
+ }
+
+ auto filesystem::root_inode() const -> kstd::shared_ptr<inode> const &
+ {
+ return m_root_inode;
+ }
+
+ auto filesystem::backing_inode() const -> kstd::shared_ptr<inode> const &
+ {
+ return m_backing_inode;
+ }
+} // namespace kernel::filesystem \ No newline at end of file
diff --git a/kernel/src/filesystem/inode.cpp b/kernel/src/filesystem/inode.cpp
new file mode 100644
index 0000000..c188917
--- /dev/null
+++ b/kernel/src/filesystem/inode.cpp
@@ -0,0 +1,24 @@
+#include <kernel/filesystem/inode.hpp>
+
+namespace kernel::filesystem
+{
+ auto inode::is_directory() const -> bool
+ {
+ return false;
+ }
+
+ auto inode::is_regular() const -> bool
+ {
+ return false;
+ }
+
+ auto inode::is_device() const -> bool
+ {
+ return false;
+ }
+
+ auto inode::is_symbolic_link() const -> bool
+ {
+ return false;
+ }
+} // namespace kernel::filesystem \ No newline at end of file
diff --git a/kernel/src/filesystem/mount.cpp b/kernel/src/filesystem/mount.cpp
new file mode 100644
index 0000000..ead7479
--- /dev/null
+++ b/kernel/src/filesystem/mount.cpp
@@ -0,0 +1,90 @@
+#include <kernel/filesystem/mount.hpp>
+
+#include <kernel/filesystem/dentry.hpp>
+#include <kernel/filesystem/filesystem.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+
+#include <cstddef>
+#include <string_view>
+
+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, kstd::shared_ptr<mount> const & parent_mount,
+ kstd::shared_ptr<mount> const & source_mount)
+ : m_mount_dentry(mount_dentry)
+ , m_root_dentry(root_dentry)
+ , m_filesystem(fs)
+ , m_parent_mount(parent_mount)
+ , m_source_mount(source_mount)
+ , m_ref_count(0)
+ {
+ if (!m_filesystem)
+ {
+ kapi::system::panic("[FILESYSTEM] mount initialized with null filesystem.");
+ }
+ }
+
+ auto mount::mount_dentry() const -> kstd::shared_ptr<dentry> const &
+ {
+ return m_mount_dentry;
+ }
+
+ auto mount::get_filesystem() const -> kstd::shared_ptr<filesystem> const &
+ {
+ return m_filesystem;
+ }
+
+ auto mount::root_dentry() const -> kstd::shared_ptr<dentry> const &
+ {
+ return m_root_dentry;
+ }
+
+ auto mount::mount_path() const -> kstd::string
+ {
+ if (m_mount_dentry)
+ {
+ return m_mount_dentry->absolute_path();
+ }
+ return "/";
+ }
+
+ auto mount::parent_mount() const -> kstd::shared_ptr<mount> const &
+ {
+ return m_parent_mount;
+ }
+
+ auto mount::source_mount() const -> kstd::shared_ptr<mount>
+ {
+ return m_source_mount.lock();
+ }
+
+ auto mount::increment_ref_count() -> void
+ {
+ m_ref_count += 1;
+ }
+
+ auto mount::decrement_ref_count() -> void
+ {
+ if (m_ref_count == 0)
+ {
+ kapi::system::panic("[FILESYSTEM] decrement_ref_count() was called but ref_count is 0");
+ }
+
+ m_ref_count -= 1;
+ }
+
+ auto mount::is_ready_to_unmount() const -> bool
+ {
+ return m_ref_count == 0;
+ }
+
+ auto mount::ref_count() const -> size_t
+ {
+ return m_ref_count;
+ }
+} // 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..c9ff82e
--- /dev/null
+++ b/kernel/src/filesystem/mount.tests.cpp
@@ -0,0 +1,94 @@
+#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>
+
+#include <stdexcept>
+
+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, 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.mount_dentry() == root_dentry);
+ REQUIRE(mount.mount_path() == "/");
+ REQUIRE(mount.is_ready_to_unmount());
+ }
+
+ THEN("the mount has no parent mount and no source mount")
+ {
+ REQUIRE(mount.parent_mount() == nullptr);
+ REQUIRE(mount.source_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, nullptr}),
+ kernel::tests::cpu::halt);
+ }
+ }
+ }
+}
+
+SCENARIO("Mount reference counting", "[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, "/");
+
+ THEN("reference count can be incremented and decremented, the mount is ready to unmount when the reference "
+ "count == 0")
+ {
+ auto mount = kernel::filesystem::mount{root_dentry, root_dentry, fs, nullptr, nullptr};
+
+ mount.increment_ref_count();
+ REQUIRE(mount.ref_count() == 1);
+ REQUIRE_FALSE(mount.is_ready_to_unmount());
+
+ mount.increment_ref_count();
+ REQUIRE(mount.ref_count() == 2);
+ REQUIRE_FALSE(mount.is_ready_to_unmount());
+
+ mount.decrement_ref_count();
+ REQUIRE(mount.ref_count() == 1);
+ REQUIRE_FALSE(mount.is_ready_to_unmount());
+
+ mount.decrement_ref_count();
+ REQUIRE(mount.ref_count() == 0);
+ REQUIRE(mount.is_ready_to_unmount());
+ }
+
+ THEN("decrementing reference count when it is already zero does not decrement it below zero")
+ {
+ auto mount = kernel::filesystem::mount{root_dentry, root_dentry, fs, nullptr, nullptr};
+
+ REQUIRE_THROWS_AS(mount.decrement_ref_count(), std::runtime_error);
+ REQUIRE(mount.ref_count() == 0);
+ REQUIRE(mount.is_ready_to_unmount());
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/src/filesystem/mount_table.cpp b/kernel/src/filesystem/mount_table.cpp
new file mode 100644
index 0000000..e4baac7
--- /dev/null
+++ b/kernel/src/filesystem/mount_table.cpp
@@ -0,0 +1,79 @@
+#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 <ranges>
+#include <string_view>
+
+namespace kernel::filesystem
+{
+ 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->parent_mount() == parent_mount; });
+ }
+
+ auto mount_table::add_mount(kstd::shared_ptr<mount> const & mount) -> void
+ {
+ m_mounts.push_back(mount);
+
+ if (auto source_mount = mount->source_mount())
+ {
+ source_mount->increment_ref_count();
+ }
+
+ if (auto mount_dentry = mount->mount_dentry())
+ {
+ mount_dentry->set_flag(dentry::dentry_flags::is_mount_point);
+ }
+ }
+
+ auto mount_table::remove_mount(std::string_view path) -> operation_result
+ {
+ auto mount_it = find_mount_iterator(path);
+ if (mount_it == m_mounts.end())
+ {
+ return operation_result::mount_not_found;
+ }
+
+ auto const & mount = *mount_it;
+ if (!mount->is_ready_to_unmount())
+ {
+ return operation_result::cannot_be_unmounted;
+ }
+ if (has_child_mounts(mount))
+ {
+ return operation_result::has_child_mounts;
+ }
+
+ if (auto source_mount = mount->source_mount())
+ {
+ source_mount->decrement_ref_count();
+ }
+
+ if (auto mount_dentry = mount->mount_dentry())
+ {
+ mount_dentry->unset_flag(dentry::dentry_flags::is_mount_point);
+ }
+
+ m_mounts.erase(mount_it);
+ return operation_result::removed;
+ }
+
+ auto mount_table::find_mount(std::string_view path) const -> kstd::shared_ptr<mount>
+ {
+ auto mount_it = find_mount_iterator(path);
+ return (mount_it != m_mounts.end()) ? *mount_it : nullptr;
+ }
+
+ auto mount_table::find_mount_iterator(std::string_view path) const
+ -> kstd::vector<kstd::shared_ptr<mount>>::const_iterator
+ {
+ return std::ranges::find_last_if(m_mounts, [&](auto const & mount) { return mount->mount_path() == path; }).begin();
+ }
+} // namespace kernel::filesystem \ No newline at end of file
diff --git a/kernel/src/filesystem/mount_table.tests.cpp b/kernel/src/filesystem/mount_table.tests.cpp
new file mode 100644
index 0000000..19b47b2
--- /dev/null
+++ b/kernel/src/filesystem/mount_table.tests.cpp
@@ -0,0 +1,184 @@
+#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("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_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount1 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry1, root_dentry1, fs1, nullptr, nullptr);
+
+ auto fs2 = kstd::make_shared<kernel::tests::filesystem::filesystem>();
+ auto root_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/mnt");
+ auto mount2 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry2, root_dentry2, fs2, nullptr, nullptr);
+
+ table.add_mount(mount1);
+ table.add_mount(mount2);
+
+ THEN("dentry flags are set correctly for mounted dentries")
+ {
+ REQUIRE(mount_dentry1->has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ REQUIRE(mount_dentry2->has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+
+ THEN("finding mounts by exact valid path returns the correct mount")
+ {
+ REQUIRE(table.find_mount("/") == mount1);
+ REQUIRE(table.find_mount("/mnt") == mount2);
+ }
+
+ THEN("finding mounts by exact invalid path returns null")
+ {
+ REQUIRE(table.find_mount("/nonexistent") == nullptr);
+ REQUIRE(table.find_mount("/mnt/file") == nullptr);
+ }
+
+ THEN("removing a mount that has no child mounts succeeds")
+ {
+ REQUIRE(table.remove_mount("/mnt") == kernel::filesystem::mount_table::operation_result::removed);
+ REQUIRE_FALSE(root_dentry2->has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+
+ 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_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount1 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry1, root_dentry1, fs1, nullptr, nullptr);
+
+ auto fs2 = kstd::make_shared<kernel::tests::filesystem::filesystem>();
+ auto root_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount2 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry2, root_dentry2, fs2, nullptr, nullptr);
+
+ table.add_mount(mount1);
+ table.add_mount(mount2);
+
+ THEN("finding mounts by exact valid path returns the correct mount")
+ {
+ REQUIRE(table.find_mount("/") == mount2);
+ }
+
+ THEN("removing the topmost mount with the same path succeeds")
+ {
+ REQUIRE(table.remove_mount("/") == kernel::filesystem::mount_table::operation_result::removed);
+ REQUIRE_FALSE(root_dentry2->has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+ }
+
+ GIVEN("a mount with child mounts")
+ {
+ kernel::filesystem::mount_table table;
+
+ auto fs1 = kstd::make_shared<kernel::tests::filesystem::filesystem>();
+ auto root_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry1 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount1 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry1, root_dentry1, fs1, nullptr, nullptr);
+
+ auto fs2 = kstd::make_shared<kernel::tests::filesystem::filesystem>();
+ auto root_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry2 = kstd::make_shared<kernel::filesystem::dentry>(
+ mount_dentry1, kstd::make_shared<kernel::tests::filesystem::inode>(), "mnt");
+ auto mount2 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry2, root_dentry2, fs2, mount1, nullptr);
+
+ auto fs3 = kstd::make_shared<kernel::tests::filesystem::filesystem>();
+ auto root_dentry3 = kstd::make_shared<kernel::filesystem::dentry>(
+ nullptr, kstd::make_shared<kernel::tests::filesystem::inode>(), "/");
+ auto mount_dentry3 = kstd::make_shared<kernel::filesystem::dentry>(
+ mount_dentry2, kstd::make_shared<kernel::tests::filesystem::inode>(), "submnt");
+ auto mount3 = kstd::make_shared<kernel::filesystem::mount>(mount_dentry3, root_dentry3, fs3, mount2, nullptr);
+
+ table.add_mount(mount1);
+ table.add_mount(mount2);
+ table.add_mount(mount3);
+
+ 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_FALSE(root_dentry3->has_flag(kernel::filesystem::dentry::dentry_flags::is_mount_point));
+ }
+ }
+}
+
+SCENARIO("Mount reference counting", "[filesystem][mount_table]")
+{
+ kernel::filesystem::mount_table table;
+
+ 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, "/");
+
+ auto source_mount = kstd::make_shared<kernel::filesystem::mount>(root_dentry, root_dentry, fs, nullptr, nullptr);
+ auto mount = kstd::make_shared<kernel::filesystem::mount>(root_dentry, root_dentry, fs, nullptr, source_mount);
+
+ THEN("reference count of source mount is incremented when a mount is added to the mount table and decremented when "
+ "the mount is removed")
+ {
+ REQUIRE(source_mount->ref_count() == 0);
+
+ table.add_mount(mount);
+ REQUIRE(source_mount->ref_count() == 1);
+
+ REQUIRE(table.remove_mount("/") == kernel::filesystem::mount_table::operation_result::removed);
+ REQUIRE(source_mount->ref_count() == 0);
+ }
+ }
+}
diff --git a/kernel/src/filesystem/open_file_descriptor.cpp b/kernel/src/filesystem/open_file_descriptor.cpp
new file mode 100644
index 0000000..a5567bf
--- /dev/null
+++ b/kernel/src/filesystem/open_file_descriptor.cpp
@@ -0,0 +1,46 @@
+#include <kernel/filesystem/open_file_descriptor.hpp>
+
+#include <kernel/filesystem/dentry.hpp>
+
+#include <kstd/memory>
+#include <kstd/os/error.hpp>
+
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ open_file_descriptor::open_file_descriptor(kstd::shared_ptr<dentry> const & dentry)
+ : m_dentry(dentry)
+ , m_offset(0)
+ {
+ if (!dentry)
+ {
+ kstd::os::panic("[FILESYSTEM] open_file_descriptor constructed with null dentry.");
+ }
+ }
+
+ auto open_file_descriptor::read(void * buffer, size_t size) -> size_t
+ {
+ auto read_bytes = m_dentry->get_inode()->read(buffer, m_offset, size);
+ m_offset += read_bytes;
+ return read_bytes;
+ }
+
+ auto open_file_descriptor::write(void const * buffer, size_t size) -> size_t
+ {
+ auto written_bytes = m_dentry->get_inode()->write(buffer, m_offset, size);
+ m_offset += written_bytes;
+ return written_bytes;
+ }
+
+ auto open_file_descriptor::offset() const -> size_t
+ {
+ return m_offset;
+ }
+
+ auto open_file_descriptor::get_dentry() const -> kstd::shared_ptr<dentry> const &
+ {
+ return m_dentry;
+ }
+
+} // namespace kernel::filesystem \ No newline at end of file
diff --git a/kernel/src/filesystem/open_file_descriptor.tests.cpp b/kernel/src/filesystem/open_file_descriptor.tests.cpp
new file mode 100644
index 0000000..8c24cf0
--- /dev/null
+++ b/kernel/src/filesystem/open_file_descriptor.tests.cpp
@@ -0,0 +1,113 @@
+#include <kernel/filesystem/open_file_descriptor.hpp>
+
+#include <kernel/filesystem/dentry.hpp>
+#include <kernel/filesystem/inode.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 descriptor construction", "[filesystem][open_file_descriptor]")
+{
+ GIVEN("a dentry and an open file descriptor for that dentry")
+ {
+ auto inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, inode, "test_dentry");
+ auto file_descriptor = kernel::filesystem::open_file_descriptor{dentry};
+
+ THEN("the initial offset is zero")
+ {
+ REQUIRE(file_descriptor.offset() == 0);
+ }
+ }
+}
+
+SCENARIO("Open file descriptor read/write offset management", "[filesystem][open_file_descriptor]")
+{
+ GIVEN("a dentry that tracks read/write calls and an open file descriptor for that dentry")
+ {
+ auto inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, inode, "test_dentry");
+ auto file_descriptor = kernel::filesystem::open_file_descriptor{dentry};
+
+ THEN("the offset is updated correctly after reads")
+ {
+ REQUIRE(file_descriptor.read(nullptr, 100) == 100);
+ REQUIRE(file_descriptor.offset() == 100);
+ REQUIRE(file_descriptor.read(nullptr, 50) == 50);
+ REQUIRE(file_descriptor.offset() == 150);
+ }
+
+ THEN("the offset is updated correctly after writes")
+ {
+ REQUIRE(file_descriptor.write(nullptr, 200) == 200);
+ REQUIRE(file_descriptor.offset() == 200);
+ REQUIRE(file_descriptor.write(nullptr, 25) == 25);
+ REQUIRE(file_descriptor.offset() == 225);
+ }
+
+ THEN("reads and writes both update the same offset")
+ {
+ REQUIRE(file_descriptor.read(nullptr, 10) == 10);
+ REQUIRE(file_descriptor.offset() == 10);
+ REQUIRE(file_descriptor.write(nullptr, 20) == 20);
+ REQUIRE(file_descriptor.offset() == 30);
+ REQUIRE(file_descriptor.read(nullptr, 5) == 5);
+ REQUIRE(file_descriptor.offset() == 35);
+ REQUIRE(file_descriptor.write(nullptr, 15) == 15);
+ REQUIRE(file_descriptor.offset() == 50);
+ }
+ }
+}
+
+SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_vfs_fixture, "Open file descriptor read with real image",
+ "[filesystem][open_file_descriptor][img]")
+{
+ auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img";
+
+ GIVEN("an open file descriptor 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 dentry = vfs.open("/information/info_1.txt");
+ REQUIRE(dentry != nullptr);
+ auto ofd = kstd::make_shared<kernel::filesystem::open_file_descriptor>(dentry);
+
+ 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 == 7);
+ REQUIRE(ofd->offset() == 7);
+
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)};
+ REQUIRE(buffer_as_str == "info_1\n");
+ }
+
+ 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/open_file_table.cpp b/kernel/src/filesystem/open_file_table.cpp
new file mode 100644
index 0000000..2afe3aa
--- /dev/null
+++ b/kernel/src/filesystem/open_file_table.cpp
@@ -0,0 +1,87 @@
+#include <kernel/filesystem/open_file_table.hpp>
+
+#include <kernel/filesystem/open_file_descriptor.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/unikstd.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <optional>
+
+namespace
+{
+ constinit auto static global_open_file_table = std::optional<kernel::filesystem::open_file_table>{};
+} // namespace
+
+namespace kernel::filesystem
+{
+ auto open_file_table::init() -> void
+ {
+ if (global_open_file_table)
+ {
+ kapi::system::panic("[FILESYSTEM] Open file table has already been initialized.");
+ }
+
+ global_open_file_table.emplace(open_file_table{});
+ }
+
+ auto open_file_table::get() -> open_file_table &
+ {
+ if (!global_open_file_table)
+ {
+ kapi::system::panic("[FILESYSTEM] Open file table has not been initialized.");
+ }
+
+ return *global_open_file_table;
+ }
+
+ auto open_file_table::add_file(kstd::shared_ptr<open_file_descriptor> const & file_descriptor) -> kstd::ssize_t
+ {
+ if (!file_descriptor)
+ {
+ return -1;
+ }
+
+ auto it = std::ranges::find_if(m_open_files, [](auto const & open_file) { return open_file == nullptr; });
+ if (it != m_open_files.end())
+ {
+ *it = file_descriptor;
+ return it - m_open_files.begin();
+ }
+
+ m_open_files.push_back(file_descriptor);
+ return m_open_files.size() - 1;
+ }
+
+ auto open_file_table::file(size_t fd) const -> kstd::shared_ptr<open_file_descriptor>
+ {
+ if (fd >= m_open_files.size())
+ {
+ return nullptr;
+ }
+
+ return m_open_files.at(fd);
+ }
+
+ auto open_file_table::remove_file(size_t fd) -> kstd::ssize_t
+ {
+ if (fd >= m_open_files.size())
+ {
+ return -1;
+ }
+
+ m_open_files.at(fd) = nullptr;
+ return 0;
+ }
+} // namespace kernel::filesystem
+
+namespace kernel::tests::filesystem::open_file_table
+{
+ auto deinit() -> void
+ {
+ global_open_file_table.reset();
+ }
+} // namespace kernel::tests::filesystem::open_file_table
diff --git a/kernel/src/filesystem/open_file_table.tests.cpp b/kernel/src/filesystem/open_file_table.tests.cpp
new file mode 100644
index 0000000..3e91111
--- /dev/null
+++ b/kernel/src/filesystem/open_file_table.tests.cpp
@@ -0,0 +1,106 @@
+#include <kernel/filesystem/open_file_table.hpp>
+
+#include <kernel/filesystem/dentry.hpp>
+#include <kernel/filesystem/open_file_descriptor.hpp>
+#include <kernel/test_support/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/print>
+#include <kstd/vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+SCENARIO("Open file table add/get file", "[filesystem][open_file_table]")
+{
+ GIVEN("a open file table and an open file descriptor")
+ {
+ auto & table = kernel::filesystem::open_file_table::get();
+ auto inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, inode, "test_dentry");
+
+ auto file_descriptor_1 = kstd::make_shared<kernel::filesystem::open_file_descriptor>(dentry);
+ auto file_descriptor_2 = kstd::make_shared<kernel::filesystem::open_file_descriptor>(dentry);
+
+ WHEN("adding the open file descriptor to the open file table")
+ {
+ auto fd_1 = table.add_file(file_descriptor_1);
+ auto fd_2 = table.add_file(file_descriptor_2);
+ auto fd_3 = table.add_file(file_descriptor_2);
+
+ THEN("a valid file descriptor is returned")
+ {
+ REQUIRE(fd_1 == 0);
+ REQUIRE(fd_2 == 1);
+ REQUIRE(fd_3 == 2);
+ }
+
+ THEN("the file descriptor can be retrieved using the returned file descriptor")
+ {
+ auto retrieved_descriptor = table.file(fd_1);
+ REQUIRE(retrieved_descriptor == file_descriptor_1);
+ }
+ }
+ }
+
+ GIVEN("a invalid open file descriptor")
+ {
+ auto & table = kernel::filesystem::open_file_table::get();
+
+ THEN("adding a null file descriptor returns an error code")
+ {
+ auto fd = table.add_file(nullptr);
+ REQUIRE(fd == -1);
+ }
+
+ THEN("retrieving a file descriptor with an out-of-bounds file descriptor returns a null pointer")
+ {
+ auto retrieved_descriptor = table.file(1000);
+ REQUIRE(retrieved_descriptor == nullptr);
+ }
+ }
+}
+
+SCENARIO("Open file table remove file", "[filesystem][open_file_table]")
+{
+ GIVEN("a open file table with an open file descriptor")
+ {
+ auto & table = kernel::filesystem::open_file_table::get();
+ auto inode = kstd::make_shared<kernel::tests::filesystem::inode>();
+ auto dentry = kstd::make_shared<kernel::filesystem::dentry>(nullptr, inode, "test_dentry");
+ auto file_descriptor = kstd::make_shared<kernel::filesystem::open_file_descriptor>(dentry);
+ auto fd = table.add_file(file_descriptor);
+
+ WHEN("removing the file descriptor using the file descriptor")
+ {
+ table.remove_file(fd);
+
+ THEN("the file descriptor can no longer be retrieved using the file descriptor")
+ {
+ auto retrieved_descriptor = table.file(fd);
+ REQUIRE(retrieved_descriptor == nullptr);
+ }
+ }
+
+ WHEN("removing a file descriptor the other file descriptor keep the same index")
+ {
+ auto fd2 = table.add_file(file_descriptor);
+ table.remove_file(fd);
+
+ THEN("the second file descriptor can still be retrieved using its file descriptor")
+ {
+ auto retrieved_descriptor = table.file(fd2);
+ REQUIRE(retrieved_descriptor == file_descriptor);
+ }
+ }
+ }
+
+ GIVEN("an invalid file descriptor")
+ {
+ auto & table = kernel::filesystem::open_file_table::get();
+
+ 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/path.tests.cpp b/kernel/src/filesystem/path.tests.cpp
new file mode 100644
index 0000000..3c18b5c
--- /dev/null
+++ b/kernel/src/filesystem/path.tests.cpp
@@ -0,0 +1,69 @@
+#include <kernel/filesystem/path.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <algorithm>
+#include <string>
+#include <string_view>
+#include <vector>
+
+SCENARIO("path utilities", "[filesystem][path]")
+{
+ GIVEN("valid and invalid paths")
+ {
+ THEN("valid absolute paths are recognized as valid")
+ {
+ REQUIRE(kernel::filesystem::path::is_valid_path("/valid/absolute/path"));
+ REQUIRE(kernel::filesystem::path::is_valid_path("/"));
+ }
+
+ THEN("valid relative paths are recognized as valid")
+ {
+ REQUIRE(kernel::filesystem::path::is_valid_path("valid/../relative/.././path"));
+ REQUIRE(kernel::filesystem::path::is_valid_path("valid/relative/path"));
+ REQUIRE(kernel::filesystem::path::is_valid_path("file.txt"));
+ }
+
+ THEN("invalid paths are recognized as invalid")
+ {
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_path(""));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_path(std::string(4096, 'a')));
+ }
+
+ THEN("valid absolute paths are recognized as absolute")
+ {
+ REQUIRE(kernel::filesystem::path::is_valid_absolute_path("/valid/absolute/path"));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_absolute_path("valid/relative/path"));
+ }
+
+ THEN("invalid paths are not recognized as absolute")
+ {
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_absolute_path(""));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_absolute_path(std::string(4096, 'a')));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_absolute_path("invalid/absolute/path"));
+ }
+
+ THEN("valid relative paths are recognized as relative")
+ {
+ REQUIRE(kernel::filesystem::path::is_valid_relative_path("valid/relative/path"));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_relative_path("/valid/absolute/path"));
+ }
+
+ THEN("invalid paths are not recognized as relative")
+ {
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_relative_path(""));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_relative_path(std::string(4096, 'a')));
+ REQUIRE_FALSE(kernel::filesystem::path::is_valid_relative_path("/invalid/absolute/path"));
+ }
+ }
+
+ GIVEN("a valid path")
+ {
+ THEN("it can be split into components")
+ {
+ auto components = kernel::filesystem::path::split("/a/b///c/d.txt");
+ std::vector<std::string_view> expected = {"a", "b", "c", "d.txt"};
+ REQUIRE(std::ranges::equal(components, expected));
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/src/filesystem/rootfs/filesystem.cpp b/kernel/src/filesystem/rootfs/filesystem.cpp
new file mode 100644
index 0000000..7fe5c1e
--- /dev/null
+++ b/kernel/src/filesystem/rootfs/filesystem.cpp
@@ -0,0 +1,47 @@
+#include <kernel/filesystem/rootfs/filesystem.hpp>
+
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+#include <kernel/filesystem/rootfs/inode.hpp>
+#include <kernel/filesystem/type.hpp>
+
+#include <kstd/memory>
+
+#include <string_view>
+
+namespace kernel::filesystem::rootfs
+{
+
+ struct type final : kernel::filesystem::type
+ {
+ [[nodiscard]] auto name() const noexcept -> std::string_view override
+ {
+ return "rootfs";
+ }
+
+ [[nodiscard]] auto requires_device() const noexcept -> bool override
+ {
+ return true;
+ }
+
+ [[nodiscard]] auto make_instance() const -> kstd::shared_ptr<kernel::filesystem::filesystem> override
+ {
+ return kstd::make_shared<filesystem>();
+ }
+ };
+
+ [[gnu::used]]
+ constexpr auto registration = type_registration<type>{};
+
+ auto filesystem::mount(kstd::shared_ptr<kernel::filesystem::inode> const &) -> operation_result
+ {
+ m_root_inode = kstd::make_shared<inode>();
+ return operation_result::success;
+ }
+
+ auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const &, std::string_view) const
+ -> kstd::shared_ptr<kernel::filesystem::inode>
+ {
+ return nullptr;
+ }
+} // namespace kernel::filesystem::rootfs
diff --git a/kernel/src/filesystem/rootfs/filesystem.tests.cpp b/kernel/src/filesystem/rootfs/filesystem.tests.cpp
new file mode 100644
index 0000000..ae320e9
--- /dev/null
+++ b/kernel/src/filesystem/rootfs/filesystem.tests.cpp
@@ -0,0 +1,38 @@
+#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 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.cpp b/kernel/src/filesystem/rootfs/inode.cpp
new file mode 100644
index 0000000..f64fb87
--- /dev/null
+++ b/kernel/src/filesystem/rootfs/inode.cpp
@@ -0,0 +1,23 @@
+#include <kernel/filesystem/inode.hpp>
+
+#include <kernel/filesystem/rootfs/inode.hpp>
+
+#include <cstddef>
+
+namespace kernel::filesystem::rootfs
+{
+ auto inode::read(void *, size_t, size_t) const -> size_t
+ {
+ return 0;
+ }
+
+ auto inode::write(void const *, size_t, size_t) -> size_t
+ {
+ return 0;
+ }
+
+ auto inode::is_directory() const -> bool
+ {
+ return true;
+ }
+} // namespace kernel::filesystem::rootfs
diff --git a/kernel/src/filesystem/rootfs/inode.tests.cpp b/kernel/src/filesystem/rootfs/inode.tests.cpp
new file mode 100644
index 0000000..f4b634f
--- /dev/null
+++ b/kernel/src/filesystem/rootfs/inode.tests.cpp
@@ -0,0 +1,37 @@
+#include <kernel/filesystem/rootfs/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/print>
+#include <kstd/vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+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/type_registry.cpp b/kernel/src/filesystem/type_registry.cpp
new file mode 100644
index 0000000..d917c81
--- /dev/null
+++ b/kernel/src/filesystem/type_registry.cpp
@@ -0,0 +1,72 @@
+#include <kernel/filesystem/type_registry.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/print>
+
+#include <algorithm>
+#include <cstddef>
+#include <optional>
+#include <ranges>
+#include <span>
+
+namespace kernel::filesystem
+{
+
+ extern "C"
+ {
+ extern type_registry::pointer const __start_fs_types;
+ extern type_registry::pointer const __stop_fs_types;
+ }
+
+ namespace
+ {
+ auto constinit instance = std::optional<type_registry>{};
+ }
+
+ auto type_registry::init() -> void
+ {
+ if (instance)
+ {
+ kapi::system::panic("[FILESYSTEM] tried to initialize type registry more than once!");
+ }
+
+ instance.emplace();
+
+ auto type_descriptors = std::span{&__start_fs_types, &__stop_fs_types} | //
+ std::views::filter([](auto p) { return p != nullptr; });
+
+ std::ranges::for_each(type_descriptors, [](auto descriptor) {
+ kstd::println("[FILESYSTEM] registering '{}'", descriptor->name());
+ instance->add(descriptor);
+ });
+ }
+
+ auto type_registry::get() -> type_registry &
+ {
+ if (!instance)
+ {
+ kapi::system::panic("[FILESYSTEM] type registry has not been initialized!");
+ }
+
+ return *instance;
+ }
+
+ auto type_registry::add(pointer descriptor) -> bool
+ {
+ auto result = m_descriptors.emplace(descriptor->name(), descriptor);
+ return result.second;
+ }
+
+ auto type_registry::all() const noexcept -> std::span<pointer const>
+ {
+ return {m_descriptors.values().begin(), m_descriptors.values().end()};
+ }
+
+ auto type_registry::size() const noexcept -> std::size_t
+ {
+ return m_descriptors.size();
+ }
+
+} // namespace kernel::filesystem
diff --git a/kernel/src/filesystem/type_registry.tests.cpp b/kernel/src/filesystem/type_registry.tests.cpp
new file mode 100644
index 0000000..8382579
--- /dev/null
+++ b/kernel/src/filesystem/type_registry.tests.cpp
@@ -0,0 +1,77 @@
+#include <kernel/filesystem/type_registry.hpp>
+
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/type.hpp>
+
+#include <kstd/memory>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <iterator>
+#include <string_view>
+
+struct test_type final : kernel::filesystem::type
+{
+ [[nodiscard]] auto name() const noexcept -> std::string_view override
+ {
+ return "bht_testfs";
+ }
+
+ [[nodiscard]] auto requires_device() const noexcept -> bool override
+ {
+ return false;
+ }
+
+ [[nodiscard]] auto make_instance() const -> kstd::shared_ptr<kernel::filesystem::filesystem> override
+ {
+ return nullptr;
+ }
+};
+
+SCENARIO("Filesystem type registry initialization and construction", "[filesystem]")
+{
+ GIVEN("A default constructed type_registry")
+ {
+ auto instance = kernel::filesystem::type_registry{};
+
+ WHEN("getting the span of filesystem descriptors")
+ {
+ auto descriptors = instance.all();
+
+ THEN("the span is empty")
+ {
+ REQUIRE(descriptors.empty());
+ }
+ }
+ }
+}
+
+SCENARIO("Filesystem type registry modifiers", "[filesystem]")
+{
+ GIVEN("A default constructed type_registry")
+ {
+ auto instance = kernel::filesystem::type_registry{};
+
+ WHEN("adding a type descriptor")
+ {
+ auto descriptor = test_type{};
+
+ instance.add(kstd::make_observer(&descriptor));
+
+ THEN("the size of the registry is one")
+ {
+ REQUIRE(instance.size() == 1);
+ }
+
+ THEN("the span is not empty")
+ {
+ REQUIRE_FALSE(instance.all().empty());
+ }
+
+ THEN("the span's size is equal to the registry size")
+ {
+ REQUIRE(std::size(instance.all()) == std::size(instance));
+ }
+ }
+ }
+}
diff --git a/kernel/src/filesystem/vfs.cpp b/kernel/src/filesystem/vfs.cpp
new file mode 100644
index 0000000..e5dff8c
--- /dev/null
+++ b/kernel/src/filesystem/vfs.cpp
@@ -0,0 +1,299 @@
+#include <kernel/filesystem/vfs.hpp>
+
+#include <kernel/filesystem/constants.hpp>
+#include <kernel/filesystem/dentry.hpp>
+#include <kernel/filesystem/devfs/filesystem.hpp>
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/mount.hpp>
+#include <kernel/filesystem/mount_table.hpp>
+#include <kernel/filesystem/path.hpp>
+#include <kernel/filesystem/rootfs/filesystem.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstdint>
+#include <optional>
+#include <ranges>
+#include <string_view>
+#include <utility>
+
+namespace
+{
+ constinit auto static active_vfs = std::optional<kernel::filesystem::vfs>{};
+} // namespace
+
+namespace kernel::filesystem
+{
+ auto vfs::init() -> void
+ {
+ if (active_vfs)
+ {
+ kapi::system::panic("[FILESYSTEM] vfs has already been initialized.");
+ }
+
+ active_vfs.emplace();
+ }
+
+ vfs::vfs()
+ {
+ // mount rootfs at /
+ auto root_fs = kstd::make_shared<rootfs::filesystem>();
+ root_fs->mount(nullptr);
+
+ auto root_fs_root_dentry = kstd::make_shared<dentry>(nullptr, root_fs->root_inode(), "/");
+ auto root_mount = kstd::make_shared<mount>(nullptr, root_fs_root_dentry, root_fs, nullptr, nullptr);
+ m_mount_table.add_mount(root_mount);
+
+ // mount devfs at /dev (inside rootfs, temporary, will be shadowed)
+ auto device_fs = kstd::make_shared<devfs::filesystem>();
+ device_fs->mount(nullptr);
+ graft_persistent_device_fs(device_fs);
+
+ // mount boot fs at / (shadows rootfs), re-graft devfs
+ auto [boot_device_dentry, boot_device_mount_context] = resolve_path_internal("/dev/ram0");
+ if (boot_device_dentry && boot_device_mount_context)
+ {
+ if (auto boot_root_fs = kernel::filesystem::filesystem::probe_and_mount(boot_device_dentry->get_inode()))
+ {
+ if (auto root_dentry = resolve_path("/"))
+ {
+ do_mount_internal(root_dentry, root_mount, boot_root_fs, boot_device_mount_context);
+ graft_persistent_device_fs(device_fs);
+ }
+ }
+ }
+ }
+
+ auto vfs::get() -> vfs &
+ {
+ if (!active_vfs)
+ {
+ kapi::system::panic("[FILESYSTEM] vfs has not been initialized.");
+ }
+
+ return *active_vfs;
+ }
+
+ auto vfs::open(std::string_view path) -> kstd::shared_ptr<dentry>
+ {
+ auto [dentry, mount] = resolve_path_internal(path);
+ if (!dentry || !mount)
+ {
+ return nullptr;
+ }
+ mount->increment_ref_count();
+ return dentry;
+ }
+
+ auto vfs::close(std::string_view path) -> operation_result
+ {
+ if (auto mount = find_mount(path))
+ {
+ mount->decrement_ref_count();
+ return operation_result::success;
+ }
+ return operation_result::invalid_path;
+ }
+
+ auto vfs::do_mount(std::string_view source, std::string_view target) -> operation_result
+ {
+ if (!path::is_valid_path(source) || !path::is_valid_path(target))
+ {
+ return operation_result::invalid_path;
+ }
+
+ auto [mount_point_dentry, mount_context] = resolve_path_internal(target);
+ if (mount_point_dentry && mount_context)
+ {
+ auto [source_dentry, source_mount_context] = resolve_path_internal(source);
+ if (source_dentry && source_mount_context)
+ {
+ if (auto fs = kernel::filesystem::filesystem::probe_and_mount(source_dentry->get_inode()))
+ {
+ do_mount_internal(mount_point_dentry, mount_context, fs, source_mount_context);
+ return operation_result::success;
+ }
+ return operation_result::invalid_filesystem;
+ }
+ return operation_result::non_existent_path;
+ }
+ return operation_result::mount_point_not_found;
+ }
+
+ auto vfs::unmount(std::string_view path) -> operation_result
+ {
+ if (!path::is_valid_path(path))
+ {
+ 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;
+ }
+ else if (remove_result == mount_table::operation_result::mount_not_found)
+ {
+ return operation_result::mount_point_not_found;
+ }
+
+ return operation_result::unmount_failed;
+ }
+
+ auto vfs::do_mount_internal(kstd::shared_ptr<dentry> const & mount_point_dentry,
+ kstd::shared_ptr<mount> const & parent_mount, kstd::shared_ptr<filesystem> const & fs,
+ kstd::shared_ptr<mount> const & source_mount) -> void
+ {
+ auto new_fs_root =
+ kstd::make_shared<dentry>(mount_point_dentry->parent(), fs->root_inode(), mount_point_dentry->name());
+ auto new_mount = kstd::make_shared<mount>(mount_point_dentry, new_fs_root, fs, parent_mount, source_mount);
+ m_mount_table.add_mount(new_mount);
+ }
+
+ auto vfs::graft_persistent_device_fs(kstd::shared_ptr<devfs::filesystem> const & device_fs) -> void
+ {
+ auto [root_mount_point_dentry, root_mount] = resolve_path_internal("/");
+ if (root_mount_point_dentry && root_mount)
+ {
+ auto dev_dentry = root_mount_point_dentry->find_child("dev");
+ if (!dev_dentry)
+ {
+ dev_dentry = kstd::make_shared<dentry>(root_mount_point_dentry, device_fs->root_inode(), "dev");
+ root_mount_point_dentry->add_child(dev_dentry);
+ }
+
+ do_mount_internal(dev_dentry, root_mount, device_fs);
+ }
+ }
+
+ auto vfs::resolve_path_internal(std::string_view path) const
+ -> std::pair<kstd::shared_ptr<dentry>, kstd::shared_ptr<mount>>
+ {
+ if (!path::is_valid_absolute_path(path))
+ {
+ return {nullptr, nullptr};
+ }
+
+ auto current_mount = m_mount_table.find_mount("/");
+ if (!current_mount)
+ {
+ kapi::system::panic("[FILESYSTEM] no root mount found.");
+ }
+
+ auto current_dentry = current_mount->root_dentry();
+
+ auto path_parts = path::split(path);
+ kstd::vector path_parts_vector(path_parts.begin(), path_parts.end());
+ std::ranges::reverse(path_parts_vector);
+
+ auto symlink_counter = 0uz;
+
+ while (!path_parts_vector.empty())
+ {
+ auto part = path_parts_vector.back();
+ path_parts_vector.pop_back();
+
+ if (part == ".")
+ {
+ continue;
+ }
+
+ if (part == "..")
+ {
+ auto parent_dentry = current_dentry->parent();
+
+ if (current_dentry == current_mount->root_dentry())
+ {
+ if (current_mount->mount_path() == "/")
+ {
+ continue;
+ }
+
+ if (auto parent_mount = current_mount->parent_mount())
+ {
+ current_mount = parent_mount;
+ current_dentry = parent_dentry;
+ }
+ }
+
+ current_dentry = parent_dentry;
+ continue;
+ }
+
+ auto next_dentry = current_dentry->find_child(part);
+ if (!next_dentry)
+ {
+ auto current_fs = current_mount->get_filesystem();
+ auto found_inode = current_fs->lookup(current_dentry->get_inode(), part);
+ if (!found_inode)
+ {
+ return {nullptr, nullptr};
+ }
+
+ next_dentry = kstd::make_shared<dentry>(current_dentry, found_inode, part);
+ current_dentry->add_child(next_dentry);
+ }
+ else if (next_dentry->has_flag(dentry::dentry_flags::is_mount_point))
+ {
+ current_mount = m_mount_table.find_mount(next_dentry->absolute_path());
+ if (!current_mount)
+ {
+ kapi::system::panic("[FILESYSTEM] mount for dentry with mounted flag not found.");
+ }
+
+ next_dentry = current_mount->root_dentry();
+ }
+
+ if (next_dentry->get_inode()->is_symbolic_link())
+ {
+ if (symlink_counter++ > constants::symloop_max)
+ {
+ return {nullptr, nullptr};
+ }
+
+ kstd::vector<uint8_t> buffer(constants::symlink_max_path_length);
+ auto const bytes_read = next_dentry->get_inode()->read(buffer.data(), 0, buffer.size());
+ auto const symbolic_link_path = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read};
+
+ auto symbolic_link_parts = path::split(symbolic_link_path);
+ kstd::vector symbolic_link_parts_vector(symbolic_link_parts.begin(), symbolic_link_parts.end());
+ std::ranges::reverse(symbolic_link_parts_vector);
+
+ path_parts_vector.insert_range(path_parts_vector.end(), symbolic_link_parts_vector);
+
+ if (path::is_valid_absolute_path(symbolic_link_path))
+ {
+ current_mount = m_mount_table.find_mount("/");
+ current_dentry = current_mount->root_dentry();
+ }
+ continue;
+ }
+
+ current_dentry = next_dentry;
+ }
+ return {current_dentry, current_mount};
+ }
+
+ auto vfs::resolve_path(std::string_view path) const -> kstd::shared_ptr<dentry>
+ {
+ return resolve_path_internal(path).first;
+ }
+
+ auto vfs::find_mount(std::string_view path) const -> kstd::shared_ptr<mount>
+ {
+ return resolve_path_internal(path).second;
+ }
+
+} // namespace kernel::filesystem
+
+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..f1d0df0
--- /dev/null
+++ b/kernel/src/filesystem/vfs.tests.cpp
@@ -0,0 +1,568 @@
+#include <kernel/filesystem/vfs.hpp>
+
+#include <kernel/filesystem/open_file_descriptor.hpp>
+#include <kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <cstddef>
+#include <filesystem>
+#include <stdexcept>
+#include <string_view>
+
+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("a real image file containing a /dev directory")
+ {
+ REQUIRE(std::filesystem::exists(image_path_2));
+ REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_2"}, {image_path_2}));
+
+ THEN("vfs hides the image's /dev behind the devfs mount")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+
+ auto image_1 = vfs.open("/dev/image_1.txt");
+ REQUIRE(image_1 == nullptr);
+
+ auto dev = vfs.open("/dev/ram0");
+ REQUIRE(dev != 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();
+
+ 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("/dev/ram16", "/information") == 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.close(mounted_monkey_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ 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("/dev/ram16", "/information") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.do_mount("/dev/ram32", "/information/monkey_house/infrastructure") ==
+ 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.close(mounted_monkey_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.close(mounted_fish1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ 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("image can be mounted, unmount only if no files are open")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/information") == 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::unmount_failed);
+
+ REQUIRE(vfs.close(mounted_monkey_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::success);
+ }
+
+ THEN("file with invalid path or not opened file cannot be closed")
+ {
+ REQUIRE(vfs.close("invalid_path") == kernel::filesystem::vfs::operation_result::invalid_path);
+ REQUIRE_THROWS_AS(vfs.close("/information/info_1.txt"), std::runtime_error);
+ }
+
+ THEN("file cannot be closed twice")
+ {
+ auto info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+
+ REQUIRE(vfs.close(info_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE_THROWS_AS(vfs.close(info_1->absolute_path()), std::runtime_error);
+ }
+
+ THEN("images can be stacked mounted and correct file system is unmounted again")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/information") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.do_mount("/dev/ram32", "/information") == kernel::filesystem::vfs::operation_result::success);
+
+ auto mounted_tickets = vfs.open("/information/entrance/tickets.txt");
+ REQUIRE(mounted_tickets != nullptr);
+
+ REQUIRE(vfs.close(mounted_tickets->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ 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("image can be mounted on / file opened and unmounted again")
+ {
+ auto info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+
+ REQUIRE(vfs.do_mount("/dev/ram16", "/") == kernel::filesystem::vfs::operation_result::success);
+
+ info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 == nullptr);
+
+ auto water = vfs.open("/monkey_house/infrastructure/water.txt");
+ REQUIRE(water != nullptr);
+
+ REQUIRE(vfs.close(water->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/") == kernel::filesystem::vfs::operation_result::success);
+
+ info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("image can be mounted on / just the boot root has /dev")
+ {
+ auto info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+
+ REQUIRE(vfs.do_mount("/dev/ram16", "/") == kernel::filesystem::vfs::operation_result::success);
+
+ info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 == nullptr);
+
+ auto water = vfs.open("/monkey_house/infrastructure/water.txt");
+ REQUIRE(water != nullptr);
+
+ REQUIRE(vfs.close(water->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ auto dev_ram_16 = vfs.open("/dev/ram16");
+ REQUIRE(dev_ram_16 == nullptr);
+
+ REQUIRE(vfs.do_mount("/dev/ram32", "/") == kernel::filesystem::vfs::operation_result::non_existent_path);
+
+ REQUIRE(vfs.unmount("/") == kernel::filesystem::vfs::operation_result::success);
+
+ auto dev_ram_32 = vfs.open("/dev/ram32");
+ REQUIRE(dev_ram_32 != nullptr);
+ }
+
+ THEN("boot root can be unmounted and remounted again but /dev is not re-grafted")
+ {
+ auto info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+
+ REQUIRE(vfs.close(info_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/dev") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.unmount("/") == kernel::filesystem::vfs::operation_result::success);
+
+ info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 == nullptr);
+
+ REQUIRE(vfs.do_mount("/dev/ram0", "/") == kernel::filesystem::vfs::operation_result::success);
+
+ info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+
+ auto dev_ram_0 = vfs.open("/dev/ram0");
+ REQUIRE(dev_ram_0 == nullptr);
+ }
+
+ THEN("mount with null file system fails")
+ {
+ REQUIRE(vfs.do_mount("/closed.txt", "/information") ==
+ kernel::filesystem::vfs::operation_result::invalid_filesystem);
+ }
+
+ THEN("mount with invalid path fails")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "") == kernel::filesystem::vfs::operation_result::invalid_path);
+ REQUIRE(vfs.do_mount("/dev/ram16", "information") ==
+ kernel::filesystem::vfs::operation_result::mount_point_not_found);
+ }
+
+ THEN("mount with non-existent source path fails")
+ {
+ REQUIRE(vfs.do_mount("/dev/nonexistent", "/information") ==
+ kernel::filesystem::vfs::operation_result::non_existent_path);
+ }
+
+ THEN("mount with non-existent mount point fails")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/information/nonexistent") ==
+ 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::mount_point_not_found);
+ }
+
+ THEN("unmounting non-existent mount point returns expected error code")
+ {
+ REQUIRE(vfs.unmount("/information/nonexistent") ==
+ kernel::filesystem::vfs::operation_result::mount_point_not_found);
+ }
+
+ THEN("a file can be access if . in the path")
+ {
+ auto info_1 = vfs.open("/information/./info_1.txt");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("a file can be accessed within the same mount if path contains .. ")
+ {
+ auto info_1 = vfs.open("/archiv/../information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+
+ auto img = vfs.open("/archiv/../information/../archiv/2024.img");
+ REQUIRE(img != nullptr);
+ }
+
+ THEN("a file can be accessed over multiple mounts if path contains .. or . ")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/information") == kernel::filesystem::vfs::operation_result::success);
+
+ auto img = vfs.open("/information/monkey_house/caretaker/../../../../../../archiv/2024.img");
+ REQUIRE(img != nullptr);
+
+ auto dev_32 = vfs.open("/information/monkey_house/caretaker/../../../dev/ram32");
+ REQUIRE(dev_32 != nullptr);
+
+ auto water = vfs.open("/information/./monkey_house/infrastructure/water.txt");
+ REQUIRE(water != nullptr);
+ }
+
+ THEN("a file can be accessed over multiple mounts (device and file) if path contains .. ")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/information") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.do_mount("/archiv/2024.img", "/information/monkey_house/infrastructure") ==
+ kernel::filesystem::vfs::operation_result::success);
+
+ auto pig_1 = vfs.open("/information/monkey_house/infrastructure/stable/pig_1.txt");
+ REQUIRE(pig_1 != nullptr);
+
+ auto isabelle =
+ vfs.open("/information/monkey_house/infrastructure/stable/../../../monkey_house/caretaker/isabelle.txt");
+ REQUIRE(isabelle != nullptr);
+
+ auto closed = vfs.open("/information/monkey_house/infrastructure/stable/../../../../closed.txt");
+ REQUIRE(closed != nullptr);
+ }
+ }
+
+ GIVEN("A real image file containing as filesystem formatted files")
+ {
+ REQUIRE(std::filesystem::exists(image_path_1));
+ REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_1"}, {image_path_1}));
+
+ THEN("the file-filesystem in the image can be mounted, files can be read and unmounted again")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ REQUIRE(vfs.do_mount("/archiv/2024.img", "/information") == kernel::filesystem::vfs::operation_result::success);
+
+ auto info_1 = vfs.open("/information/info_1.txt");
+ REQUIRE(info_1 == nullptr);
+
+ auto dentry = vfs.open("/information/sheep_1.txt");
+ REQUIRE(dentry != nullptr);
+ auto sheep_1_ofd = kstd::make_shared<kernel::filesystem::open_file_descriptor>(dentry);
+
+ kstd::vector<std::byte> buffer(7);
+ auto bytes_read = sheep_1_ofd->read(buffer.data(), buffer.size());
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), bytes_read};
+ REQUIRE(buffer_as_str == "sheep_1");
+
+ REQUIRE(vfs.close(dentry->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::success);
+ auto unmounted_sheep_1 = vfs.open("/information/sheep_1.txt");
+ REQUIRE(unmounted_sheep_1 == nullptr);
+ }
+
+ THEN("the file-filesystem in the image can be mounted and in this filesystem can another file-filesystem be "
+ "mounted, files can be read and unmounted again")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ REQUIRE(vfs.do_mount("/archiv/2024.img", "/information") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.do_mount("/archiv/2025.img", "/information/stable") ==
+ kernel::filesystem::vfs::operation_result::success);
+
+ auto sheep_1 = vfs.open("/information/sheep_1.txt");
+ auto goat_1 = vfs.open("/information/stable/petting_zoo/goat_1.txt");
+ REQUIRE(sheep_1 != nullptr);
+ REQUIRE(goat_1 != nullptr);
+
+ auto sheep_1_ofd = kstd::make_shared<kernel::filesystem::open_file_descriptor>(sheep_1);
+ auto goat_1_ofd = kstd::make_shared<kernel::filesystem::open_file_descriptor>(goat_1);
+
+ kstd::vector<std::byte> sheep_buffer(7);
+ auto bytes_read = sheep_1_ofd->read(sheep_buffer.data(), sheep_buffer.size());
+ std::string_view buffer_as_str{reinterpret_cast<char *>(sheep_buffer.data()), bytes_read};
+ REQUIRE(buffer_as_str == "sheep_1");
+
+ kstd::vector<std::byte> goat_buffer(6);
+ bytes_read = goat_1_ofd->read(goat_buffer.data(), goat_buffer.size());
+ buffer_as_str = std::string_view{reinterpret_cast<char *>(goat_buffer.data()), bytes_read};
+ REQUIRE(buffer_as_str == "goat_1");
+
+ REQUIRE(vfs.close(sheep_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.close(goat_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::unmount_failed);
+
+ REQUIRE(vfs.unmount("/information/stable") == kernel::filesystem::vfs::operation_result::success);
+ auto unmounted_goat_1 = vfs.open("/information/stable/petting_zoo/goat_1.txt");
+ REQUIRE(unmounted_goat_1 == nullptr);
+
+ auto still_mounted_sheep_1 = vfs.open("/information/sheep_1.txt");
+ REQUIRE(still_mounted_sheep_1 != nullptr);
+
+ REQUIRE(vfs.close(still_mounted_sheep_1->absolute_path()) == kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/information") == kernel::filesystem::vfs::operation_result::success);
+ auto unmounted_sheep_1 = vfs.open("/information/sheep_1.txt");
+ REQUIRE(unmounted_sheep_1 == nullptr);
+ }
+ }
+
+ GIVEN("two real image files where the second contains as filesystem formatted files")
+ {
+ REQUIRE(std::filesystem::exists(image_path_1));
+ REQUIRE(std::filesystem::exists(image_path_3));
+ REQUIRE_NOTHROW(
+ setup_modules_from_img_and_init_vfs({"test_img_module_3", "test_img_module_1"}, {image_path_3, image_path_1}));
+
+ auto & vfs = kernel::filesystem::vfs::get();
+
+ THEN("cannot unmount a filesystem if files are mounted")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/entrance") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.do_mount("/entrance/archiv/2024.img", "/enclosures") ==
+ kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/entrance") == kernel::filesystem::vfs::operation_result::unmount_failed);
+ REQUIRE(vfs.unmount("/enclosures") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.unmount("/entrance") == kernel::filesystem::vfs::operation_result::success);
+ }
+
+ THEN("can mount filesystem onto the directory that contains it")
+ {
+ REQUIRE(vfs.do_mount("/dev/ram16", "/entrance") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.do_mount("/entrance/archiv/2024.img", "/entrance") ==
+ kernel::filesystem::vfs::operation_result::success);
+
+ REQUIRE(vfs.unmount("/entrance") == kernel::filesystem::vfs::operation_result::success);
+ REQUIRE(vfs.unmount("/entrance") == kernel::filesystem::vfs::operation_result::success);
+ }
+ }
+
+ GIVEN("A real image files, containing symbolic links")
+ {
+ REQUIRE(std::filesystem::exists(image_path_1));
+ REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_1"}, {image_path_1}));
+
+ THEN("file can be opened through absolute symbolic link")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto info_1 = vfs.open("/symlinks/info_1_absolute");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("file can be opened through relative symbolic link")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto info_1 = vfs.open("/symlinks/info_1_relative");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("file can be opened through symbolic link pointing absolute to the directory containing the file")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto info_1 = vfs.open("/symlinks/information_directory_absolute/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("file can be opened through symbolic link pointing relative to the directory containing the file")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto info_1 = vfs.open("/symlinks/information_directory_relative/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("symbolic link with path traversing back to the root")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto info_1 = vfs.open("/symlinks/traverse_back_5_times/information/info_1.txt");
+ REQUIRE(info_1 != nullptr);
+ }
+
+ THEN("symbolic link containing an invalid absolute path is handled correctly")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto invalid_symlink = vfs.open("/symlinks/invalid_absolute");
+ REQUIRE(invalid_symlink == nullptr);
+ }
+
+ THEN("symbolic link containing an invalid relative path is handled correctly")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto invalid_symlink = vfs.open("/symlinks/invalid_relative");
+ REQUIRE(invalid_symlink == nullptr);
+ }
+
+ THEN("circular symbolic links are detected and handled correctly")
+ {
+ auto & vfs = kernel::filesystem::vfs::get();
+ auto circular_symlink = vfs.open("/symlinks/symloop_a");
+ REQUIRE(circular_symlink == nullptr);
+ }
+ }
+
+ GIVEN("A real image file containing as filesystem formatted files and this filesystem contains a symbolic link")
+ {
+ REQUIRE(std::filesystem::exists(image_path_1));
+ REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_1"}, {image_path_1}));
+
+ auto & vfs = kernel::filesystem::vfs::get();
+ vfs.do_mount("/archiv/2024.img", "/information");
+
+ THEN("file can be opened through symbolic link pointing to the parent filesystem")
+ {
+ auto closed_file = vfs.open("/information/symlinks/traverse_back_twice/closed.txt");
+ REQUIRE(closed_file != nullptr);
+ }
+ }
+
+ GIVEN("Two real images, one containing a symbolic link leaving and reentering filesystem again")
+ {
+ REQUIRE(std::filesystem::exists(image_path_1));
+ REQUIRE(std::filesystem::exists(image_path_2));
+ REQUIRE_NOTHROW(
+ setup_modules_from_img_and_init_vfs({"test_img_module_1", "test_img_module_2"}, {image_path_1, image_path_2}));
+
+ auto & vfs = kernel::filesystem::vfs::get();
+ vfs.do_mount("/dev/ram16", "/information");
+
+ THEN("file can be opened through symbolic link pointing to the parent filesystem and back into the mounted "
+ "filesystem again")
+ {
+ auto monkey_1 = vfs.open("/information/symlinks/leave_and_reenter_mount/monkey_1.txt");
+ REQUIRE(monkey_1 != nullptr);
+ }
+ }
+
+ GIVEN("One real image containing a very long symbolic link")
+ {
+ REQUIRE(std::filesystem::exists(image_path_3));
+ REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_3"}, {image_path_3}));
+
+ auto & vfs = kernel::filesystem::vfs::get();
+
+ THEN("file can be opened through symbolic link with a long path")
+ {
+ auto fish_30 = vfs.open("/symlinks/very_long_symlink");
+ REQUIRE(fish_30 != nullptr);
+ }
+ }
+
+ GIVEN("One real image containing a valid symbolic link chain")
+ {
+ REQUIRE(std::filesystem::exists(image_path_3));
+ REQUIRE_NOTHROW(setup_modules_from_img_and_init_vfs({"test_img_module_3"}, {image_path_3}));
+
+ auto & vfs = kernel::filesystem::vfs::get();
+
+ THEN("file can be opened through symbolic link chain")
+ {
+ auto map = vfs.open("/symlinks/symlink_chain_1/map.txt");
+ REQUIRE(map != nullptr);
+ }
+ }
+}