diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f0dfcd..e640a72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ laghu_configure_capability_header() laghu_configure_build_identity() add_library(laghu_core STATIC + src/core/clocks.cpp src/core/contract.cpp src/core/digest.cpp src/core/fingerprints.cpp @@ -102,6 +103,24 @@ laghu_apply_first_party_contract(laghu_os) laghu_configure_api_consumer(laghu_os os) target_include_directories(laghu_os PRIVATE "${CMAKE_SOURCE_DIR}/src/os/private") +# Deterministic time and entropy are test-only controls. They are kept in a +# separate non-installed target so production entropy remains OS-provided. +add_library(laghu_test_time_entropy STATIC tests/support/laghu_test_time_entropy.cpp) +laghu_apply_first_party_contract(laghu_test_time_entropy) +target_include_directories(laghu_test_time_entropy PUBLIC + "${CMAKE_SOURCE_DIR}/tests/support" + "${CMAKE_SOURCE_DIR}/src/core/contract" + "${CMAKE_SOURCE_DIR}/src/adapters/private") + +add_executable(laghu_test_time_entropy_test tests/support/time_entropy.cpp) +laghu_apply_first_party_contract(laghu_test_time_entropy_test) +laghu_configure_api_consumer(laghu_test_time_entropy_test core) +target_include_directories(laghu_test_time_entropy_test PRIVATE + "${CMAKE_SOURCE_DIR}/src/core/private") +target_link_libraries(laghu_test_time_entropy_test PRIVATE + laghu_test_support laghu_test_time_entropy laghu_core) +laghu_add_native_test(laghu.test_support.time_entropy laghu_test_time_entropy_test) + list(FIND LAGHU_EFFECTIVE_FEATURES tls laghu_tls_feature_index) if(NOT laghu_tls_feature_index EQUAL -1) add_library(laghu_crypto STATIC @@ -227,6 +246,15 @@ if(TARGET laghu_crypto) target_link_libraries(laghu_crypto_provider_test PRIVATE laghu_crypto) target_link_libraries(laghu_crypto_provider_test PRIVATE laghu_test_support) laghu_add_native_test(laghu.crypto.provider laghu_crypto_provider_test) + + add_executable(laghu_crypto_entropy_seam_test tests/adapters/entropy_seam.cpp) + laghu_apply_first_party_contract(laghu_crypto_entropy_seam_test) + laghu_configure_api_consumer(laghu_crypto_entropy_seam_test adapters) + target_include_directories(laghu_crypto_entropy_seam_test PRIVATE + "${CMAKE_SOURCE_DIR}/src/adapters/private") + target_link_libraries(laghu_crypto_entropy_seam_test PRIVATE + laghu_crypto laghu_test_support laghu_test_time_entropy) + laghu_add_native_test(laghu.crypto.entropy_seam laghu_crypto_entropy_seam_test) endif() if(LAGHU_ENABLE_TSAN) @@ -289,11 +317,13 @@ set(laghu_verify_targets laghu_core_state_transitions_test laghu_core_deadlines_cancellation_test laghu_core_shared_offsets_test laghu_core_mapped_regions_test laghu_core_binary_envelope_test laghu_core_digest_primitives_test - laghu_os_iovec_translation_test laghu_test_support laghu_test_faults laghu_test_support_fixtures_test + laghu_os_iovec_translation_test laghu_test_support laghu_test_faults laghu_test_time_entropy + laghu_test_time_entropy_test laghu_test_support_fixtures_test laghu_test_support_runner_contract_test laghu_test_fault_injection_test laghu laghu_capability_header_parity laghu_visibility_probe visibility_negative_fixture) if(TARGET laghu_crypto_provider_test) - list(APPEND laghu_verify_targets laghu_crypto laghu_crypto_provider_test) + list(APPEND laghu_verify_targets + laghu_crypto laghu_crypto_provider_test laghu_crypto_entropy_seam_test) endif() add_custom_target(laghu_verify_toolchain diff --git a/cmake/LaghuApiBoundaries.cmake b/cmake/LaghuApiBoundaries.cmake index c69653d..af69bb1 100644 --- a/cmake/LaghuApiBoundaries.cmake +++ b/cmake/LaghuApiBoundaries.cmake @@ -193,6 +193,7 @@ endfunction() function(laghu_add_visibility_probe) add_library(laghu_visibility_probe SHARED + src/core/clocks.cpp src/core/contract.cpp src/core/digest.cpp src/core/fingerprints.cpp diff --git a/cmake/LaghuBuildIdentity.cmake b/cmake/LaghuBuildIdentity.cmake index 75bfa63..ac14468 100644 --- a/cmake/LaghuBuildIdentity.cmake +++ b/cmake/LaghuBuildIdentity.cmake @@ -35,11 +35,14 @@ function(laghu_build_identity_input_hashes output) cmake/LaghuToolchain.cmake src/cli/main.cpp src/cli/private/laghu/cli/internal/build_manifest.hpp + src/core/clocks.cpp src/core/contract.cpp src/core/digest.cpp src/core/fingerprints.cpp src/core/contract/laghu/core/contract.hpp + src/core/contract/laghu/core/clocks.hpp src/core/contract/laghu/core/crypto.hpp + src/core/contract/laghu/core/deadlines_cancellation.hpp src/core/contract/laghu/core/digest.hpp src/core/contract/laghu/core/bounded_arena.hpp src/core/contract/laghu/core/binary_envelope.hpp @@ -49,6 +52,7 @@ function(laghu_build_identity_input_hashes output) src/core/contract/laghu/core/mapped_regions.hpp src/core/handles.cpp src/core/mapped_regions.cpp + src/core/private/laghu/core/internal/clock_operations.hpp src/core/private/laghu/core/internal/compiler_extensions.hpp src/core/private/laghu/core/internal/descriptor_operations.hpp src/core/private/laghu/core/internal/fingerprints.hpp diff --git a/src/core/clocks.cpp b/src/core/clocks.cpp new file mode 100644 index 0000000..924ec3d --- /dev/null +++ b/src/core/clocks.cpp @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include + +#include + +#include +#include + +namespace laghu::core { +namespace { + +inline constexpr std::int64_t nanoseconds_per_second = 1000000000; + +[[nodiscard]] Result posix_monotonic_now(void*) noexcept { + timespec value{}; + if (::clock_gettime(CLOCK_MONOTONIC, &value) != 0) { + return std::unexpected{Error::from_errno(errno, "CLOCK_MONOTONIC query failed")}; + } + if (value.tv_sec < 0 || value.tv_nsec < 0 || value.tv_nsec >= nanoseconds_per_second) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::corrupt_data, 0, + "CLOCK_MONOTONIC result is invalid"}}; + } + const auto seconds = static_cast(value.tv_sec); + const auto nanoseconds = static_cast(value.tv_nsec); + if (seconds > (std::numeric_limits::max() - nanoseconds) / + static_cast(nanoseconds_per_second)) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::overflow, 0, + "CLOCK_MONOTONIC result overflows"}}; + } + return seconds * static_cast(nanoseconds_per_second) + nanoseconds; +} + +[[nodiscard]] Result posix_realtime_now(void*) noexcept { + timespec value{}; + if (::clock_gettime(CLOCK_REALTIME, &value) != 0) { + return std::unexpected{Error::from_errno(errno, "CLOCK_REALTIME query failed")}; + } + return internal::realtime_instant_from_parts(static_cast(value.tv_sec), + static_cast(value.tv_nsec)); +} + +const ClockOperations default_operations{nullptr, posix_monotonic_now, posix_realtime_now}; + +} // namespace + +Result read_monotonic_clock(const ClockOperations& operations) noexcept { + if (operations.monotonic_now == nullptr) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::invalid_state, 0, + "monotonic clock operation is missing"}}; + } + return operations.monotonic_now(operations.context); +} + +Result read_realtime_clock(const ClockOperations& operations) noexcept { + if (operations.realtime_now == nullptr) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::invalid_state, 0, + "realtime clock operation is missing"}}; + } + return operations.realtime_now(operations.context); +} + +const ClockOperations& system_clock_operations() noexcept { return default_operations; } + +namespace internal { + +Result realtime_instant_from_parts(RealtimeInstant seconds, + RealtimeInstant nanoseconds) noexcept { + if (nanoseconds < 0 || nanoseconds >= nanoseconds_per_second) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::corrupt_data, 0, + "CLOCK_REALTIME result is invalid"}}; + } + + constexpr RealtimeInstant maximum = std::numeric_limits::max(); + constexpr RealtimeInstant minimum = std::numeric_limits::min(); + constexpr RealtimeInstant maximum_seconds = maximum / nanoseconds_per_second; + constexpr RealtimeInstant maximum_nanoseconds = maximum % nanoseconds_per_second; + constexpr RealtimeInstant minimum_truncated_seconds = minimum / nanoseconds_per_second; + constexpr RealtimeInstant minimum_remainder = minimum % nanoseconds_per_second; + constexpr RealtimeInstant minimum_seconds = minimum_truncated_seconds - 1; + constexpr RealtimeInstant minimum_nanoseconds = nanoseconds_per_second + minimum_remainder; + + if (seconds > maximum_seconds || + (seconds == maximum_seconds && nanoseconds > maximum_nanoseconds) || + seconds < minimum_seconds || + (seconds == minimum_seconds && nanoseconds < minimum_nanoseconds)) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::overflow, 0, + "CLOCK_REALTIME result overflows"}}; + } + if (seconds == minimum_seconds) { + return minimum + (nanoseconds - minimum_nanoseconds); + } + return seconds * nanoseconds_per_second + nanoseconds; +} + +} // namespace internal +} // namespace laghu::core diff --git a/src/core/contract/laghu/core/clocks.hpp b/src/core/contract/laghu/core/clocks.hpp new file mode 100644 index 0000000..a8dd8d2 --- /dev/null +++ b/src/core/contract/laghu/core/clocks.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include + +#include + +namespace laghu::core { + +using MonotonicInstant = std::uint64_t; +using RealtimeInstant = std::int64_t; + +using MonotonicNowFunction = Result (*)(void* context) noexcept; +using RealtimeNowFunction = Result (*)(void* context) noexcept; + +// A caller owns the context and every function referenced by this table. +// Monotonic time is used for expiry; realtime is for externally visible time. +struct ClockOperations final { + void* context{}; + MonotonicNowFunction monotonic_now{}; + RealtimeNowFunction realtime_now{}; +}; + +[[nodiscard]] Result read_monotonic_clock( + const ClockOperations& operations) noexcept; +[[nodiscard]] Result read_realtime_clock( + const ClockOperations& operations) noexcept; + +// Returns a non-owning reference to Laghu's process-static POSIX clock table. +// It remains valid for the process lifetime and exposes no POSIX types. +[[nodiscard]] const ClockOperations& system_clock_operations() noexcept; + +} // namespace laghu::core diff --git a/src/core/contract/laghu/core/deadlines_cancellation.hpp b/src/core/contract/laghu/core/deadlines_cancellation.hpp index a271cff..cbec66f 100644 --- a/src/core/contract/laghu/core/deadlines_cancellation.hpp +++ b/src/core/contract/laghu/core/deadlines_cancellation.hpp @@ -9,12 +9,10 @@ #include #include -#include +#include namespace laghu::core { -using MonotonicInstant = std::uint64_t; - // Clocks are supplied by the owner. Production code can inject its POSIX // monotonic clock and tests can inject a deterministic clock without a timer // thread or dynamic dispatch. @@ -40,6 +38,19 @@ class Deadline final { return Deadline{now + duration}; } + [[nodiscard]] static Result after(const ClockOperations& operations, + MonotonicInstant duration) noexcept { + const auto now = read_monotonic_clock(operations); + if (!now.has_value()) { + return std::unexpected{now.error()}; + } + if (duration > std::numeric_limits::max() - *now) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::overflow, 0, + "monotonic deadline overflows"}}; + } + return Deadline{*now + duration}; + } + [[nodiscard]] static constexpr Deadline child(Deadline parent, Deadline requested) noexcept { return Deadline{parent.instant_ < requested.instant_ ? parent.instant_ : requested.instant_}; } @@ -51,6 +62,14 @@ class Deadline final { return clock.now() >= instant_; } + [[nodiscard]] Result expired(const ClockOperations& operations) const noexcept { + const auto now = read_monotonic_clock(operations); + if (!now.has_value()) { + return std::unexpected{now.error()}; + } + return *now >= instant_; + } + template [[nodiscard]] constexpr Result require_not_expired(const Clock& clock) const noexcept { if (expired(clock)) { @@ -59,6 +78,17 @@ class Deadline final { return {}; } + [[nodiscard]] Result require_not_expired(const ClockOperations& operations) const noexcept { + const auto is_expired = expired(operations); + if (!is_expired.has_value()) { + return std::unexpected{is_expired.error()}; + } + if (*is_expired) { + return std::unexpected{deadline_error()}; + } + return {}; + } + [[nodiscard]] static constexpr Error deadline_error() noexcept { return Error{ErrorDomain::core, ErrorCode::deadline, 0, "monotonic deadline expired"}; } @@ -193,6 +223,14 @@ class CancellationToken final { return deadline.require_not_expired(clock); } + [[nodiscard]] Result require_active(const Deadline& deadline, + const ClockOperations& operations) const noexcept { + if (const Result active = require_active(); !active.has_value()) { + return std::unexpected{active.error()}; + } + return deadline.require_not_expired(operations); + } + [[nodiscard]] Result child(CancellationState& child_state) const noexcept { for (std::uint8_t index = 0; index < state_count_; ++index) { if (states_[index] == &child_state) { @@ -252,6 +290,22 @@ class CancellationSource final { return state_->finish(CancellationState::StoredTerminal::cancelled_deadline); } + [[nodiscard]] Result cancel_if_expired( + const Deadline& deadline, const ClockOperations& operations) noexcept { + const CancellationOutcome existing = state_->outcome(); + if (existing.is_terminal()) { + return existing; + } + const auto expired = deadline.expired(operations); + if (!expired.has_value()) { + return std::unexpected{expired.error()}; + } + if (!*expired) { + return state_->outcome(); + } + return state_->finish(CancellationState::StoredTerminal::cancelled_deadline); + } + private: CancellationState* state_; }; diff --git a/src/core/private/laghu/core/internal/clock_operations.hpp b/src/core/private/laghu/core/internal/clock_operations.hpp new file mode 100644 index 0000000..b74b24a --- /dev/null +++ b/src/core/private/laghu/core/internal/clock_operations.hpp @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include + +namespace laghu::core::internal { + +[[nodiscard]] Result realtime_instant_from_parts( + RealtimeInstant seconds, RealtimeInstant nanoseconds) noexcept; + +} // namespace laghu::core::internal diff --git a/tests/adapters/entropy_seam.cpp b/tests/adapters/entropy_seam.cpp new file mode 100644 index 0000000..1f4df6b --- /dev/null +++ b/tests/adapters/entropy_seam.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include + +#include "laghu_test_support.hpp" +#include "laghu_test_time_entropy.hpp" + +#include + +namespace { + +template +[[nodiscard]] laghu::core::MutableByteView mutable_view( + std::array& bytes) noexcept { + return *laghu::core::MutableByteView::from(std::span{bytes}); +} + +[[nodiscard]] bool check_deterministic_entropy_seam() noexcept { + std::array sequence{}; + for (std::size_t index = 0; index < sequence.size(); ++index) { + sequence[index] = static_cast(index); + } + std::array output{}; + laghu::test::DeterministicEntropy entropy{std::span{sequence}}; + const auto success = laghu::adapters::internal::fill_entropy_with( + mutable_view(output), entropy.call(), entropy.context()); + if (!success.has_value() || entropy.calls() != 3 || entropy.consumed() != output.size() || + output[0] != sequence[0] || output[255] != sequence[255] || + output[256] != sequence[256] || output.back() != sequence.back()) { + return false; + } + + laghu::test::DeterministicEntropy failing{std::span{sequence}}; + if (!failing.fail_on_call(2, EIO)) { + return false; + } + const auto failure = laghu::adapters::internal::fill_entropy_with( + mutable_view(output), failing.call(), failing.context()); + return !failure.has_value() && failing.calls() == 2 && failing.consumed() == 256 && + failure.error().domain() == laghu::core::ErrorDomain::posix && + failure.error().code() == laghu::core::ErrorCode::io && failure.error().native_code() == EIO; +} + +} // namespace + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"adapters.entropy.deterministic_seam", check_deterministic_entropy_seam}, + }; + return laghu::test::run_tests(tests); +} diff --git a/tests/core/deadlines_cancellation.cpp b/tests/core/deadlines_cancellation.cpp index 45d758a..d8e3736 100644 --- a/tests/core/deadlines_cancellation.cpp +++ b/tests/core/deadlines_cancellation.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only #include #include +#include #include #include #include @@ -59,6 +60,30 @@ class DeterministicClock final { [[nodiscard]] constexpr bool check(bool condition) noexcept { return condition; } +class OperationClock final { + public: + constexpr explicit OperationClock(laghu::core::MonotonicInstant now) noexcept : now_(now) {} + + [[nodiscard]] constexpr laghu::core::ClockOperations operations() noexcept { + return laghu::core::ClockOperations{this, read}; + } + constexpr void advance_to(laghu::core::MonotonicInstant now) noexcept { now_ = now; } + + private: + [[nodiscard]] static laghu::core::Result read( + void* context) noexcept { + return static_cast(context)->now_; + } + + laghu::core::MonotonicInstant now_; +}; + +[[nodiscard]] laghu::core::Result failed_clock_read(void*) noexcept { + return std::unexpected{laghu::core::Error{laghu::core::ErrorDomain::posix, + laghu::core::ErrorCode::io, EIO, + "injected clock read failure"}}; +} + [[nodiscard]] bool check_deadline_boundaries() noexcept { DeterministicClock clock{100}; const auto after = laghu::core::Deadline::after(clock, 20); @@ -142,6 +167,73 @@ class DeterministicClock final { source.token().require_active().error().code() == laghu::core::ErrorCode::deadline); } +[[nodiscard]] bool check_clock_operation_deadline_cancellation() noexcept { + OperationClock clock{49}; + const auto operations = clock.operations(); + const auto deadline = laghu::core::Deadline::at(50); + + laghu::core::CancellationState active_state; + laghu::core::CancellationSource active_source{active_state}; + const auto active = active_source.token().require_active(deadline, operations); + const auto not_expired = active_source.cancel_if_expired(deadline, operations); + if (!check(active.has_value() && not_expired.has_value() && + not_expired->terminal == laghu::core::CancellationTerminal::pending && + active_source.token().outcome().terminal == laghu::core::CancellationTerminal::pending)) { + return false; + } + + clock.advance_to(50); + const auto expired = active_source.cancel_if_expired(deadline, operations); + if (!check(expired.has_value() && expired->is_cancelled() && + expired->cause == laghu::core::CancellationCause::deadline_expired && + !active_source.token().require_active(deadline, operations).has_value() && + active_source.token().outcome().is_cancelled())) { + return false; + } + + const laghu::core::ClockOperations failed_operations{nullptr, failed_clock_read, nullptr}; + laghu::core::CancellationState failed_state; + laghu::core::CancellationSource failed_source{failed_state}; + const auto token_failure = failed_source.token().require_active(deadline, failed_operations); + const auto source_failure = failed_source.cancel_if_expired(deadline, failed_operations); + if (!check(!token_failure.has_value() && token_failure.error().code() == laghu::core::ErrorCode::io && + !source_failure.has_value() && source_failure.error().code() == laghu::core::ErrorCode::io && + failed_source.token().outcome().terminal == laghu::core::CancellationTerminal::pending)) { + return false; + } + + laghu::core::CancellationState completed_state; + laghu::core::CancellationSource completed_source{completed_state}; + const auto completion = completed_source.complete(); + const auto completed_token = completed_source.token().require_active(deadline, failed_operations); + const auto already_completed = completed_source.cancel_if_expired(deadline, failed_operations); + return check(completion.terminal == laghu::core::CancellationTerminal::completed && + !completed_token.has_value() && + completed_token.error().code() == laghu::core::ErrorCode::invalid_state && + already_completed.has_value() && + already_completed->terminal == laghu::core::CancellationTerminal::completed && + already_completed->cause == laghu::core::CancellationCause::none); +} + +[[nodiscard]] bool check_system_clock_contract() noexcept { + const auto& operations = laghu::core::system_clock_operations(); + const auto monotonic = laghu::core::read_monotonic_clock(operations); + const auto realtime = laghu::core::read_realtime_clock(operations); + if (!check(monotonic.has_value() && realtime.has_value())) { + return false; + } + + laghu::core::CancellationState state; + laghu::core::CancellationSource source{state}; + const auto active = source.token().require_active( + laghu::core::Deadline::at(std::numeric_limits::max()), + operations); + const auto expired = source.cancel_if_expired(laghu::core::Deadline::at(*monotonic), operations); + return check(active.has_value() && expired.has_value() && expired->is_cancelled() && + expired->cause == laghu::core::CancellationCause::deadline_expired && + source.token().outcome().is_cancelled()); +} + [[nodiscard]] bool check_parent_observation_and_bounds() noexcept { laghu::core::CancellationState root_state; laghu::core::CancellationSource root_source{root_state}; @@ -250,6 +342,12 @@ int main() { if (!check(check_deadline_cancellation())) { return 4; } + if (!check(check_clock_operation_deadline_cancellation())) { + return 8; + } + if (!check(check_system_clock_contract())) { + return 9; + } if (!check(check_parent_observation_and_bounds())) { return 5; } diff --git a/tests/support/laghu_test_time_entropy.cpp b/tests/support/laghu_test_time_entropy.cpp new file mode 100644 index 0000000..02b62f5 --- /dev/null +++ b/tests/support/laghu_test_time_entropy.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_time_entropy.hpp" + +#include +#include + +namespace laghu::test { + +core::Result ManualClock::advance_monotonic_to(core::MonotonicInstant target) noexcept { + if (target < monotonic_) { + return std::unexpected{core::Error{core::ErrorDomain::core, core::ErrorCode::invalid_range, 0, + "manual monotonic time cannot move backward"}}; + } + monotonic_ = target; + return {}; +} + +core::Result ManualClock::advance_monotonic_by(core::MonotonicInstant elapsed) noexcept { + if (elapsed > std::numeric_limits::max() - monotonic_) { + return std::unexpected{core::Error{core::ErrorDomain::core, core::ErrorCode::overflow, 0, + "manual monotonic time overflows"}}; + } + monotonic_ += elapsed; + return {}; +} + +core::ClockOperations ManualClock::operations() noexcept { + return core::ClockOperations{this, read_monotonic, read_realtime}; +} + +core::Result ManualClock::read_monotonic(void* context) noexcept { + if (context == nullptr) { + return std::unexpected{core::Error{core::ErrorDomain::core, core::ErrorCode::invalid_state, 0, + "manual monotonic clock has no context"}}; + } + return static_cast(context)->monotonic_; +} + +core::Result ManualClock::read_realtime(void* context) noexcept { + if (context == nullptr) { + return std::unexpected{core::Error{core::ErrorDomain::core, core::ErrorCode::invalid_state, 0, + "manual realtime clock has no context"}}; + } + return static_cast(context)->realtime_; +} + +bool DeterministicEntropy::fail_on_call(std::size_t call, int native_error) noexcept { + if (call == 0 || native_error == 0) { + return false; + } + failure_call_ = call; + failure_error_ = native_error; + return true; +} + +core::Result DeterministicEntropy::fill(core::MutableByteView output) noexcept { + if (fill_call(output, this) != 0) { + return std::unexpected{core::Error::from_errno(errno, "deterministic entropy failed")}; + } + return {}; +} + +int DeterministicEntropy::fill_call(core::MutableByteView output, void* context) noexcept { + if (context == nullptr) { + errno = EINVAL; + return -1; + } + auto& entropy = *static_cast(context); + ++entropy.calls_; + if (entropy.failure_call_ == entropy.calls_) { + errno = entropy.failure_error_; + return -1; + } + if (output.size() > entropy.bytes_.size() - entropy.offset_) { + errno = ENOSPC; + return -1; + } + for (std::size_t index = 0; index < output.size(); ++index) { + output.span()[index] = entropy.bytes_[entropy.offset_ + index]; + } + entropy.offset_ += output.size(); + return 0; +} + +} // namespace laghu::test diff --git a/tests/support/laghu_test_time_entropy.hpp b/tests/support/laghu_test_time_entropy.hpp new file mode 100644 index 0000000..c4c62b0 --- /dev/null +++ b/tests/support/laghu_test_time_entropy.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace laghu::test { + +class ManualClock final { + public: + constexpr ManualClock(core::MonotonicInstant monotonic, core::RealtimeInstant realtime) noexcept + : monotonic_(monotonic), realtime_(realtime) {} + + [[nodiscard]] core::Result advance_monotonic_to(core::MonotonicInstant target) noexcept; + [[nodiscard]] core::Result advance_monotonic_by(core::MonotonicInstant elapsed) noexcept; + constexpr void jump_realtime_to(core::RealtimeInstant target) noexcept { realtime_ = target; } + + [[nodiscard]] constexpr core::MonotonicInstant monotonic() const noexcept { return monotonic_; } + [[nodiscard]] constexpr core::RealtimeInstant realtime() const noexcept { return realtime_; } + [[nodiscard]] core::ClockOperations operations() noexcept; + + private: + [[nodiscard]] static core::Result read_monotonic( + void* context) noexcept; + [[nodiscard]] static core::Result read_realtime(void* context) noexcept; + + core::MonotonicInstant monotonic_{}; + core::RealtimeInstant realtime_{}; +}; + +class DeterministicEntropy final { + public: + explicit constexpr DeterministicEntropy(std::span bytes) noexcept + : bytes_(bytes) {} + + [[nodiscard]] bool fail_on_call(std::size_t call, int native_error) noexcept; + [[nodiscard]] core::Result fill(core::MutableByteView output) noexcept; + [[nodiscard]] constexpr adapters::internal::EntropyCall call() const noexcept { + return fill_call; + } + [[nodiscard]] constexpr void* context() noexcept { return this; } + [[nodiscard]] constexpr std::size_t calls() const noexcept { return calls_; } + [[nodiscard]] constexpr std::size_t consumed() const noexcept { return offset_; } + + private: + [[nodiscard]] static int fill_call(core::MutableByteView output, void* context) noexcept; + + std::span bytes_{}; + std::size_t offset_{}; + std::size_t calls_{}; + std::size_t failure_call_{}; + int failure_error_{}; +}; + +} // namespace laghu::test diff --git a/tests/support/time_entropy.cpp b/tests/support/time_entropy.cpp new file mode 100644 index 0000000..6e1d276 --- /dev/null +++ b/tests/support/time_entropy.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_support.hpp" +#include "laghu_test_time_entropy.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +template +[[nodiscard]] laghu::core::MutableByteView mutable_view(std::array& bytes) noexcept { + return *laghu::core::MutableByteView::from(std::span{bytes}); +} + +[[nodiscard]] bool check_manual_clocks_and_deadlines() noexcept { + laghu::test::ManualClock clock{100, 1000}; + const auto operations = clock.operations(); + const auto deadline = laghu::core::Deadline::after(operations, 20); + if (!deadline.has_value() || deadline->instant() != 120 || + !deadline->require_not_expired(operations).has_value() || + !clock.advance_monotonic_by(19).has_value()) { + return false; + } + const auto before_expiry = deadline->expired(operations); + if (!before_expiry.has_value() || *before_expiry || !clock.advance_monotonic_to(120).has_value() || + !deadline->expired(operations).has_value() || !*deadline->expired(operations) || + deadline->require_not_expired(operations).has_value()) { + return false; + } + const auto backward_monotonic = clock.advance_monotonic_to(119); + if (backward_monotonic.has_value() || + backward_monotonic.error().code() != laghu::core::ErrorCode::invalid_range || + clock.monotonic() != 120) { + return false; + } + const auto initial_realtime = laghu::core::read_realtime_clock(operations); + clock.jump_realtime_to(1500); + const auto forward_realtime = laghu::core::read_realtime_clock(operations); + clock.jump_realtime_to(750); + const auto backward_realtime = laghu::core::read_realtime_clock(operations); + return initial_realtime.has_value() && *initial_realtime == 1000 && + forward_realtime.has_value() && *forward_realtime == 1500 && + backward_realtime.has_value() && *backward_realtime == 750 && clock.monotonic() == 120; +} + +[[nodiscard]] bool check_clock_tables() noexcept { + const auto& operations = laghu::core::system_clock_operations(); + const auto monotonic = laghu::core::read_monotonic_clock(operations); + const auto realtime = laghu::core::read_realtime_clock(operations); + const laghu::core::ClockOperations incomplete{}; + const auto missing_monotonic = laghu::core::read_monotonic_clock(incomplete); + const auto missing_realtime = laghu::core::read_realtime_clock(incomplete); + return monotonic.has_value() && realtime.has_value() && !missing_monotonic.has_value() && + !missing_realtime.has_value() && + missing_monotonic.error().code() == laghu::core::ErrorCode::invalid_state && + missing_realtime.error().code() == laghu::core::ErrorCode::invalid_state; +} + +[[nodiscard]] bool check_realtime_conversion_boundaries() noexcept { + constexpr laghu::core::RealtimeInstant nanoseconds_per_second = 1000000000; + constexpr laghu::core::RealtimeInstant maximum = + std::numeric_limits::max(); + constexpr laghu::core::RealtimeInstant minimum = + std::numeric_limits::min(); + constexpr laghu::core::RealtimeInstant maximum_seconds = maximum / nanoseconds_per_second; + constexpr laghu::core::RealtimeInstant maximum_nanoseconds = maximum % nanoseconds_per_second; + constexpr laghu::core::RealtimeInstant minimum_seconds = + minimum / nanoseconds_per_second - 1; + constexpr laghu::core::RealtimeInstant minimum_nanoseconds = + nanoseconds_per_second + minimum % nanoseconds_per_second; + + const auto exact_maximum = laghu::core::internal::realtime_instant_from_parts( + maximum_seconds, maximum_nanoseconds); + const auto maximum_overflow = laghu::core::internal::realtime_instant_from_parts( + maximum_seconds, maximum_nanoseconds + 1); + const auto exact_minimum = laghu::core::internal::realtime_instant_from_parts( + minimum_seconds, minimum_nanoseconds); + const auto minimum_overflow = laghu::core::internal::realtime_instant_from_parts( + minimum_seconds, minimum_nanoseconds - 1); + const auto negative_nanoseconds = + laghu::core::internal::realtime_instant_from_parts(0, -1); + const auto excessive_nanoseconds = laghu::core::internal::realtime_instant_from_parts( + 0, nanoseconds_per_second); + return exact_maximum.has_value() && *exact_maximum == maximum && + !maximum_overflow.has_value() && + maximum_overflow.error().code() == laghu::core::ErrorCode::overflow && + exact_minimum.has_value() && *exact_minimum == minimum && + !minimum_overflow.has_value() && + minimum_overflow.error().code() == laghu::core::ErrorCode::overflow && + !negative_nanoseconds.has_value() && + negative_nanoseconds.error().code() == laghu::core::ErrorCode::corrupt_data && + !excessive_nanoseconds.has_value() && + excessive_nanoseconds.error().code() == laghu::core::ErrorCode::corrupt_data; +} + +[[nodiscard]] bool check_deterministic_entropy() noexcept { + std::array sequence{}; + for (std::size_t index = 0; index < sequence.size(); ++index) { + sequence[index] = static_cast(index); + } + std::array output{}; + laghu::test::DeterministicEntropy entropy{std::span{sequence}}; + const auto first = entropy.fill(mutable_view(output)); + const auto second = entropy.fill(mutable_view(output)); + if (!first.has_value() || !second.has_value() || entropy.calls() != 2 || + entropy.consumed() != sequence.size() || output[0] != sequence[3] || + output[1] != sequence[4] || output[2] != sequence[5]) { + return false; + } + + laghu::test::DeterministicEntropy failing{std::span{sequence}}; + if (!failing.fail_on_call(2, EIO)) { + return false; + } + const auto successful = failing.fill(mutable_view(output)); + const auto failure = failing.fill(mutable_view(output)); + return successful.has_value() && !failure.has_value() && failing.calls() == 2 && + failing.consumed() == output.size() && + failure.error().domain() == laghu::core::ErrorDomain::posix && + failure.error().code() == laghu::core::ErrorCode::io && failure.error().native_code() == EIO; +} + +} // namespace + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"time_entropy.manual_clocks_and_deadlines", + check_manual_clocks_and_deadlines}, + laghu::test::TestCase{"time_entropy.clock_tables", check_clock_tables}, + laghu::test::TestCase{"time_entropy.realtime_conversion_boundaries", + check_realtime_conversion_boundaries}, + laghu::test::TestCase{"time_entropy.deterministic_entropy", check_deterministic_entropy}, + }; + return laghu::test::run_tests(tests); +}