// SPDX-License-Identifier: MIT #pragma once #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #else #include #endif #include #include namespace FHU::Filesystem { /** * @brief Check if a filepath exists. * * @param Path The path to check for. * * @return True if the file exists, False if it doesn't. */ inline bool Exists(const char *Path) { return access(Path, F_OK) == 0; } inline bool Exists(const fextl::string &Path) { return access(Path.c_str(), F_OK) == 0; } #ifndef _WIN32 inline bool ExistsAt(int FD, const fextl::string &Path) { return faccessat(FD, Path.c_str(), F_OK, 0) == 0; } #endif enum class CreateDirectoryResult { CREATED, EXISTS, ERROR, }; /** * @brief Creates a directory at the provided path. * * @param Path The path to create a directory at. * * @return Result enum depending. */ #ifndef _WIN32 inline CreateDirectoryResult CreateDirectory(const fextl::string &Path) { auto Result = ::mkdir(Path.c_str(), 0777); if (Result == 0) { return CreateDirectoryResult::CREATED; } if (Result == -1 && errno == EEXIST) { // If it exists, we need to check if it is a file or folder. struct stat buf; if (stat(Path.c_str(), &buf) == 0) { // Check to see if the path is a file or folder. Following symlinks. return S_ISDIR(buf.st_mode) ? CreateDirectoryResult::EXISTS : CreateDirectoryResult::ERROR; } } // Couldn't create, or the path that existed wasn't a folder. return CreateDirectoryResult::ERROR; } #else inline CreateDirectoryResult CreateDirectory(const fextl::string &Path) { std::error_code ec; if (std::filesystem::exists(Path, ec)) { return CreateDirectoryResult::EXISTS; } return std::filesystem::create_directory(Path, ec) ? CreateDirectoryResult::CREATED : CreateDirectoryResult::ERROR; } #endif /** * @brief Creates a directory tree with the provided path. * * @param Path The path to create a tree at. * * @return True if the directory tree was created or already exists. */ inline bool CreateDirectories(const fextl::string &Path) { // Try to create the directory initially. if (CreateDirectory(Path) != CreateDirectoryResult::ERROR) { return true; } // Walk the path in reverse and create paths as we go. fextl::string TmpPath {Path.substr(0, Path.rfind('/', Path.size() - 1))}; if (!TmpPath.empty() && CreateDirectories(TmpPath)) { return CreateDirectory(Path) != CreateDirectoryResult::ERROR; } return false; } /** * @brief Extracts the filename component from a file path. * * @param Path The path to create a directory at. * * @return The filename component of the path. */ inline fextl::string GetFilename(const fextl::string &Path) { auto LastSeparator = Path.rfind('/'); if (LastSeparator == fextl::string::npos) { // No separator. Likely relative `.`, `..`, ``, or empty string. return Path; } return Path.substr(LastSeparator + 1); } inline fextl::string ParentPath(const fextl::string &Path) { auto LastSeparator = Path.rfind('/'); if (LastSeparator == fextl::string::npos) { // No separator. Likely relative `.`, `..`, ``, or empty string. if (Path == "." || Path == "..") { // In this edge-case, return nothing to match std::filesystem::path::parent_path behaviour. return {}; } return Path; } if (LastSeparator == 0) { // In the case of root, just return. return "/"; } auto SubString = Path.substr(0, LastSeparator); while (SubString.size() > 1 && SubString.ends_with("/")) { // If the substring still ended with `/` then we need to string that off as well. --LastSeparator; SubString = Path.substr(0, LastSeparator); } return SubString; } inline bool IsRelative(const std::string_view Path) { return !Path.starts_with('/'); } inline bool IsAbsolute(const std::string_view Path) { return Path.starts_with('/'); } enum class CopyOptions { NONE, SKIP_EXISTING, OVERWRITE_EXISTING, }; /** * @brief Copy a file from a location to another * * Behaves similarly to std::filesystem::copy_file but with less copy options. * * @param From Source file location. * @param To Destination file location. * @param Options Copy options. * * @return True if the copy succeeded, false otherwise. */ #ifndef _WIN32 inline bool CopyFile(const fextl::string &From, const fextl::string &To, CopyOptions Options = CopyOptions::NONE) { const bool DestExists = Exists(To); if (Options == CopyOptions::SKIP_EXISTING && DestExists) { // If the destination file exists already and the skip existing flag is set then // return true without error. return true; } if (Options == CopyOptions::OVERWRITE_EXISTING && DestExists) { // If we are overwriting and the file exists then we want to use `sendfile` to overwrite int SourceFD = open(From.c_str(), O_RDONLY | O_CLOEXEC); if (SourceFD == -1) { return false; } int DestinationFD = open(To.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0200); if (DestinationFD == -1) { close(SourceFD); return false; } struct stat buf; if (fstat(SourceFD, &buf) != 0) { close(DestinationFD); close(SourceFD); return false; } // Set the destination permissions to the original source permissions. if (fchmod(DestinationFD, buf.st_mode) != 0) { close(DestinationFD); close(SourceFD); return false; } bool Result = sendfile(DestinationFD, SourceFD, nullptr, buf.st_size) == buf.st_size; close(DestinationFD); close(SourceFD); return Result; } if (!DestExists) { // If the destination doesn't exist then just use rename. return rename(From.c_str(), To.c_str()) == 0; } return false; } #else inline bool CopyFile(const fextl::string &From, const fextl::string &To, CopyOptions Options = CopyOptions::NONE) { std::filesystem::copy_options options{}; if (Options == CopyOptions::SKIP_EXISTING) { options = std::filesystem::copy_options::skip_existing; } else if (Options == CopyOptions::OVERWRITE_EXISTING) { options = std::filesystem::copy_options::overwrite_existing; } std::error_code ec; return std::filesystem::copy_file(From, To, options, ec); } #endif /** * @brief Renames a file and overwrites if it already exists. * * @return No error on rename. */ [[nodiscard]] inline std::error_code RenameFile(const fextl::string &From, const fextl::string &To) { return rename(From.c_str(), To.c_str()) == 0 ? std::error_code{} : std::make_error_code(std::errc::io_error); } inline fextl::string LexicallyNormal(const fextl::string &Path) { const auto PathSize = Path.size(); // Early exit on empty paths. if (PathSize == 0) { return {}; } const auto IsAbsolutePath = IsAbsolute(Path); const auto EndsWithSeparator = Path.ends_with('/'); // Count the number of separators up front const auto SeparatorCount = std::count(Path.begin(), Path.end(), '/'); // Use std::list to store path elements to avoid iterator invalidation on insert/erase. // The list is allocated on stack to be more optimal. The size is determined by the // maximum number of list objects (separator count plus 2) multiplied by the list // element size (32-bytes per element: the string_view itself and the prev/next pointers). size_t DataSize = (sizeof(std::string_view) + sizeof(void*) * 2) * (SeparatorCount + 2); void *Data = alloca(DataSize); fextl::pmr::fixed_size_monotonic_buffer_resource mbr(Data, DataSize); std::pmr::polymorphic_allocator pa {&mbr}; std::pmr::list Parts{pa}; size_t CurrentOffset{}; do { auto FoundSeperator = Path.find('/', CurrentOffset); if (FoundSeperator == Path.npos) { FoundSeperator = PathSize; } const auto Begin = Path.begin() + CurrentOffset; const auto End = Path.begin() + FoundSeperator; const auto Size = End - Begin; // Only insert parts that contain data. if (Size != 0) { Parts.emplace_back(std::string_view(Begin, End)); } if (Size == 0) { // If the view is empty, skip over the separator. FoundSeperator += 1; } CurrentOffset = FoundSeperator; } while (CurrentOffset != PathSize); size_t CurrentIterDistance{}; for (auto iter = Parts.begin(); iter != Parts.end();) { auto &Part = *iter; if (Part == ".") { // Erase '.' directory parts if not at root. if (CurrentIterDistance > 0 || IsAbsolutePath) { // Erasing this iterator, don't increase iter distances iter = Parts.erase(iter); continue; } } if (Part == "..") { if (CurrentIterDistance > 0) { // If not at root then remove both this iterator and the previous one. // ONLY if the previous iterator is also not ".." // // If the previous iterator is '.' then /only/ erase the previous iterator. auto PreviousIter = iter; --PreviousIter; if (*PreviousIter == ".") { // Erasing the previous iterator, iterator distance has subtracted by one --CurrentIterDistance; Parts.erase(PreviousIter); } else if (*PreviousIter != "..") { // Erasing the previous iterator, iterator distance has subtracted by one // Also erasing current iterator, which means iterator distance also doesn't increase by one. --CurrentIterDistance; Parts.erase(PreviousIter); iter = Parts.erase(iter); continue; } } else if (IsAbsolutePath) { // `..` at the base. Just remove this iter = Parts.erase(iter); continue; } } // Interator distance increased by one. ++CurrentIterDistance; ++iter; } // Add a final separator unless the last element is ellipses. const bool NeedsFinalSeparator = EndsWithSeparator && (!Parts.empty() && Parts.back() != "." && Parts.back() != ".."); return fextl::fmt::format("{}{}{}", IsAbsolutePath ? "/" : "", fmt::join(Parts, "/"), NeedsFinalSeparator ? "/" : ""); } #ifndef _WIN32 inline char *Absolute(const char *Path, char Fill[PATH_MAX]) { return realpath(Path, Fill); } #else inline char *Absolute(const char *Path, char Fill[PATH_MAX]) { std::error_code ec; const auto PathAbsolute = std::filesystem::absolute(Path, ec); if (!ec) { strncpy(Fill, PathAbsolute.string().c_str(), sizeof(*Fill)); return Fill; } return nullptr; } #endif }