From b8345246dcc2121bcb6d1515a9341789de20199f Mon Sep 17 00:00:00 2001 From: Dominick Allen Date: Sun, 27 Oct 2024 09:04:05 -0500 Subject: First crack at file objects. --- CMakeLists.txt | 7 +- README.org | 26 ++++++ include/fud_c_file.hpp | 11 +-- include/fud_file.hpp | 168 ++++++++++++++++++++++++++++++++++++ include/fud_memory.hpp | 26 ++++-- include/fud_status.hpp | 45 ++++++++-- include/fud_string.hpp | 6 +- include/fud_string_view.hpp | 7 ++ source/fud_assert.cpp | 32 ++++++- source/fud_file.cpp | 201 ++++++++++++++++++++++++++++++++++++++++++++ source/fud_string.cpp | 30 +++++++ test/CMakeLists.txt | 5 ++ test/test_common.cpp | 60 ++++++++++++- test/test_common.hpp | 13 ++- test/test_directory.cpp | 45 +--------- test/test_file.cpp | 97 +++++++++++++++++++++ 16 files changed, 697 insertions(+), 82 deletions(-) create mode 100644 include/fud_file.hpp create mode 100644 source/fud_file.cpp create mode 100644 test/test_file.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88833ab..1676173 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,14 +14,14 @@ set(CXX_CPPCHECK "project=build/compile_commands.json;enable=information;force") set(CMAKE_EXPORT_COMPILE_COMMANDS true) find_package(SQLite3 REQUIRED) - add_library(fud SHARED source/libfud.cpp source/fud_allocator.cpp source/fud_assert.cpp - source/fud_format.cpp - source/fud_directory.cpp source/fud_c_file.cpp + source/fud_directory.cpp + source/fud_file.cpp + source/fud_format.cpp source/fud_memory.cpp source/fud_sqlite.cpp source/fud_string_view.cpp @@ -102,6 +102,7 @@ set(FUD_HEADERS "include/fud_c_file.hpp" "include/fud_c_string.hpp" "include/fud_directory.hpp" + "include/fud_file.hpp" "include/fud_fixed_vector.hpp" "include/fud_fud_type_traits.hpp" "include/fud_memory.hpp" diff --git a/README.org b/README.org index 61795d7..2a600c4 100644 --- a/README.org +++ b/README.org @@ -1 +1,27 @@ * libfud + +** Design Principles + ++ Full control over allocations, even the default allocator. ++ Zero exceptions, zero exceptions to zero exceptions. ++ Assertions in production release for invariants. ++ Configurable run-time assertions for indexing. ++ Safe API for users intolerant to run-time assertions for indexing. ++ Readable, understandable code. ++ Minimize undefined behavior in the API to a subset which is only decidable at + run-time. ++ Minimize runtime undefined behavior as much as possible. + +** Relatively mature features + ++ Statically sized arrays ++ String Views + +** Unstable features + ++ Customizable allocator model ++ Dynamically sized vectors taking an allocator ++ Dynamically sized strings taking an allocator ++ Unicode support with UTF8 encoding ++ Formatting à la =std::format= ++ Wrappers around C files diff --git a/include/fud_c_file.hpp b/include/fud_c_file.hpp index b54b044..197abd3 100644 --- a/include/fud_c_file.hpp +++ b/include/fud_c_file.hpp @@ -22,6 +22,7 @@ #include "fud_result.hpp" #include "fud_status.hpp" #include "fud_string.hpp" +#include "fud_file.hpp" #include #include @@ -82,16 +83,6 @@ constexpr const char* CTextFileModeFromFlags(CFileMode mode) } } -struct [[nodiscard]] ReadResult { - size_t bytesRead{0}; - FudStatus status{FudStatus::Success}; -}; - -struct [[nodiscard]] WriteResult { - size_t bytesWritten{0}; - FudStatus status{FudStatus::Success}; -}; - namespace detail { template diff --git a/include/fud_file.hpp b/include/fud_file.hpp new file mode 100644 index 0000000..b468b4b --- /dev/null +++ b/include/fud_file.hpp @@ -0,0 +1,168 @@ +/* + * libfud + * Copyright 2024 Dominick Allen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FUD_FILE_HPP +#define FUD_FILE_HPP + +#include "fud_allocator.hpp" +#include "fud_option.hpp" +#include "fud_result.hpp" +#include "fud_status.hpp" +#include "fud_string_view.hpp" + +#include + +namespace fud { + +/** \brief Access Modes for File */ +enum class FileAccessMode : uint8_t +{ + Read = 0x01, + Write = 0x02, + ReadWrite = Read | Write +}; + +enum class OpenFlagEnum : uint32_t +{ + Append = O_APPEND, + Truncate = O_TRUNC, + CloseOnExec = O_CLOEXEC, + DataSync = O_DSYNC, + Direct = O_DIRECT, + NoAtime = O_NOATIME, + NonBlock = O_NONBLOCK, + FileSync = O_SYNC +}; + +class OpenFlags { + public: + using FlagType = std::underlying_type_t; + + constexpr OpenFlags() noexcept = default; + constexpr OpenFlags(const OpenFlags& rhs) noexcept = default; + constexpr OpenFlags(OpenFlags&& rhs) noexcept = default; + constexpr ~OpenFlags() noexcept = default; + constexpr OpenFlags& operator=(const OpenFlags& rhs) noexcept = default; + constexpr OpenFlags& operator=(OpenFlags&& rhs) noexcept = default; + + constexpr OpenFlags(OpenFlagEnum mode) noexcept : m_mask{static_cast(mode)} + { + } + + constexpr OpenFlags operator|(OpenFlags rhs) const noexcept + { + OpenFlags mode{*this}; + mode.m_mask |= rhs.m_mask; + return mode; + } + + constexpr OpenFlags& operator|=(OpenFlags rhs) noexcept + { + m_mask |= rhs.m_mask; + return *this; + } + + constexpr OpenFlags& operator|=(OpenFlagEnum rhs) noexcept + { + m_mask |= static_cast(rhs); + return *this; + } + + constexpr OpenFlags operator|(OpenFlagEnum rhs) const noexcept + { + OpenFlags mode{*this}; + mode.m_mask |= static_cast(rhs); + return mode; + } + + constexpr FlagType flags() const noexcept + { + return m_mask; + } + + constexpr bool hasFlag(OpenFlagEnum flag) noexcept + { + return (m_mask & static_cast(flag)) != 0; + } + + private: + FlagType m_mask{0}; +}; + +constexpr OpenFlags operator|(OpenFlagEnum lhs, OpenFlagEnum rhs) +{ + OpenFlags mode{lhs}; + return mode | rhs; +} + +struct [[nodiscard]] ReadResult { + size_t bytesRead{0}; + FudStatus status{FudStatus::Success}; +}; + +struct [[nodiscard]] WriteResult { + size_t bytesWritten{0}; + FudStatus status{FudStatus::Success}; +}; + +class RegularFile; +using FileResult = Result; + +class RegularFile { + public: + static FileResult open( + StringView filename, + FileAccessMode mode, + OpenFlags flags, + Option dirFdoption, + Allocator* allocator = &globalFudAllocator); + + static FileResult create( + StringView filename, + FileAccessMode mode, + OpenFlags flags, + bool exclusive, + Option dirFdOption, + Allocator* allocator = &globalFudAllocator); + + FudStatus close(); + + private: + RegularFile() = default; + + public: + RegularFile(const RegularFile& rhs) = delete; + + RegularFile(RegularFile&& rhs) noexcept; + + ~RegularFile(); + + RegularFile& operator=(const RegularFile& rhs) = delete; + + RegularFile& operator=(RegularFile&& rhs) noexcept; + + FudStatus take(RegularFile& rhs); + + private: + int m_fd{-1}; + FileAccessMode m_modeFlags{}; + OpenFlags m_openFlags{}; +}; + +} // namespace fud + +#endif diff --git a/include/fud_memory.hpp b/include/fud_memory.hpp index 6ce6312..6f6816f 100644 --- a/include/fud_memory.hpp +++ b/include/fud_memory.hpp @@ -73,6 +73,18 @@ constexpr void setMemory(Container& container, T&& value) } } +template +constexpr void zeroObject(T& object) +{ + static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + + auto* objPtr = reinterpret_cast(&object); + for (size_t idx = 0; idx < sizeof(object); ++idx) { + objPtr[idx] = 0; + } +} + template void copyMem(T& destination, const U& source) { @@ -80,11 +92,16 @@ void copyMem(T& destination, const U& source) static_assert(Count <= sizeof(T)); static_assert(std::is_standard_layout_v); static_assert(std::is_standard_layout_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto* destPtr = reinterpret_cast(&destination); + const auto* srcPtr = reinterpret_cast(&source); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) for (size_t idx = 0; idx < Count; ++idx) { - // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) - reinterpret_cast(&destination)[idx] = reinterpret_cast(&source)[idx]; - // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + destPtr[idx] = srcPtr[idx]; } } @@ -126,8 +143,7 @@ int compareMem(const T& lhs, U&& rhs) int difference = 0; for (size_t idx = 0; idx < Count; ++idx) { // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) - difference = reinterpret_cast(&lhs)[idx] - - reinterpret_cast(&uRhs)[idx]; + difference = reinterpret_cast(&lhs)[idx] - reinterpret_cast(&uRhs)[idx]; // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) if (difference != 0) { break; diff --git a/include/fud_status.hpp b/include/fud_status.hpp index d57a9c5..f18c119 100644 --- a/include/fud_status.hpp +++ b/include/fud_status.hpp @@ -18,32 +18,59 @@ #ifndef FUD_STATUS_HPP #define FUD_STATUS_HPP +#include + namespace fud { -enum class [[nodiscard]] FudStatus +enum class [[nodiscard]] FudStatus : int32_t { + /** \brief Indisputable success. */ Success = 0, + /** \brief Either partially succeeding, or failing in a recoverable way. */ Partial, + /** \brief Typically a major failure in an underlying system call. */ Failure, + /** \brief An argument passed in was a null pointer and is invalid. */ NullPointer, + /** \brief An argument passed in is in an invalid state or mismatches with other arguments. */ ArgumentInvalid, - VariantInvalid, + /** \brief An handle to a managed object is not valid. */ + HandleInvalid, + /** \brief An object is uninitialized or no longer valid. */ ObjectInvalid, + /** \brief A utf sequence in a string is invalid. */ Utf8Invalid, + /** \brief A string is not terminated or has a length field exceeding its capacity. */ StringInvalid, + /** \brief The requested operation on the object is not valid for its current state. */ OperationInvalid, + /** \brief An object which is to be initialized has already been initialized. */ AlreadyInitialized, + /** \brief A format string is invalid. */ FormatInvalid, + /** \brief A string being parsed as a number is out of range for the requested type. */ RangeError, + /** \brief The given index is not in range of a randomly accessible container. */ IndexInvalid, + /** \brief An object in a container already exists in the requested slot for a new object. */ Exists, + /** \brief An object in a container does not exist in the requested slot for an existing object. */ NotFound, + /** \brief A container is empty when something was expected to be present. */ Empty, + /** \brief A container is full when attempting to add something to it. */ Full, + /** \brief An operation requiring elevated permissions failed. */ + PermissionDenied, + /** \brief Two or more objects overlap when they are expected to be distinct. */ Aliased, + /** \brief An requested allocation could not be completed. */ AllocFailure, + /** \brief An requested deallocation could not be completed. */ DeallocFailure, + /** \brief The function is not implemented, but is planned to be implemented. */ NotImplemented, + /** \brief The function or desired mode of action for a function is not supported. */ NotSupported }; @@ -58,20 +85,20 @@ constexpr const char* FudStatusToString(FudStatus status) return "Failure"; case FudStatus::NullPointer: return "NullPointer"; - case FudStatus::StringInvalid: - return "StringInvalid"; + case FudStatus::ArgumentInvalid: + return "ArgumentInvalid"; + case FudStatus::HandleInvalid: + return "HandleInvalid"; case FudStatus::ObjectInvalid: return "ObjectInvalid"; + case FudStatus::StringInvalid: + return "StringInvalid"; case FudStatus::OperationInvalid: return "OperationInvalid"; - case FudStatus::ArgumentInvalid: - return "ArgumentInvalid"; case FudStatus::Utf8Invalid: return "Utf8Invalid"; case FudStatus::RangeError: return "RangeError"; - case FudStatus::VariantInvalid: - return "VariantInvalid"; case FudStatus::FormatInvalid: return "FormatInvalid"; case FudStatus::AlreadyInitialized: @@ -86,6 +113,8 @@ constexpr const char* FudStatusToString(FudStatus status) return "Empty"; case FudStatus::Full: return "Full"; + case FudStatus::PermissionDenied: + return "PermissionDenied"; case FudStatus::Aliased: return "Aliased"; case FudStatus::AllocFailure: diff --git a/include/fud_string.hpp b/include/fud_string.hpp index 0213524..df03ad9 100644 --- a/include/fud_string.hpp +++ b/include/fud_string.hpp @@ -90,7 +90,6 @@ class String { } String output{}; - auto* data = output.m_buffer.data(); output.m_length = totalLength; output.m_allocator = allocator; if (output.m_length >= output.m_capacity) { @@ -103,10 +102,11 @@ class String { output.m_data = static_cast(dataResult.getOkay()); } + auto* data = output.data(); size_t cumulativeLength = 0; for (size_t idx = 0; idx < strPointers.size(); ++idx) { const auto* cString = strPointers[idx]; - auto copyStatus = copyMem(data, output.m_capacity - cumulativeLength, cString, lengths[idx]); + auto copyStatus = copyMem(data + cumulativeLength, output.m_capacity - cumulativeLength, cString, lengths[idx]); fudAssert(copyStatus == FudStatus::Success); cumulativeLength += lengths[idx]; } @@ -131,6 +131,8 @@ class String { static StringResult from(const String& rhs); + static StringResult from(StringView view, Allocator* allocator = &globalFudAllocator); + FudStatus copy(const String& rhs); [[nodiscard]] constexpr size_t length() const diff --git a/include/fud_string_view.hpp b/include/fud_string_view.hpp index 718b3d7..972630a 100644 --- a/include/fud_string_view.hpp +++ b/include/fud_string_view.hpp @@ -61,6 +61,13 @@ struct StringView { explicit StringView(const String& fudString) noexcept; + template + constexpr static StringView cStringView(const char (&input)[N]) + { + static_assert(N > 0); + return StringView{N, reinterpret_cast(input)}; + } + [[nodiscard]] constexpr size_t length() const { return m_length; diff --git a/source/fud_assert.cpp b/source/fud_assert.cpp index 0c4d7cf..a7c9d76 100644 --- a/source/fud_assert.cpp +++ b/source/fud_assert.cpp @@ -1,11 +1,27 @@ +/* + * libfud + * Copyright 2024 Dominick Allen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "fud_assert.hpp" #include "fud_format.hpp" -#include "fud_string.hpp" +#include "fud_string.hpp" // DrainResult +#include "fud_string_view.hpp" -#include #include -#include namespace fud { @@ -24,6 +40,7 @@ DrainResult BufferSink::drain(StringView source) result.status = FudStatus::Success; return result; } + /* TODO: give users control over this functionality */ result.bytesWritten = fwrite(reinterpret_cast(source.m_data), 1, source.m_length, stderr); if (result.bytesWritten != source.m_length) { result.status = FudStatus::Full; @@ -43,7 +60,14 @@ void assertFailMessage(const char* assertion, const std::source_location sourceL if (functionName == nullptr) { functionName = "Unknown Function"; } - format(sink, FormatCharMode::Unchecked, "{}:{}:{}: {}\n", fileName, functionName, sourceLocation.line(), assertion); + static_cast(format( + sink, + FormatCharMode::Unchecked, + "{}:{}:{}: {}\n", + fileName, + functionName, + sourceLocation.line(), + assertion)); } } // namespace impl diff --git a/source/fud_file.cpp b/source/fud_file.cpp new file mode 100644 index 0000000..e27a46d --- /dev/null +++ b/source/fud_file.cpp @@ -0,0 +1,201 @@ +/* + * libfud + * Copyright 2024 Dominick Allen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "fud_file.hpp" + +#include +#include +#include +#include +#include +#include + +namespace fud { + +FileResult RegularFile::open( + StringView filename, + FileAccessMode mode, + OpenFlags flags, + Option dirFdOption, + Allocator* allocator) +{ + if (allocator == nullptr) { + return FudStatus::NullPointer; + } + + if (!filename.nullTerminated()) { + return FudStatus::ArgumentInvalid; + } + + int dirFd = dirFdOption.valueOr(AT_FDCWD); + + RegularFile file{}; + + uint32_t openFlags = 0; + switch (mode) { + case FileAccessMode::Read: + openFlags = O_RDONLY; + break; + case FileAccessMode::Write: + openFlags = O_WRONLY; + break; + case FileAccessMode::ReadWrite: + openFlags = O_RDWR; + break; + default: + return FudStatus::ArgumentInvalid; + } + + if (flags.hasFlag(OpenFlagEnum::Append) && flags.hasFlag(OpenFlagEnum::Truncate)) { + return FudStatus::OperationInvalid; + } + + openFlags |= flags.flags(); + + open_how openHow{}; + zeroObject(openHow); + openHow.flags = openFlags; + openHow.resolve = RESOLVE_NO_SYMLINKS; + + auto status = syscall(SYS_openat2, dirFd, filename.data(), &openHow, sizeof(openHow)); + if (status == -1) { + if constexpr (EAGAIN != EWOULDBLOCK && status == EWOULDBLOCK) { + return FudStatus::Partial; + } + switch (errno) { + case ETXTBSY: + case EAGAIN: + return FudStatus::Partial; + case ENOENT: + return FudStatus::NotFound; + case EFBIG: + case EOVERFLOW: + case EINVAL: + case EISDIR: + case ENAMETOOLONG: + return FudStatus::ArgumentInvalid; + case EROFS: + case EACCES: + case EPERM: + return FudStatus::PermissionDenied; + case ELOOP: + case EXDEV: + case ENFILE: + case E2BIG: + default: + return FudStatus::Failure; + } + } + fudAssert(status <= std::numeric_limits::max()); + file.m_fd = static_cast(status); + + using Stat = struct stat; + Stat sBuffer{}; + auto fStatus = fstat(file.m_fd, &sBuffer); + if (fStatus == -1) { + return FudStatus::Failure; + } + + if ((sBuffer.st_mode & S_IFMT) != S_IFREG) { + return FudStatus::ObjectInvalid; + } + + return file; +} + +/* + static FileResult RegularFile::create( + StringView filename, + FileAccessMode mode, + OpenFlags flags, + bool exclusive, + Option dirFdOption, + Allocator* allocator = &globalFudAllocator); +{ +} +*/ + +RegularFile::~RegularFile() +{ + static_cast(this->close()); +} + +RegularFile::RegularFile(RegularFile&& rhs) noexcept : m_fd{rhs.m_fd}, m_modeFlags{rhs.m_modeFlags} +{ + rhs.m_fd = -1; +} + +RegularFile& RegularFile::operator=(RegularFile&& rhs) noexcept +{ + if (&rhs == this) { + return *this; + } + + static_cast(this->close()); + + m_fd = rhs.m_fd; + m_modeFlags = rhs.m_modeFlags; + + rhs.m_fd = -1; + return *this; +} + +FudStatus RegularFile::take(RegularFile& rhs) +{ + if (&rhs == this) { + return FudStatus::Success; + } + + auto status = this->close(); + if (status != FudStatus::Success) { + return status; + } + + m_fd = rhs.m_fd; + m_modeFlags = rhs.m_modeFlags; + + rhs.m_fd = -1; + return status; +} + +FudStatus RegularFile::close() +{ + FudStatus status = FudStatus::Success; + if (m_fd != -1) { + auto closeStatus = ::close(m_fd); + if (closeStatus == -1) { + switch (errno) { + case EBADF: + status = FudStatus::HandleInvalid; + break; + case EINTR: + case EIO: + case ENOSPC: + case EDQUOT: + status = FudStatus::Partial; + break; + default: + status = FudStatus::Failure; + break; + } + } + m_fd = -1; + } + return status; +} + +} // namespace fud diff --git a/source/fud_string.cpp b/source/fud_string.cpp index 048cc94..c444a74 100644 --- a/source/fud_string.cpp +++ b/source/fud_string.cpp @@ -84,6 +84,32 @@ StringResult String::from(const String& rhs) return StringResult::okay(std::move(output)); } +StringResult String::from(StringView view, Allocator* allocator) +{ + if (allocator == nullptr || view.m_data == nullptr) { + return StringResult::error(FudStatus::NullPointer); + } + + String output{}; + output.m_allocator = allocator; + output.m_length = view.m_length; + + if (output.m_length >= output.m_capacity) { + output.m_capacity = output.m_length + 1; + + output.m_data = static_cast(M_TakeOrReturn(output.m_allocator->allocate(output.m_capacity))); + fudAssert(output.m_data != nullptr); + } + + auto copyStatus = copyMem(output.data(), output.m_capacity, view.m_data, output.m_length); + fudAssert(copyStatus == FudStatus::Success); + + auto terminateStatus = output.nullTerminate(); + fudAssert(terminateStatus == FudStatus::Success); + + return StringResult::okay(std::move(output)); +} + String::String(String&& rhs) noexcept : m_length{rhs.m_length}, m_capacity{rhs.m_capacity}, m_allocator{rhs.m_allocator} { if (rhs.isLarge()) { @@ -131,6 +157,10 @@ FudStatus String::copy(const String& rhs) String& String::operator=(String&& rhs) noexcept { + if (&rhs == this) { + return *this; + } + cleanup(); m_length = rhs.m_length; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 788e4ba..aef8052 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,12 +13,16 @@ endif() set(gtest_URL https://github.com/google/googletest.git) set(gtest_TAG v1.14.0) +# Keep this setting above the FetchContent_Declare for googletest +set(INSTALL_GTEST OFF CACHE BOOL "Enable installation of googletest.") + FetchContent_Declare( googletest GIT_REPOSITORY ${gtest_URL} GIT_TAG ${gtest_TAG} ) FetchContent_MakeAvailable(googletest) + include(GoogleTest) enable_testing() @@ -61,6 +65,7 @@ fud_add_test(test_allocator SOURCES test_allocator.cpp) fud_add_test(test_assert SOURCES test_assert.cpp) # fud_add_test(test_c_file SOURCES test_c_file.cpp) fud_add_test(test_directory SOURCES test_directory.cpp) +fud_add_test(test_file SOURCES test_file.cpp) fud_add_test(test_format SOURCES test_format.cpp) fud_add_test(test_option SOURCES test_option.cpp) fud_add_test(test_result SOURCES test_result.cpp) diff --git a/test/test_common.cpp b/test/test_common.cpp index d3e1704..784cb0d 100644 --- a/test/test_common.cpp +++ b/test/test_common.cpp @@ -16,7 +16,12 @@ */ #include "test_common.hpp" + +#include "fud_string.hpp" + #include +#include +#include namespace fud { @@ -34,22 +39,69 @@ MockFudAlloc globalDefaultMockAlloc{}; MockFudDealloc globalDefaultMockDealloc{}; -void* MockFudAllocator::allocate(size_t size) { +void* MockFudAllocator::allocate(size_t size) +{ return (*m_allocator)(size); } -void MockFudAllocator::deallocate(void* pointer) { +void MockFudAllocator::deallocate(void* pointer) +{ return (*m_deallocator)(pointer); } MockFudAllocator globalMockFudAlloc{}; -void* fudAlloc(size_t size) { +void* fudAlloc(size_t size) +{ return globalMockFudAlloc.allocate(size); } -void fudFree(void* ptr) { +void fudFree(void* ptr) +{ return globalMockFudAlloc.deallocate(ptr); } +int unlink_cb(const char* fpath, const struct stat* sb_unused, int typeflag, struct FTW* ftwbuf) +{ + static_cast(sb_unused); + int retValue = remove(fpath); + + EXPECT_EQ(retValue, 0); + if (retValue != 0) { + perror(fpath); + } + + return retValue; +} + +FudStatus removeRecursive(const String& path) +{ + if (!path.utf8Valid()) { + return FudStatus::Utf8Invalid; + } + if (path.length() < 5) { + return FudStatus::ArgumentInvalid; + } + auto prefix{String::makeFromCString("/tmp/").takeOkay()}; + auto diffResult = compareMem(path.data(), path.length(), prefix.data(), prefix.length()); + if (diffResult.isError()) { + return FudStatus::ArgumentInvalid; + } + auto diff = diffResult.getOkay(); + if (diff != 0) { + return FudStatus::ArgumentInvalid; + } + constexpr int maxOpenFd = 64; + auto status = nftw(path.c_str(), unlink_cb, maxOpenFd, FTW_DEPTH | FTW_PHYS); + if (status == 0) { + return FudStatus::Success; + } + + if (errno == ENOENT) { + return FudStatus::Success; + } + + return FudStatus::Failure; +} + } // namespace fud diff --git a/test/test_common.hpp b/test/test_common.hpp index f049fed..3d7ecba 100644 --- a/test/test_common.hpp +++ b/test/test_common.hpp @@ -18,9 +18,15 @@ #ifndef FUD_TEST_COMMON_HPP #define FUD_TEST_COMMON_HPP +#include "fud_status.hpp" + #include #include +extern "C" { +int unlink_cb(const char* fpath, const struct stat* sb_unused, int typeflag, struct FTW* ftwbuf); +} + namespace fud { // NOLINTBEGIN(cppcoreguidelines-macro-usage) @@ -70,11 +76,14 @@ struct MockFudAllocator { void deallocate(void* pointer); MockFudAlloc* m_allocator{&globalDefaultMockAlloc}; - MockFudDealloc* m_deallocator{&globalDefaultMockDealloc};; + MockFudDealloc* m_deallocator{&globalDefaultMockDealloc}; }; extern MockFudAllocator globalMockFudAlloc; -} // namespace ext_lib +class String; +FudStatus removeRecursive(const String& path); + +} // namespace fud #endif diff --git a/test/test_directory.cpp b/test/test_directory.cpp index 2f69dab..e30dafb 100644 --- a/test/test_directory.cpp +++ b/test/test_directory.cpp @@ -18,8 +18,8 @@ #include "fud_array.hpp" #include "fud_c_file.hpp" #include "fud_directory.hpp" -#include "fud_memory.hpp" #include "fud_string.hpp" +#include "test_common.hpp" #include "gtest/gtest.h" #include @@ -30,49 +30,6 @@ namespace fud { -int unlink_cb(const char* fpath, const struct stat* sb_unused, int typeflag, struct FTW* ftwbuf) -{ - static_cast(sb_unused); - int retValue = remove(fpath); - - EXPECT_EQ(retValue, 0); - if (retValue != 0) { - perror(fpath); - } - - return retValue; -} - -FudStatus removeRecursive(const String& path) -{ - if (!path.utf8Valid()) { - return FudStatus::Utf8Invalid; - } - if (path.length() < 5) { - return FudStatus::ArgumentInvalid; - } - auto prefix{String::makeFromCString("/tmp/").takeOkay()}; - auto diffResult = compareMem(path.data(), path.length(), prefix.data(), prefix.length()); - if (diffResult.isError()) { - return FudStatus::ArgumentInvalid; - } - auto diff = diffResult.getOkay(); - if (diff != 0) { - return FudStatus::ArgumentInvalid; - } - constexpr int maxOpenFd = 64; - auto status = nftw(path.c_str(), unlink_cb, maxOpenFd, FTW_DEPTH | FTW_PHYS); - if (status == 0) { - return FudStatus::Success; - } - - if (errno == ENOENT) { - return FudStatus::Success; - } - - return FudStatus::Failure; -} - TEST(FudDirectory, Basic) { const auto testDirName{String::makeFromCString("/tmp/fud_directory_test").takeOkay()}; diff --git a/test/test_file.cpp b/test/test_file.cpp new file mode 100644 index 0000000..06e6bcc --- /dev/null +++ b/test/test_file.cpp @@ -0,0 +1,97 @@ +/* + * libfud + * Copyright 2024 Dominick Allen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "fud_file.hpp" +#include "fud_string.hpp" +#include "test_common.hpp" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include + +namespace fud { + +TEST(FudFile, Basic) +{ + constexpr const char* testDirCName = "/tmp/fud_directory_test"; + const auto testDirName{String::makeFromCString(testDirCName).takeOkay()}; + ASSERT_TRUE(testDirName.utf8Valid()); + constexpr mode_t pathMode = 0777; + + ASSERT_EQ(removeRecursive(testDirName), FudStatus::Success); + + auto mkdirResult = mkdir(testDirName.c_str(), pathMode); + EXPECT_EQ(mkdirResult, 0); + if (mkdirResult != 0) { + ASSERT_EQ(removeRecursive(testDirName), FudStatus::Success); + return; + } + + String testName1{String::makeFromCStrings(testDirCName, "/", "test1").takeOkay()}; + String testName2{String::makeFromCStrings(testDirCName, "/", "test2").takeOkay()}; + + auto rmFile = [](const auto& filename) -> int { + auto result = unlink(filename.c_str()); + if (result == -1) { + if (errno == ENOENT) { + return 0; + } + } + return result; + }; + ASSERT_EQ(rmFile(testName1), 0); + ASSERT_EQ(rmFile(testName2), 0); + + auto fileResult{RegularFile::open(testName1.asView(), FileAccessMode::Read, OpenFlags{}, NullOpt)}; + EXPECT_EQ(fileResult.takeErrorOr(FudStatus::Success), FudStatus::NotFound); + + /* The irony of the ability for TOCTOU bugs to be present in the test does + * not escape me. */ + + auto createFile = [](const auto& filename) -> int { + constexpr int allPerms = 0777; + auto handleResult = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, allPerms); + if (handleResult >= 0) { + close(handleResult); + } + return handleResult; + }; + ASSERT_GE(createFile(testName1), 0); + + fileResult = RegularFile::open(testName1.asView(), FileAccessMode::Read, OpenFlags{}, NullOpt); + ASSERT_TRUE(fileResult.isOkay()); + auto file{fileResult.takeOkay()}; + ASSERT_EQ(file.close(), FudStatus::Success); + + ASSERT_EQ(rmFile(testName1), 0); + ASSERT_GE(createFile(testName2), 0); + ASSERT_EQ(symlink(testName2.c_str(), testName1.c_str()), 0); + + fileResult = RegularFile::open(testName2.asView(), FileAccessMode::Read, OpenFlags{}, NullOpt); + ASSERT_TRUE(fileResult.isOkay()); + file = fileResult.takeOkay(); + ASSERT_EQ(file.close(), FudStatus::Success); + + fileResult = RegularFile::open(testName1.asView(), FileAccessMode::Read, OpenFlags{}, NullOpt); + ASSERT_TRUE(fileResult.isError()); + EXPECT_EQ(fileResult.getError(), FudStatus::Failure); +} + +} // namespace fud -- cgit v1.2.3