diff options
author | Dominick Allen <djallen@librehumanitas.org> | 2024-09-28 00:23:22 -0500 |
---|---|---|
committer | Dominick Allen <djallen@librehumanitas.org> | 2024-09-28 00:23:22 -0500 |
commit | 07d30629d78474ed810cad73ead71db96079aff0 (patch) | |
tree | 5f43fd8f6a818a590818e0c474f03a1f05076dba | |
parent | 63711877057f1f89b4d1774e24fe20907a3af656 (diff) |
Add directory interface.
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | include/fud_c_file.hpp | 37 | ||||
-rw-r--r-- | include/fud_directory.hpp | 87 | ||||
-rw-r--r-- | include/fud_string.hpp | 8 | ||||
-rw-r--r-- | source/fud_c_file.cpp | 65 | ||||
-rw-r--r-- | source/fud_directory.cpp | 192 | ||||
-rw-r--r-- | source/fud_string.cpp | 38 | ||||
-rw-r--r-- | test/CMakeLists.txt | 1 | ||||
-rw-r--r-- | test/test_directory.cpp | 147 |
9 files changed, 565 insertions, 12 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 348cd36..df61f24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(fud SHARED source/fud_utf8.cpp source/fud_utf8_iterator.cpp source/fud_sqlite.cpp + source/fud_directory.cpp ) include(cmake/warnings.cmake) @@ -96,6 +97,7 @@ set(FUD_HEADERS "include/fud_utf8.hpp" "include/fud_utf8_iterator.hpp" "include/fud_sqlite" + "include/fud_directory.hpp" ) set_target_properties(fud PROPERTIES PUBLIC_HEADER "${FUD_HEADERS}") diff --git a/include/fud_c_file.hpp b/include/fud_c_file.hpp index f563a35..674130c 100644 --- a/include/fud_c_file.hpp +++ b/include/fud_c_file.hpp @@ -18,8 +18,8 @@ #ifndef FUD_C_FILE_HPP #define FUD_C_FILE_HPP -#include "fud_string.hpp" #include "fud_result.hpp" +#include "fud_string.hpp" #include <cstdint> #include <cstdio> @@ -88,7 +88,8 @@ enum class FileStatus Error, }; -static inline const char* FileStatusToString(FileStatus status) { +static inline const char* FileStatusToString(FileStatus status) +{ switch (status) { case FileStatus::Success: return "Success"; @@ -114,6 +115,11 @@ struct [[nodiscard]] ReadResult { FileStatus status{FileStatus::Success}; }; +struct [[nodiscard]] WriteResult { + size_t bytesWritten{0}; + FileStatus status{FileStatus::Success}; +}; + class CBinaryFile { public: CBinaryFile(const String& filename, CFileMode mode); @@ -146,6 +152,32 @@ class CBinaryFile { return read(&destination, sizeof(destination), length, offset); } + [[nodiscard]] WriteResult write(const void* source, size_t sourceSize, size_t length); + + [[nodiscard]] WriteResult write(const void* source, size_t sourceSize, size_t length, size_t offset); + + template <typename T> + [[nodiscard]] WriteResult write(const T& source) + { + return write(source, sizeof(source), sizeof(source)); + } + + template <typename T> + [[nodiscard]] WriteResult write(const T& source, size_t sourceSize, size_t length) + { + auto offsetResult = size(); + if (offsetResult.isError()) { + return WriteResult{0, offsetResult.getError()}; + } + return write(source, sourceSize, length, offsetResult.getOkay()); + } + + template <typename T> + [[nodiscard]] WriteResult write(const T& source, size_t sourceSize, size_t length, size_t offset) + { + return write(static_cast<const void*>(&source), sourceSize, length, offset); + } + private: FileStatus reset() const; @@ -158,5 +190,4 @@ class CBinaryFile { } // namespace fud - #endif diff --git a/include/fud_directory.hpp b/include/fud_directory.hpp new file mode 100644 index 0000000..ac48b7b --- /dev/null +++ b/include/fud_directory.hpp @@ -0,0 +1,87 @@ +/* + * 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_DIRECTORY_HPP +#define FUD_DIRECTORY_HPP + +#include "fud_result.hpp" +#include "fud_status.hpp" +#include "fud_string.hpp" + +#include <cstdint> +#include <cstdio> +#include <dirent.h> +#include <optional> +#include <ctime> + +namespace fud { + +enum class DirectoryEntryType : uint8_t +{ + Block, + Character, + Directory, + NamedPipe, + SymbolicLink, + RegularFile, + UnixSocket, + Unknown +}; + +/** \brief Simplified Directory Entry */ +struct DirectoryEntry { + String name; + size_t size; + size_t links; + time_t modificationTime; + DirectoryEntryType entryType; +}; + +class Directory { + public: + explicit Directory(String name); + Directory(const Directory& rhs) = delete; + Directory(Directory&& rhs); + ~Directory(); + Directory& operator=(const Directory& rhs) = delete; + Directory& operator=(Directory&& rhs); + + constexpr FudStatus status() const + { + return m_status; + } + + constexpr int errorCode() const + { + return m_errorCode; + } + + Result<std::optional<DirectoryEntry>, FudStatus> getNextEntry(); + + FudStatus reset(); + + private: + String m_name{}; + DIR* m_directory{nullptr}; + FudStatus m_status{FudStatus::ObjectInvalid}; + int m_errorCode{-1}; + int m_dirFd{-1}; +}; + +} // namespace fud + +#endif diff --git a/include/fud_string.hpp b/include/fud_string.hpp index 97b7036..9e423ef 100644 --- a/include/fud_string.hpp +++ b/include/fud_string.hpp @@ -113,9 +113,13 @@ class String { std::optional<utf8> pop(); - [[nodiscard]] FudStatus catenate(StringView source); + [[nodiscard]] FudStatus append(StringView source); - [[nodiscard]] String append(const String& rhs) const; + [[nodiscard]] String catenate(const String& rhs) const; + + [[nodiscard]] String catenate(const char* rhs) const; + + [[nodiscard]] bool compare(const String& rhs) const; const utf8* begin() const; diff --git a/source/fud_c_file.cpp b/source/fud_c_file.cpp index a85fd00..cad4c9d 100644 --- a/source/fud_c_file.cpp +++ b/source/fud_c_file.cpp @@ -29,7 +29,7 @@ CBinaryFile::CBinaryFile(const String& filename, CFileMode mode) CBinaryFile::CBinaryFile(const String& filename, CFileMode mode, const String& extraFlags) : m_filename{filename}, m_extraFlags{extraFlags}, - m_mode{String(CBinaryFileModeFromFlags(mode)).append(extraFlags)}, + m_mode{String(CBinaryFileModeFromFlags(mode)).catenate(extraFlags)}, m_modeFlags{mode} { } @@ -137,8 +137,8 @@ ReadResult CBinaryFile::read(void* destination, size_t destinationSize, size_t l return result; } - auto* destPtr = static_cast<char*>(destination); - result.bytesRead = fread(destPtr, 1, length, m_file); + auto* destBytes = static_cast<char*>(destination); + result.bytesRead = fread(destBytes, 1, length, m_file); static_cast<void>(reset()); if (result.bytesRead != length) { result.status = FileStatus::PartialSuccess; @@ -149,6 +149,65 @@ ReadResult CBinaryFile::read(void* destination, size_t destinationSize, size_t l return result; } +WriteResult CBinaryFile::write(const void* source, size_t sourceSize, size_t length) +{ + auto offsetResult = size(); + if (offsetResult.isError()) { + return WriteResult{0, offsetResult.getError()}; + } + + return write(source, sourceSize, length, offsetResult.getOkay()); +} + +WriteResult CBinaryFile::write(const void* source, size_t sourceSize, size_t length, size_t offset) +{ + WriteResult result{}; + if (length == 0) { + return result; + } + + if (source == nullptr) { + result.status = FileStatus::NullPointer; + return result; + } + + if (offset > LONG_MAX || SIZE_MAX - offset < length || sourceSize < length) { + result.status = FileStatus::InvalidArgument; + return result; + } + + auto fileSizeResult = size(); + if (fileSizeResult.isError()) { + result.status = fileSizeResult.getError(); + return result; + } + + // TODO: proper way of handling this + auto fileSize = fileSizeResult.getOkay(); + int seekResult; + if (offset > fileSize) { + seekResult = fseek(m_file, 0, SEEK_END); + } else { + seekResult = fseek(m_file, static_cast<long>(offset), SEEK_SET); + } + + if (seekResult != 0) { + result.status = FileStatus::Error; + return result; + } + + auto* sourceBytes = static_cast<const char*>(source); + result.bytesWritten = fwrite(sourceBytes, 1, length, m_file); + static_cast<void>(reset()); + if (result.bytesWritten != length) { + result.status = FileStatus::PartialSuccess; + } else { + result.status = FileStatus::Success; + } + + return result; +} + FileStatus CBinaryFile::reset() const { if (!isOpen()) { return FileStatus::InvalidState; diff --git a/source/fud_directory.cpp b/source/fud_directory.cpp new file mode 100644 index 0000000..39f2e10 --- /dev/null +++ b/source/fud_directory.cpp @@ -0,0 +1,192 @@ +/* + * 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_directory.hpp" + +#include <cerrno> +#include <fcntl.h> +#include <sys/stat.h> + +namespace fud { + +Directory::Directory(String name) : m_name{name} +{ + if (!m_name.valid()) { + return; + } + + if (!m_name.utf8Valid()) { + m_status = FudStatus::Utf8Invalid; + return; + } + + m_directory = opendir(m_name.c_str()); + if (m_directory == nullptr) { + m_status = FudStatus::Failure; + m_errorCode = errno; + return; + } + + m_dirFd = dirfd(m_directory); + if (m_dirFd == -1) { + m_status = FudStatus::Failure; + m_errorCode = errno; + closedir(m_directory); + m_directory = nullptr; + return; + } + + m_errorCode = 0; + m_status = FudStatus::Success; +} + +Directory::Directory(Directory&& rhs) : + m_name{std::move(rhs.m_name)}, m_directory{rhs.m_directory}, m_dirFd{rhs.m_dirFd} +{ + rhs.m_directory = nullptr; + rhs.m_dirFd = -1; +} + +Directory& Directory::operator=(Directory&& rhs) +{ + m_name = std::move(rhs.m_name); + m_directory = rhs.m_directory; + m_dirFd = rhs.m_dirFd; + + rhs.m_directory = nullptr; + rhs.m_dirFd = -1; + + return *this; +} + +Directory::~Directory() +{ + if (m_directory != nullptr) { + closedir(m_directory); + m_directory = nullptr; + } +} + +Result<std::optional<DirectoryEntry>, FudStatus> Directory::getNextEntry() +{ + using RetType = Result<std::optional<DirectoryEntry>, FudStatus>; + + if (m_directory == nullptr || m_dirFd == -1) { + m_status = FudStatus::ObjectInvalid; + return RetType::error(m_status); + } + + errno = 0; + auto* dirEntry = readdir(m_directory); + if (dirEntry == nullptr) { + if (errno != 0) { + m_errorCode = errno; + m_status = FudStatus::Failure; + return RetType::error(m_status); + } else { + return RetType::okay(std::nullopt); + } + } + + const char* entryName = dirEntry->d_name; + if (entryName == nullptr) { + m_errorCode = -1; + m_status = FudStatus::NullPointer; + return RetType::error(m_status); + } + + m_errorCode = 0; + + using Stat = struct stat; + Stat statBuffer{}; + auto flags = 0; + auto status = fstatat(m_dirFd, entryName, &statBuffer, flags); + if (status == -1) { + m_errorCode = errno; + m_status = FudStatus::Failure; + return RetType::error(m_status); + } + + DirectoryEntryType entryType; + switch (statBuffer.st_mode & S_IFMT) { + case S_IFBLK: + entryType = DirectoryEntryType::Block; + break; + case S_IFCHR: + entryType = DirectoryEntryType::Character; + break; + case S_IFDIR: + entryType = DirectoryEntryType::Directory; + break; + case S_IFIFO: + entryType = DirectoryEntryType::NamedPipe; + break; + case S_IFLNK: + entryType = DirectoryEntryType::SymbolicLink; + break; + case S_IFREG: + entryType = DirectoryEntryType::RegularFile; + break; + case S_IFSOCK: + entryType = DirectoryEntryType::UnixSocket; + break; + default: + entryType = DirectoryEntryType::Unknown; + break; + } + + static_assert(std::is_same_v<decltype(statBuffer.st_size), long>); + static_assert(sizeof(decltype(statBuffer.st_size)) <= sizeof(size_t)); + + size_t size{0}; + if (statBuffer.st_size < 0) { + size = SIZE_MAX; + } else { + size = static_cast<size_t>(statBuffer.st_size); + } + + static_assert(std::is_same_v<decltype(statBuffer.st_nlink), unsigned long>); + static_assert(sizeof(decltype(statBuffer.st_nlink)) <= sizeof(size_t)); + + DirectoryEntry entry{ + String{dirEntry->d_name}, + size, + static_cast<size_t>(statBuffer.st_nlink), + statBuffer.st_mtime, + entryType}; + + if (!entry.name.valid()) { + m_status = FudStatus::StringInvalid; + return RetType::error(m_status); + } + + m_status = FudStatus::Success; + return RetType::okay(std::move(entry)); +} + +FudStatus Directory::reset() +{ + if (m_directory != nullptr) { + rewinddir(m_directory); + } else { + m_status = FudStatus::ObjectInvalid; + return m_status; + } + return FudStatus::NotImplemented; +} + +} // namespace fud diff --git a/source/fud_string.cpp b/source/fud_string.cpp index 74742e1..52d6b8f 100644 --- a/source/fud_string.cpp +++ b/source/fud_string.cpp @@ -236,7 +236,7 @@ FudStatus String::pushBack(const FudUtf8& letter) return FudStatus::Success; } -FudStatus String::catenate(StringView source) +FudStatus String::append(StringView source) { if (!valid()) { return FudStatus::StringInvalid; @@ -262,13 +262,19 @@ FudStatus String::catenate(StringView source) return status; } -String String::append(const String& rhs) const +String String::catenate(const char* rhs) const +{ + String rhsString{rhs}; + return catenate(rhsString); +} + +String String::catenate(const String& rhs) const { String output{}; output.m_length = 1; output.m_capacity = 0; - if (!valid()) { + if (!valid() || !rhs.valid()) { return output; } @@ -279,7 +285,9 @@ String String::append(const String& rhs) const } auto* destPtr = output.data(); - auto status = copyMem(destPtr, m_capacity, rhs.data(), rhs.length()); + auto status = copyMem(destPtr, m_capacity, data(), length()); + fudAssert(status == FudStatus::Success); + status = copyMem(destPtr + length(), m_capacity, rhs.data(), rhs.length()); fudAssert(status == FudStatus::Success); static_cast<void>(status); fudAssert(output.nullTerminate() == FudStatus::Success); @@ -287,6 +295,28 @@ String String::append(const String& rhs) const return output; } +bool String::compare(const String& rhs) const { + if (!valid() || !rhs.valid()) { + return false; + } + + if (length() != rhs.length()) { + return false; + } + + if (isLarge() && data() == rhs.data()) + { + return true; + } + + auto diffResult = compareMem(data(), length(), rhs.data(), rhs.length()); + if (diffResult.isError()) { + return false; + } + + return diffResult.getOkay() == 0; +} + const utf8* String::begin() const { return data(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 73968fe..9061d55 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -54,6 +54,7 @@ endfunction() fud_add_test(test_result SOURCES test_result.cpp) fud_add_test(test_string SOURCES test_string.cpp) fud_add_test(test_sqlite SOURCES test_sqlite.cpp) +fud_add_test(test_directory SOURCES test_directory.cpp) # fud_add_test(test_deserialize_number SOURCES test_deserialize_number.cpp) # fud_add_test(test_ext_algorithm SOURCES test_algorithm.cpp) # fud_add_test(test_ext_array SOURCES diff --git a/test/test_directory.cpp b/test/test_directory.cpp new file mode 100644 index 0000000..9cec80d --- /dev/null +++ b/test/test_directory.cpp @@ -0,0 +1,147 @@ +/* + * 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_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 <algorithm> +#include <cerrno> +#include <fcntl.h> +#include <ftw.h> +#include <ranges> + +namespace fud { + +int unlink_cb(const char* fpath, const struct stat* sb, int typeflag, struct FTW* ftwbuf) +{ + int retValue = remove(fpath); + + EXPECT_EQ(retValue, 0); + if (retValue) { + perror(fpath); + } + + return retValue; +} + +FudStatus removeRecursive(const String& path) +{ + if (!path.utf8Valid()) { + return FudStatus::Utf8Invalid; + } + if (path.length() < 5) { + return FudStatus::InvalidInput; + } + const String prefix{"/tmp/"}; + auto diffResult = compareMem(path.data(), path.length(), prefix.data(), prefix.length()); + if (diffResult.isError()) { + return FudStatus::InvalidInput; + } + auto diff = diffResult.getOkay(); + if (diff != 0) { + return FudStatus::InvalidInput; + } + 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 String testDirName{"/tmp/fud_directory_test"}; + ASSERT_TRUE(testDirName.utf8Valid()); + constexpr mode_t pathMode = 0777; + const Array<String, 2> files{ + String{"file1"}, + String{"file2"}, + }; + ASSERT_TRUE(files[0].utf8Valid()); + ASSERT_TRUE(files[1].utf8Valid()); + + 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; + } + + const String testDirNamePrefix = testDirName.catenate("/"); + ASSERT_TRUE(testDirNamePrefix.utf8Valid()); + for (const auto& fnameBase : files) { + const auto fname = testDirNamePrefix.catenate(fnameBase); + ASSERT_TRUE(fname.utf8Valid()); + CBinaryFile file{fname, CFileMode::ReadWriteTruncate}; + ASSERT_EQ(file.open(), FileStatus::Success); + Array<utf8, 5> data{"test"}; + WriteResult expected{data.size(), FileStatus::Success}; + auto writeResult = file.write(data); + ASSERT_EQ(writeResult.bytesWritten, expected.bytesWritten); + ASSERT_EQ(writeResult.status, expected.status); + } + + Directory directory{testDirName}; + ASSERT_EQ(directory.status(), FudStatus::Success); + ASSERT_EQ(directory.errorCode(), 0); + + const Array<DirectoryEntry, 4> expectedFiles{ + DirectoryEntry{String{"."}, 0, 2, 0, DirectoryEntryType::Directory}, + DirectoryEntry{String{".."}, 0, 1, 0, DirectoryEntryType::Directory}, + DirectoryEntry{files[0], files[0].size(), 1, 0, DirectoryEntryType::RegularFile}, + DirectoryEntry{files[1], files[1].size(), 1, 0, DirectoryEntryType::RegularFile}, + }; + ASSERT_TRUE(expectedFiles[0].name.compare(expectedFiles[0].name)); + + for (auto idx = 0; idx < expectedFiles.size(); ++idx) { + auto dirEntryResult = directory.getNextEntry(); + EXPECT_TRUE(dirEntryResult.isOkay()); + const auto dirEntryOpt = dirEntryResult.getOkay(); + if (dirEntryOpt == std::nullopt) { + break; + } + const auto dirEntry = *dirEntryOpt; + const auto expected = std::find_if( + expectedFiles.begin(), + expectedFiles.end(), + [&dirEntry](const DirectoryEntry& entry) { return entry.name.compare(dirEntry.name) && entry.entryType == dirEntry.entryType; }); + EXPECT_NE(expected, nullptr); + EXPECT_NE(expected, expectedFiles.end()); + printf("%s %u\n", dirEntry.name.c_str(), static_cast<uint8_t>(dirEntry.entryType)); + } + + auto finalDirEntryResult = directory.getNextEntry(); + EXPECT_TRUE(finalDirEntryResult.isOkay()); + EXPECT_EQ(finalDirEntryResult.getOkay(), std::nullopt); + + // ASSERT_EQ(removeRecursive(testDirName), FudStatus::Success); +} + +} // namespace fud |