diff options
| author | Lukas Oesch <lukasoesch20@gmail.com> | 2026-04-09 12:32:50 +0200 |
|---|---|---|
| committer | Lukas Oesch <lukasoesch20@gmail.com> | 2026-04-11 08:05:53 +0200 |
| commit | 01d5f1b29fa04c69ac9f942a1075354ce5b25da2 (patch) | |
| tree | cf16f63eb2bed52e2886481c629c60a7ebf2efbf | |
| parent | a03d4d9acc5115ec3f651b06f94ced9a7c0e192f (diff) | |
| download | teachos-01d5f1b29fa04c69ac9f942a1075354ce5b25da2.tar.xz teachos-01d5f1b29fa04c69ac9f942a1075354ce5b25da2.zip | |
add ext2 inode and filesystem tests
| -rw-r--r-- | kernel/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | kernel/include/kernel/filesystem/ext2/filesystem.hpp | 21 | ||||
| -rw-r--r-- | kernel/include/kernel/test_support/filesystem/ext2.hpp | 17 | ||||
| -rw-r--r-- | kernel/src/filesystem/ext2/filesystem.cpp | 38 | ||||
| -rw-r--r-- | kernel/src/filesystem/ext2/filesystem.tests.cpp | 132 | ||||
| -rw-r--r-- | kernel/src/filesystem/ext2/inode.tests.cpp | 159 | ||||
| -rw-r--r-- | kernel/src/test_support/filesystem/ext2.cpp | 62 |
7 files changed, 406 insertions, 26 deletions
diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt index 88d420b..2f6113a 100644 --- a/kernel/CMakeLists.txt +++ b/kernel/CMakeLists.txt @@ -105,6 +105,7 @@ else() "src/test_support/devices/character_device.cpp" "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" "src/test_support/log_buffer.cpp" "src/test_support/output_device.cpp" @@ -134,6 +135,8 @@ else() # 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/rootfs/filesystem.tests.cpp" "src/filesystem/rootfs/inode.tests.cpp" "src/filesystem/dentry.tests.cpp" diff --git a/kernel/include/kernel/filesystem/ext2/filesystem.hpp b/kernel/include/kernel/filesystem/ext2/filesystem.hpp index 65324c8..a71385f 100644 --- a/kernel/include/kernel/filesystem/ext2/filesystem.hpp +++ b/kernel/include/kernel/filesystem/ext2/filesystem.hpp @@ -19,6 +19,27 @@ 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 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; + } // namespace constants + + /** @brief A filesystem implementation for the ext2 filesystem format. This class provides methods for mounting an ext2 filesystem, and looking up inodes. */ 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..edbda29 --- /dev/null +++ b/kernel/include/kernel/test_support/filesystem/ext2.hpp @@ -0,0 +1,17 @@ +#ifndef TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_EXT2_HPP +#define TEACHOS_KERNEL_TEST_SUPPORT_FILESYSTEM_EXT2_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; +} // namespace kernel::tests::filesystem::ext2 + +#endif
\ No newline at end of file diff --git a/kernel/src/filesystem/ext2/filesystem.cpp b/kernel/src/filesystem/ext2/filesystem.cpp index 6d5960e..0ad5c97 100644 --- a/kernel/src/filesystem/ext2/filesystem.cpp +++ b/kernel/src/filesystem/ext2/filesystem.cpp @@ -21,29 +21,14 @@ namespace kernel::filesystem::ext2 { namespace { - constexpr size_t SUPERBLOCK_OFFSET = 1024; - constexpr uint16_t MAGIC_NUMBER = 0xEF53; - - constexpr uint32_t ROOT_INODE_NUMBER = 2; - - constexpr size_t DIRECT_BLOCK_COUNT = 12; - constexpr size_t SINGLY_INDIRECT_BLOCK_INDEX = DIRECT_BLOCK_COUNT; - constexpr size_t DOUBLY_INDIRECT_BLOCK_INDEX = SINGLY_INDIRECT_BLOCK_INDEX + 1; - constexpr size_t TRIPLY_INDIRECT_BLOCK_INDEX = DOUBLY_INDIRECT_BLOCK_INDEX + 1; - - // Mode bits - constexpr uint16_t S_IFMT = 0xF000; - constexpr uint16_t S_IFREG = 0x8000; - constexpr uint16_t S_IFDIR = 0x4000; - auto S_ISREG(uint16_t mode) -> bool { - return (mode & S_IFMT) == S_IFREG; + return (mode & constants::mode_mask) == constants::mode_regular; } auto S_ISDIR(uint16_t mode) -> bool { - return (mode & S_IFMT) == S_IFDIR; + return (mode & constants::mode_mask) == constants::mode_directory; } } // namespace @@ -51,9 +36,10 @@ namespace kernel::filesystem::ext2 { kernel::filesystem::filesystem::mount(device); - kernel::devices::block_device_utils::read(m_device, &m_superblock, SUPERBLOCK_OFFSET, sizeof(m_superblock)); + kernel::devices::block_device_utils::read(m_device, &m_superblock, constants::superblock_offset, + sizeof(m_superblock)); - if (m_superblock.magic != MAGIC_NUMBER) + if (m_superblock.magic != constants::magic_number) { return operation_result::invalid_magic_number; } @@ -69,7 +55,7 @@ namespace kernel::filesystem::ext2 block_group_descriptor_table_offset, num_block_groups * sizeof(block_group_descriptor)); - m_root_inode = read_inode(ROOT_INODE_NUMBER); + m_root_inode = read_inode(constants::root_inode_number); if (!m_root_inode || !m_root_inode->is_directory()) { @@ -161,11 +147,11 @@ namespace kernel::filesystem::ext2 auto filesystem::map_inode_block_index_to_global_block_number(uint32_t inode_block_index, inode_data data) -> uint32_t { - if (inode_block_index < DIRECT_BLOCK_COUNT) + if (inode_block_index < constants::direct_block_count) { return data.block.at(inode_block_index); } - inode_block_index -= DIRECT_BLOCK_COUNT; + inode_block_index -= constants::direct_block_count; auto const block_size = get_block_size(); auto const numbers_per_block = block_size / sizeof(uint32_t); @@ -176,14 +162,14 @@ namespace kernel::filesystem::ext2 if (inode_block_index < block_numbers_per_singly_indirect_block) { - auto const singly_indirect_block_number = data.block.at(SINGLY_INDIRECT_BLOCK_INDEX); + auto const singly_indirect_block_number = data.block.at(constants::singly_indirect_block_index); return read_block_number_at_index(singly_indirect_block_number, inode_block_index); } inode_block_index -= block_numbers_per_singly_indirect_block; if (inode_block_index < block_numbers_per_doubly_indirect_block) { - auto const doubly_indirect_block_number = data.block.at(DOUBLY_INDIRECT_BLOCK_INDEX); + auto const doubly_indirect_block_number = data.block.at(constants::doubly_indirect_block_index); auto const singly_indirect_block_index_in_doubly_indirect_block = inode_block_index / block_numbers_per_singly_indirect_block; auto const singly_indirect_block_number = read_block_number_at_index( @@ -196,7 +182,7 @@ namespace kernel::filesystem::ext2 if (inode_block_index < block_numbers_per_triply_indirect_block) { - auto const triply_indirect_block_number = data.block.at(TRIPLY_INDIRECT_BLOCK_INDEX); + auto const triply_indirect_block_number = data.block.at(constants::triply_indirect_block_index); auto const doubly_indirect_block_index_in_triply_indirect_block = inode_block_index / block_numbers_per_doubly_indirect_block; auto const doubly_indirect_block_number = read_block_number_at_index( @@ -230,7 +216,7 @@ namespace kernel::filesystem::ext2 auto filesystem::get_block_size() -> size_t { - return 1024U << m_superblock.log_block_size; + return constants::base_block_size << m_superblock.log_block_size; } auto filesystem::get_inode_size() -> size_t diff --git a/kernel/src/filesystem/ext2/filesystem.tests.cpp b/kernel/src/filesystem/ext2/filesystem.tests.cpp new file mode 100644 index 0000000..b13ebf3 --- /dev/null +++ b/kernel/src/filesystem/ext2/filesystem.tests.cpp @@ -0,0 +1,132 @@ +#include "kernel/filesystem/ext2/filesystem.hpp" + +#include "kernel/devices/storage/management.hpp" +#include "kernel/filesystem/ext2/inode.hpp" +#include "kernel/filesystem/filesystem.hpp" +#include "kernel/test_support/devices/block_device.hpp" +#include "kernel/test_support/filesystem/ext2.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_fixture.hpp" + +#include <kstd/memory> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <array> +#include <cstdint> +#include <filesystem> + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_fixture, + "Ext2 filesystem mount and lookup with real image", "[filesystem][ext2][filesystem][img]") +{ + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + + GIVEN("a mounted ext2 filesystem from a real image") + { + REQUIRE(std::filesystem::exists(image_path)); + REQUIRE_NOTHROW(setup_modules_from_img({"test_img_module"}, {image_path})); + + auto boot_device = kernel::devices::storage::management::get().determine_boot_device(); + REQUIRE(boot_device != nullptr); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(boot_device) == kernel::filesystem::filesystem::operation_result::success); + + THEN("the root inode is available and is a directory") + { + REQUIRE(fs.root_inode() != nullptr); + REQUIRE(fs.root_inode()->is_directory()); + } + + THEN("lookup resolves known entries from the image") + { + auto information = fs.lookup(fs.root_inode(), "information"); + REQUIRE(information != nullptr); + REQUIRE(information->is_directory()); + + auto info_1 = fs.lookup(information, "info_1.txt"); + REQUIRE(info_1 != nullptr); + REQUIRE(info_1->is_regular()); + } + + THEN("lookup returns null for invalid inputs") + { + REQUIRE(fs.lookup(nullptr, "information") == nullptr); + + auto information = fs.lookup(fs.root_inode(), "information"); + REQUIRE(information != nullptr); + auto info_1 = fs.lookup(information, "info_1.txt"); + REQUIRE(info_1 != nullptr); + + REQUIRE(fs.lookup(info_1, "anything") == nullptr); + REQUIRE(fs.lookup(fs.root_inode(), "does_not_exist") == nullptr); + } + } +} + +SCENARIO("Ext2 filesystem rejects invalid magic", "[filesystem][ext2][filesystem]") +{ + auto const block_size = 1024; + GIVEN("a block device that does not contain an ext2 superblock") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 2 * block_size); + REQUIRE(device != nullptr); + + auto fs = kernel::filesystem::ext2::filesystem{}; + + THEN("mount fails with invalid_magic_number") + { + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::invalid_magic_number); + } + } +} + +SCENARIO("Ext2 block mapping includes direct and all indirect levels", "[filesystem][ext2][filesystem]") +{ + auto const block_size = 1024; + + GIVEN("a minimally valid ext2 layout with configured indirect block tables") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 128 * block_size); + REQUIRE(device != nullptr); + + kernel::tests::filesystem::ext2::setup_mock_ext2_layout(*device); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::success); + + auto inode_data = kernel::filesystem::ext2::inode_data{}; + inode_data.block[0] = 7; + + inode_data.block[12] = 30; + kernel::tests::filesystem::ext2::write_u32(*device, 30 * block_size, 31); + + inode_data.block[13] = 40; + kernel::tests::filesystem::ext2::write_u32(*device, 40 * block_size, 41); + kernel::tests::filesystem::ext2::write_u32(*device, 41 * block_size, 42); + + inode_data.block[14] = 50; + kernel::tests::filesystem::ext2::write_u32(*device, 50 * block_size, 51); + kernel::tests::filesystem::ext2::write_u32(*device, 51 * block_size, 52); + kernel::tests::filesystem::ext2::write_u32(*device, 52 * block_size, 53); + + auto const numbers_per_block = static_cast<uint32_t>(block_size / sizeof(uint32_t)); + auto const singly_start = static_cast<uint32_t>(kernel::filesystem::ext2::constants::direct_block_count); + auto const doubly_start = singly_start + numbers_per_block; + auto const triply_start = doubly_start + numbers_per_block * numbers_per_block; + + THEN("mapping resolves direct, singly, doubly and triply indirect indexes") + { + REQUIRE(fs.map_inode_block_index_to_global_block_number(0, inode_data) == 7); + REQUIRE(fs.map_inode_block_index_to_global_block_number(singly_start, inode_data) == 31); + REQUIRE(fs.map_inode_block_index_to_global_block_number(doubly_start, inode_data) == 42); + REQUIRE(fs.map_inode_block_index_to_global_block_number(triply_start, inode_data) == 53); + } + + THEN("mapping returns zero for out-of-range indexes") + { + auto const beyond_triply = triply_start + numbers_per_block * numbers_per_block * numbers_per_block; + REQUIRE(fs.map_inode_block_index_to_global_block_number(beyond_triply, inode_data) == 0); + } + } +} diff --git a/kernel/src/filesystem/ext2/inode.tests.cpp b/kernel/src/filesystem/ext2/inode.tests.cpp new file mode 100644 index 0000000..795ff10 --- /dev/null +++ b/kernel/src/filesystem/ext2/inode.tests.cpp @@ -0,0 +1,159 @@ +#include "kernel/filesystem/ext2/inode.hpp" + +#include "kernel/devices/storage/management.hpp" +#include "kernel/filesystem/ext2/filesystem.hpp" +#include "kernel/filesystem/filesystem.hpp" +#include "kernel/test_support/cpu.hpp" +#include "kernel/test_support/devices/block_device.hpp" +#include "kernel/test_support/filesystem/ext2.hpp" +#include "kernel/test_support/filesystem/storage_boot_module_fixture.hpp" + +#include <kstd/memory> +#include <kstd/vector> + +#include <catch2/catch_test_macros.hpp> + +#include <cstddef> +#include <filesystem> +#include <string_view> + +SCENARIO("Ext2 inode initialization and properties", "[filesystem][ext2][inode]") +{ + GIVEN("an ext2 filesystem") + { + auto fs = kernel::filesystem::ext2::filesystem{}; + + THEN("the inode is initialized and has the kind regular") + { + auto inode = kernel::filesystem::ext2::inode{&fs}; + REQUIRE(inode.is_regular()); + REQUIRE(!inode.is_directory()); + REQUIRE(!inode.is_device()); + } + } + + GIVEN("no filesystem (null pointer)") + { + THEN("constructing an inode with a null filesystem pointer panics") + { + REQUIRE_THROWS_AS(kernel::filesystem::ext2::inode{nullptr}, kernel::tests::cpu::halt); + } + } +} + +SCENARIO_METHOD(kernel::tests::filesystem::storage_boot_module_fixture, "Ext2 inode reads from real image", + "[filesystem][ext2][inode][img]") +{ + auto const image_path = std::filesystem::path{KERNEL_TEST_ASSETS_DIR} / "ext2_1KB_fs.img"; + + GIVEN("a mounted ext2 filesystem and a regular file inode") + { + REQUIRE(std::filesystem::exists(image_path)); + REQUIRE_NOTHROW(setup_modules_from_img({"test_img_module"}, {image_path})); + + auto boot_device = kernel::devices::storage::management::get().determine_boot_device(); + REQUIRE(boot_device != nullptr); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(boot_device) == kernel::filesystem::filesystem::operation_result::success); + + auto information = fs.lookup(fs.root_inode(), "information"); + REQUIRE(information != nullptr); + auto file = fs.lookup(information, "info_1.txt"); + REQUIRE(file != nullptr); + REQUIRE(file->is_regular()); + + THEN("reading from offset zero returns expected file prefix") + { + auto buffer = kstd::vector<std::byte>(6); + auto const bytes_read = file->read(buffer.data(), 0, buffer.size()); + + REQUIRE(bytes_read == 6); + + auto const text = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read}; + REQUIRE(text == "info_1"); + } + + THEN("reading with an offset returns the expected byte") + { + auto buffer = kstd::vector<std::byte>(1); + auto const bytes_read = file->read(buffer.data(), 5, buffer.size()); + + REQUIRE(bytes_read == 1); + REQUIRE(static_cast<char>(buffer[0]) == '1'); + } + } +} + +SCENARIO("Ext2 inode read stops when block mapping resolves to zero", "[filesystem][ext2][inode]") +{ + auto const block_size = 1024; + GIVEN("an ext2 inode without mapped data blocks") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 64 * block_size); + REQUIRE(device != nullptr); + kernel::tests::filesystem::ext2::setup_mock_ext2_layout(*device); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::success); + + auto inode = kernel::filesystem::ext2::inode{&fs}; + inode.m_data.blocks = 2; + inode.m_data.block[0] = 0; + + auto buffer = kstd::vector<std::byte>(32, std::byte{0xAB}); + + THEN("no bytes are read") + { + auto const bytes_read = inode.read(buffer.data(), 0, buffer.size()); + REQUIRE(bytes_read == 0); + } + } +} + +SCENARIO("Ext2 inode read across block boundaries", "[filesystem][ext2][inode]") +{ + auto const block_size = 1024; + GIVEN("an ext2 inode with two direct blocks and a block size of 1024 bytes") + { + auto device = kstd::make_shared<kernel::tests::devices::block_device>(0, 0, "mock", block_size, 64 * block_size); + REQUIRE(device != nullptr); + kernel::tests::filesystem::ext2::setup_mock_ext2_layout(*device); + + auto fs = kernel::filesystem::ext2::filesystem{}; + REQUIRE(fs.mount(device) == kernel::filesystem::filesystem::operation_result::success); + + auto inode = kernel::filesystem::ext2::inode{&fs}; + inode.m_data.blocks = 2; + inode.m_data.block[0] = 20; + kernel::tests::filesystem::ext2::write_bytes(*device, 21 * block_size - 6, "Hello ", 6); + inode.m_data.block[1] = 21; + kernel::tests::filesystem::ext2::write_bytes(*device, 21 * block_size, "World!", 6); + + auto buffer = kstd::vector<std::byte>(12, std::byte{0x00}); + + THEN("reading across the block boundary returns the combined content") + { + auto const bytes_read = inode.read(buffer.data(), block_size - 6, buffer.size()); + REQUIRE(bytes_read == 12); + + auto const text = std::string_view{reinterpret_cast<char const *>(buffer.data()), bytes_read}; + REQUIRE(text == "Hello World!"); + } + } +} + +SCENARIO("Ext2 inode write is not implemented", "[filesystem][ext2][inode]") +{ + GIVEN("an ext2 inode") + { + auto fs = kernel::filesystem::ext2::filesystem{}; + auto inode = kernel::filesystem::ext2::inode{&fs}; + + THEN("writing to the inode panics") + { + auto buffer = kstd::vector<std::byte>(32, std::byte{0x00}); + REQUIRE_THROWS_AS(inode.write(buffer.data(), 0, buffer.size()), kernel::tests::cpu::halt); + } + } +} diff --git a/kernel/src/test_support/filesystem/ext2.cpp b/kernel/src/test_support/filesystem/ext2.cpp new file mode 100644 index 0000000..3627373 --- /dev/null +++ b/kernel/src/test_support/filesystem/ext2.cpp @@ -0,0 +1,62 @@ +#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; + 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 |
