diff options
| author | Lukas Oesch <lukas.oesch@ost.ch> | 2026-06-10 10:40:46 +0200 |
|---|---|---|
| committer | Lukas Oesch <lukas.oesch@ost.ch> | 2026-06-10 10:40:46 +0200 |
| commit | 33abd5cf264cb9e34121082105b0bc17b3cf7a36 (patch) | |
| tree | 36b15d53fea04f4f9d9af817100f7ad013bd9b5c /kernel/src | |
| parent | d01caf1c4aef3c89c68b9d1cc9fe56445f0860b5 (diff) | |
| parent | 7e27130c342b7299a1d2188a7192a7f17b5ac2ad (diff) | |
| download | kernel-33abd5cf264cb9e34121082105b0bc17b3cf7a36.tar.xz kernel-33abd5cf264cb9e34121082105b0bc17b3cf7a36.zip | |
Merge of BA-FS26 branch into develop
See merge request teachos/kernel!49
Diffstat (limited to 'kernel/src')
70 files changed, 6021 insertions, 47 deletions
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, ¤t_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 |
