aboutsummaryrefslogtreecommitdiff
path: root/kernel
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
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')
-rw-r--r--kernel/CMakeLists.txt231
-rw-r--r--kernel/include/kernel/acpi/manager.hpp32
-rw-r--r--kernel/include/kernel/devices/block_device.hpp97
-rw-r--r--kernel/include/kernel/devices/block_device_utils.hpp44
-rw-r--r--kernel/include/kernel/devices/root_bus.hpp16
-rw-r--r--kernel/include/kernel/devices/storage/controller.hpp71
-rw-r--r--kernel/include/kernel/devices/storage/management.hpp78
-rw-r--r--kernel/include/kernel/devices/storage/ram_disk/controller.hpp31
-rw-r--r--kernel/include/kernel/devices/storage/ram_disk/device.hpp58
-rw-r--r--kernel/include/kernel/filesystem/constants.hpp14
-rw-r--r--kernel/include/kernel/filesystem/dentry.hpp104
-rw-r--r--kernel/include/kernel/filesystem/devfs/filesystem.hpp46
-rw-r--r--kernel/include/kernel/filesystem/devfs/inode.hpp42
-rw-r--r--kernel/include/kernel/filesystem/device_inode.hpp64
-rw-r--r--kernel/include/kernel/filesystem/ext2/block_group_descriptor.hpp24
-rw-r--r--kernel/include/kernel/filesystem/ext2/filesystem.hpp115
-rw-r--r--kernel/include/kernel/filesystem/ext2/inode.hpp105
-rw-r--r--kernel/include/kernel/filesystem/ext2/linked_directory_entry.hpp22
-rw-r--r--kernel/include/kernel/filesystem/ext2/superblock.hpp74
-rw-r--r--kernel/include/kernel/filesystem/filesystem.hpp79
-rw-r--r--kernel/include/kernel/filesystem/inode.hpp69
-rw-r--r--kernel/include/kernel/filesystem/mount.hpp97
-rw-r--r--kernel/include/kernel/filesystem/mount_table.hpp58
-rw-r--r--kernel/include/kernel/filesystem/open_file_descriptor.hpp68
-rw-r--r--kernel/include/kernel/filesystem/open_file_table.hpp66
-rw-r--r--kernel/include/kernel/filesystem/path.hpp71
-rw-r--r--kernel/include/kernel/filesystem/rootfs/filesystem.hpp41
-rw-r--r--kernel/include/kernel/filesystem/rootfs/inode.hpp45
-rw-r--r--kernel/include/kernel/filesystem/type.hpp42
-rw-r--r--kernel/include/kernel/filesystem/type_registry.hpp53
-rw-r--r--kernel/include/kernel/filesystem/vfs.hpp113
-rw-r--r--kernel/include/kernel/memory.hpp4
-rw-r--r--kernel/include/kernel/memory/bitmap_allocator.hpp2
-rw-r--r--kernel/include/kernel/memory/block_list_allocator.hpp25
-rw-r--r--kernel/include/kernel/memory/heap_allocator.hpp7
-rw-r--r--kernel/include/kernel/memory/mmio_allocator.hpp41
-rw-r--r--kernel/include/kernel/test_support/boot_modules.hpp10
-rw-r--r--kernel/include/kernel/test_support/bump_frame_allocator.hpp54
-rw-r--r--kernel/include/kernel/test_support/cio.hpp39
-rw-r--r--kernel/include/kernel/test_support/cpu.hpp23
-rw-r--r--kernel/include/kernel/test_support/devices/block_device.hpp31
-rw-r--r--kernel/include/kernel/test_support/devices/character_device.hpp23
-rw-r--r--kernel/include/kernel/test_support/devices/storage/management.hpp10
-rw-r--r--kernel/include/kernel/test_support/filesystem/ext2.hpp21
-rw-r--r--kernel/include/kernel/test_support/filesystem/filesystem.hpp22
-rw-r--r--kernel/include/kernel/test_support/filesystem/inode.hpp19
-rw-r--r--kernel/include/kernel/test_support/filesystem/open_file_table.hpp10
-rw-r--r--kernel/include/kernel/test_support/filesystem/storage_boot_module_fixture.hpp48
-rw-r--r--kernel/include/kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp23
-rw-r--r--kernel/include/kernel/test_support/filesystem/vfs.hpp10
-rw-r--r--kernel/include/kernel/test_support/log_buffer.hpp37
-rw-r--r--kernel/include/kernel/test_support/memory.hpp22
-rw-r--r--kernel/include/kernel/test_support/page_mapper.hpp49
-rw-r--r--kernel/include/kernel/test_support/simulated_memory.hpp67
-rw-r--r--kernel/kapi/acpi.cpp40
-rw-r--r--kernel/kapi/boot_modules.cpp41
-rw-r--r--kernel/kapi/cio.cpp2
-rw-r--r--kernel/kapi/cpu.cpp35
-rw-r--r--kernel/kapi/cpu.tests.cpp19
-rw-r--r--kernel/kapi/devices.cpp86
-rw-r--r--kernel/kapi/devices/bus.cpp72
-rw-r--r--kernel/kapi/devices/cpu.cpp31
-rw-r--r--kernel/kapi/devices/device.cpp43
-rw-r--r--kernel/kapi/filesystem.cpp76
-rw-r--r--kernel/kapi/filesystem.tests.cpp199
-rw-r--r--kernel/kapi/interrupts.cpp65
-rw-r--r--kernel/kapi/memory.cpp60
-rw-r--r--kernel/kapi/system.cpp4
-rw-r--r--kernel/kapi/system.tests.cpp30
-rw-r--r--kernel/kstd/os.cpp2
-rw-r--r--kernel/kstd/print.cpp113
-rw-r--r--kernel/kstd/print.tests.cpp23
-rw-r--r--kernel/src/acpi/manager.cpp98
-rw-r--r--kernel/src/devices/block_device.cpp42
-rw-r--r--kernel/src/devices/block_device.tests.cpp46
-rw-r--r--kernel/src/devices/block_device_utils.cpp106
-rw-r--r--kernel/src/devices/block_device_utils.tests.cpp219
-rw-r--r--kernel/src/devices/root_bus.cpp12
-rw-r--r--kernel/src/devices/storage/controller.cpp44
-rw-r--r--kernel/src/devices/storage/management.cpp95
-rw-r--r--kernel/src/devices/storage/ram_disk/controller.cpp27
-rw-r--r--kernel/src/devices/storage/ram_disk/device.cpp71
-rw-r--r--kernel/src/devices/storage/ram_disk/device.tests.cpp116
-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
-rw-r--r--kernel/src/main.cpp142
-rw-r--r--kernel/src/memory.cpp15
-rw-r--r--kernel/src/memory/bitmap_allocator.cpp24
-rw-r--r--kernel/src/memory/bitmap_allocator.tests.cpp288
-rw-r--r--kernel/src/memory/block_list_allocator.cpp36
-rw-r--r--kernel/src/memory/block_list_allocator.tests.cpp85
-rw-r--r--kernel/src/memory/mmio_allocator.cpp111
-rw-r--r--kernel/src/memory/operators.cpp21
-rw-r--r--kernel/src/test_support/devices/block_device.cpp61
-rw-r--r--kernel/src/test_support/devices/character_device.cpp19
-rw-r--r--kernel/src/test_support/filesystem/ext2.cpp68
-rw-r--r--kernel/src/test_support/filesystem/filesystem.cpp17
-rw-r--r--kernel/src/test_support/filesystem/inode.cpp23
-rw-r--r--kernel/src/test_support/filesystem/storage_boot_module_fixture.cpp139
-rw-r--r--kernel/src/test_support/filesystem/storage_boot_module_vfs_fixture.cpp31
l---------kernel/src/test_support/filesystem/test_assets/README.md1
-rw-r--r--kernel/src/test_support/filesystem/test_assets/ext2_1KB_fs.img3
-rw-r--r--kernel/src/test_support/filesystem/test_assets/ext2_2KB_fs.img3
-rw-r--r--kernel/src/test_support/filesystem/test_assets/ext2_4KB_fs.img3
-rw-r--r--kernel/src/test_support/kapi/cio.cpp54
-rw-r--r--kernel/src/test_support/kapi/cpu.cpp52
-rw-r--r--kernel/src/test_support/kapi/interrupts.cpp11
-rw-r--r--kernel/src/test_support/kapi/memory.cpp74
-rw-r--r--kernel/src/test_support/log_buffer.cpp33
-rw-r--r--kernel/src/test_support/output_device.cpp28
-rw-r--r--kernel/src/test_support/page_mapper.cpp70
-rw-r--r--kernel/src/test_support/simulated_memory.cpp106
-rw-r--r--kernel/src/test_support/state_reset_listener.cpp48
142 files changed, 9484 insertions, 192 deletions
diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt
index 02e517c..2388370 100644
--- a/kernel/CMakeLists.txt
+++ b/kernel/CMakeLists.txt
@@ -1,44 +1,229 @@
-add_executable("kernel"
- # Platform-independent KAPI implementation
+#[============================================================================[
+# Library
+#]============================================================================]
+
+add_library("kernel_lib" OBJECT)
+add_library("kernel::lib" ALIAS "kernel_lib")
+
+target_sources("kernel_lib" PRIVATE
+ # Kernel-defined KAPI Implementation
+ "kapi/acpi.cpp"
+ "kapi/boot_modules.cpp"
"kapi/cio.cpp"
+ "kapi/cpu.cpp"
+ "kapi/devices.cpp"
+ "kapi/devices/bus.cpp"
+ "kapi/devices/cpu.cpp"
+ "kapi/devices/device.cpp"
+ "kapi/filesystem.cpp"
+ "kapi/interrupts.cpp"
"kapi/memory.cpp"
"kapi/system.cpp"
- # KSTD OS Implementation
+ # Kernel-defined KSTD Implementation
"kstd/os.cpp"
"kstd/print.cpp"
- # Kernel Implementation
- "src/main.cpp"
+ # ACPI Subsystem
+ "src/acpi/manager.cpp"
+
+ # Memory Subsystem
"src/memory/bitmap_allocator.cpp"
"src/memory/block_list_allocator.cpp"
- "src/memory/operators.cpp"
+ "src/memory/mmio_allocator.cpp"
"src/memory.cpp"
+
+ # Device Subsystem
+ "src/devices/block_device.cpp"
+ "src/devices/block_device_utils.cpp"
+ "src/devices/root_bus.cpp"
+
+ # Storage Device Subsystem
+ "src/devices/storage/controller.cpp"
+ "src/devices/storage/management.cpp"
+ "src/devices/storage/ram_disk/controller.cpp"
+ "src/devices/storage/ram_disk/device.cpp"
+
+ # Filesystem Subsystem
+ "src/filesystem/dentry.cpp"
+ "src/filesystem/device_inode.cpp"
+ "src/filesystem/filesystem.cpp"
+ "src/filesystem/inode.cpp"
+ "src/filesystem/mount_table.cpp"
+ "src/filesystem/mount.cpp"
+ "src/filesystem/open_file_descriptor.cpp"
+ "src/filesystem/open_file_table.cpp"
+ "src/filesystem/type_registry.cpp"
+ "src/filesystem/vfs.cpp"
+
+ # DevFS Filesystem
+ "src/filesystem/devfs/filesystem.cpp"
+ "src/filesystem/devfs/inode.cpp"
+
+ # ext2 Filesystem
+ "src/filesystem/ext2/filesystem.cpp"
+ "src/filesystem/ext2/inode.cpp"
+
+ # Rootfs Filesystem
+ "src/filesystem/rootfs/filesystem.cpp"
+ "src/filesystem/rootfs/inode.cpp"
)
-target_include_directories("kernel" PRIVATE
- "include"
+file(GLOB_RECURSE KERNEL_HEADERS
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ CONFIGURE_DEPENDS
+ "include/**.hpp"
)
-target_link_libraries("kernel" PRIVATE
- "os::arch"
- "os::kapi"
+target_sources("kernel_lib" PUBLIC
+ FILE_SET HEADERS
+ BASE_DIRS "include"
+ FILES
+ ${KERNEL_HEADERS}
)
-target_link_options("kernel" PRIVATE
- "-T${KERNEL_LINKER_SCRIPT}"
- "-no-pie"
- "-nostdlib"
+target_include_directories("kernel_lib" PUBLIC
+ "include"
)
-set_property(TARGET "kernel"
- APPEND
- PROPERTY LINK_DEPENDS
- "${KERNEL_LINKER_SCRIPT}"
+target_link_libraries("kernel_lib" PUBLIC
+ "kapi::lib"
+ "kstd::lib"
+ "acpi::lib"
)
-target_disassemble("kernel")
-target_extract_debug_symbols("kernel")
-target_strip("kernel")
+set_target_properties("kernel_lib" PROPERTIES
+ VERIFY_INTERFACE_HEADER_SETS YES
+)
+
+#[============================================================================[
+# Executable
+#]============================================================================]
+
+if(NOT BUILD_TESTING)
+ add_executable("kernel")
+ add_executable(kernel::exe ALIAS "kernel")
+
+ target_sources("kernel" PRIVATE
+ "src/main.cpp"
+ "src/memory/operators.cpp"
+ )
+
+ target_link_libraries("kernel" PRIVATE
+ "kernel::lib"
+ "arch::lib"
+ )
+
+ target_link_options("kernel" PRIVATE
+ "-T${KERNEL_LINKER_SCRIPT}"
+ "-no-pie"
+ "-nostdlib"
+ )
+
+ set_property(TARGET "kernel"
+ APPEND
+ PROPERTY LINK_DEPENDS
+ "${KERNEL_LINKER_SCRIPT}"
+ )
+
+ target_disassemble("kernel")
+ target_extract_debug_symbols("kernel")
+ target_strip("kernel")
+
+ target_generate_bootable_iso("kernel")
+endif()
+
+#[============================================================================[
+# Tests
+#]============================================================================]
+
+if(BUILD_TESTING)
+ find_package("Catch2")
+ include("Catch")
+
+ enable_coverage("kernel_lib")
+
+ add_executable("kernel_tests")
+ add_executable("kernel::tests" ALIAS "kernel_tests")
+
+ target_sources("kernel_tests" PRIVATE
+ # Platform-defined KAPI
+ "src/test_support/kapi/cpu.cpp"
+ "src/test_support/kapi/cio.cpp"
+ "src/test_support/kapi/interrupts.cpp"
+ "src/test_support/kapi/memory.cpp"
+
+ # Device Subsystem Support
+ "src/test_support/devices/block_device.cpp"
+ "src/test_support/devices/character_device.cpp"
+
+ # Filesystem Subsystem Support
+ "src/test_support/filesystem/inode.cpp"
+ "src/test_support/filesystem/filesystem.cpp"
+ "src/test_support/filesystem/ext2.cpp"
+ "src/test_support/filesystem/storage_boot_module_fixture.cpp"
+ "src/test_support/filesystem/storage_boot_module_vfs_fixture.cpp"
+
+ # I/O Support
+ "src/test_support/log_buffer.cpp"
+ "src/test_support/output_device.cpp"
+
+ # Memory Support
+ "src/test_support/page_mapper.cpp"
+ "src/test_support/simulated_memory.cpp"
+
+ # Support System Listener
+ "src/test_support/state_reset_listener.cpp"
+
+ # KAPI Shim Tests
+ "kapi/cpu.tests.cpp"
+ "kapi/system.tests.cpp"
+ "kapi/filesystem.tests.cpp"
+
+ # KSTD Shim Tests
+ "kstd/print.tests.cpp"
+
+ # Memory Subsystem Tests
+ "src/memory/bitmap_allocator.tests.cpp"
+ "src/memory/block_list_allocator.tests.cpp"
+
+ # Filesystem Subsystem Tests
+ "src/filesystem/devfs/filesystem.tests.cpp"
+ "src/filesystem/devfs/inode.tests.cpp"
+ "src/filesystem/ext2/filesystem.tests.cpp"
+ "src/filesystem/ext2/inode.tests.cpp"
+ "src/filesystem/path.tests.cpp"
+ "src/filesystem/rootfs/filesystem.tests.cpp"
+ "src/filesystem/rootfs/inode.tests.cpp"
+ "src/filesystem/dentry.tests.cpp"
+ "src/filesystem/device_inode.tests.cpp"
+ "src/filesystem/mount_table.tests.cpp"
+ "src/filesystem/mount.tests.cpp"
+ "src/filesystem/open_file_descriptor.tests.cpp"
+ "src/filesystem/open_file_table.tests.cpp"
+ "src/filesystem/type_registry.tests.cpp"
+ "src/filesystem/vfs.tests.cpp"
+
+ # Storage Subsystem Tests
+ "src/devices/block_device_utils.tests.cpp"
+ "src/devices/block_device.tests.cpp"
+ "src/devices/storage/ram_disk/device.tests.cpp"
+ )
+
+ target_link_libraries("kernel_tests" PRIVATE
+ "kernel::lib"
+ "Catch2::Catch2WithMain"
+ )
+
+ target_compile_definitions("kernel_tests" PRIVATE
+ KERNEL_TEST_ASSETS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/src/test_support/filesystem/test_assets"
+ )
+
+ set_target_properties("kernel_tests" PROPERTIES
+ C_CLANG_TIDY ""
+ CXX_CLANG_TIDY ""
+ )
-target_generate_bootable_iso("kernel")
+ enable_coverage("kernel_tests")
+ catch_discover_tests("kernel_tests" ${CATCH_TEST_ARGS})
+endif()
diff --git a/kernel/include/kernel/acpi/manager.hpp b/kernel/include/kernel/acpi/manager.hpp
new file mode 100644
index 0000000..1e8c1e8
--- /dev/null
+++ b/kernel/include/kernel/acpi/manager.hpp
@@ -0,0 +1,32 @@
+#ifndef TEACHOS_KERNEL_ACPI_MANAGER_HPP
+#define TEACHOS_KERNEL_ACPI_MANAGER_HPP
+
+#include <acpi/acpi.hpp>
+
+#include <kstd/flat_map>
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <string_view>
+
+namespace kernel::acpi
+{
+
+ struct manager
+ {
+ explicit manager(::acpi::rsdp const & sdp);
+
+ auto load_tables() -> bool;
+
+ auto get_table(std::string_view signature) -> kstd::observer_ptr<::acpi::table_header const>;
+
+ private:
+ ::acpi::rsdp const * m_sdp{};
+ ::acpi::table_header const * m_rsdt{};
+ kstd::flat_map<std::string_view, ::acpi::table_header const *> m_tables{};
+ bool m_extended{};
+ };
+
+} // namespace kernel::acpi
+
+#endif
diff --git a/kernel/include/kernel/devices/block_device.hpp b/kernel/include/kernel/devices/block_device.hpp
new file mode 100644
index 0000000..a6d68ee
--- /dev/null
+++ b/kernel/include/kernel/devices/block_device.hpp
@@ -0,0 +1,97 @@
+#ifndef TEACH_OS_KERNEL_DEVICES_BLOCK_DEVICE_HPP
+#define TEACH_OS_KERNEL_DEVICES_BLOCK_DEVICE_HPP
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/string>
+
+#include <cstddef>
+
+namespace kernel::devices
+{
+ /**
+ * @brief Base interface for block-addressable devices.
+ */
+ struct block_device : kapi::devices::device
+ {
+ /**
+ * @brief Create a block device descriptor.
+ * @param major Device major number.
+ * @param minor Device minor number.
+ * @param name Device name.
+ * @param block_size Size of one logical block in bytes.
+ */
+ block_device(size_t major, size_t minor, kstd::string const & name, size_t block_size);
+
+ /**
+ * @brief Read data from the block at @p block_index into @p buffer.
+ * @param block_index Zero-based block index.
+ * @param buffer Destination buffer.
+ * @warning Panics if @p buffer is null.
+ * @note Reads up to one logical block (see constructor @p block_size). Implementations may perform a partial
+ * transfer for the final block when fewer than @p block_size bytes remain.
+ */
+ virtual auto read_block(size_t block_index, void * buffer) const -> void = 0;
+
+ /**
+ * @brief Write data to the block at @p block_index.
+ * @param block_index Zero-based block index.
+ * @param buffer Source buffer, must not be null.
+ * @warning Panics if @p buffer is null.
+ * @note Writes up to one logical block (see constructor @p block_size).
+ * Implementations may perform a partial transfer for the final block when
+ * fewer than @p block_size bytes remain.
+ */
+ virtual auto write_block(size_t block_index, void const * buffer) -> void = 0;
+
+ /**
+ * @brief Return logical block size in bytes.
+ * @return One logical block size in bytes.
+ */
+ [[nodiscard]] auto block_size() const -> size_t;
+
+ /**
+ * @brief Return device capacity in bytes.
+ * @return Total number of addressable bytes.
+ */
+ [[nodiscard]] auto capacity() const -> size_t;
+
+ /**
+ * @brief Override to identify block devices.
+ * @return true if this device is a block device, false otherwise.
+ */
+
+ [[nodiscard]] auto is_block_device() const -> bool override
+ {
+ return true;
+ }
+
+ protected:
+ /**
+ * @brief Information describing the transfer window for one block index.
+ */
+ struct transfer_info
+ {
+ size_t offset;
+ size_t to_transfer;
+ size_t remainder;
+ };
+
+ /**
+ * @brief Return total device size in bytes.
+ * @return Total number of addressable bytes.
+ */
+ [[nodiscard]] virtual auto size() const -> size_t = 0;
+
+ /**
+ * @brief Compute transfer information for @p block_index.
+ * @param block_index Zero-based block index.
+ * @return Computed transfer information for one logical block access.
+ */
+ [[nodiscard]] auto calculate_transfer(size_t block_index) const -> transfer_info;
+
+ size_t m_block_size;
+ };
+} // namespace kernel::devices
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/devices/block_device_utils.hpp b/kernel/include/kernel/devices/block_device_utils.hpp
new file mode 100644
index 0000000..8be75b6
--- /dev/null
+++ b/kernel/include/kernel/devices/block_device_utils.hpp
@@ -0,0 +1,44 @@
+#ifndef TEACH_OS_KERNEL_DEVICES_BLOCK_DEVICE_UTILS_HPP
+#define TEACH_OS_KERNEL_DEVICES_BLOCK_DEVICE_UTILS_HPP
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/memory>
+
+#include <cstddef>
+
+namespace kernel::devices::block_device_utils
+{
+ /**
+ @brief Utility functions for block devices, such as reading/writing data at specific offsets. These functions handle
+ the necessary logic to interact with block devices, such as calculating block boundaries and ensuring proper access
+ patterns. They abstract away the details of block device interactions, providing a simple interface for reading and
+ writing data to block devices.
+ */
+
+ /**
+ @brief Reads data from a @p device into a @p buffer, starting at a specific @p offset and for a given @p size.
+ @warning Panics if @p buffer or @p device is null.
+ @param device The block device to read from.
+ @param buffer The buffer to read data into.
+ @param offset The offset on the block device to start reading from.
+ @param size The number of bytes to read.
+ @return The number of bytes actually read, which may be less than the requested size.
+ */
+ auto read(kstd::shared_ptr<kapi::devices::device> const & device, void * buffer, size_t offset, size_t size)
+ -> size_t;
+
+ /**
+ @brief Writes data from a @p buffer to a @p device, starting at a specific @p offset and for a given @p size.
+ @warning Panics if @p buffer or @p device is null.
+ @param device The block device to write to.
+ @param buffer The buffer to write data from.
+ @param offset The offset on the block device to start writing to.
+ @param size The number of bytes to write.
+ @return The number of bytes actually written, which may be less than the requested size.
+ */
+ auto write(kstd::shared_ptr<kapi::devices::device> const & device, void const * buffer, size_t offset, size_t size)
+ -> size_t;
+} // namespace kernel::devices::block_device_utils
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/devices/root_bus.hpp b/kernel/include/kernel/devices/root_bus.hpp
new file mode 100644
index 0000000..c8fee52
--- /dev/null
+++ b/kernel/include/kernel/devices/root_bus.hpp
@@ -0,0 +1,16 @@
+#ifndef TEACHOS_KERNEL_DEVICES_ROOT_BUS_HPP
+#define TEACHOS_KERNEL_DEVICES_ROOT_BUS_HPP
+
+#include <kapi/devices/bus.hpp>
+
+namespace kernel::devices
+{
+
+ struct root_bus final : kapi::devices::bus
+ {
+ root_bus();
+ };
+
+} // namespace kernel::devices
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/devices/storage/controller.hpp b/kernel/include/kernel/devices/storage/controller.hpp
new file mode 100644
index 0000000..bea18f3
--- /dev/null
+++ b/kernel/include/kernel/devices/storage/controller.hpp
@@ -0,0 +1,71 @@
+#ifndef TEACH_OS_KERNEL_DEVICES_STORAGE_CONTROLLER_HPP
+#define TEACH_OS_KERNEL_DEVICES_STORAGE_CONTROLLER_HPP
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <cstddef>
+
+namespace kernel::devices::storage
+{
+ /**
+ * @brief Base interface for storage controllers.
+ *
+ * A storage controller probes for devices and resolves devices by major/minor
+ * numbers.
+ */
+ struct controller
+ {
+ /**
+ * @brief Virtual destructor.
+ */
+ virtual ~controller() = default;
+
+ /**
+ * @brief Probe the controller and register discovered devices.
+ */
+ virtual auto probe() -> void = 0;
+
+ /**
+ * @brief Assign the major number and minor stride for this controller.
+ * @param major Major number assigned to this controller.
+ * @param minors_per_dev Minor number stride between devices.
+ */
+ auto set_ids(size_t major, size_t minors_per_dev) -> void;
+
+ /**
+ * @brief Return the assigned major number.
+ * @return Assigned major number.
+ */
+ [[nodiscard]] auto major() const -> size_t;
+
+ /**
+ * @brief Return the number of devices managed by this controller.
+ * @return Number of managed devices.
+ */
+ [[nodiscard]] auto devices_count() const -> size_t;
+
+ /**
+ * @brief Return all devices managed by this controller.
+ * @return Vector of all managed devices.
+ */
+ [[nodiscard]] auto all_devices() const -> kstd::vector<kstd::shared_ptr<kapi::devices::device>> const &;
+
+ /**
+ * @brief Find a managed device by major/minor numbers.
+ * @param major Device major number.
+ * @param minor Device minor number.
+ * @return Matching block device, or nullptr if no device matches.
+ */
+ [[nodiscard]] auto device_by_minor(size_t minor) const -> kstd::shared_ptr<kapi::devices::device>;
+
+ protected:
+ size_t m_major{};
+ size_t m_minors_per_device{};
+ kstd::vector<kstd::shared_ptr<kapi::devices::device>> m_devices{};
+ };
+} // namespace kernel::devices::storage
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/devices/storage/management.hpp b/kernel/include/kernel/devices/storage/management.hpp
new file mode 100644
index 0000000..9a84087
--- /dev/null
+++ b/kernel/include/kernel/devices/storage/management.hpp
@@ -0,0 +1,78 @@
+#ifndef TEACH_OS_KERNEL_DEVICES_STORAGE_MANAGEMENT_HPP
+#define TEACH_OS_KERNEL_DEVICES_STORAGE_MANAGEMENT_HPP
+
+#include <kernel/devices/storage/controller.hpp>
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <cstddef>
+
+namespace kernel::devices::storage
+{
+ /**
+ * @brief Global storage subsystem manager.
+ *
+ * Owns registered storage controllers and provides device lookup by
+ * major/minor numbers.
+ */
+ struct management
+ {
+ /**
+ * @brief Initialize global storage management.
+ *
+ * Creates the singleton instance, registers controllers and probes
+ * them for devices.
+ *
+ * @warning Panics if called more than once.
+ */
+ auto static init() -> void;
+
+ /**
+ * @brief Return the active storage manager singleton.
+ * @return Reference to the active storage manager.
+ * @warning Panics if storage management has not been initialized.
+ */
+ auto static get() -> management &;
+
+ /**
+ * @brief Register a storage controller.
+ * @param controller Controller to register.
+ *
+ * Assigns controller IDs (major number range and minors per device).
+ */
+ auto add_controller(kstd::shared_ptr<controller> const & controller) -> void;
+
+ /**
+ * @brief Return all registered storage controllers.
+ * @return Vector of all registered storage controllers.
+ */
+ [[nodiscard]] auto all_controllers() const -> kstd::vector<kstd::shared_ptr<controller>> const &;
+
+ /**
+ * @brief Find a device by major/minor numbers.
+ * @param major Device major number.
+ * @param minor Device minor number.
+ * @return Matching device, or nullptr if no device matches.
+ */
+ auto device_by_major_minor(size_t major, size_t minor) -> kstd::shared_ptr<kapi::devices::device>;
+
+ /**
+ * @brief Determine the boot device.
+ * @return Boot device, or nullptr if it cannot be determined.
+ */
+ auto determine_boot_device() -> kstd::shared_ptr<kapi::devices::device>;
+
+ private:
+ /**
+ * @brief Private default constructor for storage management singleton.
+ */
+ management() = default;
+
+ kstd::vector<kstd::shared_ptr<controller>> m_controllers{};
+ };
+} // namespace kernel::devices::storage
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/devices/storage/ram_disk/controller.hpp b/kernel/include/kernel/devices/storage/ram_disk/controller.hpp
new file mode 100644
index 0000000..93cf30d
--- /dev/null
+++ b/kernel/include/kernel/devices/storage/ram_disk/controller.hpp
@@ -0,0 +1,31 @@
+#ifndef TEACH_OS_KERNEL_DEVICES_STORAGE_RAM_DISK_CONTROLLER_HPP
+#define TEACH_OS_KERNEL_DEVICES_STORAGE_RAM_DISK_CONTROLLER_HPP
+
+#include <kernel/devices/storage/controller.hpp>
+
+#include <kapi/boot_module/boot_module_registry.hpp>
+
+namespace kernel::devices::storage::ram_disk
+{
+ /**
+ * @brief Storage controller that exposes boot modules as RAM-disk devices.
+ */
+ struct controller : kernel::devices::storage::controller
+ {
+ /**
+ * @brief Create a RAM-disk controller.
+ * @param registry Boot module registry as device source.
+ */
+ explicit controller(kapi::boot_modules::boot_module_registry const * registry);
+
+ /**
+ * @brief Probe boot modules and create RAM-disk devices.
+ */
+ auto probe() -> void override;
+
+ private:
+ kapi::boot_modules::boot_module_registry const * m_boot_module_registry;
+ };
+} // namespace kernel::devices::storage::ram_disk
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/devices/storage/ram_disk/device.hpp b/kernel/include/kernel/devices/storage/ram_disk/device.hpp
new file mode 100644
index 0000000..89789ea
--- /dev/null
+++ b/kernel/include/kernel/devices/storage/ram_disk/device.hpp
@@ -0,0 +1,58 @@
+#ifndef TEACH_OS_KERNEL_DEVICES_STORAGE_RAM_DISK_DEVICE_HPP
+#define TEACH_OS_KERNEL_DEVICES_STORAGE_RAM_DISK_DEVICE_HPP
+
+#include <kernel/devices/block_device.hpp>
+
+#include <kapi/boot_module/boot_module.hpp>
+
+#include <cstddef>
+
+namespace kernel::devices::storage::ram_disk
+{
+ /**
+ * @brief Block device for a boot module.
+ */
+ struct device : block_device
+ {
+ /**
+ * @brief Create a RAM disk for the @p module.
+ * @param module Boot module providing the memory region.
+ * @param major Device major number.
+ * @param minor Device minor number.
+ */
+ device(kapi::boot_modules::boot_module const & module, size_t major, size_t minor);
+
+ /**
+ * @brief Initialize the RAM disk device.
+ * @return true if module backing memory is valid, false otherwise.
+ */
+ auto init() -> bool override;
+
+ /**
+ * @brief Read one logical block into @p buffer.
+ * @param block_index Zero-based block index.
+ * @param buffer Destination buffer, must not be null.
+ * @note If the request reaches the module end, only available bytes are copied and the rest of the
+ * logical block is filled with zeros.
+ */
+ auto read_block(size_t block_index, void * buffer) const -> void override;
+
+ /**
+ * @brief Write one logical block from @p buffer.
+ * @param block_index Zero-based block index.
+ * @param buffer Source buffer, must not be null.
+ * @note If the request reaches the module end, only the bytes in the module range are written.
+ */
+ auto write_block(size_t block_index, void const * buffer) -> void override;
+
+ private:
+ /**
+ * @brief Return module size in bytes.
+ */
+ [[nodiscard]] auto size() const -> size_t override;
+
+ kapi::boot_modules::boot_module m_boot_module{};
+ };
+} // namespace kernel::devices::storage::ram_disk
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/constants.hpp b/kernel/include/kernel/filesystem/constants.hpp
new file mode 100644
index 0000000..8388d05
--- /dev/null
+++ b/kernel/include/kernel/filesystem/constants.hpp
@@ -0,0 +1,14 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_CONSTANTS_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_CONSTANTS_HPP
+
+#include <cstddef>
+
+namespace kernel::filesystem::constants
+{
+ constexpr size_t inline max_path_length = 4096;
+
+ constexpr size_t inline symlink_max_path_length = 4096;
+ constexpr size_t inline symloop_max = 40;
+} // namespace kernel::filesystem::constants
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/dentry.hpp b/kernel/include/kernel/filesystem/dentry.hpp
new file mode 100644
index 0000000..478596a
--- /dev/null
+++ b/kernel/include/kernel/filesystem/dentry.hpp
@@ -0,0 +1,104 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_DENTRY_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_DENTRY_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <cstdint>
+#include <string_view>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief Represents a directory entry (dentry) in the filesystem. A dentry is a node in the directory tree that
+ represents a file or directory. It contains a reference to its parent dentry, a reference to the associated real
+ filesystem inode, and a list of child dentries.
+ */
+ struct dentry
+ {
+ /**
+ @brief Flags for the dentry.
+ */
+ enum class dentry_flags : uint32_t
+ {
+ is_mount_point = 1 << 0
+ };
+
+ /**
+ @brief Create a dentry with the given @p parent, associated @p inode, and optional @p name. The dentry is
+ initialized with the provided parent and inode, and the name is stored for lookup purposes.
+ @param parent The parent dentry.
+ @param inode The associated inode for this dentry.
+ @param name The name of the dentry (optional).
+ */
+ dentry(kstd::shared_ptr<dentry> const & parent, kstd::shared_ptr<inode> const & inode, std::string_view name);
+
+ /**
+ @brief Get the associated inode.
+ @return A reference to the associated inode.
+ */
+ [[nodiscard]] auto get_inode() const -> kstd::shared_ptr<inode> const &;
+
+ /**
+ @brief Get the parent dentry.
+ @return A reference to the parent dentry.
+ */
+ [[nodiscard]] auto parent() const -> kstd::shared_ptr<dentry> const &;
+
+ /**
+ @brief Get the name of the dentry.
+ @return The name of the dentry.
+ */
+ [[nodiscard]] auto name() const -> std::string_view;
+
+ /**
+ @brief Get the full path of the dentry by traversing up to the root.
+ @return The full path of the dentry.
+ */
+ [[nodiscard]] auto absolute_path() const -> kstd::string;
+
+ /**
+ @brief Add a @p child dentry.
+ @param child The child dentry to add.
+ */
+ auto add_child(kstd::shared_ptr<dentry> const & child) -> void;
+
+ /**
+ @brief Find a child dentry by @p name.
+ @param name The name of the child dentry to find.
+ @return A pointer to the found child dentry, or a null pointer if not found.
+ */
+ [[nodiscard]] auto find_child(std::string_view name) const -> kstd::shared_ptr<dentry>;
+
+ /**
+ @brief Set a @p flag for the dentry.
+ @param flag The flag to set.
+ */
+ auto set_flag(dentry_flags flag) -> void;
+
+ /**
+ @brief Unset a @p flag for the dentry.
+ @param flag The flag to unset.
+ */
+ auto unset_flag(dentry_flags flag) -> void;
+
+ /**
+ @brief Check if the dentry has a specific @p flag.
+ @param flag The flag to check.
+ @return True if the dentry has the flag, false otherwise.
+ */
+ [[nodiscard]] auto has_flag(dentry_flags flag) const -> bool;
+
+ private:
+ kstd::string m_name;
+ kstd::shared_ptr<dentry> m_parent;
+ kstd::vector<kstd::shared_ptr<dentry>> m_children;
+ kstd::shared_ptr<inode> m_inode;
+ uint32_t m_flags;
+ };
+} // namespace kernel::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/devfs/filesystem.hpp b/kernel/include/kernel/filesystem/devfs/filesystem.hpp
new file mode 100644
index 0000000..dbaa387
--- /dev/null
+++ b/kernel/include/kernel/filesystem/devfs/filesystem.hpp
@@ -0,0 +1,46 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_DEVFS_FILESYSTEM_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_DEVFS_FILESYSTEM_HPP
+
+#include <kernel/filesystem/device_inode.hpp>
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <string_view>
+
+namespace kernel::filesystem::devfs
+{
+ /**
+ @brief A filesystem for managing device nodes in the virtual filesystem. This filesystem provides a way to represent
+ devices as files in the /dev directory, allowing user-space applications to interact with devices using standard file
+ operations. The devfs filesystem dynamically creates inodes for devices registered in the system, enabling seamless
+ access to device functionality through the filesystem interface.
+ */
+ struct filesystem : kernel::filesystem::filesystem
+ {
+ /**
+ @brief Initializes the devfs instance and builds the device inode table.
+ @param backing_inode Backing inode passed by the vfs (not required by devfs).
+ @return The result of the mount operation.
+ */
+ auto mount(kstd::shared_ptr<kernel::filesystem::inode> const & backing_inode) -> operation_result override;
+
+ /**
+ @brief Looks up an inode by @p name within a @p parent directory.
+ @param parent The parent directory inode.
+ @param name The name of the inode to look up.
+ @return A pointer to the found inode, or a null pointer if not found.
+ */
+ [[nodiscard]] auto lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) const
+ -> kstd::shared_ptr<kernel::filesystem::inode> override;
+
+ private:
+ auto build_device_inode_table() -> void;
+
+ kstd::vector<kstd::shared_ptr<device_inode>> m_inodes{};
+ };
+} // namespace kernel::filesystem::devfs
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/devfs/inode.hpp b/kernel/include/kernel/filesystem/devfs/inode.hpp
new file mode 100644
index 0000000..e428891
--- /dev/null
+++ b/kernel/include/kernel/filesystem/devfs/inode.hpp
@@ -0,0 +1,42 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_DEVFS_INODE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_DEVFS_INODE_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <cstddef>
+
+namespace kernel::filesystem::devfs
+{
+ /**
+ @brief Inode implementation for the devfs filesystem.
+ This inode represents root device node in the /dev directory.
+ */
+ struct inode : kernel::filesystem::inode
+ {
+ /**
+ @brief Reads from the devfs directory inode.
+ @param buffer Destination buffer.
+ @param offset Read offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes read (always 0 because this inode does not expose file data).
+ */
+ auto read(void * buffer, size_t offset, size_t size) const -> size_t override;
+
+ /**
+ @brief Writes to the devfs directory inode.
+ @param buffer Source buffer.
+ @param offset Write offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes written (always 0 because writes are not supported for this inode).
+ */
+ auto write(void const * buffer, size_t offset, size_t size) -> size_t override;
+
+ /**
+ @brief Check if this inode represents a directory.
+ @return returns true, since this inode represents the /dev directory in the devfs filesystem.
+ */
+ [[nodiscard]] auto is_directory() const -> bool override;
+ };
+} // namespace kernel::filesystem::devfs
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/device_inode.hpp b/kernel/include/kernel/filesystem/device_inode.hpp
new file mode 100644
index 0000000..f4aa2d1
--- /dev/null
+++ b/kernel/include/kernel/filesystem/device_inode.hpp
@@ -0,0 +1,64 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_DEVICE_INODE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_DEVICE_INODE_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/memory>
+
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief Inode implementation for device inodes in the filesystem. This inode represents a device file that provides
+ access to a device registered in the system. The device inode allows reading from and writing to the associated
+ device.
+ */
+ struct device_inode : inode
+ {
+ /**
+ @brief Create a device inode with the given @p device.
+ @param device The device to associate with the inode.
+ */
+ explicit device_inode(kstd::shared_ptr<kapi::devices::device> const & device);
+
+ /**
+ @brief Read data from the device inode (and in the background from the associated device) into a @p buffer, starting
+ at @p offset and reading @p size bytes.
+ @param buffer The buffer to read data into.
+ @param offset The offset to read from.
+ @param size The number of bytes to read.
+ @return The number of bytes read.
+ */
+ auto read(void * buffer, size_t offset, size_t size) const -> size_t override;
+
+ /**
+ @brief Write data to the device inode (and in the background from the associated device) from a @p buffer, starting
+ at @p offset and writing @p size bytes.
+ @param buffer The buffer containing data to write.
+ @param offset The offset to write to.
+ @param size The number of bytes to write.
+ @return The number of bytes written.
+ */
+ auto write(void const * buffer, size_t offset, size_t size) -> size_t override;
+
+ /**
+ @brief Get the associated device.
+ @return A reference to the associated device.
+ */
+ [[nodiscard]] auto device() const -> kstd::shared_ptr<kapi::devices::device> const &;
+
+ /**
+ @brief Check if this inode represents a device.
+ @return returns true, since this indoe is a device inode and represents a device.
+ */
+ [[nodiscard]] auto is_device() const -> bool override;
+
+ private:
+ kstd::shared_ptr<kapi::devices::device> m_device;
+ };
+} // namespace kernel::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/ext2/block_group_descriptor.hpp b/kernel/include/kernel/filesystem/ext2/block_group_descriptor.hpp
new file mode 100644
index 0000000..7fbba3f
--- /dev/null
+++ b/kernel/include/kernel/filesystem/ext2/block_group_descriptor.hpp
@@ -0,0 +1,24 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_EXT2_BLOCK_GROUP_DESCRIPTOR_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_EXT2_BLOCK_GROUP_DESCRIPTOR_HPP
+
+#include <array>
+#include <cstdint>
+
+namespace kernel::filesystem::ext2
+{
+ /**
+ @brief Represents a block group descriptor in the ext2 filesystem.
+ */
+ struct [[gnu::packed]] block_group_descriptor
+ {
+ uint32_t block_bitmap;
+ uint32_t inode_bitmap;
+ uint32_t inode_table;
+ uint16_t free_blocks_count;
+ uint16_t free_inodes_count;
+ uint16_t used_dirs_count;
+ std::array<uint8_t, 2> padding;
+ std::array<uint8_t, 12> reserved; // NOLINT(readability-magic-numbers)
+ };
+} // namespace kernel::filesystem::ext2
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/ext2/filesystem.hpp b/kernel/include/kernel/filesystem/ext2/filesystem.hpp
new file mode 100644
index 0000000..2284d7b
--- /dev/null
+++ b/kernel/include/kernel/filesystem/ext2/filesystem.hpp
@@ -0,0 +1,115 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_EXT2_FILESYSTEM_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_EXT2_FILESYSTEM_HPP
+
+#include <kernel/filesystem/ext2/block_group_descriptor.hpp>
+#include <kernel/filesystem/ext2/inode.hpp>
+#include <kernel/filesystem/ext2/superblock.hpp>
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/unikstd.h>
+#include <kstd/vector>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+
+namespace kernel::filesystem::ext2
+{
+ /**
+ @brief Constants related to the ext2 filesystem.
+ */
+ namespace constants
+ {
+ constexpr size_t inline base_block_size = 1024;
+ constexpr size_t inline superblock_offset = base_block_size;
+ constexpr uint16_t inline magic_number = 0xEF53;
+
+ constexpr uint32_t inline good_old_revision = 0;
+ constexpr uint32_t inline dynamic_revision = 1;
+
+ constexpr uint32_t inline root_inode_number = 2;
+
+ constexpr size_t inline direct_block_count = 12;
+ constexpr size_t inline singly_indirect_block_index = direct_block_count;
+ constexpr size_t inline doubly_indirect_block_index = singly_indirect_block_index + 1;
+ constexpr size_t inline triply_indirect_block_index = doubly_indirect_block_index + 1;
+
+ constexpr uint16_t inline mode_mask = 0xF000;
+ constexpr uint16_t inline mode_regular = 0x8000;
+ constexpr uint16_t inline mode_directory = 0x4000;
+ constexpr uint16_t inline mode_symbolic_link = 0xA000;
+ } // namespace constants
+
+ /**
+ @brief A filesystem implementation for the ext2 filesystem format. This class provides methods for mounting an ext2
+ filesystem, and looking up inodes.
+ */
+ struct filesystem : kernel::filesystem::filesystem
+ {
+ /**
+ @brief Initializes the ext2 filesystem with the given @p backing_inode.
+ @param backing_inode The backing inode to mount.
+ @return The result of the mount operation.
+ */
+ auto mount(kstd::shared_ptr<kernel::filesystem::inode> const & backing_inode) -> operation_result override;
+
+ /**
+ @brief Looks up an inode by @p name within a @p parent directory.
+ @param parent The parent directory inode.
+ @param name The name of the inode to look up.
+ @return A pointer to the found inode, or a null pointer if not found.
+ */
+ [[nodiscard]] auto lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) const
+ -> kstd::shared_ptr<kernel::filesystem::inode> override;
+
+ /**
+ @brief Gets the size of a block in the filesystem.
+ @return The size of a block in bytes.
+ */
+ [[nodiscard]] auto block_size() const -> size_t;
+
+ /**
+ @brief Gets the revision level of the filesystem.
+ @return The revision level.
+ */
+ [[nodiscard]] auto revision_level() const -> uint32_t;
+
+ /**
+ @brief Maps an inode block index to a global block number.
+ @param inode_block_index The index of the block within the inode.
+ @param data The inode data.
+ @return The global block number.
+ */
+ [[nodiscard]] auto map_inode_block_index_to_global_block_number(size_t inode_block_index, inode_data data) const
+ -> kstd::ssize_t;
+
+ private:
+ struct indirect_level
+ {
+ uint32_t slot_index;
+ size_t capacity;
+ };
+
+ [[nodiscard]] auto indirect_levels() const -> std::array<indirect_level, 3>;
+
+ [[nodiscard]] auto read_inode(uint32_t inode_number) const -> kstd::shared_ptr<kernel::filesystem::ext2::inode>;
+ [[nodiscard]] auto read_block_number_at_index(uint32_t block_number, size_t index) const -> uint32_t;
+
+ [[nodiscard]] auto inode_size() const -> uint16_t;
+ [[nodiscard]] auto inode_block_count(inode_data const & data) const -> uint32_t;
+ [[nodiscard]] auto block_group_descriptor_table_offset() const -> size_t;
+
+ [[nodiscard]] auto block_numbers_per_block() const -> size_t;
+ [[nodiscard]] auto block_numbers_per_singly_indirect_block() const -> size_t;
+ [[nodiscard]] auto block_numbers_per_doubly_indirect_block() const -> size_t;
+ [[nodiscard]] auto block_numbers_per_triply_indirect_block() const -> size_t;
+
+ superblock m_superblock{};
+ kstd::vector<block_group_descriptor> m_block_group_descriptors;
+ };
+} // namespace kernel::filesystem::ext2
+
+#endif
diff --git a/kernel/include/kernel/filesystem/ext2/inode.hpp b/kernel/include/kernel/filesystem/ext2/inode.hpp
new file mode 100644
index 0000000..f2496f0
--- /dev/null
+++ b/kernel/include/kernel/filesystem/ext2/inode.hpp
@@ -0,0 +1,105 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_EXT2_INODE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_EXT2_INODE_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+
+namespace kernel::filesystem::ext2
+{
+ struct filesystem;
+
+ /**
+ @brief Represents the data associated with an ext2 inode.
+ */
+ struct [[gnu::packed]] inode_data
+ {
+ uint16_t mode;
+ uint16_t uid;
+ uint32_t size;
+ uint32_t atime;
+ uint32_t ctime;
+ uint32_t mtime;
+ uint32_t dtime;
+ uint16_t gid;
+ uint16_t links_count;
+ uint32_t blocks;
+ uint32_t flags;
+ uint32_t osd1;
+ std::array<uint32_t, 15> block; // NOLINT(readability-magic-numbers)
+ uint32_t generation;
+ uint32_t file_acl;
+ uint32_t dir_acl;
+ uint32_t faddr;
+ std::array<uint8_t, 12> osd2; // NOLINT(readability-magic-numbers)
+ };
+
+ struct inode : kernel::filesystem::inode
+ {
+ /**
+ @brief Create an ext2 inode associated with the given filesystem.
+ @param fs The ext2 filesystem that this inode belongs to.
+ @param data The data associated with this inode, read from the disk.
+ */
+ explicit inode(filesystem const * fs, inode_data const & data);
+
+ /**
+ @brief Reads from the ext2 inode into a @p buffer, starting at the specified @p offset and for a given @p size.
+ @param buffer Destination buffer.
+ @param offset Read offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes read.
+ */
+ auto read(void * buffer, size_t offset, size_t size) const -> size_t override;
+
+ /**
+ @brief Writes to the ext2 inode into a @p buffer, starting at the specified @p offset and for a given @p size.
+ @warning This method is not implemented yet and will panic if called.
+ @param buffer Source buffer.
+ @param offset Write offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes written.
+ */
+ auto write(void const * buffer, size_t offset, size_t size) -> size_t override;
+
+ /**
+ @brief Get the data associated with this inode.
+ @return A reference to the inode data.
+ */
+ [[nodiscard]] auto data() const -> inode_data const &;
+
+ /**
+ @brief Check if this inode represents a directory.
+ @return returns true if this inode represents a directory, false otherwise.
+ */
+ [[nodiscard]] auto is_directory() const -> bool override;
+
+ /**
+ @brief Check if this inode represents a regular file.
+ @return returns true if this inode represents a regular file, false otherwise.
+ */
+ [[nodiscard]] auto is_regular() const -> bool override;
+
+ /**
+ @brief Check if this inode represents a symbolic link.
+ @return returns true if this inode represents a symbolic link, false otherwise.
+ */
+ [[nodiscard]] auto is_symbolic_link() const -> bool override;
+
+ /**
+ @brief Get the size of the file represented by this inode.
+ @return The size of the file in bytes.
+ */
+ [[nodiscard]] auto size() const -> uint64_t;
+
+ private:
+ filesystem const * m_filesystem;
+ inode_data m_data{};
+ };
+} // namespace kernel::filesystem::ext2
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/ext2/linked_directory_entry.hpp b/kernel/include/kernel/filesystem/ext2/linked_directory_entry.hpp
new file mode 100644
index 0000000..4097cbb
--- /dev/null
+++ b/kernel/include/kernel/filesystem/ext2/linked_directory_entry.hpp
@@ -0,0 +1,22 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_EXT2_LINKED_DIRECTORY_ENTRY_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_EXT2_LINKED_DIRECTORY_ENTRY_HPP
+
+#include <array>
+#include <cstdint>
+
+namespace kernel::filesystem::ext2
+{
+ /**
+ @brief Represents a linked directory entry in the ext2 filesystem.
+ */
+ struct [[gnu::packed]] linked_directory_entry
+ {
+ uint32_t inode;
+ uint16_t rec_len;
+ uint8_t name_len;
+ uint8_t file_type;
+ std::array<char, 255> name; // NOLINT(readability-magic-numbers)
+ };
+} // namespace kernel::filesystem::ext2
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/ext2/superblock.hpp b/kernel/include/kernel/filesystem/ext2/superblock.hpp
new file mode 100644
index 0000000..41ad935
--- /dev/null
+++ b/kernel/include/kernel/filesystem/ext2/superblock.hpp
@@ -0,0 +1,74 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_EXT2_SUPERBLOCK_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_EXT2_SUPERBLOCK_HPP
+
+#include <array>
+#include <cstdint>
+
+namespace kernel::filesystem::ext2
+{
+ /**
+ @brief Represents the superblock in the ext2 filesystem.
+ */
+ struct [[gnu::packed]] superblock
+ {
+ uint32_t inodes_count;
+ uint32_t blocks_count;
+ uint32_t reserved_blocks_count;
+ uint32_t free_blocks_count;
+ uint32_t free_inodes_count;
+ uint32_t first_data_block;
+ uint32_t log_block_size;
+ uint32_t log_frag_size;
+ uint32_t blocks_per_group;
+ uint32_t frags_per_group;
+ uint32_t inodes_per_group;
+ uint32_t mtime;
+ uint32_t wtime;
+ uint16_t mnt_count;
+ uint16_t max_mnt_count;
+ uint16_t magic;
+ uint16_t state;
+ uint16_t errors;
+ uint16_t minor_rev_level;
+ uint32_t lastcheck;
+ uint32_t checkinterval;
+ uint32_t creator_os;
+ uint32_t rev_level;
+ uint16_t def_resuid;
+ uint16_t def_resgid;
+
+ // EXT2_DYNAMIC_REV superblock only
+ uint32_t first_ino;
+ uint16_t inode_size;
+ uint16_t block_group_nr;
+ uint32_t feature_compat;
+ uint32_t feature_incompat;
+ uint32_t feature_ro_compat;
+ std::array<uint8_t, 16> uuid;
+ std::array<uint8_t, 16> volume_name;
+ std::array<uint8_t, 64> last_mounted;
+ uint32_t algorithm_usage_bitmap;
+
+ // Performance Hints
+ uint8_t prealloc_blocks;
+ uint8_t prealloc_dir_blocks;
+ uint16_t padding1;
+
+ // Journaling Support
+ std::array<uint8_t, 16> journal_uuid;
+ uint32_t journal_inum;
+ uint32_t journal_dev;
+ uint32_t last_orphan;
+
+ // Directory Indexing Support
+ std::array<uint32_t, 4> hash_seed;
+ uint8_t def_hash_version;
+ std::array<uint8_t, 3> padding2;
+
+ // Other options
+ uint32_t default_mount_options;
+ uint32_t first_meta_bg;
+ std::array<uint8_t, 760> unused; // NOLINT(readability-magic-numbers)
+ };
+} // namespace kernel::filesystem::ext2
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/filesystem.hpp b/kernel/include/kernel/filesystem/filesystem.hpp
new file mode 100644
index 0000000..bec1b16
--- /dev/null
+++ b/kernel/include/kernel/filesystem/filesystem.hpp
@@ -0,0 +1,79 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_FILESYSTEM_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_FILESYSTEM_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <string_view>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief A base class for implementing filesystems in the kernel. This class provides a common interface for managing
+ files and directories within the virtual filesystem.
+ */
+ struct filesystem
+ {
+ enum class operation_result : int
+ {
+ success = 0,
+ invalid_magic_number = -1,
+ invalid_root_inode = -2,
+ unmount_failed = -3
+ };
+
+ /**
+ @brief Virtual destructor for the filesystem.
+ */
+ virtual ~filesystem() = default;
+
+ /**
+ @brief Probes the given @p backing_inode to determine if it contains a recognizable filesystem, and if so, mounts it
+ and returns a pointer to the mounted filesystem instance. This method iterates through known filesystem types and
+ attempts to initialize it with the backing inode until the mount was successful or all types have been tried.
+ @param backing_inode The inode to probe and mount.
+ @return A pointer to the mounted filesystem instance if successful, or a null pointer if no recognizable filesystem
+ is found on the backing inode.
+ @warning Panics if @p backing_inode is null.
+ */
+ auto static probe_and_mount(kstd::shared_ptr<inode> const & backing_inode) -> kstd::shared_ptr<filesystem>;
+
+ /**
+ @brief Initializes the filesystem with the given @p backing_inode.
+ @param backing_inode The inode to use as the backing inode for the filesystem. (This is typically the inode
+ representing the block device or another inode which contains the filesystem data.)
+ @return The result of the mount operation.
+ */
+ virtual auto mount(kstd::shared_ptr<inode> const & backing_inode) -> operation_result;
+
+ /**
+ @brief Looks up a child inode within the given @p parent inode with the specified @p name. This method must be
+ implemented by concrete filesystem subclasses to provide the logic for traversing the filesystem structure and
+ finding the requested inode.
+ @param parent The parent inode.
+ @param name The name of the child inode to look up.
+ @return A pointer to the requested child inode, or a null pointer if not found.
+ */
+ [[nodiscard]] virtual auto lookup(kstd::shared_ptr<inode> const & parent, std::string_view name) const
+ -> kstd::shared_ptr<inode> = 0;
+
+ /**
+ @brief Returns a reference to the root inode of the filesystem.
+ */
+ [[nodiscard]] auto root_inode() const -> kstd::shared_ptr<inode> const &;
+
+ /**
+ @brief Returns a reference to the backing inode of the filesystem.
+ */
+ [[nodiscard]] auto backing_inode() const -> kstd::shared_ptr<inode> const &;
+
+ protected:
+ kstd::shared_ptr<inode> m_root_inode{};
+ kstd::shared_ptr<inode> m_backing_inode{};
+ };
+
+} // namespace kernel::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/inode.hpp b/kernel/include/kernel/filesystem/inode.hpp
new file mode 100644
index 0000000..b34b921
--- /dev/null
+++ b/kernel/include/kernel/filesystem/inode.hpp
@@ -0,0 +1,69 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_INODE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_INODE_HPP
+
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief Represents an inode in the filesystem.
+ */
+ struct inode
+ {
+ /**
+ @brief Create an inode.
+ */
+ inode() = default;
+
+ /**
+ @brief Virtual destructor for the inode.
+ */
+ virtual ~inode() = default;
+
+ /**
+ @brief Reads from the inode into a @p buffer, starting at the specified @p offset and for a given @p size. This
+ method must be implemented by concrete inode subclasses.
+ @param buffer Destination buffer.
+ @param offset Read offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes read.
+ */
+ virtual auto read(void * buffer, size_t offset, size_t size) const -> size_t = 0;
+
+ /**
+ @brief Writes to the inode into a @p buffer, starting at the specified @p offset and for a given @p size. This
+ method must be implemented by concrete inode subclasses.
+ @param buffer Source buffer.
+ @param offset Write offset in bytes.
+ @param size Number of bytes to write.
+ @return Number of bytes written.
+ */
+ virtual auto write(void const * buffer, size_t offset, size_t size) -> size_t = 0;
+
+ /**
+ @brief Returns whether the inode is a directory.
+ @return true if the inode is a directory, false otherwise.
+ */
+ [[nodiscard]] virtual auto is_directory() const -> bool;
+
+ /**
+ @brief Returns whether the inode is a regular file.
+ @return true if the inode is a regular file, false otherwise.
+ */
+ [[nodiscard]] virtual auto is_regular() const -> bool;
+
+ /**
+ @brief Returns whether the inode is a device.
+ @return true if the inode is a device, false otherwise.
+ */
+ [[nodiscard]] virtual auto is_device() const -> bool;
+
+ /**
+ @brief Returns whether the inode is a symbolic link.
+ @return true if the inode is a symbolic link, false otherwise.
+ */
+ [[nodiscard]] virtual auto is_symbolic_link() const -> bool;
+ };
+} // namespace kernel::filesystem
+
+#endif
diff --git a/kernel/include/kernel/filesystem/mount.hpp b/kernel/include/kernel/filesystem/mount.hpp
new file mode 100644
index 0000000..ced4f81
--- /dev/null
+++ b/kernel/include/kernel/filesystem/mount.hpp
@@ -0,0 +1,97 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_MOUNT_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_MOUNT_HPP
+
+#include <kernel/filesystem/dentry.hpp>
+#include <kernel/filesystem/filesystem.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+
+#include <atomic>
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief Represents a mounted filesystem in the kernel.
+ */
+ struct mount
+ {
+ /**
+ @brief Creates a mount for the given @p filesystem at the specified @p mount_path. The @p mount_dentry represents
+ the dentry where the filesystem is mounted, and the @p root_dentry represents the root dentry of the mounted
+ filesystem.
+ @param mount_dentry The dentry where the filesystem is mounted.
+ @param root_dentry The root dentry of the mounted filesystem.
+ @param fs The filesystem instance being mounted.
+ @param parent_mount The mount that contains the mount_dentry.
+ @param source_mount The mount that the filesystem originates from.
+ */
+ 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);
+
+ /**
+ @brief Get the dentry where the filesystem is mounted.
+ */
+ [[nodiscard]] auto mount_dentry() const -> kstd::shared_ptr<dentry> const &;
+
+ /**
+ @brief Get the root dentry of the mounted filesystem.
+ */
+ [[nodiscard]] auto root_dentry() const -> kstd::shared_ptr<dentry> const &;
+
+ /**
+ @brief Get the filesystem instance being mounted.
+ */
+ [[nodiscard]] auto get_filesystem() const -> kstd::shared_ptr<filesystem> const &;
+
+ /**
+ @brief Get the path at which the filesystem is mounted.
+ */
+ [[nodiscard]] auto mount_path() const -> kstd::string;
+
+ /**
+ @brief Get the parent mount that this mount was attached beneath.
+ */
+ [[nodiscard]] auto parent_mount() const -> kstd::shared_ptr<mount> const &;
+
+ /**
+ @brief Get the source mount where this mount originates from.
+ */
+ [[nodiscard]] auto source_mount() const -> kstd::shared_ptr<mount>;
+
+ /**
+ @brief Increment the reference count for this mount.
+ */
+ auto increment_ref_count() -> void;
+
+ /**
+ @brief Decrement the reference count for this mount.
+ @warning Throws if ref_count is zero.
+ */
+ auto decrement_ref_count() -> void;
+
+ /**
+ @brief Check if the mount is ready to be unmounted.
+ @return True if the mount is ready to be unmounted, false otherwise.
+ */
+ [[nodiscard]] auto is_ready_to_unmount() const -> bool;
+
+ /**
+ @brief Get the current reference count for this mount.
+ @return The current reference count.
+ */
+ [[nodiscard]] auto ref_count() const -> size_t;
+
+ private:
+ kstd::shared_ptr<dentry> m_mount_dentry{};
+ kstd::shared_ptr<dentry> m_root_dentry{};
+ kstd::shared_ptr<filesystem> m_filesystem{};
+ kstd::shared_ptr<mount> m_parent_mount{};
+ kstd::weak_ptr<mount> m_source_mount{};
+ std::atomic_size_t m_ref_count{0};
+ };
+} // namespace kernel::filesystem
+
+#endif
diff --git a/kernel/include/kernel/filesystem/mount_table.hpp b/kernel/include/kernel/filesystem/mount_table.hpp
new file mode 100644
index 0000000..4f2d1b7
--- /dev/null
+++ b/kernel/include/kernel/filesystem/mount_table.hpp
@@ -0,0 +1,58 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_MOUNT_TABLE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_MOUNT_TABLE_HPP
+
+#include <kernel/filesystem/mount.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <string_view>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief A table for managing mounted filesystems in the kernel.
+ */
+ struct mount_table
+ {
+ /**
+ @brief Results for mount table operations.
+ */
+ enum class operation_result : int
+ {
+ removed = 0,
+ has_child_mounts = -1,
+ mount_not_found = -2,
+ cannot_be_unmounted = -3
+ };
+
+ /**
+ @brief Adds a mount to the table.
+ @param mount The mount to add.
+ */
+ auto add_mount(kstd::shared_ptr<mount> const & mount) -> void;
+
+ /**
+ @brief Removes the topmost mount at the given @p path.
+ @param path The mount path to remove.
+ @return The result of the removal operation.
+ */
+ [[nodiscard]] auto remove_mount(std::string_view path) -> operation_result;
+
+ /**
+ @brief Finds the mount with the exact mount path matching the given @p path.
+ @param path The path to match against the mount paths in the table.
+ @return A pointer to the mount with the exact matching path, or a null pointer if no mount matches the path.
+ */
+ [[nodiscard]] auto find_mount(std::string_view path) const -> kstd::shared_ptr<mount>;
+
+ private:
+ [[nodiscard]] auto has_child_mounts(kstd::shared_ptr<mount> const & parent_mount) const -> bool;
+ [[nodiscard]] auto find_mount_iterator(std::string_view path) const
+ -> kstd::vector<kstd::shared_ptr<mount>>::const_iterator;
+
+ kstd::vector<kstd::shared_ptr<mount>> m_mounts;
+ };
+} // namespace kernel::filesystem
+
+#endif
diff --git a/kernel/include/kernel/filesystem/open_file_descriptor.hpp b/kernel/include/kernel/filesystem/open_file_descriptor.hpp
new file mode 100644
index 0000000..fd10e64
--- /dev/null
+++ b/kernel/include/kernel/filesystem/open_file_descriptor.hpp
@@ -0,0 +1,68 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_OPEN_FILE_DESCRIPTOR_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_OPEN_FILE_DESCRIPTOR_HPP
+
+#include <kernel/filesystem/dentry.hpp>
+
+#include <kstd/memory>
+
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief Represents an open file descriptor in the filesystem. This class encapsulates the state of an open file,
+ including a reference to the associated dentry and the current file offset.
+ */
+ struct open_file_descriptor
+ {
+ /**
+ @brief Constructs an open file descriptor for the given @p dentry.
+ @param dentry The dentry to associate with the open file descriptor.
+ */
+ explicit open_file_descriptor(kstd::shared_ptr<dentry> const & dentry);
+
+ /**
+ @brief Destructor for the open file descriptor.
+ */
+ ~open_file_descriptor() = default;
+
+ /**
+ @brief Reads data from the open file descriptor into a @p buffer, starting at the current file offset and for a
+ given
+ @p size. The file offset is advanced by the number of bytes read.
+ @param buffer The buffer to read data into.
+ @param size The number of bytes to read.
+ @return The number of bytes read.
+ */
+ auto read(void * buffer, size_t size) -> size_t;
+
+ /**
+ @brief Writes data to the open file descriptor from a @p buffer, starting at the current file offset and for a
+ given
+ @p size. The file offset is advanced by the number of bytes written.
+ @param buffer The buffer to write data from.
+ @param size The number of bytes to write.
+ @return The number of bytes written.
+ */
+ auto write(void const * buffer, size_t size) -> size_t;
+
+ /**
+ @brief Returns the current file offset for this open file descriptor.
+ @return The current file offset in bytes.
+ */
+ [[nodiscard]] auto offset() const -> size_t;
+
+ /**
+ @brief Return a reference to the dentry associated with this open file descriptor.
+ @return A reference to the associated dentry.
+ */
+ [[nodiscard]] auto get_dentry() const -> kstd::shared_ptr<dentry> const &;
+
+ private:
+ kstd::shared_ptr<dentry> m_dentry;
+ size_t m_offset;
+ };
+
+} // namespace kernel::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/open_file_table.hpp b/kernel/include/kernel/filesystem/open_file_table.hpp
new file mode 100644
index 0000000..7e754ac
--- /dev/null
+++ b/kernel/include/kernel/filesystem/open_file_table.hpp
@@ -0,0 +1,66 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_OPEN_FILE_TABLE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_OPEN_FILE_TABLE_HPP
+
+#include <kernel/filesystem/open_file_descriptor.hpp>
+
+#include <kstd/memory>
+#include <kstd/unikstd.h>
+#include <kstd/vector>
+
+#include <cstddef>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief A table for managing file descriptors in the filesystem. This class provides methods for adding, retrieving,
+ and removing open file descriptors.
+ */
+ struct open_file_table
+ {
+ /**
+ @brief Initialize the global open file table. This method creates the singleton instance of the open file table.
+ @warning Panics if called more than once.
+ */
+ auto static init() -> void;
+
+ /**
+ @brief Get the global open file table instance.
+ @return A reference to the global open file table.
+ @warning Panics if the open file table has not been initialized.
+ */
+ auto static get() -> open_file_table &;
+
+ /**
+ @brief Destructor for the open file table.
+ */
+ ~open_file_table() = default;
+
+ /**
+ @brief Add a file to the open file table.
+ @param fd The file descriptor to add.
+ @return The file descriptor index assigned to the file, or -1 on failure.
+ */
+ auto add_file(kstd::shared_ptr<open_file_descriptor> const & fd) -> kstd::ssize_t;
+
+ /**
+ @brief Get a file from the open file table.
+ @param fd The file descriptor index to retrieve.
+ @return A pointer to the requested file descriptor, or a null pointer if not found.
+ */
+ [[nodiscard]] auto file(size_t fd) const -> kstd::shared_ptr<open_file_descriptor>;
+
+ /**
+ @brief Remove a file from the open file table.
+ @param fd The file descriptor index to remove.
+ @return 0 on success, or -1 on failure.
+ */
+ auto remove_file(size_t fd) -> kstd::ssize_t;
+
+ private:
+ open_file_table() = default;
+
+ kstd::vector<kstd::shared_ptr<open_file_descriptor>> m_open_files{};
+ };
+} // namespace kernel::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/path.hpp b/kernel/include/kernel/filesystem/path.hpp
new file mode 100644
index 0000000..4845bf1
--- /dev/null
+++ b/kernel/include/kernel/filesystem/path.hpp
@@ -0,0 +1,71 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_PATH_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_PATH_HPP
+
+#include <kernel/filesystem/constants.hpp>
+
+#include <kstd/string>
+
+#include <ranges>
+#include <string_view>
+
+namespace kernel::filesystem::path
+{
+ /**
+ @brief Provides utilities for handling filesystem paths, including validation and splitting into components.
+ */
+
+ /**
+ @brief Checks if the given path is within the maximum allowed length.
+ @param path The path to check.
+ @return true if the path length is valid, false otherwise.
+ */
+ auto inline is_valid_path_length(std::string_view path) -> bool
+ {
+ return path.length() < kernel::filesystem::constants::max_path_length;
+ }
+
+ /**
+ @brief Checks if the given path is a valid absolute path (starts with '/').
+ @param path The path to check.
+ @return true if the path is a valid absolute path, false otherwise.
+ */
+ auto inline is_valid_absolute_path(std::string_view path) -> bool
+ {
+ return !path.empty() && path.front() == '/' && is_valid_path_length(path);
+ }
+
+ /**
+ @brief Checks if the given path is a valid relative path (doesn't start with '/').
+ @param path The path to check.
+ @return true if the path is a valid relative path, false otherwise.
+ */
+ auto inline is_valid_relative_path(std::string_view path) -> bool
+ {
+ return !path.empty() && path.front() != '/' && is_valid_path_length(path);
+ }
+
+ /**
+ @brief Checks if the given path is a valid path (either absolute or relative).
+ @param path The path to check.
+ @return true if the path is a valid path, false otherwise.
+ */
+ auto inline is_valid_path(std::string_view path) -> bool
+ {
+ return is_valid_absolute_path(path) || is_valid_relative_path(path);
+ }
+
+ /**
+ @brief Splits the given path into its components.
+ @param path The path to split.
+ @return A range of strings representing the components of the path.
+ */
+ auto inline split(std::string_view path)
+ {
+ return std::views::split(path, '/') | std::views::filter([](auto const & part) { return !part.empty(); }) |
+ std::views::transform(
+ [](auto const & part) { return kstd::string(std::string_view(part.begin(), part.end())); });
+ }
+
+} // namespace kernel::filesystem::path
+
+#endif // TEACH_OS_KERNEL_FILESYSTEM_PATH_HPP \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/rootfs/filesystem.hpp b/kernel/include/kernel/filesystem/rootfs/filesystem.hpp
new file mode 100644
index 0000000..3c2dcb1
--- /dev/null
+++ b/kernel/include/kernel/filesystem/rootfs/filesystem.hpp
@@ -0,0 +1,41 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_ROOTFS_FILESYSTEM_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_ROOTFS_FILESYSTEM_HPP
+
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <string_view>
+
+namespace kernel::filesystem::rootfs
+{
+ /**
+ @brief A filesystem for the root filesystem. This filesystem provides access to the root directory and its contents,
+ which are typically populated by the init process during system startup. The rootfs filesystem serves as the top-level
+ directory in the filesystem hierarchy. It is responsible for providing a stable and consistent interface to the root
+ directory.
+ */
+ struct filesystem : kernel::filesystem::filesystem
+ {
+ /**
+ @brief Initializes the rootfs filesystem with the given @p backing_inode.
+ @param backing_inode The backing inode to mount (not required by rootfs).
+ @return The result of the mount operation.
+ */
+ auto mount(kstd::shared_ptr<kernel::filesystem::inode> const & backing_inode) -> operation_result override;
+
+ /**
+ @brief Looks up an inode by @p name within a @p parent directory.
+ @param parent The parent directory inode.
+ @param name The name of the inode to look up.
+ @return Always returns nullptr.
+ */
+ [[nodiscard]] auto lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) const
+ -> kstd::shared_ptr<kernel::filesystem::inode> override;
+ };
+} // namespace kernel::filesystem::rootfs
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/filesystem/rootfs/inode.hpp b/kernel/include/kernel/filesystem/rootfs/inode.hpp
new file mode 100644
index 0000000..0f21eaa
--- /dev/null
+++ b/kernel/include/kernel/filesystem/rootfs/inode.hpp
@@ -0,0 +1,45 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_ROOTFS_INODE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_ROOTFS_INODE_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <cstddef>
+
+namespace kernel::filesystem::rootfs
+{
+ /**
+ @brief Represents an inode in the rootfs filesystem.
+ */
+ struct inode : kernel::filesystem::inode
+ {
+ /**
+ @brief Reads from the rootfs directory inode.
+ @param buffer Destination buffer.
+ @param offset Read offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes read (always 0 because this inode does not expose file data).
+ */
+ auto read(void * buffer, size_t offset, size_t size) const -> size_t override;
+
+ /**
+ @brief Writes to the rootfs directory inode.
+ @param buffer Source buffer.
+ @param offset Write offset in bytes.
+ @param size Number of bytes requested.
+ @return Number of bytes written (always 0 because writes are not supported for this inode).
+ */
+ auto write(void const * buffer, size_t offset, size_t size) -> size_t override;
+
+ /**
+ @brief Check if this inode represents a directory.
+ @return returns true, since this inode represents the / directory in the rootfs filesystem.
+ */
+ [[nodiscard]] auto is_directory() const -> bool override;
+ };
+} // namespace kernel::filesystem::rootfs
+
+#endif
diff --git a/kernel/include/kernel/filesystem/type.hpp b/kernel/include/kernel/filesystem/type.hpp
new file mode 100644
index 0000000..0948e54
--- /dev/null
+++ b/kernel/include/kernel/filesystem/type.hpp
@@ -0,0 +1,42 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_TYPE_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_TYPE_HPP
+
+#include <kernel/filesystem/filesystem.hpp>
+
+#include <kstd/memory>
+
+#include <string_view>
+
+namespace kernel::filesystem
+{
+
+ //! A type descriptor for a filesystem driver.
+ //!
+ //! Each filesystem must expose an instance of a class derived from this type in order to be registered with the vfs
+ //! filesystem registry.
+ struct type
+ {
+ virtual ~type() = default;
+
+ //! Get the name of the filesystem represented by this descriptor.
+ [[nodiscard]] virtual auto name() const noexcept -> std::string_view = 0;
+
+ //! Check if filesystems of this type require a device to back them.
+ [[nodiscard]] virtual auto requires_device() const noexcept -> bool = 0;
+
+ //! Create a new instance of the filesytem represented by this descriptor.
+ [[nodiscard]] virtual auto make_instance() const -> kstd::shared_ptr<filesystem> = 0;
+ };
+
+ template<typename Type>
+ struct type_registration
+ {
+ constexpr auto static instance = Type{};
+ [[using gnu: section("fs_types"), used, visibility("hidden")]] constexpr auto static pointer{
+ kstd::make_observer<type const>(&instance),
+ };
+ };
+
+} // namespace kernel::filesystem
+
+#endif
diff --git a/kernel/include/kernel/filesystem/type_registry.hpp b/kernel/include/kernel/filesystem/type_registry.hpp
new file mode 100644
index 0000000..3be7295
--- /dev/null
+++ b/kernel/include/kernel/filesystem/type_registry.hpp
@@ -0,0 +1,53 @@
+#ifndef TEACH_OS_KERNEL_TYPE_REGISRY_HPP
+#define TEACH_OS_KERNEL_TYPE_REGISRY_HPP
+
+#include <kernel/filesystem/type.hpp>
+
+#include <kstd/flat_map>
+#include <kstd/memory>
+#include <kstd/string>
+
+#include <cstddef>
+#include <span>
+#include <string_view>
+
+namespace kernel::filesystem
+{
+
+ struct type_registry
+ {
+ using value_type = type;
+ using pointer = kstd::observer_ptr<value_type const>;
+
+ auto static init() -> void;
+ auto static get() -> type_registry &;
+
+ constexpr type_registry() noexcept = default;
+
+ //! Add a type descriptor to this registry.
+ //!
+ //! This function will register the given descriptor with this registry, given that no descriptor for a filesystem
+ //! with the same name exists in this registry already.
+ //!
+ //! @param descriptor The filesystem type descriptor to add to the registry.
+ //! @return @p true iff. the descriptor was successfully added, @p false if not.
+ auto add(pointer descriptor) -> bool;
+
+ //! Get all currently registered type descriptors.
+ //!
+ //! @return A span containing all currently registered filesystem type descriptors.
+ [[nodiscard]] auto all() const noexcept -> std::span<pointer const>;
+
+ //! Get the number of registered filesystem types.
+ //!
+ //! @return The number of filesystem descriptors currently registered with this registry.
+ [[nodiscard]] auto size() const noexcept -> std::size_t;
+
+ private:
+ //! A map from filesystem names (identifiers) to filesystem type descriptors.
+ kstd::flat_map<std::string_view, pointer> m_descriptors{};
+ };
+
+} // namespace kernel::filesystem
+
+#endif
diff --git a/kernel/include/kernel/filesystem/vfs.hpp b/kernel/include/kernel/filesystem/vfs.hpp
new file mode 100644
index 0000000..ddc9a9b
--- /dev/null
+++ b/kernel/include/kernel/filesystem/vfs.hpp
@@ -0,0 +1,113 @@
+#ifndef TEACH_OS_KERNEL_FILESYSTEM_VFS_HPP
+#define TEACH_OS_KERNEL_FILESYSTEM_VFS_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 <kstd/memory>
+
+#include <string_view>
+#include <utility>
+
+namespace kernel::filesystem
+{
+ /**
+ @brief The virtual filesystem (VFS) is responsible for managing mounted filesystems and providing a unified interface
+ for file operations across different filesystem types. The VFS maintains a mount table to keep track of mounted
+ filesystems and their associated mount points. It provides methods for opening files by path, which involvesresolving
+ the path to the appropriate mounted filesystem and delegating the file operation to that filesystem's implementation.
+ */
+ struct vfs
+ {
+ /**
+ @brief Results for VFS operations.
+ */
+ enum class operation_result : int
+ {
+ success = 0,
+ invalid_path = -1,
+ non_existent_path = -2,
+ mount_point_not_found = -3,
+ unmount_failed = -4,
+ invalid_filesystem = -5
+ };
+
+ vfs();
+
+ /**
+ @brief Initialize the virtual filesystem.
+ @warning Panics if the VFS has already been initialized.
+ */
+ auto static init() -> void;
+
+ /**
+ @brief Get the singleton instance of the virtual filesystem.
+ @return A reference to the VFS instance.
+ @warning Panics if the VFS has not been initialized yet.
+ */
+ auto static get() -> vfs &;
+
+ /**
+ @brief Destructor for the VFS.
+ */
+ ~vfs() = default;
+
+ /**
+ @brief Open a file by its @p path. This method resolves the path and returns the corresponding dentry.
+ @param path The path to the file to open.
+ @return A shared pointer to the dentry or a null pointer if the file could not be opened.
+ */
+ auto open(std::string_view path) -> kstd::shared_ptr<dentry>;
+
+ /**
+ @brief Close a file by its associated @p path.
+ @param path The path to the file to close.
+ @return The result of the close operation.
+ */
+ auto close(std::string_view path) -> operation_result;
+
+ /**
+ @brief Mount a @p source path to a specific @p target path.
+ @param source The source of the filesystem to mount.
+ @param target The path where the filesystem should be mounted.
+ @return The result of the mount operation.
+ */
+ auto do_mount(std::string_view source, std::string_view target) -> operation_result;
+
+ /**
+ @brief Unmount the filesystem mounted at the specified @p path.
+ @param path The path where the filesystem is mounted.
+ @return The result of the unmount operation.
+ */
+ auto unmount(std::string_view path) -> operation_result;
+
+ private:
+ /**
+ * Note: Resolving a dentry requires traversing mount points; since the
+ * associated 'mount' object is discovered as a byproduct of this
+ * traversal, we return it alongside the dentry to avoid redundant
+ * lookups in callers that require mount context.
+ *
+ * If only one component is needed, the convenience wrappers can be used:
+ * - resolve_path() for the dentry only.
+ * - find_mount() for the mount context only.
+ */
+ [[nodiscard]] auto resolve_path_internal(std::string_view path) const
+ -> std::pair<kstd::shared_ptr<dentry>, kstd::shared_ptr<mount>>;
+ [[nodiscard]] auto resolve_path(std::string_view path) const -> kstd::shared_ptr<dentry>;
+ [[nodiscard]] auto find_mount(std::string_view path) const -> kstd::shared_ptr<mount>;
+
+ auto 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 = nullptr) -> void;
+
+ auto graft_persistent_device_fs(kstd::shared_ptr<devfs::filesystem> const & device_fs) -> void;
+
+ mount_table m_mount_table{};
+ };
+} // namespace kernel::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/memory.hpp b/kernel/include/kernel/memory.hpp
index f09c519..17fb82b 100644
--- a/kernel/include/kernel/memory.hpp
+++ b/kernel/include/kernel/memory.hpp
@@ -1,9 +1,9 @@
#ifndef TEACHOS_KERNEL_MEMORY_HPP
#define TEACHOS_KERNEL_MEMORY_HPP
-#include "kapi/memory.hpp"
+#include <kernel/memory/heap_allocator.hpp> // IWYU pragma: export
-#include "kernel/memory/heap_allocator.hpp" // IWYU pragma: export
+#include <kapi/memory.hpp>
namespace kernel::memory
{
diff --git a/kernel/include/kernel/memory/bitmap_allocator.hpp b/kernel/include/kernel/memory/bitmap_allocator.hpp
index fb5bf55..370ce64 100644
--- a/kernel/include/kernel/memory/bitmap_allocator.hpp
+++ b/kernel/include/kernel/memory/bitmap_allocator.hpp
@@ -1,7 +1,7 @@
#ifndef TEACHOS_KERNEL_MEMORY_BITMAP_ALLOCATOR_HPP
#define TEACHOS_KERNEL_MEMORY_BITMAP_ALLOCATOR_HPP
-#include "kapi/memory.hpp"
+#include <kapi/memory.hpp>
#include <cstddef>
#include <cstdint>
diff --git a/kernel/include/kernel/memory/block_list_allocator.hpp b/kernel/include/kernel/memory/block_list_allocator.hpp
index 5e81c44..51b226e 100644
--- a/kernel/include/kernel/memory/block_list_allocator.hpp
+++ b/kernel/include/kernel/memory/block_list_allocator.hpp
@@ -1,11 +1,12 @@
#ifndef TEACHOS_KERNEL_MEMORY_BLOCK_LIST_ALLOCATOR_HPP
#define TEACHOS_KERNEL_MEMORY_BLOCK_LIST_ALLOCATOR_HPP
-#include "kapi/memory.hpp"
+#include <kernel/memory/heap_allocator.hpp>
-#include "kernel/memory/heap_allocator.hpp"
+#include <kapi/memory.hpp>
#include <kstd/mutex>
+#include <kstd/units>
#include <cstddef>
#include <new>
@@ -32,7 +33,7 @@ namespace kernel::memory
//! @param size The size of the block to allocate
//! @param alignment The desired alignment of the allocated block
//! @return A pointer to the beginning of the block on success, @p nullptr otherwise.
- [[nodiscard]] auto allocate(std::size_t size, std::align_val_t alignment) noexcept -> void * override;
+ [[nodiscard]] auto allocate(kstd::units::bytes size, kstd::units::bytes alignment) noexcept -> void * override;
//! Deallocate a block of memory previously allocated.
//!
@@ -42,26 +43,26 @@ namespace kernel::memory
private:
struct block_header final
{
- std::size_t usable_size;
- bool free;
- block_header * next;
- block_header * prev;
+ kstd::units::bytes usable_size{};
+ bool free{};
+ block_header * next{};
+ block_header * prev{};
};
//! The size of the metadata required for each allocated block.
//!
//! Each allocated block carries a block header, like any unallocated one, but in addition also has a back-pointer
//! to the block header to support padding due to alignment.
- constexpr auto static allocated_metadata_size = sizeof(block_header) + sizeof(block_header *);
+ constexpr auto static allocated_metadata_size = kstd::units::bytes{sizeof(block_header) + sizeof(block_header *)};
//! The minimum number of bytes for an allocation.
- constexpr auto static minimum_allocation_size = 16uz;
+ constexpr auto static minimum_allocation_size = kstd::units::bytes{16uz};
//! Try to expand the heap to accommodate the given size.
//!
//! @param delta The size to expand the heap by.
//! @return @p true if the heap was expanded, @p false otherwise.
- auto expand(std::size_t delta) noexcept -> bool;
+ auto expand(kstd::units::bytes delta) noexcept -> bool;
//! Split a given free block to accommodate and allocation.
//!
@@ -70,7 +71,7 @@ namespace kernel::memory
//! @param block The block to split.
//! @param size The size of the allocation.
//! @param padding The amount of padding to apply.
- auto split(block_header * block, std::size_t size, std::size_t padding) noexcept -> void;
+ auto split(block_header * block, kstd::units::bytes size, kstd::units::bytes padding) noexcept -> void;
//! Try to coalesce a given block with it's preceding and/or following block.
//!
@@ -86,4 +87,4 @@ namespace kernel::memory
} // namespace kernel::memory
-#endif \ No newline at end of file
+#endif
diff --git a/kernel/include/kernel/memory/heap_allocator.hpp b/kernel/include/kernel/memory/heap_allocator.hpp
index bc771e3..fd39bef 100644
--- a/kernel/include/kernel/memory/heap_allocator.hpp
+++ b/kernel/include/kernel/memory/heap_allocator.hpp
@@ -1,10 +1,7 @@
#ifndef TEACHOS_KERNEL_MEMORY_HEAP_ALLOCATOR_HPP
#define TEACHOS_KERNEL_MEMORY_HEAP_ALLOCATOR_HPP
-#include <kstd/mutex>
-
-#include <cstddef>
-#include <new>
+#include <kstd/units>
namespace kernel::memory
{
@@ -19,7 +16,7 @@ namespace kernel::memory
//! @param size The size of the block to allocate
//! @param alignment The desired alignment of the allocated block
//! @return A pointer to the beginning of the block on success, @p nullptr otherwise.
- [[nodiscard]] virtual auto allocate(std::size_t size, std::align_val_t alignment) noexcept -> void * = 0;
+ [[nodiscard]] virtual auto allocate(kstd::units::bytes size, kstd::units::bytes alignment) noexcept -> void * = 0;
//! Deallocate a block of memory previously allocated.
//!
diff --git a/kernel/include/kernel/memory/mmio_allocator.hpp b/kernel/include/kernel/memory/mmio_allocator.hpp
new file mode 100644
index 0000000..c7a8ed0
--- /dev/null
+++ b/kernel/include/kernel/memory/mmio_allocator.hpp
@@ -0,0 +1,41 @@
+#ifndef TEACHOS_KERNEL_MEMORY_MMIO_ALLOCATOR_HPP
+#define TEACHOS_KERNEL_MEMORY_MMIO_ALLOCATOR_HPP
+
+#include <kapi/memory.hpp>
+
+#include <kstd/allocator>
+#include <kstd/memory>
+
+#include <cstddef>
+
+namespace kernel::memory
+{
+
+ struct mmio_allocator
+ {
+ mmio_allocator(kapi::memory::linear_address base, std::size_t pages);
+
+ [[nodiscard]] auto allocate(std::size_t page_count) -> kapi::memory::linear_address;
+ auto release(kapi::memory::linear_address base) -> void;
+
+ private:
+ struct node
+ {
+ kapi::memory::linear_address base{};
+ std::size_t page_count{};
+ node * next{};
+ node * previous{};
+ bool is_free{};
+ };
+
+ auto make_node(kapi::memory::linear_address base, std::size_t page_count, node * next, node * previous,
+ bool is_free) -> node *;
+ auto destroy_node(node *) -> void;
+
+ [[no_unique_address]] kstd::allocator<node> m_allocator{};
+ node * m_head{};
+ };
+
+} // namespace kernel::memory
+
+#endif
diff --git a/kernel/include/kernel/test_support/boot_modules.hpp b/kernel/include/kernel/test_support/boot_modules.hpp
new file mode 100644
index 0000000..2343a10
--- /dev/null
+++ b/kernel/include/kernel/test_support/boot_modules.hpp
@@ -0,0 +1,10 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_BOOT_MODULES_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_BOOT_MODULES_HPP
+
+namespace kernel::tests::boot_modules
+{
+ //! Deinitialize the boot module registry.
+ auto deinit() -> void;
+} // namespace kernel::tests::boot_modules
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/bump_frame_allocator.hpp b/kernel/include/kernel/test_support/bump_frame_allocator.hpp
new file mode 100644
index 0000000..a8ffd48
--- /dev/null
+++ b/kernel/include/kernel/test_support/bump_frame_allocator.hpp
@@ -0,0 +1,54 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_BUMP_FRAME_ALLOCATOR_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_BUMP_FRAME_ALLOCATOR_HPP
+
+#include <kapi/memory.hpp>
+
+#include <cstddef>
+#include <optional>
+#include <utility>
+
+namespace kernel::tests
+{
+
+ //! A simple bump-based frame allocator used for initial test bootstrap.
+ //!
+ //! We emulate the expected behavior of a platform, in that there is a handover to the kernel PMM that needs to happen
+ //! during boot. The expectation of the PMM initializer is that there is an active (if simple) frame allocator it can
+ //! use to reserve space for it's own metadata.
+ //!
+ //! @see kapi::memory::init_pmm
+ struct bump_frame_allocator : kapi::memory::frame_allocator
+ {
+ //! @copydoc kapi::memory::frame_allocator::mark_used
+ //!
+ //! @note Due to the simple nature of this allocator, all frames up to the given frame will be marked as used as
+ //! well.
+ auto mark_used(kapi::memory::frame frame) -> void override
+ {
+ if (frame.number() >= next_free_frame)
+ {
+ next_free_frame = frame.number() + 1;
+ }
+ }
+
+ //! @copydoc kapi::memory::frame_allocator::allocate_many
+ auto allocate_many(std::size_t count) noexcept
+ -> std::optional<std::pair<kapi::memory::frame, std::size_t>> override
+ {
+ auto start = next_free_frame;
+ next_free_frame += count;
+ return std::pair{kapi::memory::frame{start}, count};
+ }
+
+ //! @copydoc kapi::memory::frame_allocator::release_many
+ //!
+ //! @note Due to the simple nature of this allocator, frames are never actually released.
+ auto release_many(std::pair<kapi::memory::frame, std::size_t>) -> void override {}
+
+ //! The next free frame to be allocated.
+ std::size_t next_free_frame{};
+ };
+
+} // namespace kernel::tests
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/cio.hpp b/kernel/include/kernel/test_support/cio.hpp
new file mode 100644
index 0000000..afe27e0
--- /dev/null
+++ b/kernel/include/kernel/test_support/cio.hpp
@@ -0,0 +1,39 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_CIO_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_CIO_HPP
+
+#include <kernel/test_support/log_buffer.hpp>
+
+#include <kapi/cio.hpp>
+
+#include <string_view>
+
+namespace kernel::tests::cio
+{
+
+ //! A simple output device that writes to one of the standard output streams and a log buffer.
+ struct output_device : kapi::cio::output_device
+ {
+ //! @copydoc kapi::cio::output_device::write
+ auto write(kapi::cio::output_stream stream, std::string_view text) -> void override;
+
+ //! Get the log buffer associated with this device.
+ //!
+ //! @return The associated log buffer.
+ [[nodiscard]] auto log_buffer() noexcept -> kernel::tests::log_buffer &;
+
+ private:
+ //! The log buffer of this device.
+ kernel::tests::log_buffer m_log_buffer{};
+ };
+
+ //! Deinitialize the CIO subsystem.
+ auto deinit() -> void;
+
+ //! Get the log buffer of the currently active output device.
+ //!
+ //! @throws std::runtime_error if no output device has been registered.
+ //! @return The currently active device's log buffer.
+ [[nodiscard]] auto log_buffer() -> kernel::tests::log_buffer &;
+} // namespace kernel::tests::cio
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/cpu.hpp b/kernel/include/kernel/test_support/cpu.hpp
new file mode 100644
index 0000000..c99d1a7
--- /dev/null
+++ b/kernel/include/kernel/test_support/cpu.hpp
@@ -0,0 +1,23 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_CPU_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_CPU_HPP
+
+#include <stdexcept>
+
+namespace kernel::tests::cpu
+{
+
+ //! Exception thrown when the CPU is halted.
+ struct halt : std::runtime_error
+ {
+ //! Construct a new halt exception.
+ halt()
+ : std::runtime_error{"CPU halt requested!"}
+ {}
+ };
+
+ //! Deinitialize the CPU subsystem.
+ auto deinit() -> void;
+
+} // namespace kernel::tests::cpu
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/devices/block_device.hpp b/kernel/include/kernel/test_support/devices/block_device.hpp
new file mode 100644
index 0000000..89a2bf1
--- /dev/null
+++ b/kernel/include/kernel/test_support/devices/block_device.hpp
@@ -0,0 +1,31 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_DEVICES_BLOCK_DEVICE_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_DEVICES_BLOCK_DEVICE_HPP
+
+#include <kernel/devices/block_device.hpp>
+
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <cstddef>
+#include <cstdint>
+
+namespace kernel::tests::devices
+{
+
+ struct block_device : kernel::devices::block_device
+ {
+ block_device(size_t major, size_t minor, kstd::string const & name, size_t block_size, size_t initial_size = 0);
+
+ auto init() -> bool override;
+
+ auto read_block(size_t block_index, void * buffer) const -> void override;
+ auto write_block(size_t block_index, void const * buffer) -> void override;
+
+ [[nodiscard]] auto size() const -> size_t override;
+
+ kstd::vector<uint8_t> data{};
+ };
+
+} // namespace kernel::tests::devices
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/devices/character_device.hpp b/kernel/include/kernel/test_support/devices/character_device.hpp
new file mode 100644
index 0000000..aba183a
--- /dev/null
+++ b/kernel/include/kernel/test_support/devices/character_device.hpp
@@ -0,0 +1,23 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_DEVICES_CHARACTER_DEVICE_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_DEVICES_CHARACTER_DEVICE_HPP
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <cstddef>
+
+namespace kernel::tests::devices
+{
+ // TODO fix inheritance when character devices are implemented
+ struct character_device : kapi::devices::device
+ {
+ character_device(size_t major, size_t minor, kstd::string const & name);
+
+ auto init() -> bool override;
+ };
+
+} // namespace kernel::tests::devices
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/devices/storage/management.hpp b/kernel/include/kernel/test_support/devices/storage/management.hpp
new file mode 100644
index 0000000..581aa91
--- /dev/null
+++ b/kernel/include/kernel/test_support/devices/storage/management.hpp
@@ -0,0 +1,10 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_DEVICES_STORAGE_MANAGEMENT_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_DEVICES_STORAGE_MANAGEMENT_HPP
+
+namespace kernel::tests::devices::storage::management
+{
+ //! Deinitialize the storage management singleton.
+ auto deinit() -> void;
+} // namespace kernel::tests::devices::storage::management
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/filesystem/ext2.hpp b/kernel/include/kernel/test_support/filesystem/ext2.hpp
new file mode 100644
index 0000000..18cef1c
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/ext2.hpp
@@ -0,0 +1,21 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_EXT2_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_EXT2_HPP
+
+#include <kernel/filesystem/ext2/superblock.hpp>
+#include <kernel/test_support/devices/block_device.hpp>
+
+#include <cstddef>
+#include <cstdint>
+
+namespace kernel::tests::filesystem::ext2
+{
+ auto write_bytes(kernel::tests::devices::block_device & device, size_t offset, void const * source, size_t size)
+ -> void;
+ auto write_u32(kernel::tests::devices::block_device & device, size_t offset, uint32_t value) -> void;
+ auto setup_mock_ext2_layout(kernel::tests::devices::block_device & device) -> void;
+ auto setup_mock_ext2_layout(kernel::tests::devices::block_device & device,
+ kernel::filesystem::ext2::superblock const & superblock) -> void;
+
+} // namespace kernel::tests::filesystem::ext2
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/filesystem/filesystem.hpp b/kernel/include/kernel/test_support/filesystem/filesystem.hpp
new file mode 100644
index 0000000..5f26022
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/filesystem.hpp
@@ -0,0 +1,22 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_FILESYSTEM_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_FILESYSTEM_HPP
+
+#include <kernel/filesystem/filesystem.hpp>
+#include <kernel/filesystem/inode.hpp>
+
+#include <kstd/memory>
+
+#include <string_view>
+
+namespace kernel::tests::filesystem
+{
+ struct filesystem : kernel::filesystem::filesystem
+ {
+ filesystem() = default;
+
+ [[nodiscard]] auto lookup(kstd::shared_ptr<kernel::filesystem::inode> const & parent, std::string_view name) const
+ -> kstd::shared_ptr<kernel::filesystem::inode> override;
+ };
+} // namespace kernel::tests::filesystem
+
+#endif
diff --git a/kernel/include/kernel/test_support/filesystem/inode.hpp b/kernel/include/kernel/test_support/filesystem/inode.hpp
new file mode 100644
index 0000000..8a76437
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/inode.hpp
@@ -0,0 +1,19 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_INODE_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_INODE_HPP
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <cstddef>
+
+namespace kernel::tests::filesystem
+{
+ struct inode : kernel::filesystem::inode
+ {
+ auto read(void * buffer, size_t offset, size_t size) const -> size_t override;
+ auto write(void const * buffer, size_t offset, size_t size) -> size_t override;
+
+ [[nodiscard]] auto is_regular() const -> bool override;
+ };
+} // namespace kernel::tests::filesystem
+
+#endif
diff --git a/kernel/include/kernel/test_support/filesystem/open_file_table.hpp b/kernel/include/kernel/test_support/filesystem/open_file_table.hpp
new file mode 100644
index 0000000..46b0334
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/open_file_table.hpp
@@ -0,0 +1,10 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_OPEN_FILE_TABLE_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_OPEN_FILE_TABLE_HPP
+
+namespace kernel::tests::filesystem::open_file_table
+{
+ //! Deinitialize the open file table singleton.
+ auto deinit() -> void;
+} // namespace kernel::tests::filesystem::open_file_table
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/filesystem/storage_boot_module_fixture.hpp b/kernel/include/kernel/test_support/filesystem/storage_boot_module_fixture.hpp
new file mode 100644
index 0000000..94a6668
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/storage_boot_module_fixture.hpp
@@ -0,0 +1,48 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_STORAGE_BOOT_MODULE_FIXTURE_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_STORAGE_BOOT_MODULE_FIXTURE_HPP
+
+#include <kapi/boot_module/boot_module_registry.hpp>
+
+#include <cstddef>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace kernel::tests::filesystem
+{
+ struct storage_boot_module_fixture
+ {
+ ~storage_boot_module_fixture();
+
+ auto setup_modules(std::size_t module_count, std::size_t module_size = 4096) -> void;
+ auto setup_modules_from_img(std::vector<std::string> const & module_names,
+ std::vector<std::filesystem::path> const & img_paths) -> void;
+
+ protected:
+ struct mapped_image
+ {
+ explicit mapped_image(std::filesystem::path path);
+ ~mapped_image();
+
+ mapped_image(mapped_image const &) = delete;
+ auto operator=(mapped_image const &) -> mapped_image & = delete;
+
+ mapped_image(mapped_image &&) noexcept;
+ auto operator=(mapped_image &&) noexcept -> mapped_image &;
+
+ int file_descriptor;
+ std::byte * mapping;
+ std::size_t size;
+ };
+
+ kapi::boot_modules::boot_module_registry m_registry{};
+ std::vector<std::string> m_module_names{};
+ std::vector<std::vector<std::byte>> m_module_data{};
+ std::vector<mapped_image> m_mapped_images{};
+
+ private:
+ auto setup_module_from_img(std::string const & module_name, std::filesystem::path const & img_path) -> void;
+ };
+} // namespace kernel::tests::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp b/kernel/include/kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp
new file mode 100644
index 0000000..55f4b29
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp
@@ -0,0 +1,23 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_STORAGE_BOOT_MODULE_VFS_FIXTURE_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_STORAGE_BOOT_MODULE_VFS_FIXTURE_HPP
+
+#include <kernel/test_support/filesystem/storage_boot_module_fixture.hpp>
+
+#include <cstddef>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace kernel::tests::filesystem
+{
+ struct storage_boot_module_vfs_fixture : storage_boot_module_fixture
+ {
+ ~storage_boot_module_vfs_fixture();
+
+ auto setup_modules_and_init_vfs(std::size_t module_count, std::size_t module_size = 4096) -> void;
+ auto setup_modules_from_img_and_init_vfs(std::vector<std::string> const & module_names,
+ std::vector<std::filesystem::path> const & img_paths) -> void;
+ };
+} // namespace kernel::tests::filesystem
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/filesystem/vfs.hpp b/kernel/include/kernel/test_support/filesystem/vfs.hpp
new file mode 100644
index 0000000..739e353
--- /dev/null
+++ b/kernel/include/kernel/test_support/filesystem/vfs.hpp
@@ -0,0 +1,10 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_VFS_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_VFS_HPP
+
+namespace kernel::tests::filesystem::vfs
+{
+ //! Deinitialize the VFS singleton.
+ auto deinit() -> void;
+} // namespace kernel::tests::filesystem::vfs
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/log_buffer.hpp b/kernel/include/kernel/test_support/log_buffer.hpp
new file mode 100644
index 0000000..41d9a76
--- /dev/null
+++ b/kernel/include/kernel/test_support/log_buffer.hpp
@@ -0,0 +1,37 @@
+#ifndef KERNEL_TEST_SUPPORT_LOG_BUFFER_HPP
+#define KERNEL_TEST_SUPPORT_LOG_BUFFER_HPP
+
+#include <string>
+#include <vector>
+
+namespace kernel::tests
+{
+
+ struct log_buffer
+ {
+ //! Append a message to this buffer.
+ //! @param message The message to append.
+ auto append(std::string const & message) -> void;
+
+ //! Clear this buffer.
+ auto clear() -> void;
+
+ //! Get all messages in this buffer as a single string.
+ //!
+ //! @return All messages in this buffer as a single string.
+ auto flat_messages() -> std::string;
+
+ //! Get all messages in this buffer.
+ //!
+ //! @note Messages may be split across multiple entries if they are longer than the internal kernel print buffer
+ //! size.
+ //!
+ //! @return All messages in this buffer.
+ auto messages() -> std::vector<std::string> const &;
+
+ private:
+ std::vector<std::string> m_messages{};
+ };
+} // namespace kernel::tests
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/memory.hpp b/kernel/include/kernel/test_support/memory.hpp
new file mode 100644
index 0000000..21030a4
--- /dev/null
+++ b/kernel/include/kernel/test_support/memory.hpp
@@ -0,0 +1,22 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_MEMORY_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_MEMORY_HPP
+
+#include <kapi/memory.hpp>
+
+namespace kernel::tests::memory
+{
+
+ //! Deinitialize the memory subsystem.
+ auto deinit() -> void;
+
+ //! Get the virtual base address of the simulated system.
+ //!
+ //! Since we do not have direct access to an actual MMU, we simulate the virtual address space by mapping the
+ //! physical address space starting at some process local virtual address.
+ //!
+ //! @return The virtual base address of the simulated system.
+ auto virtual_base() -> kapi::memory::linear_address;
+
+} // namespace kernel::tests::memory
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/page_mapper.hpp b/kernel/include/kernel/test_support/page_mapper.hpp
new file mode 100644
index 0000000..be4403b
--- /dev/null
+++ b/kernel/include/kernel/test_support/page_mapper.hpp
@@ -0,0 +1,49 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_PAGE_MAPPER_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_PAGE_MAPPER_HPP
+
+#include <kernel/test_support/simulated_memory.hpp>
+
+#include <kapi/memory.hpp>
+
+#include <kstd/units>
+
+#include <cstddef>
+#include <cstdint>
+#include <unordered_map>
+
+namespace kernel::tests
+{
+
+ struct page_mapper : kapi::memory::page_mapper
+ {
+ //! Construct a new page mapper.
+ //!
+ //! @param physical_size The size of the physical memory.
+ //! @param virtual_size The size of the virtual address space.
+ page_mapper(kstd::units::bytes physical_size, kstd::units::bytes virtual_size);
+
+ //! @copydoc kapi::memory::page_mapper::map
+ //!
+ //! @throws std::invalid_argument if the page has already been mapped.
+ //! @throws std::runtime_error if the page cannot be mapped.
+ //! @throws std::runtime_error if the underlying simulated memory cannot map the page.
+ auto map(kapi::memory::page page, kapi::memory::frame frame, flags) -> std::byte * override;
+
+ //! @copydoc kapi::memory::page_mapper::unmap
+ //!
+ //! @throws std::invalid_argument if the page has not been mapped.
+ auto unmap(kapi::memory::page page) -> void override;
+
+ //! @copydoc kapi::memory::page_mapper::try_unmap
+ auto try_unmap(kapi::memory::page page) noexcept -> bool override;
+
+ //! The simulated memory.
+ kernel::tests::simulated_memory memory;
+
+ //! The simplified page table entries.
+ std::unordered_map<std::uint64_t, kapi::memory::frame> page_mappings;
+ };
+
+} // namespace kernel::tests
+
+#endif \ No newline at end of file
diff --git a/kernel/include/kernel/test_support/simulated_memory.hpp b/kernel/include/kernel/test_support/simulated_memory.hpp
new file mode 100644
index 0000000..a201c3d
--- /dev/null
+++ b/kernel/include/kernel/test_support/simulated_memory.hpp
@@ -0,0 +1,67 @@
+#ifndef TEACHOS_KERNEL_TEST_SUPPORT_SIMULATED_MEMORY_HPP
+#define TEACHOS_KERNEL_TEST_SUPPORT_SIMULATED_MEMORY_HPP
+
+#include <kapi/memory.hpp>
+
+#include <kstd/units>
+
+#include <cstddef>
+
+#include <sys/types.h>
+
+namespace kernel::tests
+{
+
+ //! A simulated memory device that can be used in tests.
+ //!
+ //! The general idea is to provide a means to simulate the physical and virtual address spaces of the system. This
+ //! implementation provides for a single physical and a single virtual address space.
+ //!
+ //! @note This is not a full-featured MMU. It is a simple simulation that can be used for testing only.
+ struct simulated_memory
+ {
+ //! Construct a new simulated memory device.
+ //! @param physical_size The size of the physical memory.
+ //! @param virtual_size The size of the virtual address space.
+ simulated_memory(kstd::units::bytes physical_size, kstd::units::bytes virtual_size);
+
+ //! Destroy this device
+ ~simulated_memory();
+
+ //! Clear the contents of the physical memory of this device.
+ auto clear() -> void;
+
+ //! Get the base address of the physical memory of this device.
+ [[nodiscard]] auto physical_base() noexcept -> std::byte *;
+
+ //! Get the base address of the physical memory of this device.
+ [[nodiscard]] auto physical_base() const noexcept -> std::byte const *;
+
+ //! Get the size of the physical memory of this device.
+ [[nodiscard]] auto physical_size() const noexcept -> kstd::units::bytes;
+
+ //! Get the base address of the virtual address space of this device.
+ [[nodiscard]] auto virtual_base() const noexcept -> kapi::memory::linear_address;
+
+ //! Get the size of the virtual address space of this device.
+ [[nodiscard]] auto virtual_size() const noexcept -> kstd::units::bytes;
+
+ //! Map a region of physical memory to a region of virtual memory.
+ //!
+ //! @param size The size of the region to map.
+ //! @param to The base address of the virtual region.
+ //! @param offset The offset into the physical memory to map.
+ //! @return A pointer to the first byte of the mapped region.
+ [[nodiscard]] auto map(kstd::units::bytes size, std::byte * to, off_t offset) -> std::byte *;
+
+ private:
+ int m_descriptor{};
+ kstd::units::bytes m_physical_size{0};
+ kstd::units::bytes m_virtual_size{0};
+ std::byte * m_physical_base{nullptr};
+ std::byte * m_virtual_base{nullptr};
+ };
+
+} // namespace kernel::tests
+
+#endif \ No newline at end of file
diff --git a/kernel/kapi/acpi.cpp b/kernel/kapi/acpi.cpp
new file mode 100644
index 0000000..b6d5cdf
--- /dev/null
+++ b/kernel/kapi/acpi.cpp
@@ -0,0 +1,40 @@
+#include <kapi/acpi.hpp>
+
+#include <kernel/acpi/manager.hpp>
+
+#include <kapi/system.hpp>
+
+#include <acpi/acpi.hpp>
+
+#include <kstd/memory>
+
+#include <atomic>
+#include <optional>
+#include <string_view>
+
+namespace kapi::acpi
+{
+
+ namespace
+ {
+ auto constinit manager = std::optional<kernel::acpi::manager>{};
+ } // namespace
+
+ auto init(::acpi::rsdp const & sdp) -> bool
+ {
+ auto static constinit initialized = std::atomic_flag{};
+ if (initialized.test_and_set())
+ {
+ system::panic("[OS:ACPI] The ACPI manager has already been initialized!");
+ }
+
+ manager.emplace(sdp);
+ return manager->load_tables();
+ }
+
+ auto get_table(std::string_view signature) -> kstd::observer_ptr<::acpi::table_header const>
+ {
+ return manager->get_table(signature);
+ }
+
+}; // namespace kapi::acpi
diff --git a/kernel/kapi/boot_modules.cpp b/kernel/kapi/boot_modules.cpp
new file mode 100644
index 0000000..5629b77
--- /dev/null
+++ b/kernel/kapi/boot_modules.cpp
@@ -0,0 +1,41 @@
+#include <kapi/boot_modules.hpp>
+
+#include <kapi/system.hpp>
+
+#include <optional>
+
+namespace
+{
+ constinit auto static registry = std::optional<kapi::boot_modules::boot_module_registry>{};
+} // namespace
+
+namespace kapi::boot_modules
+{
+ auto set_boot_module_registry(boot_module_registry & new_registry) -> void
+ {
+ if (registry)
+ {
+ system::panic("[x86_64] Boot module registry has already been set.");
+ }
+
+ registry = new_registry;
+ }
+
+ auto get_boot_module_registry() -> boot_module_registry const &
+ {
+ if (!registry)
+ {
+ system::panic("[x86_64] Boot module registry has not been initialized.");
+ }
+
+ return *registry;
+ }
+} // namespace kapi::boot_modules
+
+namespace kernel::tests::boot_modules
+{
+ auto deinit() -> void
+ {
+ registry.reset();
+ }
+} // namespace kernel::tests::boot_modules
diff --git a/kernel/kapi/cio.cpp b/kernel/kapi/cio.cpp
index d447a6a..96f043c 100644
--- a/kernel/kapi/cio.cpp
+++ b/kernel/kapi/cio.cpp
@@ -1,4 +1,4 @@
-#include "kapi/cio.hpp"
+#include <kapi/cio.hpp>
#include <optional>
#include <string_view>
diff --git a/kernel/kapi/cpu.cpp b/kernel/kapi/cpu.cpp
new file mode 100644
index 0000000..7b1a43b
--- /dev/null
+++ b/kernel/kapi/cpu.cpp
@@ -0,0 +1,35 @@
+#include <kapi/cpu.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/print>
+
+namespace kapi::cpu
+{
+
+ namespace
+ {
+ auto handle_page_fault(kapi::cpu::exception const & context) -> bool
+ {
+ kstd::println(kstd::print_sink::stderr, "\tFault address: {:#018x}", context.access_address);
+ kstd::println(kstd::print_sink::stderr, "\tPresent: {}", context.is_present);
+ kstd::println(kstd::print_sink::stderr, "\tWrite: {}", context.is_write_access);
+ kstd::println(kstd::print_sink::stderr, "\tUser: {}", context.is_user_mode);
+
+ kapi::system::panic("Halting the system due to an unrecoverable page fault.");
+ }
+ } // namespace
+
+ auto dispatch(exception const & context) -> bool
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:CPU] {} @ {:#018x}", context.type, context.instruction_pointer);
+ switch (context.type)
+ {
+ case kapi::cpu::exception::type::page_fault:
+ return handle_page_fault(context);
+ default:
+ return false;
+ }
+ }
+
+} // namespace kapi::cpu \ No newline at end of file
diff --git a/kernel/kapi/cpu.tests.cpp b/kernel/kapi/cpu.tests.cpp
new file mode 100644
index 0000000..9ce2917
--- /dev/null
+++ b/kernel/kapi/cpu.tests.cpp
@@ -0,0 +1,19 @@
+#include <kapi/cpu.hpp>
+
+#include <kernel/test_support/cpu.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+SCENARIO("Kernel testing kapi::cpu shims", "[support]")
+{
+ GIVEN("the test support infrastructure is initialized")
+ {
+ WHEN("a CPU halt is requested")
+ {
+ THEN("the correct exception is thrown")
+ {
+ REQUIRE_THROWS_AS(kapi::cpu::halt(), kernel::tests::cpu::halt);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/kapi/devices.cpp b/kernel/kapi/devices.cpp
new file mode 100644
index 0000000..572227e
--- /dev/null
+++ b/kernel/kapi/devices.cpp
@@ -0,0 +1,86 @@
+#include <kapi/devices.hpp>
+
+#include <kernel/devices/root_bus.hpp>
+
+#include <kapi/system.hpp>
+
+#include <kstd/flat_map>
+#include <kstd/memory>
+#include <kstd/print>
+
+#include <atomic>
+#include <cstddef>
+#include <optional>
+#include <string_view>
+#include <utility>
+
+namespace kapi::devices
+{
+ namespace
+ {
+ auto constinit next_major_number = std::atomic_size_t{1};
+ auto constinit root_bus = std::optional<kernel::devices::root_bus>{};
+ auto constinit device_tree = kstd::flat_map<std::pair<std::size_t, std::size_t>, kstd::observer_ptr<device>>{};
+ } // namespace
+
+ auto init() -> void
+ {
+ auto static is_initialized = std::atomic_flag{};
+ if (is_initialized.test_and_set())
+ {
+ return;
+ }
+
+ auto & bus = root_bus.emplace();
+ register_device(bus);
+ bus.init();
+ }
+
+ auto get_root_bus() -> bus &
+ {
+ if (!root_bus.has_value())
+ {
+ kapi::system::panic("[OS:DEV] Root bus not initialized!");
+ }
+ return *root_bus;
+ }
+
+ auto allocate_major_number() -> std::size_t
+ {
+ return next_major_number++;
+ }
+
+ auto register_device(device & device) -> bool
+ {
+ kstd::println("[OS:DEV] Registering device {}@{}:{}", device.name(), device.major(), device.minor());
+ return device_tree.emplace(std::pair{device.major(), device.minor()}, &device).second;
+ }
+
+ auto unregister_device(device &) -> bool
+ {
+ kstd::println("[OS:DEV] TODO: implement device deregistration");
+ return false;
+ }
+
+ auto find_device(std::size_t major, std::size_t minor) -> kstd::observer_ptr<device>
+ {
+ if (device_tree.contains(std::pair{major, minor}))
+ {
+ return device_tree.at(std::pair{major, minor});
+ }
+ return nullptr;
+ }
+
+ auto find_device(std::string_view name) -> kstd::observer_ptr<device>
+ {
+ for (auto const & [key, value] : device_tree)
+ {
+ if (value->name() == name)
+ {
+ return value;
+ }
+ }
+ return nullptr;
+ }
+
+} // namespace kapi::devices \ No newline at end of file
diff --git a/kernel/kapi/devices/bus.cpp b/kernel/kapi/devices/bus.cpp
new file mode 100644
index 0000000..59753f7
--- /dev/null
+++ b/kernel/kapi/devices/bus.cpp
@@ -0,0 +1,72 @@
+#include <kapi/devices/bus.hpp>
+
+#include <kapi/devices.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/print>
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstddef>
+#include <utility>
+
+namespace kapi::devices
+{
+ bus::bus(std::size_t major, std::size_t minor, kstd::string const & name)
+ : device(major, minor, name)
+ {}
+
+ auto bus::init() -> bool
+ {
+ if (m_init_was_called.test_and_set())
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:DEV] Bus {}:{}:{} already initialized", name(), major(), minor());
+ return true;
+ }
+
+ if (!probe())
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:DEV] Bus {}:{}:{} probe failed", name(), major(), minor());
+ return false;
+ }
+
+ auto child_status = std::ranges::fold_left(m_devices, true, [&](bool acc, auto & child) -> bool {
+ kstd::println("[OS:DEV] Initializing child device {}@{}", child->name(), name());
+ return child->init() && acc;
+ });
+
+ m_initialized.test_and_set();
+
+ return child_status;
+ }
+
+ auto bus::add_child(kstd::unique_ptr<device> child) -> void
+ {
+ auto observer = m_observers.emplace_back(child.get());
+ child->set_parent(kstd::make_observer(this));
+ m_devices.push_back(std::move(child));
+ kapi::devices::register_device(*observer);
+
+ if (m_initialized.test())
+ {
+ kstd::println("[OS:DEV] Initializing child device {}@{}", observer->name(), name());
+ if (!observer->init())
+ {
+ kapi::system::panic("[OS:DEV] Failed to initialize child device");
+ }
+ }
+ }
+
+ [[nodiscard]] auto bus::children() const -> kstd::vector<kstd::observer_ptr<device>> const &
+ {
+ return m_observers;
+ }
+
+ auto bus::probe() -> bool
+ {
+ return true;
+ }
+
+} // namespace kapi::devices \ No newline at end of file
diff --git a/kernel/kapi/devices/cpu.cpp b/kernel/kapi/devices/cpu.cpp
new file mode 100644
index 0000000..f0e1d72
--- /dev/null
+++ b/kernel/kapi/devices/cpu.cpp
@@ -0,0 +1,31 @@
+#include <kapi/devices/cpu.hpp>
+
+#include <kapi/devices.hpp>
+
+#include <cstddef>
+#include <cstdint>
+
+namespace kapi::devices
+{
+
+ cpu::core::core(std::size_t major_number, std::size_t minor_number, std::uint64_t hardware_id, bool is_bsp)
+ : kapi::devices::bus{major_number, minor_number, "cpu_core"}
+ , m_hardware_id{hardware_id}
+ , m_is_bsp{is_bsp}
+ {}
+
+ auto cpu::core::hardware_id() const -> std::uint64_t
+ {
+ return m_hardware_id;
+ }
+
+ auto cpu::core::is_bsp() const -> bool
+ {
+ return m_is_bsp;
+ }
+
+ cpu::cpu(std::size_t major, std::size_t minor)
+ : kapi::devices::bus{major, minor, "cpu"}
+ {}
+
+} // namespace kapi::devices \ No newline at end of file
diff --git a/kernel/kapi/devices/device.cpp b/kernel/kapi/devices/device.cpp
new file mode 100644
index 0000000..8b5d6b9
--- /dev/null
+++ b/kernel/kapi/devices/device.cpp
@@ -0,0 +1,43 @@
+#include <kapi/devices/device.hpp>
+
+#include <kapi/devices/bus.hpp>
+
+#include <kstd/memory>
+#include <kstd/string>
+
+#include <cstddef>
+
+namespace kapi::devices
+{
+ device::device(size_t major, size_t minor, kstd::string const & name)
+ : m_major(major)
+ , m_minor(minor)
+ , m_name(name)
+ {}
+
+ [[nodiscard]] auto device::major() const -> size_t
+ {
+ return m_major;
+ }
+
+ [[nodiscard]] auto device::minor() const -> size_t
+ {
+ return m_minor;
+ }
+
+ [[nodiscard]] auto device::name() const -> kstd::string const &
+ {
+ return m_name;
+ }
+
+ [[nodiscard]] auto device::is_block_device() const -> bool
+ {
+ return false;
+ }
+
+ auto device::set_parent(kstd::observer_ptr<bus> parent) -> void
+ {
+ m_parent = parent;
+ }
+
+} // namespace kapi::devices \ No newline at end of file
diff --git a/kernel/kapi/filesystem.cpp b/kernel/kapi/filesystem.cpp
new file mode 100644
index 0000000..68b51c9
--- /dev/null
+++ b/kernel/kapi/filesystem.cpp
@@ -0,0 +1,76 @@
+#include <kapi/filesystem.hpp>
+
+#include <kernel/filesystem/open_file_descriptor.hpp>
+#include <kernel/filesystem/open_file_table.hpp>
+#include <kernel/filesystem/vfs.hpp>
+
+#include <kstd/memory>
+#include <kstd/unikstd.h>
+
+#include <cstddef>
+#include <string_view>
+
+namespace kapi::filesystem
+{
+ auto mount(std::string_view source, std::string_view target) -> kstd::ssize_t
+ {
+ if (kernel::filesystem::vfs::get().do_mount(source, target) == kernel::filesystem::vfs::operation_result::success)
+ {
+ return 0;
+ }
+ return -1;
+ }
+
+ auto umount(std::string_view target) -> kstd::ssize_t
+ {
+ if (kernel::filesystem::vfs::get().unmount(target) == kernel::filesystem::vfs::operation_result::success)
+ {
+ return 0;
+ }
+ return -1;
+ }
+
+ auto open(std::string_view path) -> kstd::ssize_t
+ {
+ if (auto dentry = kernel::filesystem::vfs::get().open(path))
+ {
+ auto open_file_descriptor = kstd::make_shared<kernel::filesystem::open_file_descriptor>(dentry);
+ return kernel::filesystem::open_file_table::get().add_file(open_file_descriptor);
+ }
+
+ return -1;
+ }
+
+ auto close(size_t file_descriptor) -> kstd::ssize_t
+ {
+ if (auto open_file_descriptor = kernel::filesystem::open_file_table::get().file(file_descriptor))
+ {
+ if (kernel::filesystem::vfs::get().close(open_file_descriptor->get_dentry()->absolute_path()) ==
+ kernel::filesystem::vfs::operation_result::success)
+ {
+ return kernel::filesystem::open_file_table::get().remove_file(file_descriptor);
+ }
+ }
+ return -1;
+ }
+
+ auto read(size_t file_descriptor, void * buffer, size_t size) -> kstd::ssize_t
+ {
+ if (auto open_file_descriptor = kernel::filesystem::open_file_table::get().file(file_descriptor))
+ {
+ return open_file_descriptor->read(buffer, size);
+ }
+
+ return -1;
+ }
+
+ auto write(size_t file_descriptor, void const * buffer, size_t size) -> kstd::ssize_t
+ {
+ if (auto open_file_descriptor = kernel::filesystem::open_file_table::get().file(file_descriptor))
+ {
+ return open_file_descriptor->write(buffer, size);
+ }
+
+ return -1;
+ }
+} // namespace kapi::filesystem \ No newline at end of file
diff --git a/kernel/kapi/filesystem.tests.cpp b/kernel/kapi/filesystem.tests.cpp
new file mode 100644
index 0000000..d241afa
--- /dev/null
+++ b/kernel/kapi/filesystem.tests.cpp
@@ -0,0 +1,199 @@
+#include <kapi/filesystem.hpp>
+
+#include <kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <algorithm>
+#include <cstddef>
+#include <filesystem>
+#include <string_view>
+#include <vector>
+
+SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_vfs_fixture, "Kapi filesystem with real images",
+ "[kapi][filesystem]")
+{
+ 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";
+
+ GIVEN("Two real image files")
+ {
+ 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}));
+
+ THEN("files can be opened, read and closed again")
+ {
+ auto fd = kapi::filesystem::open("/information/info_1.txt");
+ REQUIRE(fd >= 0);
+
+ auto buffer = std::vector<std::byte>(6);
+ auto bytes_read = kapi::filesystem::read(fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)};
+ REQUIRE(buffer_as_str == "info_1");
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ }
+
+ THEN("files can be opened through absolute symbolic link, read and closed again")
+ {
+ auto fd = kapi::filesystem::open("/symlinks/information_directory_absolute/info_1.txt");
+ REQUIRE(fd >= 0);
+
+ auto buffer = std::vector<std::byte>(6);
+ auto bytes_read = kapi::filesystem::read(fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)};
+ REQUIRE(buffer_as_str == "info_1");
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ }
+
+ THEN("files can be opened through relative symbolic link, read and closed again")
+ {
+ auto fd = kapi::filesystem::open("/symlinks/information_directory_relative/info_1.txt");
+ REQUIRE(fd >= 0);
+
+ auto buffer = std::vector<std::byte>(6);
+ auto bytes_read = kapi::filesystem::read(fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)};
+ REQUIRE(buffer_as_str == "info_1");
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ }
+
+ THEN("files can be opened through relative symbolic link over multiple mount points, read and closed again")
+ {
+ kapi::filesystem::mount("/archiv/2024.img", "/information");
+
+ auto fd = kapi::filesystem::open("/information/symlinks/traverse_back_twice/information/sheep_1.txt");
+ REQUIRE(fd >= 0);
+
+ auto buffer = std::vector<std::byte>(7);
+ auto bytes_read = kapi::filesystem::read(fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)};
+ REQUIRE(buffer_as_str == "sheep_1");
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ }
+
+ THEN("a filesystem can be mounted, files can be opened, read and closed again and unmounted")
+ {
+ REQUIRE(kapi::filesystem::mount("/dev/ram16", "/information") == 0);
+
+ auto fd = kapi::filesystem::open("/information/monkey_house/monkey_1.txt");
+ REQUIRE(fd >= 0);
+
+ auto buffer = std::vector<std::byte>(8);
+ auto bytes_read = kapi::filesystem::read(fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ std::string_view buffer_as_str{reinterpret_cast<char *>(buffer.data()), static_cast<size_t>(bytes_read)};
+ REQUIRE(buffer_as_str == "monkey_1");
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ REQUIRE(kapi::filesystem::umount("/information") == 0);
+ }
+
+ THEN("a filesystem cannot be unmounted if files are still open and can be unmounted after files are closed")
+ {
+ REQUIRE(kapi::filesystem::mount("/dev/ram16", "/information") == 0);
+
+ auto fd = kapi::filesystem::open("/information/monkey_house/monkey_1.txt");
+ REQUIRE(fd >= 0);
+
+ REQUIRE(kapi::filesystem::umount("/information") < 0);
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ REQUIRE(kapi::filesystem::umount("/information") == 0);
+ }
+
+ THEN("device can be opened as file and read from")
+ {
+ auto fd = kapi::filesystem::open("/dev/ram0");
+ REQUIRE(fd >= 0);
+
+ auto buffer = std::vector<std::byte>(512);
+ auto bytes_read = kapi::filesystem::read(fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ }
+
+ THEN("device can be opened as file and written to and read from again")
+ {
+ auto read_fd = kapi::filesystem::open("/dev/ram16");
+ REQUIRE(read_fd >= 0);
+
+ auto buffer = std::vector<std::byte>(512, std::byte{0xAB});
+ auto bytes_written = kapi::filesystem::write(read_fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_written >= 0);
+
+ auto write_fd = kapi::filesystem::open("/dev/ram16");
+ REQUIRE(write_fd >= 0);
+
+ auto read_buffer = std::vector<std::byte>(512);
+ auto bytes_read = kapi::filesystem::read(write_fd, read_buffer.data(), read_buffer.size());
+ REQUIRE(bytes_read >= 0);
+
+ REQUIRE(std::equal(buffer.begin(), buffer.end(), read_buffer.begin()));
+
+ REQUIRE(kapi::filesystem::close(write_fd) == 0);
+ REQUIRE(kapi::filesystem::close(read_fd) == 0);
+ }
+
+ THEN("invalid paths cannot be mounted or unmounted")
+ {
+ REQUIRE(kapi::filesystem::mount("/dev/ram16", "invalid_path") < 0);
+ }
+
+ THEN("invalid paths cannot be unmounted")
+ {
+ REQUIRE(kapi::filesystem::umount("invalid_path") < 0);
+ }
+
+ THEN("non existent files cannot be opened")
+ {
+ auto fd = kapi::filesystem::open("/information/non_existent.txt");
+ REQUIRE(fd < 0);
+ }
+
+ THEN("not opened files cannot closed")
+ {
+ REQUIRE(kapi::filesystem::close(999) < 0);
+ }
+
+ THEN("same file cannot be closed twice")
+ {
+ auto fd = kapi::filesystem::open("/information/info_1.txt");
+ REQUIRE(fd >= 0);
+
+ REQUIRE(kapi::filesystem::close(fd) == 0);
+ REQUIRE(kapi::filesystem::close(fd) < 0);
+ }
+
+ THEN("not opened files cannot be read from")
+ {
+ std::vector<std::byte> buffer(10);
+ auto const invalid_fd = 999uz;
+ auto bytes_read = kapi::filesystem::read(invalid_fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_read < 0);
+ }
+
+ THEN("not opened files cannot be written to")
+ {
+ std::vector<std::byte> buffer(10);
+ auto const invalid_fd = 999uz;
+ auto bytes_written = kapi::filesystem::write(invalid_fd, buffer.data(), buffer.size());
+ REQUIRE(bytes_written < 0);
+ }
+ }
+}
diff --git a/kernel/kapi/interrupts.cpp b/kernel/kapi/interrupts.cpp
new file mode 100644
index 0000000..4efcaa3
--- /dev/null
+++ b/kernel/kapi/interrupts.cpp
@@ -0,0 +1,65 @@
+#include <kapi/interrupts.hpp>
+
+#include <kstd/flat_map>
+#include <kstd/print>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstdint>
+
+namespace kapi::interrupts
+{
+
+ namespace
+ {
+ auto constinit handlers = kstd::flat_map<std::uint32_t, kstd::vector<handler *>>{};
+ } // namespace
+
+ auto register_handler(std::uint32_t irq_number, handler & handler) -> void
+ {
+ if (handlers.contains(irq_number))
+ {
+ auto & handler_list = handlers.at(irq_number);
+ handler_list.push_back(&handler);
+ }
+ else
+ {
+ handlers.emplace(irq_number, kstd::vector{&handler});
+ }
+ }
+
+ auto unregister_handler(std::uint32_t irq_number, handler & handler) -> void
+ {
+ auto & handler_list = handlers.at(irq_number);
+ auto [first, last] = std::ranges::remove(handler_list, &handler);
+ handler_list.erase(first, last);
+ }
+
+ auto dispatch(std::uint32_t irq_number) -> status
+ {
+ if (!handlers.contains(irq_number))
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:interrupts] No handler for IRQ{}", irq_number);
+ return status::unhandled;
+ }
+
+ auto & handler_list = handlers.at(irq_number);
+
+ if (handler_list.empty())
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:interrupts] No handler for IRQ{}", irq_number);
+ return status::unhandled;
+ }
+
+ for (auto handler : handler_list)
+ {
+ if (handler && handler->handle_interrupt(irq_number) == status::handled)
+ {
+ return status::handled;
+ }
+ }
+
+ return status::unhandled;
+ }
+
+} // namespace kapi::interrupts \ No newline at end of file
diff --git a/kernel/kapi/memory.cpp b/kernel/kapi/memory.cpp
index d8065d4..5ea08b1 100644
--- a/kernel/kapi/memory.cpp
+++ b/kernel/kapi/memory.cpp
@@ -1,10 +1,12 @@
-#include "kapi/memory.hpp"
+#include <kapi/memory.hpp>
-#include "kapi/system.hpp"
+#include <kernel/memory/bitmap_allocator.hpp>
+#include <kernel/memory/mmio_allocator.hpp>
-#include "kernel/memory/bitmap_allocator.hpp"
+#include <kapi/system.hpp>
#include <kstd/print>
+#include <kstd/units>
#include <algorithm>
#include <cstddef>
@@ -62,6 +64,7 @@ namespace kapi::memory
constinit bad_frame_allocator bad_frame_allocator::instance{};
constinit bad_page_mapper bad_page_mapper::instance{};
auto constinit allocator = std::optional<kernel::memory::bitmap_frame_allocator>{};
+ auto constinit mmio_allocator = std::optional<kernel::memory::mmio_allocator>{};
} // namespace
constinit auto static active_frame_allocator = static_cast<frame_allocator *>(&bad_frame_allocator::instance);
@@ -112,8 +115,10 @@ namespace kapi::memory
auto init_pmm(std::size_t frame_count, void (&handoff_handler)(frame_allocator &)) -> void
{
- auto const bitmap_bytes = (frame_count + 7uz) / 8uz;
- auto const bitmap_pages = (bitmap_bytes + page::size - 1uz) / page::size;
+ using namespace kstd::units_literals;
+
+ auto const bitmap_bytes = kstd::units::bytes{(frame_count + 7uz) / 8uz};
+ auto const bitmap_pages = (bitmap_bytes + page::size - 1_B) / page::size;
auto const bitmap_frames = allocate_many_frames(bitmap_pages);
if (!bitmap_frames)
@@ -122,15 +127,20 @@ namespace kapi::memory
}
auto const flags = page_mapper::flags::writable | page_mapper::flags::supervisor_only | page_mapper::flags::global;
+ auto bitmap_ptr = static_cast<std::uint64_t *>(nullptr);
std::ranges::for_each(std::views::iota(0uz, bitmap_pages), [&](auto index) {
auto page = page::containing(pmm_metadata_base + index * page::size);
auto frame = memory::frame(bitmap_frames->first.number() + index);
- active_page_mapper->map(page, frame, flags);
+ auto mapped = active_page_mapper->map(page, frame, flags);
+ if (!bitmap_ptr)
+ {
+ bitmap_ptr = reinterpret_cast<std::uint64_t *>(mapped);
+ }
});
- auto bitmap_ptr = static_cast<std::uint64_t *>(pmm_metadata_base);
- auto bitmap = std::span{bitmap_ptr, (bitmap_bytes + sizeof(std::uint64_t) - 1uz) / sizeof(std::uint64_t)};
+ auto bitmap =
+ std::span{bitmap_ptr, (bitmap_bytes + kstd::type_size<std::uint64_t> - 1_B) / kstd::type_size<std::uint64_t>};
allocator.emplace(bitmap, frame_count);
@@ -139,4 +149,38 @@ namespace kapi::memory
kstd::println("[OS:MEM] Physical memory manager initialized.");
}
+ auto init_mmio(linear_address base, std::size_t page_count) -> void
+ {
+ mmio_allocator.emplace(base, page_count);
+ }
+
+ auto allocate_mmio_region(std::size_t page_count) -> mmio_region
+ {
+ auto region = mmio_allocator->allocate(page_count);
+ return {region, page_count};
+ }
+
+ auto map_mmio_region(mmio_region region, physical_address hw_base, page_mapper::flags flags) -> std::byte *
+ {
+ auto start_page = page::containing(region.first);
+ auto start_frame = frame::containing(hw_base);
+
+ flags |= page_mapper::flags::uncached;
+
+ auto start = map(start_page, start_frame, flags);
+
+ std::ranges::for_each(std::views::iota(1uz, region.second), [&](auto index) {
+ auto page = page::containing(region.first + index * page::size);
+ auto frame = frame::containing(hw_base + index * page::size);
+ map(page, frame, flags);
+ });
+
+ return start;
+ }
+
+ auto release_mmio_region(mmio_region region) -> void
+ {
+ mmio_allocator->release(region.first);
+ }
+
} // namespace kapi::memory
diff --git a/kernel/kapi/system.cpp b/kernel/kapi/system.cpp
index a17d9b9..9819ceb 100644
--- a/kernel/kapi/system.cpp
+++ b/kernel/kapi/system.cpp
@@ -1,6 +1,6 @@
-#include "kapi/system.hpp"
+#include <kapi/system.hpp>
-#include "kapi/cpu.hpp"
+#include <kapi/cpu.hpp>
#include <kstd/print>
diff --git a/kernel/kapi/system.tests.cpp b/kernel/kapi/system.tests.cpp
new file mode 100644
index 0000000..1e30031
--- /dev/null
+++ b/kernel/kapi/system.tests.cpp
@@ -0,0 +1,30 @@
+#include <kapi/system.hpp>
+
+#include <kernel/test_support/cio.hpp>
+#include <kernel/test_support/cpu.hpp>
+
+#include <kstd/print>
+
+#include <catch2/catch_test_macros.hpp>
+
+SCENARIO("Kernel testing kapi::system shims", "[support]")
+{
+ GIVEN("the test support infrastructure is initialized")
+ {
+ WHEN("a the system panics")
+ {
+ kernel::tests::cio::log_buffer().clear();
+
+ THEN("the correct exception is thrown")
+ {
+ REQUIRE_THROWS_AS(kapi::system::panic("[kernel:tests] Test Panic"), kernel::tests::cpu::halt);
+ }
+
+ THEN("the message is appended to the log buffer")
+ {
+ CHECK_THROWS(kapi::system::panic("[kernel:tests] Test Panic"));
+ REQUIRE(kernel::tests::cio::log_buffer().flat_messages().contains("[kernel:tests] Test Panic"));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/kstd/os.cpp b/kernel/kstd/os.cpp
index 21254c4..ae69e7e 100644
--- a/kernel/kstd/os.cpp
+++ b/kernel/kstd/os.cpp
@@ -1,4 +1,4 @@
-#include "kapi/system.hpp"
+#include <kapi/system.hpp>
#include <kstd/os/error.hpp>
diff --git a/kernel/kstd/print.cpp b/kernel/kstd/print.cpp
index c7d26ba..d0611b2 100644
--- a/kernel/kstd/print.cpp
+++ b/kernel/kstd/print.cpp
@@ -1,12 +1,14 @@
-#include "kapi/cio.hpp"
+#include <kstd/os/print.hpp>
+
+#include <kapi/cio.hpp>
+#include <kstd/bits/format/output_buffer.hpp>
#include <kstd/format>
-#include <kstd/os/print.hpp>
#include <kstd/print>
+#include <algorithm>
#include <array>
#include <cstddef>
-#include <iterator>
#include <string_view>
namespace kstd::os
@@ -14,7 +16,7 @@ namespace kstd::os
namespace
{
- struct write_buffer
+ struct write_buffer final : kstd::bits::format::output_buffer
{
using output_stream = kapi::cio::output_stream;
@@ -29,35 +31,36 @@ namespace kstd::os
: m_stream{stream}
{}
- ~write_buffer() noexcept
+ ~write_buffer() noexcept final
{
flush();
}
- auto flush() noexcept -> void
+ auto push(std::string_view text) -> void final
{
- if (m_position > 0)
+ std::ranges::for_each(text, [this](auto c) { this->push(c); });
+ }
+
+ auto push(char character) -> void final
+ {
+ if (m_position >= size)
{
- std::string_view chunk{m_buffer.data(), m_position};
- kapi::cio::write(m_stream, chunk);
- m_position = 0;
+ flush();
}
+ m_buffer.at(m_position++) = character;
}
- auto static callback(void * object, std::string_view text) -> void
+ private:
+ auto flush() noexcept -> void
{
- auto * self = static_cast<write_buffer *>(object);
- for (char const character : text)
+ if (m_position > 0)
{
- if (self->m_position >= size)
- {
- self->flush();
- }
- self->m_buffer.at(self->m_position++) = character;
+ std::string_view chunk{m_buffer.data(), m_position};
+ kapi::cio::write(m_stream, chunk);
+ m_position = 0;
}
}
- private:
output_stream m_stream;
std::array<char, size> m_buffer{};
std::size_t m_position{};
@@ -69,77 +72,7 @@ namespace kstd::os
{
auto writer = write_buffer{(sink == print_sink::stderr) ? kapi::cio::output_stream::stderr
: kapi::cio::output_stream::stdout};
- auto context = kstd::format_context{.writer = write_buffer::callback, .user_data = &writer};
-
- auto current = format.begin();
- auto end = format.end();
- auto next_automatic_index = 0uz;
-
- while (current != end)
- {
- if (*current != '{')
- {
- auto start = current;
- while (current != end && *current != '{')
- {
- std::advance(current, 1);
- }
- context.push(std::string_view(start, current - start));
- continue;
- }
-
- if (std::next(current) != end && *(std::next(current)) == '{')
- {
- context.push('{');
- std::advance(current, 2);
- continue;
- }
-
- std::advance(current, 1);
-
- auto index = 0uz;
- if (current != end && *current >= '0' && *current <= '9')
- {
- while (current != end && *current >= '0' && *current <= '9')
- {
- index = index * 10 + static_cast<std::size_t>(*current - '0');
- std::advance(current, 1);
- }
- }
- else
- {
- index = next_automatic_index++;
- }
-
- auto remaining_fmt = std::string_view{current, static_cast<std::size_t>(std::distance(current, end))};
-
- auto const arg = args.get(index);
- if (arg.format)
- {
- auto const after_specs = arg.format(arg.value, remaining_fmt, context);
- auto const consumed = remaining_fmt.size() - after_specs.size();
- std::advance(current, consumed);
- }
- else
- {
- context.push("{?}");
- while (current != end && *current != '}')
- std::advance(current, 1);
- }
-
- if (current != end && *current == '}')
- {
- std::advance(current, 1);
- }
- else
- {
- context.push("{fmt-err}");
- while (current != end && *current != '}')
- std::advance(current, 1);
- if (current != end)
- std::advance(current, 1);
- }
- }
+ kstd::bits::format::vformat_to(writer, format, args);
}
} // namespace kstd::os
diff --git a/kernel/kstd/print.tests.cpp b/kernel/kstd/print.tests.cpp
new file mode 100644
index 0000000..4963f46
--- /dev/null
+++ b/kernel/kstd/print.tests.cpp
@@ -0,0 +1,23 @@
+#include <kstd/print>
+
+#include <kernel/test_support/cio.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+SCENARIO("Kernel testing kstd shims", "[support]")
+{
+ GIVEN("the test support infrastructure is initialized")
+ {
+ WHEN("a regular print is issued")
+ {
+ kernel::tests::cio::log_buffer().clear();
+
+ kstd::println("[kernel:tests] Test Print");
+
+ THEN("the message is appended to the log buffer")
+ {
+ REQUIRE(kernel::tests::cio::log_buffer().flat_messages().contains("[kernel:tests] Test Print"));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/src/acpi/manager.cpp b/kernel/src/acpi/manager.cpp
new file mode 100644
index 0000000..bb895fd
--- /dev/null
+++ b/kernel/src/acpi/manager.cpp
@@ -0,0 +1,98 @@
+#include <kernel/acpi/manager.hpp>
+
+#include <kapi/memory.hpp>
+#include <kapi/system.hpp>
+
+#include <acpi/acpi.hpp>
+
+#include <kstd/memory>
+#include <kstd/print>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <ranges>
+#include <string_view>
+
+namespace kernel::acpi
+{
+
+ manager::manager(::acpi::rsdp const & sdp)
+ : m_sdp{&sdp}
+ {
+ if (!m_sdp->validate())
+ {
+ kapi::system::panic("[OS:ACPI] Invalid RSDP signature!");
+ }
+
+ m_extended = m_sdp->revision() >= 2;
+
+ if (m_extended)
+ {
+ if (!static_cast<::acpi::xsdp const *>(m_sdp)->validate())
+ {
+ kapi::system::panic("[OS:ACPI] Invalid XSDP signature!");
+ }
+ }
+ else
+ {
+ if (!m_sdp->validate())
+ {
+ kapi::system::panic("[OS:ACPI] Invalid RSDP checksum!");
+ }
+ }
+
+ auto physical_address = kapi::memory::physical_address{m_sdp->table_address()};
+ auto linear_address = kapi::memory::hhdm_to_linear(physical_address);
+ m_rsdt = static_cast<::acpi::table_header const *>(linear_address);
+ }
+
+ auto manager::load_tables() -> bool
+ {
+ if (!::acpi::validate_checksum({reinterpret_cast<std::byte const *>(m_rsdt), m_rsdt->length().value}))
+ {
+ kapi::system::panic("[OS:ACPI] Invalid RSDT checksum!");
+ }
+
+ auto check_and_register_table = [&](auto table_address) -> void {
+ auto physical_table_address = kapi::memory::physical_address{reinterpret_cast<std::uintptr_t>(table_address)};
+ auto mapped_table = kapi::memory::hhdm_to_linear(physical_table_address);
+ auto table = static_cast<::acpi::table_header const *>(mapped_table);
+
+ if (!::acpi::validate_checksum({static_cast<std::byte const *>(mapped_table), table->length().value}))
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:ACPI] Invalid table checksum!");
+ }
+ else
+ {
+ kstd::println("[OS:ACPI] Found '{}' ACPI table", table->signature());
+ m_tables.emplace(table->signature(), table);
+ }
+ };
+
+ if (m_extended)
+ {
+ auto xsdt = static_cast<::acpi::xsdt const *>(m_rsdt);
+ std::ranges::for_each(*xsdt | std::views::transform([](auto const & entry) { return entry.address(); }),
+ check_and_register_table);
+ }
+ else
+ {
+ auto rsdt = static_cast<::acpi::rsdt const *>(m_rsdt);
+ std::ranges::for_each(*rsdt | std::views::transform([](auto const & entry) { return entry.address(); }),
+ check_and_register_table);
+ }
+
+ return !m_tables.empty();
+ }
+
+ auto manager::get_table(std::string_view signature) -> kstd::observer_ptr<::acpi::table_header const>
+ {
+ if (m_tables.contains(signature))
+ {
+ return kstd::make_observer(m_tables.at(signature));
+ }
+ return nullptr;
+ }
+
+} // namespace kernel::acpi
diff --git a/kernel/src/devices/block_device.cpp b/kernel/src/devices/block_device.cpp
new file mode 100644
index 0000000..13d73ac
--- /dev/null
+++ b/kernel/src/devices/block_device.cpp
@@ -0,0 +1,42 @@
+#include <kernel/devices/block_device.hpp>
+
+#include <kapi/devices/device.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/string>
+
+#include <cstddef>
+
+namespace kernel::devices
+{
+ block_device::block_device(size_t major, size_t minor, kstd::string const & name, size_t block_size)
+ : kapi::devices::device(major, minor, name)
+ , m_block_size(block_size)
+ {
+ if (m_block_size == 0)
+ {
+ kapi::system::panic("[DEVICES] block_device constructed with zero block size.");
+ }
+ }
+
+ auto block_device::calculate_transfer(size_t block_index) const -> transfer_info
+ {
+ size_t const offset = block_index * m_block_size;
+ size_t const limit = size();
+
+ size_t const available = (offset < limit) ? (limit - offset) : 0;
+ size_t const to_transfer = (available < m_block_size) ? available : m_block_size;
+
+ return {offset, to_transfer, m_block_size - to_transfer};
+ }
+
+ auto block_device::block_size() const -> size_t
+ {
+ return m_block_size;
+ }
+
+ auto block_device::capacity() const -> size_t
+ {
+ return size();
+ }
+} // namespace kernel::devices \ No newline at end of file
diff --git a/kernel/src/devices/block_device.tests.cpp b/kernel/src/devices/block_device.tests.cpp
new file mode 100644
index 0000000..a2ddd2b
--- /dev/null
+++ b/kernel/src/devices/block_device.tests.cpp
@@ -0,0 +1,46 @@
+#include <kernel/test_support/devices/block_device.hpp>
+
+#include <kernel/test_support/cpu.hpp>
+
+#include <kstd/memory>
+#include <kstd/print>
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <cstddef>
+
+SCENARIO("Block device construction", "[devices][block_device]")
+{
+ GIVEN("parameters for a block device")
+ {
+ size_t major = 1;
+ size_t minor = 0;
+ kstd::string name = "test_block_device";
+ size_t block_size = 512;
+
+ WHEN("constructing a block device")
+ {
+ auto device =
+ kstd::make_shared<kernel::tests::devices::block_device>(major, minor, name, block_size, 3 * block_size);
+
+ THEN("the block device has the correct properties")
+ {
+ REQUIRE(device->major() == major);
+ REQUIRE(device->minor() == minor);
+ REQUIRE(device->name() == name);
+ REQUIRE(device->block_size() == block_size);
+ REQUIRE(device->capacity() == 3 * 512);
+ }
+ }
+
+ WHEN("constructing a block device with zero block size")
+ {
+ THEN("the constructor panics")
+ {
+ REQUIRE_THROWS_AS((kernel::tests::devices::block_device(major, minor, name, 0)), kernel::tests::cpu::halt);
+ }
+ }
+ }
+}
diff --git a/kernel/src/devices/block_device_utils.cpp b/kernel/src/devices/block_device_utils.cpp
new file mode 100644
index 0000000..18d1e9d
--- /dev/null
+++ b/kernel/src/devices/block_device_utils.cpp
@@ -0,0 +1,106 @@
+#include <kernel/devices/block_device_utils.hpp>
+
+#include <kernel/devices/block_device.hpp>
+
+#include <kapi/devices/device.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/cstring>
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstddef>
+
+namespace kernel::devices::block_device_utils
+{
+
+ using block_op = void (*)(size_t idx, size_t off, size_t len, size_t done, devices::block_device * device,
+ std::byte * scratch, void * buffer);
+
+ auto process_blocks(kstd::shared_ptr<kapi::devices::device> const & device, size_t offset, size_t size, void * buffer,
+ block_op op) -> size_t
+ {
+ if (buffer == nullptr)
+ {
+ kapi::system::panic("[FILESYSTEM] device_file::process_blocks called with null buffer.");
+ }
+
+ if (size == 0)
+ {
+ return 0;
+ }
+
+ if (!device->is_block_device())
+ {
+ kapi::system::panic("[FILESYSTEM] device_file: expected block_device.");
+ }
+
+ auto * block_dev = static_cast<devices::block_device *>(device.get());
+
+ size_t const block_size = block_dev->block_size();
+ size_t const capacity = block_dev->capacity();
+
+ if (offset >= capacity)
+ {
+ return 0;
+ }
+ size_t const total_to_process = std::min(size, capacity - offset);
+
+ kstd::vector<std::byte> scratch_buffer{block_size};
+ auto processed = 0uz;
+
+ while (processed < total_to_process)
+ {
+ size_t const absolute_offset = offset + processed;
+ size_t const block_index = absolute_offset / block_size;
+ size_t const in_block_offset = absolute_offset % block_size;
+ size_t const chunk_size = std::min(total_to_process - processed, block_size - in_block_offset);
+
+ op(block_index, in_block_offset, chunk_size, processed, block_dev, scratch_buffer.data(), buffer);
+
+ processed += chunk_size;
+ }
+
+ return processed;
+ }
+
+ auto read(kstd::shared_ptr<kapi::devices::device> const & device, void * buffer, size_t offset, size_t size) -> size_t
+ {
+ return process_blocks(device, offset, size, buffer,
+ [](size_t idx, size_t off, size_t len, size_t done, devices::block_device * device,
+ std::byte * scratch, void * buffer) {
+ auto * out = static_cast<std::byte *>(buffer);
+ if (off == 0 && len == device->block_size())
+ {
+ device->read_block(idx, out + done);
+ }
+ else
+ {
+ device->read_block(idx, scratch);
+ kstd::libc::memcpy(out + done, scratch + off, len);
+ }
+ });
+ }
+
+ auto write(kstd::shared_ptr<kapi::devices::device> const & device, void const * buffer, size_t offset, size_t size)
+ -> size_t
+ {
+ return process_blocks(device, offset, size, const_cast<void *>(buffer),
+ [](size_t idx, size_t off, size_t len, size_t done, devices::block_device * device,
+ std::byte * scratch, void * buffer) {
+ auto const * in = static_cast<std::byte const *>(buffer);
+ if (off == 0 && len == device->block_size())
+ {
+ device->write_block(idx, in + done);
+ }
+ else
+ {
+ device->read_block(idx, scratch);
+ kstd::libc::memcpy(scratch + off, in + done, len);
+ device->write_block(idx, scratch);
+ }
+ });
+ }
+
+} // namespace kernel::devices::block_device_utils \ No newline at end of file
diff --git a/kernel/src/devices/block_device_utils.tests.cpp b/kernel/src/devices/block_device_utils.tests.cpp
new file mode 100644
index 0000000..e2e1e65
--- /dev/null
+++ b/kernel/src/devices/block_device_utils.tests.cpp
@@ -0,0 +1,219 @@
+#include <kernel/devices/block_device_utils.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("reading from a block device with block_device_utils", "[devices][block_device_utils]")
+{
+ GIVEN("a block device with known data")
+ {
+ auto const block_size = 512;
+ auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "test_block_device", block_size);
+ kstd::vector<uint8_t> block_data(block_size);
+ for (size_t i = 0; i < block_data.size(); ++i)
+ {
+ block_data[i] = static_cast<uint8_t>(i % 256);
+ }
+ device->write_block(0, block_data.data());
+ device->write_block(1, block_data.data());
+
+ WHEN("reading from the block device using block_device_utils")
+ {
+ kstd::vector<uint8_t> read_buffer(block_size);
+ auto bytes_read = kernel::devices::block_device_utils::read(device, read_buffer.data(), 0, read_buffer.size());
+
+ THEN("the correct number of bytes is read")
+ {
+ REQUIRE(bytes_read == read_buffer.size());
+ }
+
+ THEN("the data read matches the data written to the block device")
+ {
+ REQUIRE(read_buffer == block_data);
+ }
+ }
+
+ WHEN("reading over block boundaries")
+ {
+ kstd::vector<uint8_t> read_buffer(1024);
+ auto bytes_read = kernel::devices::block_device_utils::read(device, read_buffer.data(), 256, read_buffer.size());
+
+ THEN("the correct number of bytes is read")
+ {
+ REQUIRE(bytes_read == 1.5 * block_size);
+ }
+
+ THEN("the data read matches the expected data across block boundaries")
+ {
+ for (size_t i = 0; i < bytes_read; ++i)
+ {
+ uint8_t expected_value = static_cast<uint8_t>((256 + i) % 256);
+ REQUIRE(read_buffer[i] == expected_value);
+ }
+ }
+ }
+
+ WHEN("reading beyond the device capacity")
+ {
+ kstd::vector<uint8_t> read_buffer(block_size);
+ auto bytes_read = kernel::devices::block_device_utils::read(device, read_buffer.data(), 1024, read_buffer.size());
+
+ THEN("no bytes are read")
+ {
+ REQUIRE(bytes_read == 0);
+ }
+ }
+
+ WHEN("reading nothing")
+ {
+ kstd::vector<uint8_t> read_buffer(block_size);
+ auto bytes_read = kernel::devices::block_device_utils::read(device, read_buffer.data(), 0, 0);
+
+ THEN("no bytes are read")
+ {
+ REQUIRE(bytes_read == 0);
+ }
+ }
+
+ WHEN("reading with a null buffer")
+ {
+ THEN("the system panics")
+ {
+ REQUIRE_THROWS_AS(kernel::devices::block_device_utils::read(device, nullptr, 0, 512), kernel::tests::cpu::halt);
+ }
+ }
+ }
+}
+
+SCENARIO("writing to a block device using block_device_utils", "[devices][block_device_utils]")
+{
+ GIVEN("a block device")
+ {
+ auto const block_size = 512;
+ auto device =
+ kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "test_block_device", block_size, 2 * block_size);
+
+ WHEN("writing to the block device using block_device_utils")
+ {
+ kstd::vector<uint8_t> write_buffer(block_size);
+ for (size_t i = 0; i < write_buffer.size(); ++i)
+ {
+ write_buffer[i] = static_cast<uint8_t>(i % 256);
+ }
+
+ auto bytes_written =
+ kernel::devices::block_device_utils::write(device, write_buffer.data(), 0, write_buffer.size());
+
+ THEN("the correct number of bytes is written")
+ {
+ REQUIRE(bytes_written == write_buffer.size());
+ }
+
+ THEN("the data written matches the data read back from the block device")
+ {
+ kstd::vector<uint8_t> read_buffer(block_size);
+ device->read_block(0, read_buffer.data());
+ REQUIRE(read_buffer == write_buffer);
+ }
+ }
+
+ WHEN("writing over block boundaries")
+ {
+ kstd::vector<uint8_t> write_buffer(2 * block_size);
+ for (size_t i = 0; i < write_buffer.size(); ++i)
+ {
+ write_buffer[i] = static_cast<uint8_t>(i % 256);
+ }
+
+ auto bytes_written =
+ kernel::devices::block_device_utils::write(device, write_buffer.data(), 256, write_buffer.size());
+
+ THEN("the correct number of bytes is written")
+ {
+ REQUIRE(bytes_written == 1.5 * block_size);
+ }
+
+ THEN("the data written matches the data read back from the block device across block boundaries")
+ {
+ kstd::vector<uint8_t> read_buffer(2 * block_size);
+ auto bytes_read = kernel::devices::block_device_utils::read(device, read_buffer.data(), 256, 2 * block_size);
+
+ for (size_t i = 0; i < bytes_read; ++i)
+ {
+ REQUIRE(read_buffer[i] == write_buffer[i]);
+ }
+ }
+ }
+
+ WHEN("writing beyond the device capacity")
+ {
+ kstd::vector<uint8_t> write_buffer(block_size);
+ auto bytes_written =
+ kernel::devices::block_device_utils::write(device, write_buffer.data(), 1024, write_buffer.size());
+
+ THEN("no bytes are written")
+ {
+ REQUIRE(bytes_written == 0);
+ }
+ }
+
+ WHEN("writing nothing")
+ {
+ kstd::vector<uint8_t> write_buffer(block_size);
+ auto bytes_written = kernel::devices::block_device_utils::write(device, write_buffer.data(), 0, 0);
+
+ THEN("no bytes are written")
+ {
+ REQUIRE(bytes_written == 0);
+ }
+ }
+
+ WHEN("writing with a null buffer")
+ {
+ THEN("the system panics")
+ {
+ REQUIRE_THROWS_AS(kernel::devices::block_device_utils::write(device, nullptr, 0, block_size),
+ kernel::tests::cpu::halt);
+ }
+ }
+ }
+}
+
+SCENARIO("block_device_utils with a non-block device", "[devices][block_device_utils]")
+{
+ GIVEN("a non-block device")
+ {
+ auto device = kstd::make_shared<kernel::tests::devices::character_device>(0, 0, "test_character_device");
+
+ WHEN("attempting to read from the non-block device using block_device_utils")
+ {
+ kstd::vector<uint8_t> read_buffer(512);
+ THEN("the system panics")
+ {
+ REQUIRE_THROWS_AS(kernel::devices::block_device_utils::read(device, read_buffer.data(), 0, read_buffer.size()),
+ kernel::tests::cpu::halt);
+ }
+ }
+
+ WHEN("attempting to write to the non-block device using block_device_utils")
+ {
+ kstd::vector<uint8_t> write_buffer(512);
+ THEN("the system panics")
+ {
+ REQUIRE_THROWS_AS(
+ kernel::devices::block_device_utils::write(device, write_buffer.data(), 0, write_buffer.size()),
+ kernel::tests::cpu::halt);
+ }
+ }
+ }
+}
diff --git a/kernel/src/devices/root_bus.cpp b/kernel/src/devices/root_bus.cpp
new file mode 100644
index 0000000..1b754f2
--- /dev/null
+++ b/kernel/src/devices/root_bus.cpp
@@ -0,0 +1,12 @@
+#include <kernel/devices/root_bus.hpp>
+
+#include <kapi/devices.hpp>
+
+namespace kernel::devices
+{
+
+ root_bus::root_bus()
+ : kapi::devices::bus{0, 0, "system"}
+ {}
+
+} // namespace kernel::devices \ No newline at end of file
diff --git a/kernel/src/devices/storage/controller.cpp b/kernel/src/devices/storage/controller.cpp
new file mode 100644
index 0000000..171b918
--- /dev/null
+++ b/kernel/src/devices/storage/controller.cpp
@@ -0,0 +1,44 @@
+#include <kernel/devices/storage/controller.hpp>
+
+#include <kapi/devices/device.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstddef>
+
+namespace kernel::devices::storage
+{
+ auto controller::set_ids(size_t major, size_t minors_per_dev) -> void
+ {
+ m_major = major;
+ m_minors_per_device = minors_per_dev;
+ }
+
+ auto controller::major() const -> size_t
+ {
+ return m_major;
+ }
+
+ auto controller::device_by_minor(size_t minor) const -> kstd::shared_ptr<kapi::devices::device>
+ {
+ auto it = std::ranges::find_if(m_devices, [minor](auto const & device) { return device->minor() == minor; });
+
+ if (it != m_devices.end())
+ {
+ return *it;
+ }
+ return nullptr;
+ }
+
+ auto controller::devices_count() const -> size_t
+ {
+ return m_devices.size();
+ }
+
+ auto controller::all_devices() const -> kstd::vector<kstd::shared_ptr<kapi::devices::device>> const &
+ {
+ return m_devices;
+ }
+} // namespace kernel::devices::storage \ No newline at end of file
diff --git a/kernel/src/devices/storage/management.cpp b/kernel/src/devices/storage/management.cpp
new file mode 100644
index 0000000..06efc27
--- /dev/null
+++ b/kernel/src/devices/storage/management.cpp
@@ -0,0 +1,95 @@
+#include <kernel/devices/storage/management.hpp>
+
+#include <kernel/devices/storage/controller.hpp>
+#include <kernel/devices/storage/ram_disk/controller.hpp>
+
+#include <kapi/boot_modules.hpp>
+#include <kapi/devices/device.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/memory>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstddef>
+#include <optional>
+#include <ranges>
+
+namespace
+{
+ constexpr size_t static minors_per_device = 16;
+ constexpr size_t static start_major = 1;
+ constinit size_t static next_free_major = start_major;
+
+ constinit auto static storage_management = std::optional<kernel::devices::storage::management>{};
+} // namespace
+
+namespace kernel::devices::storage
+{
+ auto management::init() -> void
+ {
+ if (storage_management)
+ {
+ kapi::system::panic("[DEVICES] Storage management has already been initialized.");
+ }
+ storage_management.emplace(management{});
+
+ auto ram_disk_controller = kstd::make_shared<ram_disk::controller>(&kapi::boot_modules::get_boot_module_registry());
+ storage_management->add_controller(ram_disk_controller);
+
+ std::ranges::for_each(storage_management->m_controllers, [](auto controller) { controller->probe(); });
+ }
+
+ auto management::get() -> management &
+ {
+ if (!storage_management)
+ {
+ kapi::system::panic("[DEVICES] Storage management has not been initialized.");
+ }
+
+ return *storage_management;
+ }
+
+ auto management::add_controller(kstd::shared_ptr<controller> const & controller) -> void
+ {
+ controller->set_ids(next_free_major++, minors_per_device);
+ m_controllers.push_back(controller);
+ }
+
+ auto management::all_controllers() const -> kstd::vector<kstd::shared_ptr<controller>> const &
+ {
+ return m_controllers;
+ }
+
+ auto management::device_by_major_minor(size_t major, size_t minor) -> kstd::shared_ptr<kapi::devices::device>
+ {
+ auto found = std::ranges::find_if(m_controllers, [=](auto const & controller) {
+ if (controller != nullptr && controller->major() == major)
+ {
+ return controller->device_by_minor(minor) != nullptr;
+ }
+ return false;
+ });
+
+ if (found != std::ranges::cend(m_controllers))
+ {
+ return found->get()->device_by_minor(minor);
+ }
+
+ return nullptr;
+ }
+
+ auto management::determine_boot_device() -> kstd::shared_ptr<kapi::devices::device>
+ {
+ return device_by_major_minor(start_major, 0);
+ }
+} // namespace kernel::devices::storage
+
+namespace kernel::tests::devices::storage::management
+{
+ auto deinit() -> void
+ {
+ storage_management.reset();
+ next_free_major = start_major;
+ }
+} // namespace kernel::tests::devices::storage::management
diff --git a/kernel/src/devices/storage/ram_disk/controller.cpp b/kernel/src/devices/storage/ram_disk/controller.cpp
new file mode 100644
index 0000000..30441fa
--- /dev/null
+++ b/kernel/src/devices/storage/ram_disk/controller.cpp
@@ -0,0 +1,27 @@
+#include <kernel/devices/storage/ram_disk/controller.hpp>
+
+#include <kernel/devices/storage/ram_disk/device.hpp>
+
+#include <kapi/boot_module/boot_module_registry.hpp>
+
+#include <kstd/memory>
+
+#include <algorithm>
+#include <cstddef>
+
+namespace kernel::devices::storage::ram_disk
+{
+ controller::controller(kapi::boot_modules::boot_module_registry const * registry)
+ : m_boot_module_registry(registry)
+ {}
+
+ auto controller::probe() -> void
+ {
+ size_t current_device_index = 0;
+
+ std::ranges::for_each(*m_boot_module_registry, [this, &current_device_index](auto const & module) {
+ auto const minor = current_device_index++ * m_minors_per_device;
+ m_devices.push_back(kstd::make_shared<device>(module, m_major, minor));
+ });
+ }
+} // namespace kernel::devices::storage::ram_disk \ No newline at end of file
diff --git a/kernel/src/devices/storage/ram_disk/device.cpp b/kernel/src/devices/storage/ram_disk/device.cpp
new file mode 100644
index 0000000..1557204
--- /dev/null
+++ b/kernel/src/devices/storage/ram_disk/device.cpp
@@ -0,0 +1,71 @@
+#include <kernel/devices/storage/ram_disk/device.hpp>
+
+#include <kernel/devices/block_device.hpp>
+
+#include <kapi/boot_module/boot_module.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/cstring>
+#include <kstd/string>
+
+#include <cstddef>
+
+namespace kernel::devices::storage::ram_disk
+{
+ namespace
+ {
+ constexpr size_t ram_disk_block_size = 512uz;
+ } // namespace
+
+ device::device(kapi::boot_modules::boot_module const & module, size_t major, size_t minor)
+ : block_device(major, minor, "ram" + kstd::to_string(minor), ram_disk_block_size)
+ , m_boot_module(module)
+ {}
+
+ auto device::init() -> bool
+ {
+ return m_boot_module.start_address.raw() != 0 && m_boot_module.size > 0;
+ }
+
+ auto device::read_block(size_t block_index, void * buffer) const -> void
+ {
+ if (buffer == nullptr)
+ {
+ kapi::system::panic("[RAM DISK DEVICE] read_block called with null buffer.");
+ }
+
+ auto const info = calculate_transfer(block_index);
+
+ if (info.to_transfer > 0)
+ {
+ auto const src = static_cast<std::byte const *>(m_boot_module.start_address) + info.offset;
+ kstd::libc::memcpy(buffer, src, info.to_transfer);
+ }
+
+ if (info.remainder > 0)
+ {
+ kstd::libc::memset(static_cast<std::byte *>(buffer) + info.to_transfer, 0, info.remainder);
+ }
+ }
+
+ auto device::write_block(size_t block_index, void const * buffer) -> void
+ {
+ if (buffer == nullptr)
+ {
+ kapi::system::panic("[RAM DISK DEVICE] write_block called with null buffer.");
+ }
+
+ auto const info = calculate_transfer(block_index);
+
+ if (info.to_transfer > 0)
+ {
+ auto const dest = static_cast<std::byte *>(m_boot_module.start_address) + info.offset;
+ kstd::libc::memcpy(dest, buffer, info.to_transfer);
+ }
+ }
+
+ auto device::size() const -> size_t
+ {
+ return m_boot_module.size;
+ }
+} // namespace kernel::devices::storage::ram_disk \ No newline at end of file
diff --git a/kernel/src/devices/storage/ram_disk/device.tests.cpp b/kernel/src/devices/storage/ram_disk/device.tests.cpp
new file mode 100644
index 0000000..d0fab76
--- /dev/null
+++ b/kernel/src/devices/storage/ram_disk/device.tests.cpp
@@ -0,0 +1,116 @@
+#include <kernel/devices/storage/ram_disk/device.hpp>
+
+#include <kapi/boot_module/boot_module.hpp>
+#include <kapi/memory.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers.hpp>
+#include <catch2/matchers/catch_matchers_range_equals.hpp>
+
+#include <cstddef>
+#include <ranges>
+#include <stdexcept>
+#include <vector>
+
+SCENARIO("RAM Disk Device Construction and Initialization", "[ram_disk_device]")
+{
+ GIVEN("an empty boot module")
+ {
+ kapi::boot_modules::boot_module boot_module{};
+ boot_module.start_address = kapi::memory::linear_address{nullptr};
+ boot_module.size = 0;
+
+ WHEN("constructing the device")
+ {
+ kernel::devices::storage::ram_disk::device device{boot_module, 0, 0};
+
+ THEN("init return false")
+ {
+ REQUIRE_FALSE(device.init());
+ }
+ }
+ }
+
+ GIVEN("a boot module with a valid start address and size")
+ {
+ kapi::boot_modules::boot_module boot_module{};
+ boot_module.start_address = kapi::memory::linear_address{reinterpret_cast<std::byte *>(0x1000)};
+ boot_module.size = 512;
+
+ WHEN("constructing the device")
+ {
+ kernel::devices::storage::ram_disk::device device{boot_module, 0, 0};
+
+ THEN("init return true")
+ {
+ REQUIRE(device.init());
+ }
+ }
+ }
+}
+
+SCENARIO("RAM Disk Device Read and Write", "[ram_disk_device]")
+{
+ GIVEN("a device initialized with a valid boot module")
+ {
+ auto storage = std::vector{4096, std::byte{0xff}};
+ auto module =
+ kapi::boot_modules::boot_module{"test_module", kapi::memory::linear_address{storage.data()}, storage.size()};
+ auto device = kernel::devices::storage::ram_disk::device{module, 0, 0};
+ REQUIRE(device.init());
+
+ WHEN("reading a full block from the device")
+ {
+ auto buffer = std::vector<std::byte>(device.block_size());
+ device.read_block(0, buffer.data());
+
+ THEN("the buffer is filled with the module data")
+ {
+ REQUIRE_THAT(buffer, Catch::Matchers::RangeEquals(std::views::take(storage, device.block_size())));
+ }
+ }
+
+ WHEN("reading from a block index beyond the module size")
+ {
+ auto buffer = std::vector<std::byte>(device.block_size());
+ device.read_block(10, buffer.data());
+
+ THEN("the buffer is filled with zeros")
+ {
+ REQUIRE_THAT(buffer, Catch::Matchers::RangeEquals(std::vector<std::byte>(device.block_size(), std::byte{0})));
+ }
+ }
+
+ WHEN("reading into a null buffer")
+ {
+ REQUIRE_THROWS_AS(device.read_block(0, nullptr), std::runtime_error);
+ }
+
+ WHEN("writing to a full block")
+ {
+ auto buffer = std::vector<std::byte>(device.block_size(), std::byte{0x01});
+ device.write_block(0, buffer.data());
+
+ THEN("the module data is updated")
+ {
+ REQUIRE_THAT(std::views::take(storage, device.block_size()), Catch::Matchers::RangeEquals(buffer));
+ }
+ }
+
+ WHEN("writing to a block index beyond the module size")
+ {
+ auto buffer = std::vector<std::byte>(device.block_size(), std::byte{0x01});
+ device.write_block(10, buffer.data());
+
+ THEN("the module data is not updated")
+ {
+ REQUIRE_THAT(storage, Catch::Matchers::RangeEquals(std::vector{4096, std::byte{0xff}}));
+ }
+ }
+
+ WHEN("writing from a null buffer")
+ {
+ REQUIRE_THROWS_AS(device.write_block(0, nullptr), std::runtime_error);
+ }
+ }
+} \ No newline at end of file
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);
+ }
+ }
+}
diff --git a/kernel/src/main.cpp b/kernel/src/main.cpp
index 01bcbb0..8dc1349 100644
--- a/kernel/src/main.cpp
+++ b/kernel/src/main.cpp
@@ -1,20 +1,152 @@
-#include "kapi/cio.hpp"
-#include "kapi/memory.hpp"
-#include "kapi/system.hpp"
+#include "kernel/filesystem/type_registry.hpp"
+#include <kernel/devices/storage/management.hpp>
+#include <kernel/filesystem/open_file_table.hpp>
+#include <kernel/filesystem/vfs.hpp>
+#include <kernel/memory.hpp>
-#include "kernel/memory.hpp"
+#include <kapi/boot_modules.hpp>
+#include <kapi/cio.hpp>
+#include <kapi/cpu.hpp>
+#include <kapi/devices.hpp>
+#include <kapi/filesystem.hpp>
+#include <kapi/interrupts.hpp>
+#include <kapi/memory.hpp>
+#include <kapi/system.hpp>
+#include <kstd/format>
#include <kstd/print>
+#include <kstd/units>
+#include <kstd/vector>
+
+#include <cstddef>
+#include <string_view>
+
+using namespace kstd::units_literals;
+
+auto run_demo() -> void
+{
+ // 1) open a file
+ kstd::println("attempting to open /entrance/tickets.txt");
+ auto fd_1 = kapi::filesystem::open("/entrance/tickets.txt");
+ if (fd_1 == -1)
+ {
+ kapi::system::panic("demo failed");
+ }
+ else
+ {
+ kstd::println("--> successfully opened /entrance/tickets.txt with file descriptor {}", fd_1);
+ }
+
+ // 2) read from the file
+ kstd::vector<std::byte> buffer_1{10};
+ auto bytes_read = kapi::filesystem::read(fd_1, buffer_1.data(), buffer_1.size());
+ auto buffer_as_str = std::string_view{reinterpret_cast<char *>(buffer_1.data()), static_cast<size_t>(bytes_read)};
+ kstd::println("--> read {} bytes from /entrance/tickets.txt: {}", bytes_read, buffer_as_str);
+ kstd::println("");
+
+ // 3) show that /entrance/information/info_1.txt is not accessible before mounting
+ kstd::println("attempting to open /entrance/information/info_1.txt before mounting");
+ auto fd_before_mount = kapi::filesystem::open("/entrance/information/info_1.txt");
+ if (fd_before_mount == -1)
+ {
+ kstd::println("--> as expected the file could not be opened before mounting");
+ }
+
+ // 4) mount a new filesystem on top of /entrance
+ kstd::println("mount /dev/ram16 to /entrance");
+ if (kapi::filesystem::mount("/dev/ram16", "/entrance") == 0)
+ {
+ kstd::println("--> successfully mounted /dev/ram16 to /entrance");
+ }
+ else
+ {
+ kapi::system::panic("demo failed");
+ }
+ kstd::println("");
+
+ // 5) open a file from the new filesystem
+ kstd::println("attempting to open /entrance/information/info_1.txt");
+ auto fd_2 = kapi::filesystem::open("/entrance/information/info_1.txt");
+ if (fd_2 != -1)
+ {
+ kstd::println("--> successfully opened /entrance/information/info_1.txt with file descriptor {}", fd_2);
+ }
+ else
+ {
+ kapi::system::panic("demo failed");
+ }
+
+ // 6) read from the new file
+ kstd::vector<std::byte> buffer_2{10};
+ bytes_read = kapi::filesystem::read(fd_2, buffer_2.data(), buffer_2.size());
+ buffer_as_str = std::string_view{reinterpret_cast<char *>(buffer_2.data()), static_cast<size_t>(bytes_read)};
+ kstd::println("--> read {} bytes from /entrance/information/info_1.txt: {} ", bytes_read, buffer_as_str);
+
+ // 7) open device as file
+ kstd::println("attempting to open /dev/ram32 as a file");
+ auto fd_3 = kapi::filesystem::open("/dev/ram32");
+ if (fd_3 != -1)
+ {
+ kstd::println("--> successfully opened /dev/ram32 as a file with file descriptor {}", fd_3);
+ }
+ else
+ {
+ kapi::system::panic("demo failed");
+ }
+
+ // 8) read from the device file
+ kstd::vector<std::byte> buffer_3{2};
+ bytes_read = kapi::filesystem::read(fd_3, buffer_3.data(), buffer_3.size());
+ kstd::println("--> read {} bytes from /dev/ram32: {::#04x} ", bytes_read, buffer_3);
+
+ // 9) write to the device file
+ auto const default_buffer_value = std::byte{0xAA};
+ kstd::vector<std::byte> write_buffer{default_buffer_value, default_buffer_value};
+ auto bytes_written = kapi::filesystem::write(fd_3, write_buffer.data(), write_buffer.size());
+ kstd::println("--> written {} bytes to /dev/ram32: {::#04x}", bytes_written, write_buffer);
+
+ // 10) do memory dump to show that the write to the device file had an effect
+}
auto main() -> int
{
kapi::cio::init();
kstd::println("[OS] IO subsystem initialized.");
+ kapi::cpu::init();
+
kapi::memory::init();
kernel::memory::init_heap(kapi::memory::heap_base);
- kstd::println("[OS] Memory subsystem initialized.");
kapi::system::memory_initialized();
+ kapi::memory::init_mmio(kapi::memory::mmio_base, 1_GiB / kapi::memory::page::size);
+ kstd::println("[OS] Memory subsystem initialized.");
+
+ kapi::devices::init();
+ kstd::println("[OS] System root bus initialized.");
+
+ kapi::devices::init_platform_devices();
+ kstd::println("[OS] Platform devices initialized.");
+
+ kapi::interrupts::enable();
+ kstd::println("[OS] Interrupts enabled.");
+
+ kapi::boot_modules::init();
+ kstd::println("[OS] Boot module registry initialized.");
+
+ kernel::devices::storage::management::init();
+ kstd::println("[OS] Storage management initialized.");
+
+ kernel::filesystem::open_file_table::init();
+ kstd::println("[OS] Global open file table initialized.");
+
+ kernel::filesystem::type_registry::init();
+ kstd::println("[OS] Builtin filesystems registered.");
+
+ kernel::filesystem::vfs::init();
+ kstd::println("[OS] Virtual filesystem initialized.");
+
+ // TODO BA-FS26 remove demo code?
+ // run_demo();
kapi::system::panic("Returning from kernel main!");
}
diff --git a/kernel/src/memory.cpp b/kernel/src/memory.cpp
index 0f614f0..6a85c0e 100644
--- a/kernel/src/memory.cpp
+++ b/kernel/src/memory.cpp
@@ -1,16 +1,15 @@
-#include "kernel/memory.hpp"
+#include <kernel/memory.hpp>
-#include "kapi/memory.hpp"
-#include "kapi/system.hpp"
+#include <kernel/memory/block_list_allocator.hpp>
+#include <kernel/memory/heap_allocator.hpp>
-#include "kernel/memory/block_list_allocator.hpp"
-#include "kernel/memory/heap_allocator.hpp"
+#include <kapi/memory.hpp>
+#include <kapi/system.hpp>
#include <kstd/print>
+#include <kstd/units>
#include <atomic>
-#include <cstddef>
-#include <new>
#include <optional>
namespace kernel::memory
@@ -22,7 +21,7 @@ namespace kernel::memory
{
null_allocator static instance;
- [[nodiscard]] auto allocate(std::size_t, std::align_val_t) noexcept -> void * override
+ [[nodiscard]] auto allocate(kstd::units::bytes, kstd::units::bytes) noexcept -> void * override
{
kstd::print(kstd::print_sink::stderr, "Tried to allocate memory without an active heap!");
return nullptr;
diff --git a/kernel/src/memory/bitmap_allocator.cpp b/kernel/src/memory/bitmap_allocator.cpp
index c010f77..240e2af 100644
--- a/kernel/src/memory/bitmap_allocator.cpp
+++ b/kernel/src/memory/bitmap_allocator.cpp
@@ -1,11 +1,12 @@
-#include "kernel/memory/bitmap_allocator.hpp"
+#include <kernel/memory/bitmap_allocator.hpp>
-#include "kapi/memory.hpp"
+#include <kapi/memory.hpp>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <optional>
+#include <ranges>
#include <span>
#include <utility>
@@ -17,7 +18,9 @@ namespace kernel::memory
, m_frame_count{frame_count}
, m_last_index{}
{
- std::ranges::fill(m_bitmap, ~0uz);
+ constexpr auto bits_per_word = 64uz;
+ auto to_fill = (frame_count + bits_per_word - 1) / bits_per_word;
+ std::ranges::fill(std::views::take(m_bitmap, to_fill), ~0uz);
}
auto bitmap_frame_allocator::allocate_many(std::size_t count) noexcept
@@ -78,22 +81,25 @@ namespace kernel::memory
auto bitmap_frame_allocator::test(std::size_t index) const noexcept -> bool
{
- auto entry_entry = index / 64;
- auto entry_offset = index % 64;
+ constexpr auto bits_per_word = 64uz;
+ auto entry_entry = index / bits_per_word;
+ auto entry_offset = index % bits_per_word;
return (m_bitmap[entry_entry] & (1uz << entry_offset));
}
auto bitmap_frame_allocator::set(std::size_t index) noexcept -> void
{
- auto entry_entry = index / 64;
- auto entry_offset = index % 64;
+ constexpr auto bits_per_word = 64uz;
+ auto entry_entry = index / bits_per_word;
+ auto entry_offset = index % bits_per_word;
m_bitmap[entry_entry] |= (1uz << entry_offset);
}
auto bitmap_frame_allocator::clear(std::size_t index) noexcept -> void
{
- auto entry_entry = index / 64;
- auto entry_offset = index % 64;
+ constexpr auto bits_per_word = 64uz;
+ auto entry_entry = index / bits_per_word;
+ auto entry_offset = index % bits_per_word;
m_bitmap[entry_entry] &= ~(1uz << entry_offset);
}
diff --git a/kernel/src/memory/bitmap_allocator.tests.cpp b/kernel/src/memory/bitmap_allocator.tests.cpp
new file mode 100644
index 0000000..05d11a3
--- /dev/null
+++ b/kernel/src/memory/bitmap_allocator.tests.cpp
@@ -0,0 +1,288 @@
+#include <kernel/memory/bitmap_allocator.hpp>
+
+#include <kapi/memory.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers.hpp>
+#include <catch2/matchers/catch_matchers_range_equals.hpp>
+
+#include <cstdint>
+#include <limits>
+#include <ranges>
+#include <span>
+#include <vector>
+
+constexpr auto all_bits_set = std::numeric_limits<std::uint64_t>::max();
+constexpr auto available_frames = 1024uz;
+
+SCENARIO("Bitmap allocator construction and initialization", "[memory][bitmap_allocator]")
+{
+ GIVEN("A storage region")
+ {
+ auto storage = std::vector(available_frames / 64, 0uz);
+
+ WHEN("constructing the allocator with 0 frames")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), 0};
+
+ THEN("the storage region is not modified")
+ {
+ REQUIRE_THAT(storage, Catch::Matchers::RangeEquals(std::vector(16, 0uz)));
+ }
+ }
+
+ WHEN("constructing with 1 frame")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), 1};
+
+ THEN("the first word of the storage region is set to all ones")
+ {
+ REQUIRE_THAT(std::views::take(storage, 1), Catch::Matchers::RangeEquals(std::vector(1, all_bits_set)));
+ }
+
+ THEN("the rest of the storage region is not modified")
+ {
+ REQUIRE_THAT(std::views::drop(storage, 1), Catch::Matchers::RangeEquals(std::vector(15, 0uz)));
+ }
+ }
+
+ WHEN("constructing with 64 frames")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), 64};
+
+ THEN("the first word of the storage region is set to all ones")
+ {
+ REQUIRE_THAT(std::views::take(storage, 1), Catch::Matchers::RangeEquals(std::vector(1, all_bits_set)));
+ }
+
+ THEN("the rest of the storage region is not modified")
+ {
+ REQUIRE_THAT(std::views::drop(storage, 1), Catch::Matchers::RangeEquals(std::vector(15, 0uz)));
+ }
+ }
+
+ WHEN("constructing with all available frames")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames};
+
+ THEN("the storage region is filled with all ones")
+ {
+ REQUIRE_THAT(storage, Catch::Matchers::RangeEquals(std::vector(16, all_bits_set)));
+ }
+ }
+
+ WHEN("constructing with half the available frames")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames / 2};
+
+ THEN("the first half of the storage region is filled with all ones")
+ {
+ REQUIRE_THAT(std::views::take(storage, (available_frames / 2) / 64),
+ Catch::Matchers::RangeEquals(std::vector((available_frames / 2) / 64, all_bits_set)));
+ }
+
+ THEN("the second half of the storage region is filled with all zeros")
+ {
+ REQUIRE_THAT(std::views::drop(storage, (available_frames / 2) / 64),
+ Catch::Matchers::RangeEquals(std::vector((available_frames / 2) / 64, 0uz)));
+ }
+ }
+ }
+}
+
+SCENARIO("Bitmap allocator frame allocation", "[memory][bitmap_allocator]")
+{
+ GIVEN("A storage region")
+ {
+ auto storage = std::vector(available_frames / 64, 0uz);
+
+ AND_GIVEN("an allocator constructed with all available frames but no free ones")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames};
+
+ WHEN("allocating 1 frame")
+ {
+ auto result = allocator.allocate_many(1);
+
+ THEN("the result is empty")
+ {
+ REQUIRE_FALSE(result.has_value());
+ }
+ }
+ }
+
+ AND_GIVEN("an allocator constructed with all available frames but only one free one")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames};
+ allocator.release_many({kapi::memory::frame{0}, 1});
+
+ WHEN("allocating 1 frame")
+ {
+ auto result = allocator.allocate_many(1);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+
+ THEN("the result contains 1 frame")
+ {
+ REQUIRE(result->second == 1);
+ }
+ }
+
+ WHEN("allocating more frames than are free")
+ {
+ auto result = allocator.allocate_many(2);
+
+ THEN("the result is empty")
+ {
+ REQUIRE_FALSE(result.has_value());
+ }
+
+ AND_WHEN("allocating a single frame")
+ {
+ auto result = allocator.allocate_many(1);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+ }
+
+ WHEN("allocating 0 frames")
+ {
+ auto result = allocator.allocate_many(0);
+
+ THEN("the result is empty")
+ {
+ REQUIRE_FALSE(result.has_value());
+ }
+ }
+ }
+ }
+
+ AND_GIVEN("an allocator with many single frame holes")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames};
+ for (auto i = 0uz; i < available_frames; i += 2)
+ {
+ allocator.release_many({kapi::memory::frame{i}, 1});
+ }
+
+ WHEN("allocating 1 frame")
+ {
+ auto result = allocator.allocate_many(1);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+
+ THEN("the result contains 1 frame")
+ {
+ REQUIRE(result->second == 1);
+ }
+ }
+
+ WHEN("allocating 2 frames")
+ {
+ auto result = allocator.allocate_many(2);
+
+ THEN("the result is empty")
+ {
+ REQUIRE_FALSE(result.has_value());
+ }
+ }
+ }
+
+ AND_GIVEN("and allocator with all frames marked as free")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames};
+ allocator.release_many({kapi::memory::frame{0}, available_frames});
+
+ WHEN("allocating 1 frame")
+ {
+ auto result = allocator.allocate_many(1);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+
+ THEN("the result contains 1 frame")
+ {
+ REQUIRE(result->second == 1);
+ }
+ }
+
+ WHEN("allocating multiple frames")
+ {
+ auto result = allocator.allocate_many(20);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+
+ THEN("the result contains 20 frames")
+ {
+ REQUIRE(result->second == 20);
+ }
+ }
+
+ WHEN("marking all frames as used")
+ {
+ for (auto i = 0uz; i < available_frames; i++)
+ {
+ allocator.mark_used(kapi::memory::frame{i});
+ }
+
+ THEN("the allocator has no free frames")
+ {
+ REQUIRE_FALSE(allocator.allocate_many(1).has_value());
+ }
+ }
+ }
+
+ AND_GIVEN("an allocator with a contiguous block of free frames")
+ {
+ auto allocator = kernel::memory::bitmap_frame_allocator{std::span(storage), available_frames};
+ allocator.release_many({kapi::memory::frame{0}, available_frames});
+ for (auto i = 0uz; i < available_frames / 2; i++)
+ {
+ allocator.mark_used(kapi::memory::frame{i});
+ }
+
+ WHEN("allocating a single frame")
+ {
+ auto result = allocator.allocate_many(1);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+
+ THEN("the result contains 1 frame")
+ {
+ REQUIRE(result->second == 1);
+ }
+ }
+
+ WHEN("allocating multiple frames")
+ {
+ auto result = allocator.allocate_many(20);
+
+ THEN("the result is not empty")
+ {
+ REQUIRE(result.has_value());
+ }
+
+ THEN("the result contains 20 frames")
+ {
+ REQUIRE(result->second == 20);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/kernel/src/memory/block_list_allocator.cpp b/kernel/src/memory/block_list_allocator.cpp
index 5d870d8..6e68ada 100644
--- a/kernel/src/memory/block_list_allocator.cpp
+++ b/kernel/src/memory/block_list_allocator.cpp
@@ -1,24 +1,26 @@
-#include "kernel/memory/block_list_allocator.hpp"
+#include <kernel/memory/block_list_allocator.hpp>
-#include "kapi/memory.hpp"
-#include "kapi/system.hpp"
+#include <kernel/memory/heap_allocator.hpp>
-#include "kernel/memory/heap_allocator.hpp"
+#include <kapi/memory.hpp>
+#include <kapi/system.hpp>
#include <kstd/mutex>
+#include <kstd/units>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <memory>
-#include <new>
+
+using namespace kstd::units_literals;
namespace kernel::memory
{
namespace
{
- [[nodiscard]] constexpr auto align_up(std::byte * pointer, std::align_val_t alignment) noexcept -> std::byte *
+ [[nodiscard]] constexpr auto align_up(std::byte * pointer, kstd::units::bytes alignment) noexcept -> std::byte *
{
auto const remainder = std::bit_cast<std::uintptr_t>(pointer) % static_cast<std::size_t>(alignment);
return remainder == 0 ? pointer : pointer + static_cast<std::size_t>(alignment) - remainder;
@@ -33,7 +35,7 @@ namespace kernel::memory
, m_lock{}
{}
- auto block_list_allocator::allocate(std::size_t size, std::align_val_t alignment) noexcept -> void *
+ auto block_list_allocator::allocate(kstd::units::bytes size, kstd::units::bytes alignment) noexcept -> void *
{
kstd::lock_guard guard{m_lock};
@@ -47,13 +49,13 @@ namespace kernel::memory
auto const raw_block = reinterpret_cast<std::byte *>(current);
auto const unaligned_payload = raw_block + allocated_metadata_size;
auto const aligned_payload = align_up(unaligned_payload, alignment);
- auto const required_padding = static_cast<std::size_t>(aligned_payload - unaligned_payload);
+ auto const required_padding = static_cast<kstd::units::bytes>(aligned_payload - unaligned_payload);
auto const total_required_size = required_padding + allocated_metadata_size + size;
if (current->usable_size >= total_required_size)
{
auto const payload_header = aligned_payload - sizeof(block_header *) - sizeof(block_header);
- auto const front_padding = static_cast<std::size_t>(payload_header - raw_block);
+ auto const front_padding = static_cast<kstd::units::bytes>(payload_header - raw_block);
auto payload_block = current;
@@ -72,9 +74,10 @@ namespace kernel::memory
payload_block->prev = current;
}
- auto const payload_size =
- aligned_payload - reinterpret_cast<std::byte *>(payload_block) - allocated_metadata_size + size;
- split(payload_block, payload_size, 0uz);
+ auto const header_size =
+ static_cast<kstd::units::bytes>(aligned_payload - reinterpret_cast<std::byte *>(payload_block));
+ auto const payload_size = header_size - allocated_metadata_size + size;
+ split(payload_block, payload_size, 0_B);
payload_block->free = false;
@@ -87,7 +90,7 @@ namespace kernel::memory
current = current->next;
}
- auto const search_size = size + static_cast<std::size_t>(alignment);
+ auto const search_size = size + alignment;
if (attempt == 0uz && !expand(search_size))
{
return nullptr;
@@ -114,10 +117,10 @@ namespace kernel::memory
coalesce(block);
}
- auto block_list_allocator::expand(std::size_t size) noexcept -> bool
+ auto block_list_allocator::expand(kstd::units::bytes size) noexcept -> bool
{
auto const total_required_size = size + allocated_metadata_size;
- auto const frames_needed = (total_required_size + kapi::memory::frame::size - 1) / kapi::memory::frame::size;
+ auto const frames_needed = (total_required_size + kapi::memory::frame::size - 1_B) / kapi::memory::frame::size;
auto const flags = kapi::memory::page_mapper::flags::writable | kapi::memory::page_mapper::flags::supervisor_only |
kapi::memory::page_mapper::flags::global;
@@ -187,7 +190,8 @@ namespace kernel::memory
}
}
- auto block_list_allocator::split(block_header * block, std::size_t size, std::size_t padding) noexcept -> void
+ auto block_list_allocator::split(block_header * block, kstd::units::bytes size, kstd::units::bytes padding) noexcept
+ -> void
{
auto const new_block_size = size + padding;
diff --git a/kernel/src/memory/block_list_allocator.tests.cpp b/kernel/src/memory/block_list_allocator.tests.cpp
new file mode 100644
index 0000000..c5f84c5
--- /dev/null
+++ b/kernel/src/memory/block_list_allocator.tests.cpp
@@ -0,0 +1,85 @@
+#include <kernel/memory/block_list_allocator.hpp>
+
+#include <kernel/test_support/memory.hpp>
+
+#include <kapi/memory.hpp>
+
+#include <kstd/units>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <cstddef>
+
+using namespace kstd::units_literals;
+
+SCENARIO("Block List Allocator Operations", "[memory][allocator]")
+{
+ GIVEN("A newly initialized block list allocator mapped via the test sandbox")
+ {
+ kernel::tests::memory::deinit();
+ kapi::memory::init();
+
+ auto sandbox_base = kernel::tests::memory::virtual_base();
+ kernel::memory::block_list_allocator allocator{sandbox_base};
+
+ WHEN("a basic allocation request is made")
+ {
+ void * ptr = allocator.allocate(128_B, 8_B);
+
+ THEN("a valid, non-null pointer is returned")
+ {
+ REQUIRE(ptr != nullptr);
+ }
+
+ AND_THEN("the returned memory is writeable without causing segmentation faults")
+ {
+ auto byte_ptr = static_cast<std::byte *>(ptr);
+ byte_ptr[0] = std::byte{0xDE};
+ byte_ptr[127] = std::byte{0xAD};
+ REQUIRE(byte_ptr[0] == std::byte{0xDE});
+ REQUIRE(byte_ptr[127] == std::byte{0xAD});
+ }
+
+ allocator.deallocate(ptr);
+ }
+
+ WHEN("multiple allocations are made sequentially")
+ {
+ void * ptr1 = allocator.allocate(64_B, 8_B);
+ void * ptr2 = allocator.allocate(64_B, 8_B);
+ void * ptr3 = allocator.allocate(1_KiB, 16_B);
+
+ THEN("they return distinct, non-overlapping memory blocks")
+ {
+ REQUIRE(ptr1 != nullptr);
+ REQUIRE(ptr2 != nullptr);
+ REQUIRE(ptr3 != nullptr);
+ REQUIRE(ptr1 != ptr2);
+ REQUIRE(ptr2 != ptr3);
+ REQUIRE(ptr1 != ptr3);
+ }
+
+ allocator.deallocate(ptr1);
+ allocator.deallocate(ptr2);
+ allocator.deallocate(ptr3);
+ }
+
+ WHEN("a block is allocated and then completely freed")
+ {
+ void * original_ptr = allocator.allocate(512_B, 16_B);
+ allocator.deallocate(original_ptr);
+
+ AND_WHEN("a new allocation of equal or smaller size is requested")
+ {
+ void * new_ptr = allocator.allocate(128_B, 16_B);
+
+ THEN("the allocator actively reuses the coalesced space")
+ {
+ REQUIRE(new_ptr == original_ptr);
+ }
+
+ allocator.deallocate(new_ptr);
+ }
+ }
+ }
+}
diff --git a/kernel/src/memory/mmio_allocator.cpp b/kernel/src/memory/mmio_allocator.cpp
new file mode 100644
index 0000000..ba23dbd
--- /dev/null
+++ b/kernel/src/memory/mmio_allocator.cpp
@@ -0,0 +1,111 @@
+#include <kernel/memory/mmio_allocator.hpp>
+
+#include <kapi/memory.hpp>
+#include <kapi/system.hpp>
+
+#include <kstd/allocator>
+
+#include <cstddef>
+#include <memory>
+#include <utility>
+
+namespace kernel::memory
+{
+
+ mmio_allocator::mmio_allocator(kapi::memory::linear_address base, std::size_t pages)
+ : m_head{make_node(base, pages, nullptr, nullptr, true)}
+ {}
+
+ auto mmio_allocator::allocate(std::size_t count) -> kapi::memory::linear_address
+ {
+ if (count == 0 || !m_head)
+ {
+ return {};
+ }
+
+ auto current = m_head;
+ while (current)
+ {
+ if (current->is_free && current->page_count >= count)
+ {
+ if (current->page_count > count)
+ {
+ auto new_base = current->base + (count * kapi::memory::page::size);
+ auto split_node = make_node(new_base, current->page_count - count, std::move(current->next), current, true);
+
+ if (current->next)
+ {
+ current->next->previous = split_node;
+ }
+ current->next = split_node;
+ current->page_count = count;
+ }
+
+ current->is_free = false;
+ return current->base;
+ }
+ current = current->next;
+ }
+
+ kapi::system::panic("[OS:MEM] MMIO alloctor out of memory!");
+ return {};
+ }
+
+ auto mmio_allocator::release(kapi::memory::linear_address base) -> void
+ {
+ auto current = m_head;
+
+ while (current)
+ {
+ if (current->base == base && !current->is_free)
+ {
+ current->is_free = true;
+
+ if (current->next && current->next->is_free)
+ {
+ auto removed = current->next;
+ current->page_count += removed->page_count;
+ current->next = removed->next;
+ if (current->next)
+ {
+ current->next->previous = current;
+ }
+ destroy_node(removed);
+ }
+
+ if (current->previous && current->previous->is_free)
+ {
+ auto removed = current;
+ removed->previous->page_count += removed->page_count;
+ removed->previous->next = removed->next;
+ if (removed->next)
+ {
+ removed->next->previous = removed->previous;
+ }
+ destroy_node(removed);
+ }
+ return;
+ }
+ current = current->next;
+ }
+ }
+
+ auto mmio_allocator::make_node(kapi::memory::linear_address base, std::size_t page_count, node * next,
+ node * previous, bool is_free) -> node *
+ {
+ using traits = std::allocator_traits<kstd::allocator<node>>;
+
+ auto new_node = traits::allocate(m_allocator, 1);
+ traits::construct(m_allocator, new_node, base, page_count, next, previous, is_free);
+ return new_node;
+ }
+
+ auto mmio_allocator::destroy_node(node * instance) -> void
+ {
+ using traits = std::allocator_traits<kstd::allocator<node>>;
+
+ traits::destroy(m_allocator, instance);
+ traits::deallocate(m_allocator, instance, 1);
+ }
+
+} // namespace kernel::memory
diff --git a/kernel/src/memory/operators.cpp b/kernel/src/memory/operators.cpp
index 57e31e6..5673d68 100644
--- a/kernel/src/memory/operators.cpp
+++ b/kernel/src/memory/operators.cpp
@@ -1,6 +1,8 @@
-#include "kapi/system.hpp"
+#include <kernel/memory.hpp>
-#include "kernel/memory.hpp"
+#include <kapi/system.hpp>
+
+#include <kstd/units>
#include <cstddef>
#include <new>
@@ -8,7 +10,8 @@
[[nodiscard]] auto operator new(std::size_t size, std::align_val_t alignment, std::nothrow_t const &) noexcept -> void *
{
auto & allocator = kernel::memory::get_heap_allocator();
- return allocator.allocate(size, alignment);
+ return allocator.allocate(static_cast<kstd::units::bytes>(size),
+ static_cast<kstd::units::bytes>(static_cast<std::size_t>(alignment)));
}
[[nodiscard]] auto operator new(std::size_t size, std::align_val_t alignment) -> void *
@@ -73,9 +76,9 @@ auto operator delete(void * pointer, std::align_val_t) noexcept -> void
::operator delete(pointer);
}
-auto operator delete(void * pointer, std::size_t, std::align_val_t) noexcept -> void
+auto operator delete(void * pointer, std::size_t size, std::align_val_t) noexcept -> void
{
- ::operator delete(pointer);
+ ::operator delete(pointer, size);
}
auto operator delete[](void * pointer) noexcept -> void
@@ -83,9 +86,9 @@ auto operator delete[](void * pointer) noexcept -> void
::operator delete(pointer);
}
-auto operator delete[](void * pointer, std::size_t) noexcept -> void
+auto operator delete[](void * pointer, std::size_t size) noexcept -> void
{
- ::operator delete(pointer);
+ ::operator delete(pointer, size);
}
auto operator delete[](void * pointer, std::align_val_t) noexcept -> void
@@ -93,7 +96,7 @@ auto operator delete[](void * pointer, std::align_val_t) noexcept -> void
::operator delete(pointer);
}
-auto operator delete[](void * pointer, std::size_t, std::align_val_t) noexcept -> void
+auto operator delete[](void * pointer, std::size_t size, std::align_val_t) noexcept -> void
{
- ::operator delete(pointer);
+ ::operator delete(pointer, size);
}
diff --git a/kernel/src/test_support/devices/block_device.cpp b/kernel/src/test_support/devices/block_device.cpp
new file mode 100644
index 0000000..9a9e544
--- /dev/null
+++ b/kernel/src/test_support/devices/block_device.cpp
@@ -0,0 +1,61 @@
+#include <kernel/test_support/devices/block_device.hpp>
+
+#include <kernel/devices/block_device.hpp>
+
+#include <kstd/string>
+#include <kstd/vector>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string.h>
+
+namespace kernel::tests::devices
+{
+ block_device::block_device(size_t major, size_t minor, kstd::string const & name, size_t block_size,
+ size_t initial_size)
+ : kernel::devices::block_device(major, minor, name, block_size)
+ {
+ data.resize(initial_size, 0);
+ }
+
+ auto block_device::init() -> bool
+ {
+ return true;
+ }
+
+ auto block_device::read_block(size_t block_index, void * buffer) const -> void
+ {
+ auto const offset = block_index * block_size();
+ if (offset >= data.size())
+ {
+ kstd::libc::memset(buffer, 0, block_size());
+ return;
+ }
+
+ auto const bytes_to_read = std::min(block_size(), data.size() - offset);
+ kstd::libc::memcpy(buffer, data.data() + offset, bytes_to_read);
+ if (bytes_to_read < block_size())
+ {
+ kstd::libc::memset(static_cast<uint8_t *>(buffer) + bytes_to_read, 0, block_size() - bytes_to_read);
+ }
+ }
+
+ auto block_device::write_block(size_t block_index, void const * buffer) -> void
+ {
+ auto const offset = block_index * block_size();
+ auto const write_end = offset + block_size();
+ if (write_end > data.size())
+ {
+ data.resize(write_end, 0);
+ }
+
+ kstd::libc::memcpy(data.data() + offset, static_cast<uint8_t const *>(buffer), block_size());
+ }
+
+ auto block_device::size() const -> size_t
+ {
+ return data.size();
+ }
+} // namespace kernel::tests::devices \ No newline at end of file
diff --git a/kernel/src/test_support/devices/character_device.cpp b/kernel/src/test_support/devices/character_device.cpp
new file mode 100644
index 0000000..3806654
--- /dev/null
+++ b/kernel/src/test_support/devices/character_device.cpp
@@ -0,0 +1,19 @@
+#include <kernel/test_support/devices/character_device.hpp>
+
+#include <kapi/devices.hpp>
+
+#include <kstd/string>
+
+#include <cstddef>
+
+namespace kernel::tests::devices
+{
+ character_device::character_device(size_t major, size_t minor, kstd::string const & name)
+ : kapi::devices::device(major, minor, name)
+ {}
+
+ auto character_device::init() -> bool
+ {
+ return true;
+ }
+} // namespace kernel::tests::devices \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/ext2.cpp b/kernel/src/test_support/filesystem/ext2.cpp
new file mode 100644
index 0000000..52b6efe
--- /dev/null
+++ b/kernel/src/test_support/filesystem/ext2.cpp
@@ -0,0 +1,68 @@
+#include <kernel/test_support/filesystem/ext2.hpp>
+
+#include <kernel/filesystem/ext2/block_group_descriptor.hpp>
+#include <kernel/filesystem/ext2/filesystem.hpp>
+#include <kernel/filesystem/ext2/inode.hpp>
+#include <kernel/filesystem/ext2/superblock.hpp>
+#include <kernel/test_support/devices/block_device.hpp>
+
+#include <cstdint>
+#include <cstring>
+
+namespace kernel::tests::filesystem::ext2
+{
+ namespace
+ {
+ constexpr uint32_t root_directory_data_block = 20;
+ } // namespace
+
+ auto write_bytes(kernel::tests::devices::block_device & device, size_t offset, void const * source, size_t size)
+ -> void
+ {
+ auto const required_size = offset + size;
+ if (device.data.size() < required_size)
+ {
+ device.data.resize(required_size, 0);
+ }
+
+ std::memcpy(device.data.data() + offset, source, size);
+ }
+
+ auto write_u32(kernel::tests::devices::block_device & device, size_t offset, uint32_t value) -> void
+ {
+ write_bytes(device, offset, &value, sizeof(value));
+ }
+
+ auto setup_mock_ext2_layout(kernel::tests::devices::block_device & device) -> void
+ {
+ 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.rev_level = 1;
+ superblock.inode_size = 128;
+ setup_mock_ext2_layout(device, superblock);
+ }
+
+ auto setup_mock_ext2_layout(kernel::tests::devices::block_device & device,
+ kernel::filesystem::ext2::superblock const & superblock) -> void
+ {
+ write_bytes(device, kernel::filesystem::ext2::constants::superblock_offset, &superblock, sizeof(superblock));
+
+ auto group_descriptor = kernel::filesystem::ext2::block_group_descriptor{};
+ group_descriptor.inode_table = 5;
+ write_bytes(device, 2048, &group_descriptor, sizeof(group_descriptor));
+
+ auto root_inode_data = kernel::filesystem::ext2::inode_data{};
+ root_inode_data.mode = kernel::filesystem::ext2::constants::mode_directory;
+ root_inode_data.blocks = 2;
+ root_inode_data.block[0] = root_directory_data_block;
+
+ auto const root_inode_offset =
+ static_cast<size_t>(group_descriptor.inode_table) * kernel::filesystem::ext2::constants::base_block_size +
+ (kernel::filesystem::ext2::constants::root_inode_number - 1) * superblock.inode_size;
+ write_bytes(device, root_inode_offset, &root_inode_data, sizeof(root_inode_data));
+ }
+} // namespace kernel::tests::filesystem::ext2 \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/filesystem.cpp b/kernel/src/test_support/filesystem/filesystem.cpp
new file mode 100644
index 0000000..ec70607
--- /dev/null
+++ b/kernel/src/test_support/filesystem/filesystem.cpp
@@ -0,0 +1,17 @@
+#include <kernel/test_support/filesystem/filesystem.hpp>
+
+#include <kernel/filesystem/inode.hpp>
+#include <kernel/test_support/filesystem/inode.hpp>
+
+#include <kstd/memory>
+
+#include <string_view>
+
+namespace kernel::tests::filesystem
+{
+ auto filesystem::lookup(kstd::shared_ptr<kernel::filesystem::inode> const &, std::string_view) const
+ -> kstd::shared_ptr<kernel::filesystem::inode>
+ {
+ return kstd::make_shared<inode>();
+ }
+} // namespace kernel::tests::filesystem \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/inode.cpp b/kernel/src/test_support/filesystem/inode.cpp
new file mode 100644
index 0000000..0c8d956
--- /dev/null
+++ b/kernel/src/test_support/filesystem/inode.cpp
@@ -0,0 +1,23 @@
+#include <kernel/test_support/filesystem/inode.hpp>
+
+#include <kernel/filesystem/inode.hpp>
+
+#include <cstddef>
+
+namespace kernel::tests::filesystem
+{
+ auto inode::read(void *, size_t, size_t size) const -> size_t
+ {
+ return size;
+ }
+
+ auto inode::write(void const *, size_t, size_t size) -> size_t
+ {
+ return size;
+ }
+
+ auto inode::is_regular() const -> bool
+ {
+ return true;
+ }
+} // namespace kernel::tests::filesystem \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/storage_boot_module_fixture.cpp b/kernel/src/test_support/filesystem/storage_boot_module_fixture.cpp
new file mode 100644
index 0000000..aabaace
--- /dev/null
+++ b/kernel/src/test_support/filesystem/storage_boot_module_fixture.cpp
@@ -0,0 +1,139 @@
+#include <kernel/test_support/filesystem/storage_boot_module_fixture.hpp>
+
+#include <kernel/devices/storage/management.hpp>
+#include <kernel/test_support/boot_modules.hpp>
+#include <kernel/test_support/devices/storage/management.hpp>
+
+#include <kapi/boot_module/boot_module.hpp>
+#include <kapi/boot_modules.hpp>
+#include <kapi/memory.hpp>
+
+#include <cstddef>
+#include <fcntl.h>
+#include <filesystem>
+#include <format>
+#include <stdexcept>
+#include <string>
+#include <unistd.h>
+#include <utility>
+#include <vector>
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+namespace kernel::tests::filesystem
+{
+ storage_boot_module_fixture::mapped_image::mapped_image(std::filesystem::path path)
+ : file_descriptor(::open(path.c_str(), O_RDWR))
+ {
+ if (file_descriptor < 0)
+ {
+ throw std::runtime_error{"Failed to open image file for test boot module: " + path.string()};
+ }
+
+ struct stat statistics{};
+ if (::fstat(file_descriptor, &statistics) < 0)
+ {
+ throw std::runtime_error{"Failed to get statistics for image file: " + path.string()};
+ }
+
+ size = static_cast<std::size_t>(statistics.st_size);
+
+ mapping = static_cast<std::byte *>(::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, file_descriptor, 0));
+ if (mapping == MAP_FAILED)
+ {
+ throw std::runtime_error{"Failed to map image file for test boot module: " + path.string()};
+ }
+ }
+
+ storage_boot_module_fixture::mapped_image::~mapped_image()
+ {
+ if (mapping != nullptr)
+ {
+ ::munmap(mapping, size);
+ }
+ if (file_descriptor >= 0)
+ {
+ ::close(file_descriptor);
+ }
+ }
+
+ storage_boot_module_fixture::mapped_image::mapped_image(mapped_image && other) noexcept
+ : file_descriptor{std::exchange(other.file_descriptor, -1)}
+ , mapping{std::exchange(other.mapping, nullptr)}
+ , size{std::exchange(other.size, 0)}
+ {}
+
+ auto storage_boot_module_fixture::mapped_image::operator=(mapped_image && other) noexcept -> mapped_image &
+ {
+ if (this != &other)
+ {
+ file_descriptor = std::exchange(other.file_descriptor, -1);
+ mapping = std::exchange(other.mapping, nullptr);
+ size = std::exchange(other.size, 0);
+ }
+ return *this;
+ }
+
+ storage_boot_module_fixture::~storage_boot_module_fixture()
+ {
+ kernel::tests::devices::storage::management::deinit();
+ kernel::tests::boot_modules::deinit();
+ }
+
+ auto storage_boot_module_fixture::setup_modules(std::size_t module_count, std::size_t module_size) -> void
+ {
+ m_module_names.clear();
+ m_module_data.clear();
+ m_registry = {};
+
+ m_module_names.reserve(module_count);
+ m_module_data.reserve(module_count);
+
+ for (std::size_t i = 0; i < module_count; ++i)
+ {
+ m_module_names.push_back(std::format("test_mod{}", i));
+ m_module_data.emplace_back(module_size, std::byte{static_cast<unsigned char>(0x40 + (i % 16))});
+ }
+
+ for (std::size_t i = 0; i < module_count; ++i)
+ {
+ m_registry.add_boot_module(kapi::boot_modules::boot_module{
+ m_module_names[i].c_str(), kapi::memory::linear_address{m_module_data[i].data()}, m_module_data[i].size()});
+ }
+
+ kapi::boot_modules::set_boot_module_registry(m_registry);
+ kernel::devices::storage::management::init();
+ }
+
+ auto storage_boot_module_fixture::setup_modules_from_img(std::vector<std::string> const & module_names,
+ std::vector<std::filesystem::path> const & img_paths) -> void
+ {
+ m_module_names.clear();
+ m_module_data.clear();
+ m_registry = {};
+
+ if (module_names.size() != img_paths.size())
+ {
+ throw std::invalid_argument{"Module names and image paths vectors must have the same size."};
+ }
+
+ for (size_t i = 0; i < module_names.size(); ++i)
+ {
+ setup_module_from_img(module_names[i], img_paths[i]);
+ }
+
+ kapi::boot_modules::set_boot_module_registry(m_registry);
+ kernel::devices::storage::management::init();
+ }
+
+ auto storage_boot_module_fixture::setup_module_from_img(std::string const & module_name,
+ std::filesystem::path const & img_path) -> void
+ {
+ m_module_names.push_back(module_name);
+ auto & mapped_image = m_mapped_images.emplace_back(img_path);
+
+ m_registry.add_boot_module(kapi::boot_modules::boot_module{
+ m_module_names.back().c_str(), kapi::memory::linear_address{mapped_image.mapping}, mapped_image.size});
+ }
+} // namespace kernel::tests::filesystem \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/storage_boot_module_vfs_fixture.cpp b/kernel/src/test_support/filesystem/storage_boot_module_vfs_fixture.cpp
new file mode 100644
index 0000000..02ccfec
--- /dev/null
+++ b/kernel/src/test_support/filesystem/storage_boot_module_vfs_fixture.cpp
@@ -0,0 +1,31 @@
+#include <kernel/test_support/filesystem/storage_boot_module_vfs_fixture.hpp>
+
+#include <kernel/filesystem/vfs.hpp>
+#include <kernel/test_support/filesystem/vfs.hpp>
+
+#include <cstddef>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace kernel::tests::filesystem
+{
+ storage_boot_module_vfs_fixture::~storage_boot_module_vfs_fixture()
+ {
+ kernel::tests::filesystem::vfs::deinit();
+ }
+
+ auto storage_boot_module_vfs_fixture::setup_modules_and_init_vfs(std::size_t module_count, std::size_t module_size)
+ -> void
+ {
+ setup_modules(module_count, module_size);
+ kernel::filesystem::vfs::init();
+ }
+
+ auto storage_boot_module_vfs_fixture::setup_modules_from_img_and_init_vfs(
+ std::vector<std::string> const & module_names, std::vector<std::filesystem::path> const & img_paths) -> void
+ {
+ setup_modules_from_img(module_names, img_paths);
+ kernel::filesystem::vfs::init();
+ }
+} // namespace kernel::tests::filesystem \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/test_assets/README.md b/kernel/src/test_support/filesystem/test_assets/README.md
new file mode 120000
index 0000000..718a227
--- /dev/null
+++ b/kernel/src/test_support/filesystem/test_assets/README.md
@@ -0,0 +1 @@
+/arch/x86_64/support/modules/README.md \ No newline at end of file
diff --git a/kernel/src/test_support/filesystem/test_assets/ext2_1KB_fs.img b/kernel/src/test_support/filesystem/test_assets/ext2_1KB_fs.img
new file mode 100644
index 0000000..a5202ca
--- /dev/null
+++ b/kernel/src/test_support/filesystem/test_assets/ext2_1KB_fs.img
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:98ac3c1be872806e25fb14eea168ca79a91959f4e6a5ac3d00c5d8224c1f73a3
+size 10485760
diff --git a/kernel/src/test_support/filesystem/test_assets/ext2_2KB_fs.img b/kernel/src/test_support/filesystem/test_assets/ext2_2KB_fs.img
new file mode 100644
index 0000000..7f297f0
--- /dev/null
+++ b/kernel/src/test_support/filesystem/test_assets/ext2_2KB_fs.img
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d9e872916e7d9107b321cc007e151899d5f19400a694666c0b24d482aef61ca
+size 5242880
diff --git a/kernel/src/test_support/filesystem/test_assets/ext2_4KB_fs.img b/kernel/src/test_support/filesystem/test_assets/ext2_4KB_fs.img
new file mode 100644
index 0000000..c3f6daf
--- /dev/null
+++ b/kernel/src/test_support/filesystem/test_assets/ext2_4KB_fs.img
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:026ca30269dbd80beb2dd74677c94676d1d4a7f6b5fe406c4ddb82836ba2dc00
+size 10485760
diff --git a/kernel/src/test_support/kapi/cio.cpp b/kernel/src/test_support/kapi/cio.cpp
new file mode 100644
index 0000000..98bc99d
--- /dev/null
+++ b/kernel/src/test_support/kapi/cio.cpp
@@ -0,0 +1,54 @@
+#include <kernel/test_support/cio.hpp>
+
+#include <kernel/test_support/log_buffer.hpp>
+
+#include <kapi/cio.hpp>
+
+#include <atomic>
+#include <optional>
+#include <stdexcept>
+
+namespace
+{
+
+ auto constinit is_initialized = std::atomic_flag{};
+ auto constinit device = std::optional<kernel::tests::cio::output_device>{};
+
+} // namespace
+
+namespace kapi::cio
+{
+
+ auto init() -> void
+ {
+ if (is_initialized.test_and_set())
+ {
+ throw std::logic_error("kapi::cio::init() called more than once");
+ }
+
+ device.emplace();
+ set_output_device(*device);
+ }
+
+} // namespace kapi::cio
+
+namespace kernel::tests::cio
+{
+
+ auto deinit() -> void
+ {
+ if (!is_initialized.test())
+ {
+ throw std::logic_error("kapi::cio::deinit() called before kapi::cio::init()");
+ }
+
+ device.reset();
+ is_initialized.clear();
+ }
+
+ auto log_buffer() -> kernel::tests::log_buffer &
+ {
+ return device->log_buffer();
+ }
+
+} // namespace kernel::tests::cio \ No newline at end of file
diff --git a/kernel/src/test_support/kapi/cpu.cpp b/kernel/src/test_support/kapi/cpu.cpp
new file mode 100644
index 0000000..5d95633
--- /dev/null
+++ b/kernel/src/test_support/kapi/cpu.cpp
@@ -0,0 +1,52 @@
+#include <kernel/test_support/cpu.hpp>
+
+#include <kapi/cpu.hpp>
+
+#include <atomic>
+#include <stdexcept>
+
+namespace
+{
+ auto static initialized = std::atomic_flag{};
+}
+
+namespace kapi::cpu
+{
+
+ auto init() -> void
+ {
+ if (initialized.test_and_set())
+ {
+ throw std::logic_error("kapi::cpu::init() called more than once");
+ }
+
+ // TODO: make sure that simulated interrupt can run.
+ }
+
+ auto halt() -> void
+ {
+ throw kernel::tests::cpu::halt{};
+ }
+
+ auto discover_topology() -> bool
+ {
+ // TODO: implement more meaningful simulated CPU topology discovery
+ return true;
+ }
+
+} // namespace kapi::cpu
+
+namespace kernel::tests::cpu
+{
+
+ auto deinit() -> void
+ {
+ if (!initialized.test())
+ {
+ throw std::logic_error{"kapi::cpu::reset() called before kapi::cpu::init()"};
+ }
+
+ initialized.clear();
+ }
+
+} // namespace kernel::tests::cpu \ No newline at end of file
diff --git a/kernel/src/test_support/kapi/interrupts.cpp b/kernel/src/test_support/kapi/interrupts.cpp
new file mode 100644
index 0000000..0077266
--- /dev/null
+++ b/kernel/src/test_support/kapi/interrupts.cpp
@@ -0,0 +1,11 @@
+#include <kapi/interrupts.hpp>
+
+namespace kapi::interrupts
+{
+
+ auto enable() -> void
+ {
+ // TODO: enable simulated interrupts.
+ }
+
+} // namespace kapi::interrupts \ No newline at end of file
diff --git a/kernel/src/test_support/kapi/memory.cpp b/kernel/src/test_support/kapi/memory.cpp
new file mode 100644
index 0000000..7fc95cb
--- /dev/null
+++ b/kernel/src/test_support/kapi/memory.cpp
@@ -0,0 +1,74 @@
+#include <kapi/memory.hpp>
+
+#include <kernel/test_support/bump_frame_allocator.hpp>
+#include <kernel/test_support/page_mapper.hpp>
+
+#include <kapi/memory.hpp>
+
+#include <kstd/units>
+
+#include <optional>
+
+namespace
+{
+ //! The size of the simulated RAM.
+ constexpr auto physical_size = kstd::units::MiB(32);
+ constexpr auto virtual_size = kstd::units::GiB(1);
+
+ constexpr auto number_of_frames = physical_size / kapi::memory::frame::size;
+
+ auto constinit bump_allocator = std::optional<kernel::tests::bump_frame_allocator>{};
+ auto constinit test_mapper = std::optional<kernel::tests::page_mapper>{};
+
+ auto constinit old_allocator = std::optional<kapi::memory::frame_allocator *>{};
+ auto constinit old_mapper = std::optional<kapi::memory::page_mapper *>{};
+
+ auto handoff_to_kernel_pmm(kapi::memory::frame_allocator & new_allocator) -> void
+ {
+ auto first_free_frame = bump_allocator->next_free_frame;
+ auto number_of_free_frames = number_of_frames - first_free_frame;
+ new_allocator.release_many({kapi::memory::frame{first_free_frame}, number_of_free_frames});
+ }
+
+} // namespace
+
+namespace kapi::memory
+{
+
+ auto init() -> void
+ {
+ bump_allocator.emplace();
+ test_mapper.emplace(physical_size, virtual_size);
+
+ old_allocator = set_frame_allocator(*bump_allocator);
+ old_mapper = set_page_mapper(*test_mapper);
+
+ init_pmm(physical_size / frame::size, handoff_to_kernel_pmm);
+ }
+} // namespace kapi::memory
+
+namespace kernel::tests::memory
+{
+
+ auto deinit() -> void
+ {
+ if (old_allocator && *old_allocator)
+ {
+ set_frame_allocator(**old_allocator);
+ }
+
+ if (old_mapper && *old_mapper)
+ {
+ set_page_mapper(**old_mapper);
+ }
+
+ bump_allocator.reset();
+ test_mapper.reset();
+ }
+
+ auto virtual_base() -> kapi::memory::linear_address
+ {
+ return test_mapper->memory.virtual_base();
+ }
+
+} // namespace kernel::tests::memory \ No newline at end of file
diff --git a/kernel/src/test_support/log_buffer.cpp b/kernel/src/test_support/log_buffer.cpp
new file mode 100644
index 0000000..04d875b
--- /dev/null
+++ b/kernel/src/test_support/log_buffer.cpp
@@ -0,0 +1,33 @@
+#include <kernel/test_support/log_buffer.hpp>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+namespace kernel::tests
+{
+
+ auto log_buffer::append(std::string const & message) -> void
+ {
+ m_messages.push_back(message);
+ }
+
+ auto log_buffer::clear() -> void
+ {
+ m_messages.clear();
+ }
+
+ auto log_buffer::flat_messages() -> std::string
+ {
+ return std::ranges::fold_left(m_messages, std::string{}, [](std::string accumulator, std::string const & message) {
+ accumulator += message;
+ return accumulator;
+ });
+ }
+
+ auto log_buffer::messages() -> std::vector<std::string> const &
+ {
+ return m_messages;
+ }
+
+} // namespace kernel::tests
diff --git a/kernel/src/test_support/output_device.cpp b/kernel/src/test_support/output_device.cpp
new file mode 100644
index 0000000..45fb4bc
--- /dev/null
+++ b/kernel/src/test_support/output_device.cpp
@@ -0,0 +1,28 @@
+#include <kernel/test_support/cio.hpp>
+#include <kernel/test_support/log_buffer.hpp>
+
+#include <kapi/cio.hpp>
+
+#include <iostream>
+#include <string>
+#include <string_view>
+
+namespace kernel::tests::cio
+{
+
+ auto output_device::write(kapi::cio::output_stream stream, std::string_view text) -> void
+ {
+ auto & standard_stream = stream == kapi::cio::output_stream::stdout ? std::cout : std::cerr;
+ standard_stream << text;
+ if (text != "\n")
+ {
+ m_log_buffer.append(std::string{text});
+ }
+ }
+
+ auto output_device::log_buffer() noexcept -> kernel::tests::log_buffer &
+ {
+ return m_log_buffer;
+ }
+
+} // namespace kernel::tests::cio \ No newline at end of file
diff --git a/kernel/src/test_support/page_mapper.cpp b/kernel/src/test_support/page_mapper.cpp
new file mode 100644
index 0000000..3d50ff1
--- /dev/null
+++ b/kernel/src/test_support/page_mapper.cpp
@@ -0,0 +1,70 @@
+#include <kernel/test_support/page_mapper.hpp>
+
+#include <kapi/memory.hpp>
+
+#include <kstd/units>
+
+#include <cstddef>
+#include <format>
+#include <stdexcept>
+
+namespace kernel::tests
+{
+
+ page_mapper::page_mapper(kstd::units::bytes physical_size, kstd::units::bytes virtual_size)
+ : memory{physical_size, virtual_size}
+ {}
+
+ auto page_mapper::map(kapi::memory::page page, kapi::memory::frame frame, flags) -> std::byte *
+ {
+ auto result = page_mappings.insert({page.number(), frame});
+ if (!result.second)
+ {
+ auto error = std::format("Page {} was already mapped!", page.number());
+ throw std::invalid_argument{error};
+ }
+
+ auto page_address = page.start_address();
+ auto virtual_base = memory.virtual_base();
+ auto virtual_end = virtual_base + memory.virtual_size();
+
+ if (page_address >= virtual_base && page_address < virtual_end)
+ {
+ auto virtual_target = static_cast<std::byte *>(page_address);
+ auto physical_offset = frame.number() * kapi::memory::frame::size;
+ return memory.map(kapi::memory::page::size, virtual_target, physical_offset.value);
+ }
+ else if (page_address >= kapi::memory::mmio_base)
+ {
+ throw std::runtime_error("MMIO mapping not yet supported in testing!");
+ }
+ else if (page_address >= kapi::memory::higher_half_direct_map_base)
+ {
+ auto offset = frame.number() * kapi::memory::frame::size;
+ return memory.physical_base() + offset;
+ }
+
+ return nullptr;
+ }
+
+ auto page_mapper::unmap(kapi::memory::page page) -> void
+ {
+ if (!try_unmap(page))
+ {
+ auto error = std::format("Page {} was never mapped!", page.number());
+ throw std::invalid_argument{error};
+ }
+ }
+
+ auto page_mapper::try_unmap(kapi::memory::page page) noexcept -> bool
+ {
+ if (page_mappings.contains(page.number()))
+ {
+ page_mappings.erase(page.number());
+ return true;
+ }
+
+ return false;
+ }
+
+} // namespace kernel::tests \ No newline at end of file
diff --git a/kernel/src/test_support/simulated_memory.cpp b/kernel/src/test_support/simulated_memory.cpp
new file mode 100644
index 0000000..074e6b1
--- /dev/null
+++ b/kernel/src/test_support/simulated_memory.cpp
@@ -0,0 +1,106 @@
+#include <kernel/test_support/simulated_memory.hpp>
+
+#include <kapi/memory.hpp>
+
+#include <kstd/units>
+
+#include <cerrno>
+#include <cstddef>
+#include <cstring>
+#include <format>
+#include <stdexcept>
+#include <unistd.h>
+
+#include <sys/mman.h>
+#include <sys/types.h>
+
+namespace kernel::tests
+{
+
+ simulated_memory::simulated_memory(kstd::units::bytes physical_size, kstd::units::bytes virtual_size)
+ : m_descriptor{memfd_create("teachos_simulated_memory", 0)}
+ , m_physical_size{physical_size}
+ , m_virtual_size{virtual_size}
+ {
+ if (m_descriptor < 0)
+ {
+ auto error = std::format("Failed to allocate backing memory: {}", strerror(errno));
+ throw std::runtime_error(error);
+ }
+
+ if (ftruncate(m_descriptor, static_cast<off_t>(m_physical_size.value)) < 0)
+ {
+ auto error = std::format("Failed to reserve backing memory: {}", strerror(errno));
+ throw std::runtime_error(error);
+ }
+
+ auto physical_storage = mmap(nullptr, m_physical_size.value, PROT_READ | PROT_WRITE, MAP_SHARED, m_descriptor, 0);
+ if (physical_storage == MAP_FAILED)
+ {
+ auto error = std::format("Failed to map backing memory: {}", strerror(errno));
+ throw std::runtime_error(error);
+ }
+
+ auto virtual_pointer = mmap(nullptr, virtual_size.value, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (virtual_pointer == MAP_FAILED)
+ {
+ auto error = std::format("Failed to reserve virtual memory: {}", strerror(errno));
+ throw std::runtime_error(error);
+ }
+
+ m_physical_base = static_cast<std::byte *>(physical_storage);
+ m_virtual_base = static_cast<std::byte *>(virtual_pointer);
+
+ clear();
+ }
+
+ simulated_memory::~simulated_memory()
+ {
+ munmap(m_virtual_base, m_virtual_size.value);
+ munmap(m_physical_base, m_physical_size.value);
+ close(m_descriptor);
+ }
+
+ auto simulated_memory::clear() -> void
+ {
+ std::memset(m_physical_base, 0, m_physical_size.value);
+ }
+
+ auto simulated_memory::physical_base() noexcept -> std::byte *
+ {
+ return m_physical_base;
+ }
+
+ auto simulated_memory::physical_base() const noexcept -> std::byte const *
+ {
+ return m_physical_base;
+ }
+
+ auto simulated_memory::physical_size() const noexcept -> kstd::units::bytes
+ {
+ return m_physical_size;
+ }
+
+ auto simulated_memory::virtual_base() const noexcept -> kapi::memory::linear_address
+ {
+ return kapi::memory::linear_address{m_virtual_base};
+ }
+
+ auto simulated_memory::virtual_size() const noexcept -> kstd::units::bytes
+ {
+ return m_virtual_size;
+ }
+
+ auto simulated_memory::map(kstd::units::bytes size, std::byte * to, off_t offset) -> std::byte *
+ {
+ auto mapped_ptr = mmap(to, size.value, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, m_descriptor, offset);
+ if (mapped_ptr == MAP_FAILED)
+ {
+ auto error = std::format("Failed to map page: {}", strerror(errno));
+ throw std::runtime_error(error);
+ }
+
+ return static_cast<std::byte *>(mapped_ptr);
+ }
+
+} // namespace kernel::tests \ No newline at end of file
diff --git a/kernel/src/test_support/state_reset_listener.cpp b/kernel/src/test_support/state_reset_listener.cpp
new file mode 100644
index 0000000..6bb7537
--- /dev/null
+++ b/kernel/src/test_support/state_reset_listener.cpp
@@ -0,0 +1,48 @@
+#include <kernel/filesystem/open_file_table.hpp>
+#include <kernel/test_support/boot_modules.hpp>
+#include <kernel/test_support/cio.hpp>
+#include <kernel/test_support/cpu.hpp>
+#include <kernel/test_support/devices/storage/management.hpp>
+#include <kernel/test_support/filesystem/open_file_table.hpp>
+#include <kernel/test_support/filesystem/vfs.hpp>
+#include <kernel/test_support/memory.hpp>
+
+#include <kapi/cio.hpp>
+#include <kapi/cpu.hpp>
+#include <kapi/memory.hpp>
+
+#include <catch2/catch_test_case_info.hpp>
+#include <catch2/interfaces/catch_interfaces_reporter.hpp>
+#include <catch2/reporters/catch_reporter_event_listener.hpp>
+#include <catch2/reporters/catch_reporter_registrars.hpp>
+
+struct state_reset_listener : Catch::EventListenerBase
+{
+ using EventListenerBase::EventListenerBase;
+
+ void testCaseStarting(Catch::TestCaseInfo const &) override
+ {
+ kernel::filesystem::open_file_table::init();
+
+ kapi::cio::init();
+ kapi::cpu::init();
+ kapi::memory::init();
+ }
+
+ void testCaseEnded(Catch::TestCaseStats const &) override
+ {
+ kernel::tests::filesystem::open_file_table::deinit();
+ kernel::tests::filesystem::vfs::deinit();
+ kernel::tests::boot_modules::deinit();
+ kernel::tests::devices::storage::management::deinit();
+
+ kernel::tests::memory::deinit();
+ kernel::tests::cpu::deinit();
+ kernel::tests::cio::deinit();
+ }
+};
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpedantic"
+CATCH_REGISTER_LISTENER(state_reset_listener);
+#pragma GCC diagnostic pop \ No newline at end of file