diff --git a/.clang-tidy-minimal b/.clang-tidy-minimal index 7a364f0d7..f455c4691 100644 --- a/.clang-tidy-minimal +++ b/.clang-tidy-minimal @@ -18,7 +18,7 @@ Checks: > readability-string-compare, readability-uniqueptr-delete-release, -ExtraArgs: +ExtraArgsBefore: - '-Wno-error=deprecated-declarations' CheckOptions: diff --git a/.github/workflows/coverage_report.yml b/.github/workflows/coverage_report.yml index 641733de7..48138b848 100644 --- a/.github/workflows/coverage_report.yml +++ b/.github/workflows/coverage_report.yml @@ -24,7 +24,7 @@ on: workflow_call: jobs: coverage-report: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository uses: actions/checkout@v6 @@ -50,6 +50,7 @@ jobs: genhtml "$(bazel info output_path)/_coverage/_coverage_report.dat" \ -o=cpp_coverage \ --show-details \ + --source-directory="$(bazel info execution_root)" \ --legend \ --function-coverage \ --branch-coverage diff --git a/.gitignore b/.gitignore index d6fd2f7c2..48b9d398b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ compile_commands.json # docs build artifacts _build docs/ubproject.toml + +# coverage +cpp_coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b58eb6727..d058a6630 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,4 +38,6 @@ repos: files: \.(c|cpp|h|hpp)$ - id: clang-tidy args: ["--config-file=.clang-tidy-minimal", "-p=$(pwd)", "--warnings-as-errors=*"] - exclude: '_test\.cpp$' + # load_buffer_internal.hpp is a private header with no compile commands, so clang-tidy will fail on it. + # The header is still checked via load_buffer.cpp. + exclude: '(_test\.cpp|load_buffer_internal\.hpp)$' diff --git a/score/flatbuffers/BUILD b/score/flatbuffers/BUILD index 7069e3209..ae0891a7d 100644 --- a/score/flatbuffers/BUILD +++ b/score/flatbuffers/BUILD @@ -10,12 +10,13 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:cc_library.bzl", "cc_library") + +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") cc_library( name = "flatbufferscpp", srcs = [ - "src/idl_parser.cpp", + "details/idl_parser.cpp", ], hdrs = [ "@flatbuffers//:include/flatbuffers/allocator.h", @@ -38,3 +39,44 @@ cc_library( strip_include_prefix = "/include", visibility = ["//visibility:public"], ) + +cc_library( + name = "flatbufferutils", + srcs = [ + "details/load_buffer.cpp", + "details/load_buffer_internal.hpp", + ], + hdrs = [ + "load_buffer.hpp", + ], + visibility = ["//visibility:public"], + deps = [ + "@score_baselibs//score/filesystem", + "@score_baselibs//score/os:fcntl", + "@score_baselibs//score/os:stat", + "@score_baselibs//score/os:unistd", + "@score_baselibs//score/result", + ], +) + +cc_test( + name = "load_buffer_unit_test", + srcs = ["details/load_buffer_test.cpp"], + tags = ["unit"], + deps = [ + ":flatbufferutils", + "@googletest//:gtest_main", + "@score_baselibs//score/os/mocklib:fcntl_mock", + "@score_baselibs//score/os/mocklib:stat_mock", + "@score_baselibs//score/os/mocklib:unistd_mock", + ], +) + +cc_test( + name = "load_buffer_test", + srcs = ["test/load_buffer_test.cpp"], + deps = [ + ":flatbufferutils", + "@googletest//:gtest_main", + ], +) diff --git a/score/flatbuffers/src/idl_parser.cpp b/score/flatbuffers/details/idl_parser.cpp similarity index 100% rename from score/flatbuffers/src/idl_parser.cpp rename to score/flatbuffers/details/idl_parser.cpp diff --git a/score/flatbuffers/details/load_buffer.cpp b/score/flatbuffers/details/load_buffer.cpp new file mode 100644 index 000000000..d7e7d1059 --- /dev/null +++ b/score/flatbuffers/details/load_buffer.cpp @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/load_buffer.hpp" +#include "score/flatbuffers/details/load_buffer_internal.hpp" + +namespace score +{ + +namespace flatbuffers +{ + +score::os::Result> LoadBuffer(const score::filesystem::Path& path) noexcept +{ + std::vector data; + const auto read_result = detail::LoadBufferImpl(detail::OS{}, path, data); + if (read_result.has_value()) + { + return std::move(data); + } + return score::cpp::make_unexpected(read_result.error()); +} + +score::os::Result LoadBuffer(const score::filesystem::Path& path, + std::pmr::vector& data) noexcept +{ + return detail::LoadBufferImpl(detail::OS{}, path, data); +} + +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/details/load_buffer_internal.hpp b/score/flatbuffers/details/load_buffer_internal.hpp new file mode 100644 index 000000000..ce5efa0fd --- /dev/null +++ b/score/flatbuffers/details/load_buffer_internal.hpp @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_FLATBUFFERS_LOAD_BUFFER_INTERNAL_HPP +#define SCORE_LIB_FLATBUFFERS_LOAD_BUFFER_INTERNAL_HPP + +#include "score/filesystem/path.h" +#include "score/os/errno.h" + +#include "score/os/fcntl_impl.h" +#include "score/os/stat_impl.h" +#include "score/os/unistd.h" + +#include +#include +#include +#include +#include + +namespace score +{ + +namespace flatbuffers +{ + +namespace detail +{ + +struct OS +{ + score::os::FcntlImpl fcntl{}; + score::os::StatImpl stat{}; + score::os::internal::UnistdImpl unistd{}; +}; + +// OST must expose three members: fcntl (open), stat (fstat), unistd (close/read). +// Container must provide resize() and data(). +template +score::os::Result LoadBufferImpl(const OST& os, + const score::filesystem::Path& path, + Container& data) noexcept +{ + const auto fd_result = os.fcntl.open(path.CStr(), score::os::Fcntl::Open::kReadOnly); + if (!fd_result.has_value()) + { + return score::cpp::make_unexpected(fd_result.error()); + } + const std::int32_t file_desc = fd_result.value(); + + // Helper to ensure the file descriptor in an error case is always closed. + // Any close error is only reported when no prior error occurred. + auto close_fd = [&os, + file_desc](const score::os::Error* prior_error) noexcept -> score::os::Result { + const auto close_result = os.unistd.close(file_desc); + if (prior_error != nullptr) + { + return score::cpp::make_unexpected(*prior_error); + } + return close_result; + }; + + // Obtain the file size via fstat + score::os::StatBuffer stat_buf{}; + const auto stat_result = os.stat.fstat(file_desc, stat_buf); + if (!stat_result.has_value()) + { // defensive error handling + // fstat failure is impossible on a just-opened valid fd. Mocks are used to + // cover this line in unit tests. + return close_fd(&stat_result.error()); + } + + if (stat_buf.st_size < 0) + { // defensive error handling + // No real Linux filesystem reports negative sizes. Mocks are used to cover + // this line in unit tests. + const auto err = score::os::Error::createFromErrno(EINVAL); + return close_fd(&err); + } + + const auto file_size = static_cast(stat_buf.st_size); + + try + { + data.resize(file_size); + } + catch (const std::bad_alloc&) + { + const auto err = score::os::Error::createFromErrno(ENOMEM); + return close_fd(&err); + } + catch (...) + { // potential custom exception + const auto err = score::os::Error::createUnspecifiedError(); + return close_fd(&err); + } + + // Read the entire file, handling partial reads + std::size_t total_bytes_read = 0U; + while (total_bytes_read < file_size) + { + const auto read_result = os.unistd.read(file_desc, + std::next(data.data(), static_cast(total_bytes_read)), + file_size - total_bytes_read); + if (!read_result.has_value()) + { + const auto& read_error = read_result.error(); + // Retry on EINTR (interrupted system call) + if (read_error == score::os::Error::Code::kOperationWasInterruptedBySignal) + { // The EINTR retry requires a signal to arrive during a regular-file + // read syscall, which is non-deterministic. Mocks are used to cover + // this line in unit tests. + continue; + } + return close_fd(&read_error); + } + + const auto bytes_read = read_result.value(); + if (bytes_read == 0) + { + // Unexpected EOF before reading the full file + const auto err = score::os::Error::createFromErrno(EIO); + return close_fd(&err); + } + + total_bytes_read += static_cast(bytes_read); + } + + return close_fd(nullptr); +} + +} // namespace detail +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_LOAD_BUFFER_INTERNAL_HPP diff --git a/score/flatbuffers/details/load_buffer_test.cpp b/score/flatbuffers/details/load_buffer_test.cpp new file mode 100644 index 000000000..fb9f9ddae --- /dev/null +++ b/score/flatbuffers/details/load_buffer_test.cpp @@ -0,0 +1,523 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/load_buffer.hpp" +#include "score/flatbuffers/details/load_buffer_internal.hpp" + +#include "score/os/mocklib/fcntl_mock.h" +#include "score/os/mocklib/stat_mock.h" +#include "score/os/mocklib/unistdmock.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace score +{ + +namespace flatbuffers +{ + +namespace unit_test +{ + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Invoke; +using ::testing::Return; + +constexpr std::int32_t kTestFd = 42; +const score::filesystem::Path kTestPath{"/tmp/test.bin"}; +constexpr std::int64_t kTestFileSize = 10; +constexpr std::int64_t kInvalidNegativeSize = -100; + +class ThrowingBadAllocResource : public std::pmr::memory_resource +{ + void* do_allocate(std::size_t /*bytes*/, std::size_t /*align*/) override + { + throw std::bad_alloc{}; + } + void do_deallocate(void* /*p*/, std::size_t /*bytes*/, std::size_t /*align*/) override {} + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override + { + return this == &other; + } +}; + +struct CustomResizeException : public std::exception +{ +}; + +class ThrowingCustomExceptionResource : public std::pmr::memory_resource +{ + void* do_allocate(std::size_t /*bytes*/, std::size_t /*align*/) override + { + throw CustomResizeException{}; + } + void do_deallocate(void* /*p*/, std::size_t /*bytes*/, std::size_t /*align*/) override {} + bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override + { + return this == &other; + } +}; + +class LoadFlatbufferTest : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("Description", + "covers the defensive error handling branches for code coverage as well as all others"); + RecordProperty("TestType", "structural-branch-coverage"); + RecordProperty("TestType", "fault-injection"); + } + + void SetUpSuccessfulOpen() + { + ON_CALL(os_.fcntl, open(_, _)) + .WillByDefault(Return(score::cpp::expected{kTestFd})); + } + + void SetUpSuccessfulFstat(std::int64_t file_size) + { + ON_CALL(os_.stat, fstat(kTestFd, _)) + .WillByDefault(DoAll(Invoke([file_size](std::int32_t, score::os::StatBuffer& buf) { + buf.st_size = file_size; + }), + Return(score::cpp::expected_blank{}))); + } + + void SetUpSuccessfulClose() + { + ON_CALL(os_.unistd, close(kTestFd)).WillByDefault(Return(score::cpp::expected_blank{})); + } + + void SetUpSuccessfulReadAll(const std::vector& content) + { + ON_CALL(os_.unistd, read(kTestFd, _, _)) + .WillByDefault(Invoke([content](std::int32_t, + void* buf, + std::size_t count) -> score::cpp::expected { + const auto to_copy = std::min(count, content.size()); + std::memcpy(buf, content.data(), to_copy); + return static_cast(to_copy); + })); + } + + template + score::cpp::expected_blank call_impl(const score::filesystem::Path& path, Container& data) + { + return detail::LoadBufferImpl(os_, path, data); + } + + struct OSMock + { + ::testing::NiceMock fcntl{}; + ::testing::NiceMock stat{}; + ::testing::NiceMock unistd{}; + }; + + OSMock os_{}; +}; + +TEST_F(LoadFlatbufferTest, OpenFailureReturnsError) +{ + const auto open_error = score::os::Error::createFromErrno(ENOENT); + EXPECT_CALL(os_.fcntl, open(_, _)).WillOnce(Return(score::cpp::make_unexpected(open_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kNoSuchFileOrDirectory); +} + +TEST_F(LoadFlatbufferTest, FstatFailureReturnsErrorAndClosesFile) +{ + SetUpSuccessfulOpen(); + + const auto stat_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.stat, fstat(kTestFd, _)).WillOnce(Return(score::cpp::make_unexpected(stat_error))); + EXPECT_CALL(os_.unistd, close(kTestFd)).WillOnce(Return(score::cpp::expected_blank{})); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, NegativeFileSizeReturnsErrorAndClosesFile) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulClose(); + + EXPECT_CALL(os_.stat, fstat(kTestFd, _)) + .WillOnce(DoAll(Invoke([](std::int32_t, score::os::StatBuffer& buf) { + buf.st_size = kInvalidNegativeSize; + }), + Return(score::cpp::expected_blank{}))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInvalidArgument); +} + +TEST_F(LoadFlatbufferTest, ReadFailureReturnsErrorAndClosesFile) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(kTestFileSize); + SetUpSuccessfulClose(); + + const auto read_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)).WillOnce(Return(score::cpp::make_unexpected(read_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, ReadReturnsZeroBytesReportsUnexpectedEof) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(kTestFileSize); + SetUpSuccessfulClose(); + + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)) + .WillOnce(Return(score::cpp::expected{static_cast(0)})); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, PartialReadsAreHandledCorrectly) +{ + const std::vector expected_data{'A', 'B', 'C', 'D', 'E'}; + + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(static_cast(expected_data.size())); + SetUpSuccessfulClose(); + + std::size_t offset = 0U; + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)) + .WillOnce(Invoke([&](std::int32_t, void* buf, std::size_t) -> score::cpp::expected { + std::memcpy(buf, std::next(expected_data.data(), static_cast(offset)), 2U); + offset += 2U; + return static_cast(2); + })) + .WillOnce(Invoke([&](std::int32_t, void* buf, std::size_t) -> score::cpp::expected { + std::memcpy(buf, std::next(expected_data.data(), static_cast(offset)), 2U); + offset += 2U; + return static_cast(2); + })) + .WillOnce(Invoke([&](std::int32_t, void* buf, std::size_t) -> score::cpp::expected { + std::memcpy(buf, std::next(expected_data.data(), static_cast(offset)), 1U); + offset += 1U; + return static_cast(1); + })); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(data, expected_data); +} + +TEST_F(LoadFlatbufferTest, InterruptedReadIsRetried) +{ + const std::vector expected_data{'X', 'Y'}; + const auto eintr_error = score::os::Error::createFromErrno(EINTR); + + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(static_cast(expected_data.size())); + SetUpSuccessfulClose(); + + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)) + .WillOnce(Return(score::cpp::make_unexpected(eintr_error))) + .WillOnce( + Invoke([&](std::int32_t, void* buf, std::size_t count) -> score::cpp::expected { + const auto to_copy = std::min(count, expected_data.size()); + std::memcpy(buf, expected_data.data(), to_copy); + return static_cast(to_copy); + })); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(data, expected_data); +} + +TEST_F(LoadFlatbufferTest, EmptyFileReturnsEmptyVector) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(0); + SetUpSuccessfulClose(); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(data.empty()); +} + +TEST_F(LoadFlatbufferTest, CloseFailureOnEmptyFilePropagatesCloseError) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(0); + + const auto close_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, close(kTestFd)).WillOnce(Return(score::cpp::make_unexpected(close_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, CloseFailureAfterSuccessfulReadPropagatesCloseError) +{ + const std::vector expected_data{'Z'}; + + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(static_cast(expected_data.size())); + SetUpSuccessfulReadAll(expected_data); + + const auto close_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, close(kTestFd)).WillOnce(Return(score::cpp::make_unexpected(close_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, CloseFailureAfterReadErrorPropagatesOriginalError) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(kTestFileSize); + + const auto read_error = score::os::Error::createFromErrno(ENOENT); + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)).WillOnce(Return(score::cpp::make_unexpected(read_error))); + + const auto close_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, close(kTestFd)).WillOnce(Return(score::cpp::make_unexpected(close_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kNoSuchFileOrDirectory); +} + +TEST_F(LoadFlatbufferTest, ReadErrorAfterPartialReadReportsError) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(4); + SetUpSuccessfulClose(); + + const auto read_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)) + .WillOnce(Invoke([](std::int32_t, void* buf, std::size_t) -> score::cpp::expected { + const char byte = 'A'; + std::memcpy(buf, &byte, 1U); + return static_cast(1); + })) + .WillOnce(Return(score::cpp::make_unexpected(read_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, UnexpectedEofAfterPartialReadReportsError) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(4); + SetUpSuccessfulClose(); + + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)) + .WillOnce(Invoke([](std::int32_t, void* buf, std::size_t) -> score::cpp::expected { + const char byte = 'B'; + std::memcpy(buf, &byte, 1U); + return static_cast(1); + })) + .WillOnce(Return(score::cpp::expected{static_cast(0)})); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInputOutput); +} + +TEST_F(LoadFlatbufferTest, MultipleInterruptsFollowedBySuccessfulRead) +{ + const std::vector expected_data{'I'}; + const auto eintr_error = score::os::Error::createFromErrno(EINTR); + + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(static_cast(expected_data.size())); + SetUpSuccessfulClose(); + + EXPECT_CALL(os_.unistd, read(kTestFd, _, _)) + .WillOnce(Return(score::cpp::make_unexpected(eintr_error))) + .WillOnce(Return(score::cpp::make_unexpected(eintr_error))) + .WillOnce( + Invoke([&](std::int32_t, void* buf, std::size_t count) -> score::cpp::expected { + const auto to_copy = std::min(count, expected_data.size()); + std::memcpy(buf, expected_data.data(), to_copy); + return static_cast(to_copy); + })); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(data, expected_data); +} + +TEST_F(LoadFlatbufferTest, CloseFailureAfterFstatErrorPropagatesFstatError) +{ + SetUpSuccessfulOpen(); + + const auto stat_error = score::os::Error::createFromErrno(EBADF); + EXPECT_CALL(os_.stat, fstat(kTestFd, _)).WillOnce(Return(score::cpp::make_unexpected(stat_error))); + + const auto close_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, close(kTestFd)).WillOnce(Return(score::cpp::make_unexpected(close_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kBadFileDescriptor); +} + +TEST_F(LoadFlatbufferTest, ResizeBadAllocReturnsEnomemAndClosesFile) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(kTestFileSize); + SetUpSuccessfulClose(); + + ThrowingBadAllocResource bad_alloc_resource; + std::pmr::vector data{&bad_alloc_resource}; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kNotEnoughSpace); +} + +TEST_F(LoadFlatbufferTest, ResizeCustomExceptionReturnsErrorAndClosesFile) +{ + SetUpSuccessfulOpen(); + SetUpSuccessfulFstat(kTestFileSize); + SetUpSuccessfulClose(); + + ThrowingCustomExceptionResource custom_resource; + std::pmr::vector data{&custom_resource}; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kUnexpected); +} + +TEST_F(LoadFlatbufferTest, CloseFailureAfterNegativeSizePropagatesInvalidArgument) +{ + SetUpSuccessfulOpen(); + + EXPECT_CALL(os_.stat, fstat(kTestFd, _)) + .WillOnce(DoAll(Invoke([](std::int32_t, score::os::StatBuffer& buf) { + buf.st_size = kInvalidNegativeSize; + }), + Return(score::cpp::expected_blank{}))); + + const auto close_error = score::os::Error::createFromErrno(EIO); + EXPECT_CALL(os_.unistd, close(kTestFd)).WillOnce(Return(score::cpp::make_unexpected(close_error))); + + std::vector data; + const auto result = call_impl(kTestPath, data); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kInvalidArgument); +} + +// ==================================== +// Tests for the public LoadBuffer API +// ==================================== + +// These smoke tests use real OS resources (/dev/null, non-existent path). +const score::filesystem::Path kDevNull{"/dev/null"}; +const score::filesystem::Path kNonExistent{"/tmp/score_flatbuffers_nonexistent_xyz987.bin"}; + +TEST(LoadBufferPublicApiTest, VectorOverloadSuccessPathReturnsEmptyVector) +{ + RecordProperty("TestType", "structural-branch-coverage"); + RecordProperty("DerivationTechnique", "boundary-values"); + + const auto result = score::flatbuffers::LoadBuffer(kDevNull); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result.value().empty()); +} + +TEST(LoadBufferPublicApiTest, VectorOverloadErrorPathReturnsError) +{ + RecordProperty("TestType", "structural-branch-coverage"); + RecordProperty("TestType", "fault-injection"); + + const auto result = score::flatbuffers::LoadBuffer(kNonExistent); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kNoSuchFileOrDirectory); +} + +TEST(LoadBufferPublicApiTest, PmrVectorOverloadSuccessPathReturnsEmpty) +{ + RecordProperty("TestType", "structural-branch-coverage"); + RecordProperty("DerivationTechnique", "boundary-values"); + + std::pmr::vector data; + const auto result = score::flatbuffers::LoadBuffer(kDevNull, data); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(data.empty()); +} + +TEST(LoadBufferPublicApiTest, PmrVectorOverloadErrorPathReturnsError) +{ + RecordProperty("TestType", "structural-branch-coverage"); + RecordProperty("TestType", "fault-injection"); + + std::pmr::vector data; + const auto result = score::flatbuffers::LoadBuffer(kNonExistent, data); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::os::Error::Code::kNoSuchFileOrDirectory); +} + +} // namespace unit_test +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/examples/config_usecase/BUILD b/score/flatbuffers/examples/config_usecase/BUILD index e47fad15a..ad8eb43b8 100644 --- a/score/flatbuffers/examples/config_usecase/BUILD +++ b/score/flatbuffers/examples/config_usecase/BUILD @@ -50,5 +50,6 @@ cc_test( "@googletest//:gtest_main", "@score_baselibs//score/filesystem", "@score_baselibs//score/flatbuffers:flatbufferscpp", + "@score_baselibs//score/flatbuffers:flatbufferutils", ], ) diff --git a/score/flatbuffers/examples/config_usecase/demo_app_test.cpp b/score/flatbuffers/examples/config_usecase/demo_app_test.cpp index 5ba22b721..c82bd714e 100644 --- a/score/flatbuffers/examples/config_usecase/demo_app_test.cpp +++ b/score/flatbuffers/examples/config_usecase/demo_app_test.cpp @@ -18,7 +18,7 @@ /// Build and run via Bazel: /// bazel test //demo_config_usecase:demo_app -#include "score/filesystem/filestream/file_factory.h" +#include "score/flatbuffers/load_buffer.hpp" // Generated C++ accessor header produced by generate_cpp() – depends on flatbufferscpp #include "score/flatbuffers/examples/config_usecase/component_config.h" @@ -36,20 +36,13 @@ TEST(DemoAppTest, LoadsAndVerifiesBuffer) // Step 1: Load the binary FlatBuffer file from disk (standard file I/O) // ------------------------------------------------------------------------- const std::string_view bin_path = "score/flatbuffers/examples/config_usecase/demo_config.bin"; - - /// TODO: Replace with planned binary file loading utility. - - score::filesystem::FileFactory file_factory{}; - auto open_result = file_factory.Open(score::filesystem::Path{bin_path}, std::ios::binary | std::ios::in); - ASSERT_TRUE(open_result.has_value()) << "Failed to open binary config from '" << bin_path << "'"; - - auto& stream = *open_result.value(); - std::vector buffer{std::istreambuf_iterator(stream), std::istreambuf_iterator()}; + const auto buffer = score::flatbuffers::LoadBuffer(bin_path); + ASSERT_TRUE(buffer.has_value()) << buffer.error().ToString(); // ------------------------------------------------------------------------- // Step 2: Verify buffer integrity (flatbuffercpp / FlatBuffers verifier) // ------------------------------------------------------------------------- - flatbuffers::Verifier verifier(reinterpret_cast(buffer.data()), buffer.size()); + flatbuffers::Verifier verifier(buffer.value().data(), buffer.value().size()); ASSERT_TRUE(my_component::demo::VerifyMyComponentConfigBuffer(verifier)) << "FlatBuffer verification failed for '" << bin_path << "'"; @@ -57,7 +50,8 @@ TEST(DemoAppTest, LoadsAndVerifiesBuffer) // ------------------------------------------------------------------------- // Step 3: Access config values via the generated flatbuffercpp API // ------------------------------------------------------------------------- - const my_component::demo::MyComponentConfig* config = my_component::demo::GetMyComponentConfig(buffer.data()); + const my_component::demo::MyComponentConfig* config = + my_component::demo::GetMyComponentConfig(buffer.value().data()); ASSERT_NE(config, nullptr); EXPECT_EQ(config->component_name()->str(), "demo_component"); diff --git a/score/flatbuffers/load_buffer.hpp b/score/flatbuffers/load_buffer.hpp new file mode 100644 index 000000000..d4f9cbc85 --- /dev/null +++ b/score/flatbuffers/load_buffer.hpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// @file load_buffer.hpp +/// @brief Utility for loading FlatBuffer files from the filesystem. + +#ifndef SCORE_LIB_FLATBUFFERS_LOAD_BUFFER_HPP +#define SCORE_LIB_FLATBUFFERS_LOAD_BUFFER_HPP + +#include "score/filesystem/path.h" +#include "score/os/errno.h" + +#include +#include +#include + +namespace score +{ + +namespace flatbuffers +{ + +/// @brief Loads the entire contents of a binary file into a +/// `std::vector`. +/// +/// @param[in] path The filesystem path to the file to load. +/// +/// @returns a `std::vector` on success, a `score::os::Error` on failure +/// +/// @note `std::bad_alloc` thrown by `resize()` is caught and returned +/// as `score::os::Error::Code::kNotEnoughSpace`. +/// Any other exception from `resize()` is returned as an unspecified error. +/// +/// @see LoadBuffer(const score::filesystem::Path&, std::pmr::vector&) +/// for a variant using polymorphic memory resources. +score::os::Result> LoadBuffer(const score::filesystem::Path& path) noexcept; + +/// @brief Loads the entire contents of a binary file into a +/// `std::pmr::vector`. +/// +/// @param[in] path The filesystem path to the file to load. +/// @param[out] data Output container where the file contents will be placed. +/// This vector will be resized to fit the file's contents. +/// +/// @returns a `score::cpp::blank` on success, a `score::os::Error` on failure +/// +/// @note `std::bad_alloc` thrown by `resize()` is caught and returned +/// as `score::os::Error::Code::kNotEnoughSpace`. +/// Any other exception from `resize()` is returned as an unspecified error. +/// +/// @note `data` is not cleared before use. On error, its contents are +/// unspecified — it may have been resized and partially populated. +score::os::Result LoadBuffer(const score::filesystem::Path& path, + std::pmr::vector& data) noexcept; +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_LOAD_BUFFER_HPP diff --git a/score/flatbuffers/test/load_buffer_test.cpp b/score/flatbuffers/test/load_buffer_test.cpp new file mode 100644 index 000000000..65853c224 --- /dev/null +++ b/score/flatbuffers/test/load_buffer_test.cpp @@ -0,0 +1,281 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "score/flatbuffers/load_buffer.hpp" + +namespace score +{ + +namespace flatbuffers +{ + +namespace test +{ + +using score::flatbuffers::LoadBuffer; + +/// Test fixture that manages a temporary directory for test files. +/// All test files are created under this directory and cleaned up +/// automatically. +class LoadFlatbufferTest : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("Verifies", "ADDID"); + RecordProperty("Description", "defensive error handling is not part of this test suite"); + + test_dir_ = std::filesystem::current_path().string(); + } + + void TearDown() override + { + for (const auto& file : files_) + { + std::filesystem::remove(file.c_str()); + } + } + + /// Creates a file with the given binary content and returns its Path. + score::filesystem::Path WriteFile(const std::string& name, const std::vector& content) const + { + const std::string filepath = test_dir_ + "/LoadFlatbufferTest" + name; + std::ofstream ofs(filepath, std::ios::binary); + if (!ofs.is_open()) + { + ADD_FAILURE() << "Failed to open file for writing: " << filepath; + return score::filesystem::Path{}; + } + ofs.write(static_cast(static_cast(content.data())), + static_cast(content.size())); + ofs.close(); + files_.push_back(filepath); + return score::filesystem::Path{filepath}; + } + + /// Convenience overload for string content. + score::filesystem::Path WriteFile(const std::string& name, const std::string& content) const + { + return WriteFile(name, std::vector(content.begin(), content.end())); + } + + mutable std::vector files_; + std::string test_dir_; +}; + +void ExpectVectorLoad(const score::filesystem::Path& path, const std::vector& expected) +{ + SCOPED_TRACE(path.CStr()); + const auto result = LoadBuffer(path); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), expected); +} + +void ExpectPmrVectorLoad(const score::filesystem::Path& path, const std::vector& expected) +{ + SCOPED_TRACE(path.CStr()); + std::pmr::vector data; + const auto result = LoadBuffer(path, data); + ASSERT_TRUE(result.has_value()); + const std::vector data_copy(data.cbegin(), data.cend()); + EXPECT_EQ(data_copy, expected); +} + +void ExpectVectorLoad(const score::filesystem::Path& path, const score::os::Error::Code& expected) +{ + SCOPED_TRACE(path.CStr()); + const auto result = LoadBuffer(path); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), expected); +} + +void ExpectPmrVectorLoad(const score::filesystem::Path& path, const score::os::Error::Code& expected) +{ + SCOPED_TRACE(path.CStr()); + std::pmr::vector data; + const auto result = LoadBuffer(path, data); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), expected); +} + +TEST_F(LoadFlatbufferTest, LoadsRegularFileContents) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "loaded content from file"); + + const std::vector content{'H', 'e', 'l', 'l', 'o'}; + const auto path = WriteFile("hello.bin", content); + + ExpectVectorLoad(path, content); + ExpectPmrVectorLoad(path, content); +} + +TEST_F(LoadFlatbufferTest, LoadsEmptyFileAsEmptyVector) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "empty file is loaded as empty vector"); + + const auto path = WriteFile("empty.bin", std::vector{}); + + ExpectVectorLoad(path, std::vector{}); + ExpectPmrVectorLoad(path, std::vector{}); +} + +TEST_F(LoadFlatbufferTest, LoadsBinaryContentIncludingNullBytes) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "null bytes are not truncated, file is read in binary mode"); + + const std::vector content{0x00U, 0x01U, 0xFEU, 0xFFU, 0x00U, 0xABU}; + const auto path = WriteFile("binary.bin", content); + + ExpectVectorLoad(path, content); + ExpectPmrVectorLoad(path, content); +} + +TEST_F(LoadFlatbufferTest, LoadsSingleByteFile) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "single byte is loaded from file"); + + const std::vector content{'X'}; + const auto path = WriteFile("one_byte.bin", content); + + ExpectVectorLoad(path, content); + ExpectPmrVectorLoad(path, content); +} + +TEST_F(LoadFlatbufferTest, LoadsLarge5MBFileCorrectly) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "large content is loaded from file"); + + constexpr auto kSize = static_cast(5U * 1024U * 1024U); // 5 MB + constexpr int kPrimeModulus = 251; + std::vector content(kSize); + for (std::size_t i = 0U; i < kSize; ++i) + { + content[i] = static_cast(i % kPrimeModulus); // prime modulus for varied pattern + } + const auto path = WriteFile("large.bin", content); + + ExpectVectorLoad(path, content); + ExpectPmrVectorLoad(path, content); +} + +TEST_F(LoadFlatbufferTest, LoadsFileContainingOnlyNullBytes) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "only nulls is loaded from file"); + + const std::vector content(512, '\0'); + const auto path = WriteFile("nulls.bin", content); + + ExpectVectorLoad(path, content); + ExpectPmrVectorLoad(path, content); +} + +TEST_F(LoadFlatbufferTest, NonexistentFileReturnsError) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("Description", "kNoSuchFileOrDirectory is returned if file does not exist"); + + const score::filesystem::Path path{test_dir_ + "/does_not_exist.bin"}; + + ExpectVectorLoad(path, score::os::Error::Code::kNoSuchFileOrDirectory); + ExpectPmrVectorLoad(path, score::os::Error::Code::kNoSuchFileOrDirectory); +} + +TEST_F(LoadFlatbufferTest, NoReadPermissionReturnsPermissionDenied) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("Description", "kPermissionDenied is returned if the file cannot be accessed"); + + if (::getuid() == 0U) + { + GTEST_SKIP() << "Cannot test permission denial when running as root"; + } + + const auto path = WriteFile("no_perm.bin", "secret"); + ::chmod(path.CStr(), 0000); + + ExpectVectorLoad(path, score::os::Error::Code::kPermissionDenied); + ExpectPmrVectorLoad(path, score::os::Error::Code::kPermissionDenied); +} + +TEST_F(LoadFlatbufferTest, DirectoryPathFailsOnReadWithIsADirectory) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("Description", "kIsADirectory is returned if path is a directory instead of a file"); + + const auto path = test_dir_; + + ExpectVectorLoad(path, score::os::Error::Code::kIsADirectory); + ExpectPmrVectorLoad(path, score::os::Error::Code::kIsADirectory); +} + +TEST_F(LoadFlatbufferTest, PmrOverloadFailsOnResize) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("Description", + "kNotEnoughSpace is returned if user defined buffer is insufficient to hold the data"); + + constexpr auto kSize = static_cast(1024 * 1024); // 1 MB + constexpr int kPrimeModulus = 251; + constexpr std::size_t kBufferSize = 1024U; + + std::vector content(kSize); + for (std::size_t i = 0U; i < kSize; ++i) + { + content[i] = static_cast(i % kPrimeModulus); + } + + const auto path = WriteFile("1mb.bin", content); + + std::array buffer{}; // not enough to fit 1mb file + std::pmr::monotonic_buffer_resource mbr{buffer.data(), buffer.size(), std::pmr::null_memory_resource()}; + std::pmr::polymorphic_allocator pmr_alloc{&mbr}; + std::pmr::vector data{pmr_alloc}; + const auto result = LoadBuffer(path, data); + ASSERT_EQ(result.error(), score::os::Error::Code::kNotEnoughSpace); + ASSERT_EQ(data.size(), 0U); +} + +} // namespace test +} // namespace flatbuffers +} // namespace score