(original) (raw)
//---------------------------------------------------------------------------------------------------------------------- // SaneCppFileSystem.h - Sane C++ FileSystem Library (single file build) //---------------------------------------------------------------------------------------------------------------------- // Dependencies: SaneCppFoundation.h // Version: release/2025/11 (cf7313e5) // LOC header: 117 (code) + 232 (comments) // LOC implementation: 1408 (code) + 230 (comments) // Documentation: https://pagghiu.github.io/SaneCppLibraries // Source Code: https://github.com/pagghiu/SaneCppLibraries //---------------------------------------------------------------------------------------------------------------------- // All copyrights and SPDX information for this library (each amalgamated section has its own copyright attributions): // Copyright (c) Stefano Cristiano // SPDX-License-Identifier: MIT //---------------------------------------------------------------------------------------------------------------------- #include "SaneCppFoundation.h" #if !defined(SANE_CPP_FILESYSTEM_HEADER) #define SANE_CPP_FILESYSTEM_HEADER 1 //---------------------------------------------------------------------------------------------------------------------- // FileSystem/FileSystem.h //---------------------------------------------------------------------------------------------------------------------- // Copyright (c) Stefano Cristiano // SPDX-License-Identifier: MIT namespace SC { //! @defgroup group_file_system FileSystem //! @copybrief library_file_system (see @ref library_file_system for more details) //! @addtogroup group_file_system //! @{ /// @brief A structure to describe file stats struct FileSystemStat { size_t fileSize = 0; ///< Size of the file in bytes TimeMs modifiedTime; ///< Time when file was last modified }; /// @brief A structure to describe copy flags struct FileSystemCopyFlags { FileSystemCopyFlags() { overwrite = false; useCloneIfSupported = true; } /// @brief If `true` copy will overwrite existing files in the destination /// @param value `true` if to overwrite /// @return itself FileSystemCopyFlags& setOverwrite(bool value) { overwrite = value; return *this; } /// @brief If `true` copy will use native filesystem clone os api /// @param value `true` if using clone is wanted /// @return itself FileSystemCopyFlags& setUseCloneIfSupported(bool value) { useCloneIfSupported = value; return *this; } bool overwrite; ///< If `true` copy will overwrite existing files in the destination bool useCloneIfSupported; ///< If `true` copy will use native filesystem clone os api }; /// @brief Execute fs operations { exists, copy, delete } for { files and directories }. /// It will scope all operations on relative paths to the `initialWorkingDirectory` passed in SC::FileSystem::init. /// All methods can always return failure due to access or disk I/O errors, and they will be omitted in the return /// clauses for each method. Only the specific returned result behaviour of the given method will be described. /// /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp FileSystemQuickSheetSnippet struct SC_COMPILER_EXPORT FileSystem { bool preciseErrorMessages = false; ///< Formats errors in an internal buffer when returning failed Result /// @brief Call init function when instantiating the class to set directory for all operations using relative paths. /// @param initialDirectory The wanted directory /// @return Valid Result if `initialDirectory` exists and it's accessible Result init(StringSpan initialDirectory); /// @brief Changes current directory. All operations with relative paths will be relative to this directory. /// @param newDirectory The wanted directory /// @return Valid Result if `initialWorkingDirectory` exists and it's accessible Result changeDirectory(StringSpan newDirectory); /// @brief Specify copy options like overwriting existing files using CopyFlags = FileSystemCopyFlags; /// @brief Specify source, destination and flags for a copy operation struct CopyOperation { StringSpan source; ///< Copy operation source (can be a {relative | absolute} {file | directory} path) StringSpan destination; ///< Copy operation sink (can be a {relative | absolute} {file | directory} path) CopyFlags copyFlags; ///< Copy operation flags (overwrite, use clone api etc.) }; /// @brief Copies many files /// @param sourceDestination View over a sequence of CopyOperation describing copies to be done /// @return Valid Result if all copies succeeded /// /// Example: /// @see copyFile (for an usage example) Result copyFiles(Span sourceDestination); /// @brief Copy a single file /// @param source Source file path /// @param destination Destination file path /// @param copyFlags Copy flags (overwrite, use clone api etc.) /// @return Valid Result if copy succeeded /// /// Example: /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp copyExistsFileSnippet Result copyFile(StringSpan source, StringSpan destination, CopyFlags copyFlags = CopyFlags()) { return copyFiles(CopyOperation{source, destination, copyFlags}); } /// @brief Copy many directories /// @param sourceDestination View over a sequence of CopyOperation describing copies to be done /// @return Valid Result if all copies succeeded /// @see copyDirectory (for an usage example) Result copyDirectories(Span sourceDestination); /// @brief Copy a single directory /// @param source Source directory path /// @param destination Destination directory path /// @param copyFlags Copy flags (overwrite, use clone api etc.) /// @return Valid Result if copy succeeded /// /// Example: /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp copyDirectoryRecursiveSnippet Result copyDirectory(StringSpan source, StringSpan destination, CopyFlags copyFlags = CopyFlags()) { return copyDirectories(CopyOperation{source, destination, copyFlags}); } /// @brief Rename a file or directory /// @param path The path to the file or directory to rename /// @param newPath The new path to the file or directory /// @return Valid Result if the file or directory was renamed /// /// Example: /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp renameFileSnippet /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp renameDirectorySnippet Result rename(StringSpan path, StringSpan newPath); /// @brief Remove multiple files /// @param files View over a list of paths /// @return Valid Result if file was removed Result removeFiles(Span files); /// @brief Remove a single file /// @param source A single file path to be removed /// @return Valid Result if file was existing and it has been removed successfully /// @see write (for an usage example) Result removeFile(StringSpan source) { return removeFiles({source}); } /// @brief Remove a single file, giving no error if it doesn't exist /// @param source The file to be removed if it exists /// @return Valid Result if the file doesn't exist or if it exists and it has been successfully removed. Result removeFileIfExists(StringSpan source); /// @brief Remove a single link, giving no error if it doesn't exist /// @param source The link to be removed if it exists /// @return Valid Result if the file doesn't exist or if it exists and it has been successfully removed. Result removeLinkIfExists(StringSpan source); /// @brief Remove multiple directories with their entire content (like posix `rm -rf`) /// @param directories List of directories to remove /// @return Valid Result if all directories and their contents have been successfully removed /// @see removeDirectoryRecursive (for an usage example) Result removeDirectoriesRecursive(Span directories); /// @brief Remove single directory with its entire content (like posix `rm -rf`) /// @param directory Directory to remove /// @return Valid Result if directory contents has been successfully deleted /// /// Example: /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp removeDirectoryRecursiveSnippet Result removeDirectoryRecursive(StringSpan directory) { return removeDirectoriesRecursive({directory}); } /// @brief Removes multiple empty directories /// @param directories List of empty directories to remove /// @return Invalid Result if one of the directories doesn't exist or it's not empty /// @see makeDirectoryRecursive (for an usage example) Result removeEmptyDirectories(Span directories); /// @brief Removes an empty directory /// @param directory Empty directory to remove /// @return Invalid Result if the directory doesn't exist or it's not empty /// @see makeDirectoryRecursive (for an usage example) Result removeEmptyDirectory(StringSpan directory) { return removeEmptyDirectories({directory}); } /// @brief Creates new directories that do not already exist /// @param directories List of paths where to create such directories /// @return Invalid Results if directories already exist /// @see makeDirectoryRecursive (for an usage example) Result makeDirectories(Span directories); /// @brief Creates a new directory that does not already exist /// @param directory Path where the directory should be created /// @return Invalid Results if directory already exist /// @see makeDirectoryRecursive (for an usage example) Result makeDirectory(StringSpan directory) { return makeDirectories({directory}); } /// @brief Creates new directories, if they don't already exist at the given path /// @param directories List of paths where to create such directories /// @return Invalid Results in case of I/O or access error Result makeDirectoriesIfNotExists(Span directories); /// @brief Creates a new directory, if it doesn't already exists at the given path /// @param directory Path where to create the new directory /// @return Invalid Results in case of I/O or access error Result makeDirectoryIfNotExists(StringSpan directory) { return makeDirectoriesIfNotExists({directory}); } /// @brief Create new directories, creating also intermediate non existing directories (like posix `mkdir -p`) /// @param directories List of paths where to create such directories /// @return Invalid Result in case of I/O or access error /// @see makeDirectoryRecursive (for an usage example) Result makeDirectoriesRecursive(Span directories); /// @brief Create a new directory, creating also intermediate non existing directories (like posix `mkdir -p`) /// @param directory Path where to create such directory /// @return Invalid Result in case of I/O or access error /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp makeDirectoryRecursive Result makeDirectoryRecursive(StringSpan directory) { return makeDirectoriesRecursive({directory}); } /// @brief Creates a symbolic link at location linkFile pointing at sourceFileOrDirectory /// @param sourceFileOrDirectory The target of the link (can be a folder or directory) /// @param linkFile The location where the symbolic link will be created /// @return Invalid result if it's not possible creating the requested symbolic link Result createSymbolicLink(StringSpan sourceFileOrDirectory, StringSpan linkFile); /// @brief Check if a file or directory exists at a given path /// @param fileOrDirectory Path to check /// @return `true` if a file or directory exists at the given path /// @see existsAndIsFile (for an usage example) [[nodiscard]] bool exists(StringSpan fileOrDirectory); /// @brief Check if a directory exists at given path /// @param directory Directory path to check /// @return `true` if a directory exists at the given path [[nodiscard]] bool existsAndIsDirectory(StringSpan directory); /// @brief Check if a file exists at given path /// @param file File path to check /// @return `true` if a file exists at the given path /// /// Example: /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp copyExistsFileSnippet [[nodiscard]] bool existsAndIsFile(StringSpan file); /// @brief Check if a link exists at given path /// @param file Link path to check /// @return `true` if a file exists at the given path [[nodiscard]] bool existsAndIsLink(StringSpan file); /// @brief Moves a directory from source to destination /// @param sourceDirectory The source directory that will be moved to destination /// @param destinationDirectory The destination directory /// @return `true` if the move succeeded [[nodiscard]] bool moveDirectory(StringSpan sourceDirectory, StringSpan destinationDirectory); /// @brief Writes a block of memory to a file /// @param file Path to the file that is meant to be written /// @param data Block of memory to write /// @return Valid Result if the memory was successfully written /// /// Example: /// \snippet Tests/Libraries/FileSystem/FileSystemTest.cpp writeReadRemoveFileSnippet Result write(StringSpan file, Span data); Result write(StringSpan file, Span data); /// @brief Replace the entire content of a file with the provided StringSpan /// @param file Path to the file that is meant to be written /// @param text Text to be written /// @return Valid Result if the memory was successfully written /// @see write (for an usage example) Result writeString(StringSpan file, StringSpan text); /// @brief Appends a StringSpan to a file /// @param file Path to the file that is meant to be appended /// @param text Text to be appended /// @return Valid Result if the memory was successfully appended Result writeStringAppend(StringSpan file, StringSpan text); /// @brief Read contents of a file into a String or Buffer /// @param[in] file Path to the file to read /// @param[out] data Destination String or Buffer that will receive file contents /// @return Valid Result if the entire file has been read successfully /// @see write (for an usage example) template Result read(StringSpan file, T& data) { return read(file, GrowableBuffer{data}); } /// @brief Read contents of a file into an IGrowableBuffer /// @param[in] file Path to the file to read /// @param[out] buffer Destination IGrowableBuffer that will receive file contents /// @return Valid Result if the entire file has been read successfully Result read(StringSpan file, IGrowableBuffer&& buffer); /// @brief A structure to describe modified time using FileStat = FileSystemStat; /// @brief Obtains stats (size, modified time) about a file /// @param file Path to the file of interest /// @param[out] fileStat Destination structure that will receive statistics about the file /// @return Valid Result if file stats for the given file was successfully read Result getFileStat(StringSpan file, FileStat& fileStat); /// @brief Change last modified time of a given file /// @param file Path to the file of interest /// @param time The new last modified time, as specified in the AbsoluteTime struct /// @return Valid Result if file time for the given file was successfully set Result setLastModifiedTime(StringSpan file, TimeMs time); /// @brief Low level filesystem API, requiring paths in native encoding (UTF-16 on Windows, UTF-8 elsewhere) /// @see SC::FileSystem is the higher level API that also handles paths in a different encoding is needed struct SC_COMPILER_EXPORT Operations { static Result createSymbolicLink(StringSpan sourceFileOrDirectory, StringSpan linkFile); static Result makeDirectory(StringSpan dir); static Result exists(StringSpan path); static Result existsAndIsDirectory(StringSpan path); static Result existsAndIsFile(StringSpan path); static Result existsAndIsLink(StringSpan path); static Result makeDirectoryRecursive(StringSpan path); static Result removeEmptyDirectory(StringSpan path); static Result moveDirectory(StringSpan source, StringSpan destination); static Result removeFile(StringSpan path); static Result copyFile(StringSpan srcPath, StringSpan destPath, FileSystemCopyFlags flags); static Result rename(StringSpan path, StringSpan newPath); static Result copyDirectory(StringSpan srcPath, StringSpan destPath, FileSystemCopyFlags flags); static Result removeDirectoryRecursive(StringSpan directory); static Result getFileStat(StringSpan path, FileSystemStat& fileStat); static Result setLastModifiedTime(StringSpan path, TimeMs time); static StringSpan getExecutablePath(StringPath& executablePath); static StringSpan getCurrentWorkingDirectory(StringPath& currentWorkingDirectory); static StringSpan getApplicationRootDirectory(StringPath& applicationRootDirectory); private: struct Internal; }; private: [[nodiscard]] bool convert(const StringSpan file, StringPath& destination, StringSpan* encodedPath = nullptr); StringPath fileFormatBuffer1; StringPath fileFormatBuffer2; StringPath currentDirectory; char errorMessageBuffer[256] = {0}; Result formatError(int errorNumber, StringSpan item, bool isWindowsNativeError); struct Internal; }; //! @} } // namespace SC #endif // SANE_CPP_FILESYSTEM_HEADER #if defined(SANE_CPP_IMPLEMENTATION) && !defined(SANE_CPP_FILESYSTEM_IMPLEMENTATION) #define SANE_CPP_FILESYSTEM_IMPLEMENTATION 1 //---------------------------------------------------------------------------------------------------------------------- // FileSystem/FileSystem.cpp //---------------------------------------------------------------------------------------------------------------------- // Copyright (c) Stefano Cristiano // SPDX-License-Identifier: MIT #if _WIN32 #include #include #define WIN32_LEAN_AND_MEAN #include namespace SC { static constexpr const SC::Result getErrorCode(int errorCode) { switch (errorCode) { case EACCES: return Result::Error("EACCES"); #if !SC_PLATFORM_WINDOWS case EDQUOT: return Result::Error("EDQUOT"); #endif case EEXIST: return Result::Error("EEXIST"); case EFAULT: return Result::Error("EFAULT"); case EIO: return Result::Error("EIO"); case ELOOP: return Result::Error("ELOOP"); case EMLINK: return Result::Error("EMLINK"); case ENAMETOOLONG: return Result::Error("ENAMETOOLONG"); case ENOENT: return Result::Error("ENOENT"); case ENOSPC: return Result::Error("ENOSPC"); case ENOTDIR: return Result::Error("ENOTDIR"); case EROFS: return Result::Error("EROFS"); case EBADF: return Result::Error("EBADF"); case EPERM: return Result::Error("EPERM"); case ENOMEM: return Result::Error("ENOMEM"); case ENOTSUP: return Result::Error("ENOTSUP"); case EINVAL: return Result::Error("EINVAL"); } return Result::Error("Unknown"); } } // namespace SC struct SC::FileSystem::Internal { static Result formatWindowsError(int errorNumber, Span buffer) { LPWSTR messageBuffer = nullptr; size_t size = ::FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorNumber, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast(&messageBuffer), 0, NULL); auto deferFree = MakeDeferred([&]() { LocalFree(messageBuffer); }); // TODO: Write buffer from messageBuffer converting from UTF-16 to UTF-8 if (size == 0) { return Result::Error("SC::FileSystem::Internal::formatWindowsError - Cannot format error"); } int res = ::WideCharToMultiByte(CP_UTF8, 0, messageBuffer, static_cast(size), buffer.data(), static_cast(buffer.sizeInBytes()), nullptr, nullptr); return Result(res > 0); } static bool formatError(int errorNumber, Span buffer) { wchar_t messageBuffer[1024]; const int err = ::_wcserror_s(messageBuffer, sizeof(messageBuffer) / sizeof(wchar_t), errorNumber); if (err == 0) { const int messageLength = static_cast(::wcsnlen_s(messageBuffer, 1024)); const int res = ::WideCharToMultiByte(CP_UTF8, 0, messageBuffer, messageLength, buffer.data(), static_cast(buffer.sizeInBytes()), nullptr, nullptr); return Result(res > 0); } return false; } }; #else #include // errno #include // open, O_* #include // strerror_r #include // stat, fstat #include // write, close, read namespace SC { // This is shared with FileSystemIterator Result getErrorCode(int errorCode) { switch (errorCode) { case EACCES: return Result::Error("EACCES"); case EDQUOT: return Result::Error("EDQUOT"); case EEXIST: return Result::Error("EEXIST"); case EFAULT: return Result::Error("EFAULT"); case EIO: return Result::Error("EIO"); case ELOOP: return Result::Error("ELOOP"); case EMLINK: return Result::Error("EMLINK"); case ENAMETOOLONG: return Result::Error("ENAMETOOLONG"); case ENOENT: return Result::Error("ENOENT"); case ENOSPC: return Result::Error("ENOSPC"); case ENOTDIR: return Result::Error("ENOTDIR"); case EROFS: return Result::Error("EROFS"); case EBADF: return Result::Error("EBADF"); case EPERM: return Result::Error("EPERM"); case ENOMEM: return Result::Error("ENOMEM"); case ENOTSUP: return Result::Error("ENOTSUP"); case EINVAL: return Result::Error("EINVAL"); } return Result::Error("Unknown"); } } // namespace SC struct SC::FileSystem::Internal { static bool formatError(int errorNumber, Span buffer) { #if SC_PLATFORM_APPLE const int res = ::strerror_r(errorNumber, buffer.data(), buffer.sizeInBytes()); return res == 0; #else char* res = ::strerror_r(errorNumber, buffer.data(), buffer.sizeInBytes()); return res != buffer.data(); #endif } }; #endif SC::Result SC::FileSystem::init(StringSpan currentWorkingDirectory) { return changeDirectory(currentWorkingDirectory); } SC::Result SC::FileSystem::changeDirectory(StringSpan currentWorkingDirectory) { SC_TRY_MSG(currentDirectory.assign(currentWorkingDirectory), "FileSystem::changeDirectory - Cannot assign working directory"); // TODO: Assert if path is not absolute return Result(existsAndIsDirectory(".")); } bool SC::FileSystem::convert(const StringSpan file, StringPath& destination, StringSpan* encodedPath) { SC_TRY(destination.assign(file)); if (encodedPath) { *encodedPath = destination.view(); } auto destinationBuffer = destination.writableSpan().data(); #if SC_PLATFORM_WINDOWS const bool absolute = (destination.view().sizeInBytes() >= 4 and destinationBuffer[0] == L'\\' and destinationBuffer[1] == L'\\') or (destination.view().sizeInBytes() >= 4 and destinationBuffer[1] == L':'); #else const bool absolute = not destination.view().isEmpty() and destinationBuffer[0] == '/'; #endif if (absolute) { if (encodedPath != nullptr) { *encodedPath = destination.view(); } return true; } if (currentDirectory.view().isEmpty()) return false; StringPath relative = destination; destination = currentDirectory; #if SC_PLATFORM_WINDOWS const size_t destLen = destination.view().sizeInBytes() / sizeof(wchar_t); const size_t relLen = relative.view().sizeInBytes() / sizeof(wchar_t); destinationBuffer[destLen] = L'\\'; ::memcpy(destinationBuffer + destLen + 1, relative.view().bytesIncludingTerminator(), relLen * sizeof(wchar_t)); #else destinationBuffer[destination.view().sizeInBytes()] = '/'; ::memcpy(destinationBuffer + destination.view().sizeInBytes() + 1, relative.view().bytesWithoutTerminator(), relative.view().sizeInBytes()); #endif const size_t lastPos = (destination.view().sizeInBytes() + relative.view().sizeInBytes()) / sizeof(native_char_t) + 1; destinationBuffer[lastPos] = 0; (void)destination.resize(lastPos); if (encodedPath != nullptr) { *encodedPath = destination.view(); } return true; } #define SC_TRY_FORMAT_ERRNO(path, func) \ { \ if (not func) \ { \ return formatError(errno, path, false); \ } \ } #if SC_PLATFORM_WINDOWS #define SC_TRY_FORMAT_NATIVE(path, func) \ { \ auto tempRes = func; \ if (not tempRes) \ { \ if (TypeTraits::IsSame<decltype(tempres), result="">::value) \ { \ return Result(tempRes); \ } \ else \ { \ return formatError(GetLastError(), path, true); \ } \ } \ } #else #define SC_TRY_FORMAT_NATIVE(path, func) \ { \ auto tempRes = func; \ if (not tempRes) \ { \ if (SC::TypeTraits::IsSame<decltype(tempres), result="">::value) \ { \ return Result(tempRes); \ } \ else \ { \ return formatError(errno, path, false); \ } \ } \ } #endif SC::Result SC::FileSystem::write(StringSpan path, Span data) { StringSpan encodedPath; SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); #if SC_PLATFORM_WINDOWS HANDLE hFile = ::CreateFileW(encodedPath.getNullTerminatedNative(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { return formatError(GetLastError(), path, true); } DWORD bytesWritten; if (!::WriteFile(hFile, data.data(), static_cast(data.sizeInBytes()), &bytesWritten, nullptr)) { ::CloseHandle(hFile); return formatError(GetLastError(), path, true); } ::CloseHandle(hFile); if (bytesWritten != data.sizeInBytes()) { return Result::Error("Write incomplete"); } return Result(true); #else int fd = ::open(encodedPath.getNullTerminatedNative(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd == -1) { return formatError(errno, path, false); } ssize_t bytesWritten = ::write(fd, data.data(), data.sizeInBytes()); ::close(fd); if (bytesWritten == -1) { return formatError(errno, path, false); } if (static_cast(bytesWritten) != data.sizeInBytes()) { return Result::Error("Write incomplete"); } return Result(true); #endif } SC::Result SC::FileSystem::write(StringSpan path, Span data) { return write(path, {reinterpret_cast(data.data()), data.sizeInBytes()}); } SC::Result SC::FileSystem::writeString(StringSpan path, StringSpan text) { return write(path, text.toCharSpan()); } SC::Result SC::FileSystem::writeStringAppend(StringSpan path, StringSpan text) { StringSpan encodedPath; SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); #if SC_PLATFORM_WINDOWS HANDLE hFile = ::CreateFileW(encodedPath.getNullTerminatedNative(), FILE_APPEND_DATA, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { return formatError(GetLastError(), path, true); } DWORD bytesWritten; if (!::WriteFile(hFile, text.bytesWithoutTerminator(), static_cast(text.sizeInBytes()), &bytesWritten, nullptr)) { ::CloseHandle(hFile); return formatError(GetLastError(), path, true); } ::CloseHandle(hFile); if (bytesWritten != text.sizeInBytes()) { return Result::Error("Write incomplete"); } return Result(true); #else int fd = ::open(encodedPath.getNullTerminatedNative(), O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd == -1) { return formatError(errno, path, false); } ssize_t bytesWritten = ::write(fd, text.bytesWithoutTerminator(), text.sizeInBytes()); ::close(fd); if (bytesWritten == -1) { return formatError(errno, path, false); } if (static_cast(bytesWritten) != text.sizeInBytes()) { return Result::Error("Write incomplete"); } return Result(true); #endif } SC::Result SC::FileSystem::read(StringSpan path, IGrowableBuffer&& buffer) { StringSpan encodedPath; SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); #if SC_PLATFORM_WINDOWS HANDLE hFile = ::CreateFileW(encodedPath.getNullTerminatedNative(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { return formatError(GetLastError(), path, true); } auto deferClose = MakeDeferred([&]() { ::CloseHandle(hFile); }); // Get file size LARGE_INTEGER fileSize; if (!::GetFileSizeEx(hFile, &fileSize)) { return formatError(GetLastError(), path, true); } // Grow buffer to accommodate the file if (!buffer.resizeWithoutInitializing(static_cast(fileSize.QuadPart))) { return Result::Error("Failed to grow buffer"); } // Read the file DWORD bytesRead; if (!::ReadFile(hFile, buffer.data(), static_cast(fileSize.QuadPart), &bytesRead, nullptr)) { return formatError(GetLastError(), path, true); } if (bytesRead != static_cast(fileSize.QuadPart)) { return Result::Error("Read incomplete"); } return Result(true); #else int fd = ::open(encodedPath.getNullTerminatedNative(), O_RDONLY); if (fd == -1) { return formatError(errno, path, false); } auto deferClose = MakeDeferred([&]() { ::close(fd); }); // Get file size struct stat fileStat; if (::fstat(fd, &fileStat) == -1) { return formatError(errno, path, false); } // Grow buffer to accommodate the file if (!buffer.resizeWithoutInitializing(static_cast(fileStat.st_size))) { return Result::Error("Failed to grow buffer"); } // Read the file ssize_t bytesRead = ::read(fd, buffer.data(), static_cast(fileStat.st_size)); if (bytesRead == -1) { return formatError(errno, path, false); } if (static_cast(bytesRead) != static_cast(fileStat.st_size)) { return Result::Error("Read incomplete"); } return Result(true); #endif } SC::Result SC::FileSystem::formatError(int errorNumber, StringSpan item, bool isWindowsNativeError) { #if SC_PLATFORM_WINDOWS if (isWindowsNativeError) { if (not preciseErrorMessages) { return Result::Error("Windows Error"); } if (not Internal::formatWindowsError(errorNumber, errorMessageBuffer)) { return Result::Error("SC::FileSystem::formatError - Cannot format error"); } } else #endif { SC_COMPILER_UNUSED(isWindowsNativeError); if (not preciseErrorMessages) { return getErrorCode(errorNumber); } if (not Internal::formatError(errorNumber, errorMessageBuffer)) { return Result::Error("SC::FileSystem::formatError - Cannot format error"); } } (void)item; // TODO: Append item name return Result::FromStableCharPointer(errorMessageBuffer); } SC::Result SC::FileSystem::rename(StringSpan path, StringSpan newPath) { StringSpan encodedPath1, encodedPath2; SC_TRY(convert(path, fileFormatBuffer1, &encodedPath1)); SC_TRY(convert(newPath, fileFormatBuffer2, &encodedPath2)); return FileSystem::Operations::rename(encodedPath1, encodedPath2); } SC::Result SC::FileSystem::removeFiles(Span files) { StringSpan encodedPath; for (auto& path : files) { SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); SC_TRY_FORMAT_ERRNO(path, FileSystem::Operations::removeFile(encodedPath)); } return Result(true); } SC::Result SC::FileSystem::removeFileIfExists(StringSpan source) { if (existsAndIsFile(source)) return removeFiles(Span{source}); return Result(true); } SC::Result SC::FileSystem::removeLinkIfExists(StringSpan source) { if (existsAndIsLink(source)) return removeFiles(Span{source}); return Result(true); } SC::Result SC::FileSystem::removeDirectoriesRecursive(Span directories) { for (auto& path : directories) { SC_TRY(convert(path, fileFormatBuffer1)); // force write SC_TRY_FORMAT_ERRNO(path, FileSystem::Operations::removeDirectoryRecursive(fileFormatBuffer1.view())); } return Result(true); } SC::Result SC::FileSystem::copyFiles(Span sourceDestination) { if (currentDirectory.view().isEmpty()) return Result(false); StringSpan encodedPath1, encodedPath2; for (const CopyOperation& op : sourceDestination) { SC_TRY(convert(op.source, fileFormatBuffer1, &encodedPath1)); SC_TRY(convert(op.destination, fileFormatBuffer2, &encodedPath2)); SC_TRY_FORMAT_NATIVE(op.source, FileSystem::Operations::copyFile(encodedPath1, encodedPath2, op.copyFlags)); } return Result(true); } SC::Result SC::FileSystem::copyDirectories(Span sourceDestination) { if (currentDirectory.view().isEmpty()) return Result(false); for (const CopyOperation& op : sourceDestination) { SC_TRY(convert(op.source, fileFormatBuffer1)); // force write SC_TRY(convert(op.destination, fileFormatBuffer2)); // force write SC_TRY_FORMAT_NATIVE(op.source, FileSystem::Operations::copyDirectory(fileFormatBuffer1.view(), fileFormatBuffer2.view(), op.copyFlags)); } return Result(true); } SC::Result SC::FileSystem::removeEmptyDirectories(Span directories) { StringSpan encodedPath; for (StringSpan path : directories) { SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); SC_TRY_FORMAT_ERRNO(path, FileSystem::Operations::removeEmptyDirectory(encodedPath)); } return Result(true); } SC::Result SC::FileSystem::makeDirectories(Span directories) { StringSpan encodedPath; for (auto& path : directories) { SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); SC_TRY_FORMAT_ERRNO(path, FileSystem::Operations::makeDirectory(encodedPath)); } return Result(true); } SC::Result SC::FileSystem::makeDirectoriesRecursive(Span directories) { for (const auto& path : directories) { StringSpan encodedPath; SC_TRY(convert(path, fileFormatBuffer1, &encodedPath)); SC_TRY(FileSystem::Operations::makeDirectoryRecursive(encodedPath)); } return Result(true); } SC::Result SC::FileSystem::makeDirectoriesIfNotExists(Span directories) { for (const auto& path : directories) { if (not existsAndIsDirectory(path)) { SC_TRY(makeDirectory({path})); } } return Result(true); } SC::Result SC::FileSystem::createSymbolicLink(StringSpan sourceFileOrDirectory, StringSpan linkFile) { StringSpan sourceFileNative, linkFileNative; SC_TRY(convert(sourceFileOrDirectory, fileFormatBuffer1, &sourceFileNative)); SC_TRY(convert(linkFile, fileFormatBuffer2, &linkFileNative)); SC_TRY(FileSystem::Operations::createSymbolicLink(sourceFileNative, linkFileNative)); return Result(true); } bool SC::FileSystem::exists(StringSpan fileOrDirectory) { StringSpan encodedPath; SC_TRY(convert(fileOrDirectory, fileFormatBuffer1, &encodedPath)); return FileSystem::Operations::exists(encodedPath); } bool SC::FileSystem::existsAndIsDirectory(StringSpan directory) { StringSpan encodedPath; SC_TRY(convert(directory, fileFormatBuffer1, &encodedPath)); return FileSystem::Operations::existsAndIsDirectory(encodedPath); } bool SC::FileSystem::existsAndIsFile(StringSpan file) { StringSpan encodedPath; SC_TRY(convert(file, fileFormatBuffer1, &encodedPath)); return FileSystem::Operations::existsAndIsFile(encodedPath); } bool SC::FileSystem::existsAndIsLink(StringSpan file) { StringSpan encodedPath; SC_TRY(convert(file, fileFormatBuffer1, &encodedPath)); return FileSystem::Operations::existsAndIsLink(encodedPath); } bool SC::FileSystem::moveDirectory(StringSpan sourceDirectory, StringSpan destinationDirectory) { StringSpan encodedPath1; StringSpan encodedPath2; SC_TRY(convert(sourceDirectory, fileFormatBuffer1, &encodedPath1)); SC_TRY(convert(destinationDirectory, fileFormatBuffer2, &encodedPath2)); return FileSystem::Operations::moveDirectory(encodedPath1, encodedPath2); } SC::Result SC::FileSystem::getFileStat(StringSpan file, FileStat& fileStat) { StringSpan encodedPath; SC_TRY(convert(file, fileFormatBuffer1, &encodedPath)); SC_TRY(FileSystem::Operations::getFileStat(encodedPath, fileStat)); return Result(true); } SC::Result SC::FileSystem::setLastModifiedTime(StringSpan file, TimeMs time) { StringSpan encodedPath; SC_TRY(convert(file, fileFormatBuffer1, &encodedPath)); return FileSystem::Operations::setLastModifiedTime(encodedPath, time); } #undef SC_TRY_FORMAT_ERRNO #ifdef _WIN32 struct SC::FileSystem::Operations::Internal { static Result validatePath(StringSpan path) { if (path.sizeInBytes() == 0) return Result::Error("Path is empty"); if (path.getEncoding() != StringEncoding::Utf16) return Result::Error("Path is not native (UTF16)"); return Result(true); } static Result copyFile(StringSpan source, StringSpan destination, FileSystemCopyFlags options, bool isDirectory = false); static Result copyDirectoryRecursive(const wchar_t* source, const wchar_t* destination, FileSystemCopyFlags flags); static Result removeDirectoryRecursiveInternal(const wchar_t* path); }; #define SC_TRY_WIN32(func, msg) \ { \ if (func == FALSE) \ { \ return Result::Error(msg); \ } \ } SC::Result SC::FileSystem::Operations::createSymbolicLink(StringSpan sourceFileOrDirectory, StringSpan linkFile) { SC_TRY_MSG(Internal::validatePath(sourceFileOrDirectory), "createSymbolicLink: Invalid source path"); SC_TRY_MSG(Internal::validatePath(linkFile), "createSymbolicLink: Invalid link path"); DWORD dwFlags = existsAndIsDirectory(sourceFileOrDirectory) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0; dwFlags |= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; SC_TRY_WIN32(::CreateSymbolicLinkW(linkFile.getNullTerminatedNative(), sourceFileOrDirectory.getNullTerminatedNative(), dwFlags), "createSymbolicLink: Failed to create symbolic link"); return Result(true); } SC::Result SC::FileSystem::Operations::makeDirectory(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "makeDirectory: Invalid path"); SC_TRY_WIN32(::CreateDirectoryW(path.getNullTerminatedNative(), nullptr), "makeDirectory: Failed to create directory"); return Result(true); } SC::Result SC::FileSystem::Operations::makeDirectoryRecursive(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "makeDirectoryRecursive: Invalid path"); const size_t pathLength = path.sizeInBytes() / sizeof(wchar_t); if (pathLength < 2) return Result::Error("makeDirectoryRecursive: Path is empty"); wchar_t temp[MAX_PATH]; // Copy path to temp, ensure null-terminated if (pathLength >= MAX_PATH) return Result::Error("makeDirectoryRecursive: Path too long"); ::memcpy(temp, path.bytesWithoutTerminator(), pathLength * sizeof(wchar_t)); temp[pathLength] = 0; // Ensure null-termination // Skip \\\\ or drive letter if present size_t idx = 0; if (pathLength >= 3) { if (temp[0] == L'\\' and temp[1] == L'\\') { // Skip until next backslash or forward slash for (idx = 3; idx < pathLength; ++idx) { if (temp[idx] == L'\\' or temp[idx] == L'/') { idx += 1; // Skip the backslash or forward slash break; } } } else if (temp[1] == L':' and (temp[2] == L'\\' or temp[2] == L'/')) { idx = 3; // Skip drive letter and colon and backslash } } // Iterate and create directories for (; idx < pathLength; ++idx) { if (temp[idx] == L'\\' or temp[idx] == L'/') { if (idx == 0) continue; // Skip root wchar_t old = temp[idx]; temp[idx] = 0; if (temp[0] != 0) // skip empty { if (!::CreateDirectoryW(temp, nullptr)) { DWORD err = ::GetLastError(); if (err != ERROR_ALREADY_EXISTS) return Result::Error("makeDirectoryRecursive: Failed to create parent directory"); } } temp[idx] = old; } } // Create the final directory if (!::CreateDirectoryW(temp, nullptr)) { DWORD err = ::GetLastError(); if (err != ERROR_ALREADY_EXISTS) return Result::Error("makeDirectoryRecursive: Failed to create directory"); } return Result(true); } SC::Result SC::FileSystem::Operations::exists(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "exists: Invalid path"); const DWORD res = ::GetFileAttributesW(path.getNullTerminatedNative()); return Result(res != INVALID_FILE_ATTRIBUTES); } SC::Result SC::FileSystem::Operations::existsAndIsDirectory(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "existsAndIsDirectory: Invalid path"); const DWORD res = ::GetFileAttributesW(path.getNullTerminatedNative()); if (res == INVALID_FILE_ATTRIBUTES) return Result(false); return Result((res & FILE_ATTRIBUTE_DIRECTORY) != 0); } SC::Result SC::FileSystem::Operations::existsAndIsFile(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "existsAndIsFile: Invalid path"); const DWORD res = GetFileAttributesW(path.getNullTerminatedNative()); if (res == INVALID_FILE_ATTRIBUTES) return Result(false); return Result((res & FILE_ATTRIBUTE_DIRECTORY) == 0); } SC::Result SC::FileSystem::Operations::existsAndIsLink(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "existsAndIsLink: Invalid path"); const DWORD res = ::GetFileAttributesW(path.getNullTerminatedNative()); if (res == INVALID_FILE_ATTRIBUTES) return Result(false); return Result((res & FILE_ATTRIBUTE_REPARSE_POINT) != 0); } SC::Result SC::FileSystem::Operations::removeEmptyDirectory(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeEmptyDirectory: Invalid path"); SC_TRY_WIN32(::RemoveDirectoryW(path.getNullTerminatedNative()), "removeEmptyDirectory: Failed to remove directory"); return Result(true); } SC::Result SC::FileSystem::Operations::moveDirectory(StringSpan source, StringSpan destination) { SC_TRY_MSG(Internal::validatePath(source), "moveDirectory: Invalid source path"); SC_TRY_MSG(Internal::validatePath(destination), "moveDirectory: Invalid destination path"); SC_TRY_WIN32(::MoveFileExW(source.getNullTerminatedNative(), destination.getNullTerminatedNative(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED), "moveDirectory: Failed to move directory"); return Result(true); } SC::Result SC::FileSystem::Operations::removeFile(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeFile: Invalid path"); SC_TRY_WIN32(::DeleteFileW(path.getNullTerminatedNative()), "removeFile: Failed to remove file"); return Result(true); } SC::Result SC::FileSystem::Operations::getFileStat(StringSpan path, FileSystemStat& fileStat) { SC_TRY_MSG(Internal::validatePath(path), "getFileStat: Invalid path"); HANDLE hFile = ::CreateFileW(path.getNullTerminatedNative(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { return Result::Error("getFileStat: Failed to open file"); } auto deferClose = MakeDeferred([&]() { CloseHandle(hFile); }); FILETIME creationTime, lastAccessTime, modifiedTime; if (!::GetFileTime(hFile, &creationTime, &lastAccessTime, &modifiedTime)) { return Result::Error("getFileStat: Failed to get file times"); } ULARGE_INTEGER fileTimeValue; fileTimeValue.LowPart = modifiedTime.dwLowDateTime; fileTimeValue.HighPart = modifiedTime.dwHighDateTime; fileTimeValue.QuadPart -= 116444736000000000ULL; fileStat.modifiedTime = TimeMs{static_cast(fileTimeValue.QuadPart / 10000ULL)}; LARGE_INTEGER fileSize; if (!::GetFileSizeEx(hFile, &fileSize)) { return Result::Error("getFileStat: Failed to get file size"); } fileStat.fileSize = static_cast(fileSize.QuadPart); return Result(true); } SC::Result SC::FileSystem::Operations::setLastModifiedTime(StringSpan path, TimeMs time) { SC_TRY_MSG(Internal::validatePath(path), "setLastModifiedTime: Invalid path"); HANDLE hFile = ::CreateFileW(path.getNullTerminatedNative(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { return Result::Error("setLastModifiedTime: Failed to open file"); } auto deferClose = MakeDeferred([&]() { CloseHandle(hFile); }); FILETIME creationTime, lastAccessTime; if (!::GetFileTime(hFile, &creationTime, &lastAccessTime, nullptr)) { return Result::Error("setLastModifiedTime: Failed to get file times"); } FILETIME modifiedTime; ULARGE_INTEGER fileTimeValue; fileTimeValue.QuadPart = time.milliseconds * 10000ULL + 116444736000000000ULL; modifiedTime.dwLowDateTime = fileTimeValue.LowPart; modifiedTime.dwHighDateTime = fileTimeValue.HighPart; SC_TRY_WIN32(::SetFileTime(hFile, &creationTime, &lastAccessTime, &modifiedTime), "setLastModifiedTime: Failed to set file time"); return Result(true); } SC::Result SC::FileSystem::Operations::rename(StringSpan path, StringSpan newPath) { SC_TRY_MSG(Internal::validatePath(path), "rename: Invalid path"); SC_TRY_MSG(Internal::validatePath(newPath), "rename: Invalid new path"); SC_TRY_WIN32(::MoveFileW(path.getNullTerminatedNative(), newPath.getNullTerminatedNative()), "rename: Failed to rename"); return Result(true); } SC::Result SC::FileSystem::Operations::copyFile(StringSpan source, StringSpan destination, FileSystemCopyFlags flags) { SC_TRY_MSG(Internal::validatePath(source), "copyFile: Invalid source path"); SC_TRY_MSG(Internal::validatePath(destination), "copyFile: Invalid destination path"); DWORD copyFlags = COPY_FILE_FAIL_IF_EXISTS; if (flags.overwrite) copyFlags &= ~COPY_FILE_FAIL_IF_EXISTS; SC_TRY_WIN32(CopyFileExW(source.getNullTerminatedNative(), destination.getNullTerminatedNative(), nullptr, nullptr, nullptr, copyFlags), "copyFile: Failed to copy file"); return Result(true); } SC::Result SC::FileSystem::Operations::copyDirectory(StringSpan source, StringSpan destination, FileSystemCopyFlags flags) { SC_TRY_MSG(Internal::validatePath(source), "copyDirectory: Invalid source path"); SC_TRY_MSG(Internal::validatePath(destination), "copyDirectory: Invalid destination path"); if (flags.overwrite == false and existsAndIsDirectory(destination)) { return Result::Error("copyDirectory: Destination directory already exists"); } return Internal::copyDirectoryRecursive(source.getNullTerminatedNative(), destination.getNullTerminatedNative(), flags); } SC::Result SC::FileSystem::Operations::removeDirectoryRecursive(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeDirectoryRecursive: Invalid path"); return Internal::removeDirectoryRecursiveInternal(path.getNullTerminatedNative()); } SC::Result SC::FileSystem::Operations::Internal::copyDirectoryRecursive(const wchar_t* source, const wchar_t* destination, FileSystemCopyFlags flags) { // Create destination directory if it doesn't exist if (::CreateDirectoryW(destination, nullptr) == FALSE) { if (::GetLastError() != ERROR_ALREADY_EXISTS) { return Result::Error("copyDirectoryRecursive: Failed to create destination directory"); } } // Prepare search pattern wchar_t searchPattern[MAX_PATH]; if (::swprintf_s(searchPattern, MAX_PATH, L"%s\\*", source) == -1) { return Result::Error("copyDirectoryRecursive: Path too long"); } WIN32_FIND_DATAW findData; HANDLE hFind = ::FindFirstFileW(searchPattern, &findData); if (hFind == INVALID_HANDLE_VALUE) { return Result::Error("copyDirectoryRecursive: Failed to enumerate directory"); } auto deferClose = MakeDeferred([&]() { ::FindClose(hFind); }); do { // Skip . and .. entries if (::wcscmp(findData.cFileName, L".") == 0 || ::wcscmp(findData.cFileName, L"..") == 0) continue; // Build full paths wchar_t sourcePath[MAX_PATH]; wchar_t destPath[MAX_PATH]; if (::swprintf_s(sourcePath, MAX_PATH, L"%s\\%s", source, findData.cFileName) == -1 || ::swprintf_s(destPath, MAX_PATH, L"%s\\%s", destination, findData.cFileName) == -1) { return Result::Error("copyDirectoryRecursive: Path too long"); } if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // Recursively copy subdirectory SC_TRY(copyDirectoryRecursive(sourcePath, destPath, flags)); } else { // Copy file DWORD copyFlags = COPY_FILE_FAIL_IF_EXISTS; if (flags.overwrite) copyFlags &= ~COPY_FILE_FAIL_IF_EXISTS; if (::CopyFileExW(sourcePath, destPath, nullptr, nullptr, nullptr, copyFlags) == FALSE) { return Result::Error("copyDirectoryRecursive: Failed to copy file"); } } } while (::FindNextFileW(hFind, &findData)); if (::GetLastError() != ERROR_NO_MORE_FILES) { return Result::Error("copyDirectoryRecursive: Failed to enumerate directory"); } return Result(true); } SC::Result SC::FileSystem::Operations::Internal::removeDirectoryRecursiveInternal(const wchar_t* path) { // Prepare search pattern wchar_t searchPattern[MAX_PATH]; if (::swprintf_s(searchPattern, MAX_PATH, L"%s\\*", path) == -1) { return Result::Error("removeDirectoryRecursive: Path too long"); } WIN32_FIND_DATAW findData; HANDLE hFind = ::FindFirstFileW(searchPattern, &findData); if (hFind == INVALID_HANDLE_VALUE) { return Result::Error("removeDirectoryRecursive: Failed to enumerate directory"); } auto deferClose = MakeDeferred([&]() { ::FindClose(hFind); }); do { // Skip . and .. entries if (::wcscmp(findData.cFileName, L".") == 0 || ::wcscmp(findData.cFileName, L"..") == 0) continue; // Build full path wchar_t fullPath[MAX_PATH]; if (swprintf_s(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName) == -1) { return Result::Error("removeDirectoryRecursive: Path too long"); } if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // Recursively remove subdirectory SC_TRY(removeDirectoryRecursiveInternal(fullPath)); } else { // Remove file if (::DeleteFileW(fullPath) == FALSE) { return Result::Error("removeDirectoryRecursive: Failed to delete file"); } } } while (::FindNextFileW(hFind, &findData)); if (::GetLastError() != ERROR_NO_MORE_FILES) { return Result::Error("removeDirectoryRecursive: Failed to enumerate directory"); } // Remove the now-empty directory if (::RemoveDirectoryW(path) == FALSE) { return Result::Error("removeDirectoryRecursive: Failed to remove directory"); } return Result(true); } SC::StringSpan SC::FileSystem::Operations::getExecutablePath(StringPath& executablePath) { // Use GetModuleFileNameW to get the executable path in UTF-16 DWORD length = ::GetModuleFileNameW(nullptr, executablePath.writableSpan().data(), static_cast(StringPath::MaxPath)); if (length == 0 || length >= StringPath::MaxPath) { (void)executablePath.resize(0); return {}; } (void)executablePath.resize(length); // length does not include null terminator return executablePath.view(); } SC::StringSpan SC::FileSystem::Operations::getCurrentWorkingDirectory(StringPath& currentWorkingDirectory) { DWORD length = ::GetCurrentDirectoryW(static_cast(StringPath::MaxPath), currentWorkingDirectory.writableSpan().data()); if (length == 0 || length >= StringPath::MaxPath) { (void)currentWorkingDirectory.resize(0); return {}; } (void)currentWorkingDirectory.resize(length); // length does not include null terminator return currentWorkingDirectory.view(); } SC::StringSpan SC::FileSystem::Operations::getApplicationRootDirectory(StringPath& applicationRootDirectory) { StringSpan exeView = getExecutablePath(applicationRootDirectory); if (exeView.isEmpty()) return {}; // Find the last path separator (either '\\' or '/') ssize_t lastSeparator = -1; wchar_t* buffer = applicationRootDirectory.writableSpan().data(); const size_t bufferLength = applicationRootDirectory.view().sizeInBytes() / sizeof(wchar_t); for (size_t i = 0; i < bufferLength; ++i) { if (buffer[i] == L'\\' || buffer[i] == L'/') lastSeparator = static_cast(i); } if (lastSeparator < 0) { // No separator found, return empty (void)applicationRootDirectory.resize(0); ::memset(buffer, 0, StringPath::MaxPath * sizeof(wchar_t)); return {}; } const size_t copyLen = static_cast(lastSeparator); buffer[copyLen] = 0; (void)applicationRootDirectory.resize(copyLen); // null terminate the path ::memset(buffer + copyLen + 1, 0, (StringPath::MaxPath - copyLen - 1) * sizeof(wchar_t)); return applicationRootDirectory.view(); } #else #include // DIR, opendir, readdir, closedir #include // errno #include // AT_FDCWD #include // PATH_MAX #include // round #include // rename #include // strcmp #include // mkdir #include // rmdir #if __APPLE__ #include #include #include // NSGetExecutablePath #include #include #include #elif SC_PLATFORM_LINUX #include #include // SYS_getcwd #endif struct SC::FileSystem::Operations::Internal { static Result validatePath(StringSpan path) { if (path.sizeInBytes() == 0) return Result::Error("Path is empty"); if (path.getEncoding() == StringEncoding::Utf16) return Result::Error("Path is not native (UTF8)"); return Result(true); } static Result copyFile(StringSpan source, StringSpan destination, FileSystemCopyFlags options, bool isDirectory = false); }; #define SC_TRY_POSIX(func, msg) \ { \ \ if (func != 0) \ { \ return Result::Error(msg); \ } \ } SC::Result SC::FileSystem::Operations::createSymbolicLink(StringSpan sourceFileOrDirectory, StringSpan linkFile) { SC_TRY_MSG(Internal::validatePath(sourceFileOrDirectory), "createSymbolicLink: Invalid source file or directory path"); SC_TRY_MSG(Internal::validatePath(linkFile), "createSymbolicLink: Invalid link file path"); SC_TRY_POSIX(::symlink(sourceFileOrDirectory.getNullTerminatedNative(), linkFile.getNullTerminatedNative()), "createSymbolicLink: Failed to create symbolic link"); return Result(true); } SC::Result SC::FileSystem::Operations::makeDirectory(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "makeDirectory: Invalid path"); SC_TRY_POSIX(::mkdir(path.getNullTerminatedNative(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH), "makeDirectory: Failed to create directory"); return Result(true); } SC::Result SC::FileSystem::Operations::makeDirectoryRecursive(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "makeDirectoryRecursive: Invalid path"); const size_t pathLength = path.sizeInBytes(); char temp[PATH_MAX]; // Copy path to temp, ensure null-terminated if (pathLength >= PATH_MAX) return Result::Error("makeDirectoryRecursive: Path too long"); ::memcpy(temp, path.bytesWithoutTerminator(), pathLength); // Iterate and create directories for (size_t idx = 0; idx < pathLength; ++idx) { if (temp[idx] == '/' or temp[idx] == '\\') { if (idx == 0) continue; // Skip root char old = temp[idx]; temp[idx] = 0; if (temp[0] != 0) // skip empty { if (::mkdir(temp, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) { if (errno != EEXIST) return Result::Error("makeDirectoryRecursive: Failed to create parent directory"); } } temp[idx] = old; } } // Create the final directory if (::mkdir(path.getNullTerminatedNative(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) { if (errno != EEXIST) return Result::Error("makeDirectoryRecursive: Failed to create directory"); } return Result(true); } SC::Result SC::FileSystem::Operations::exists(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "exists: Invalid path"); struct stat path_stat; SC_TRY_POSIX(::stat(path.getNullTerminatedNative(), &path_stat), "exists: Failed to get file stats"); return Result(true); } SC::Result SC::FileSystem::Operations::existsAndIsDirectory(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "existsAndIsDirectory: Invalid path"); struct stat path_stat; SC_TRY_POSIX(::stat(path.getNullTerminatedNative(), &path_stat), "existsAndIsDirectory: Failed to get file stats"); return Result(S_ISDIR(path_stat.st_mode)); } SC::Result SC::FileSystem::Operations::existsAndIsFile(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "existsAndIsFile: Invalid path"); struct stat path_stat; SC_TRY_POSIX(::stat(path.getNullTerminatedNative(), &path_stat), "existsAndIsFile: Failed to get file stats"); return Result(S_ISREG(path_stat.st_mode)); } SC::Result SC::FileSystem::Operations::existsAndIsLink(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "existsAndIsLink: Invalid path"); struct stat path_stat; SC_TRY_POSIX(::stat(path.getNullTerminatedNative(), &path_stat), "existsAndIsLink: Failed to get file stats"); return Result(S_ISLNK(path_stat.st_mode)); } SC::Result SC::FileSystem::Operations::removeEmptyDirectory(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeEmptyDirectory: Invalid path"); SC_TRY_POSIX(::rmdir(path.getNullTerminatedNative()), "removeEmptyDirectory: Failed to remove directory"); return Result(true); } SC::Result SC::FileSystem::Operations::moveDirectory(StringSpan source, StringSpan destination) { SC_TRY_MSG(Internal::validatePath(source), "moveDirectory: Invalid source path"); SC_TRY_MSG(Internal::validatePath(destination), "moveDirectory: Invalid destination path"); SC_TRY_POSIX(::rename(source.getNullTerminatedNative(), destination.getNullTerminatedNative()), "moveDirectory: Failed to move directory"); return Result(true); } SC::Result SC::FileSystem::Operations::removeFile(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeFile: Invalid path"); SC_TRY_POSIX(::remove(path.getNullTerminatedNative()), "removeFile: Failed to remove file"); return Result(true); } SC::Result SC::FileSystem::Operations::getFileStat(StringSpan path, FileSystemStat& fileStat) { SC_TRY_MSG(Internal::validatePath(path), "getFileStat: Invalid path"); struct stat path_stat; SC_TRY_POSIX(::stat(path.getNullTerminatedNative(), &path_stat), "getFileStat: Failed to get file stats"); fileStat.fileSize = static_cast(path_stat.st_size); #if __APPLE__ auto ts = path_stat.st_mtimespec; #else auto ts = path_stat.st_mtim; #endif fileStat.modifiedTime = TimeMs{static_cast(::round(ts.tv_nsec / 1.0e6) + ts.tv_sec * 1000)}; return Result(true); } SC::Result SC::FileSystem::Operations::setLastModifiedTime(StringSpan path, TimeMs time) { SC_TRY_MSG(Internal::validatePath(path), "setLastModifiedTime: Invalid path"); struct timespec times[2]; times[0].tv_sec = time.milliseconds / 1000; times[0].tv_nsec = (time.milliseconds % 1000) * 1000 * 1000; times[1] = times[0]; SC_TRY_POSIX(::utimensat(AT_FDCWD, path.getNullTerminatedNative(), times, 0), "setLastModifiedTime: Failed to set last modified time"); return Result(true); } SC::Result SC::FileSystem::Operations::rename(StringSpan path, StringSpan newPath) { SC_TRY_MSG(Internal::validatePath(path), "rename: Invalid path"); SC_TRY_MSG(Internal::validatePath(newPath), "rename: Invalid new path"); SC_TRY_POSIX(::rename(path.getNullTerminatedNative(), newPath.getNullTerminatedNative()), "rename: Failed to rename"); return Result(true); } SC::Result SC::FileSystem::Operations::copyFile(StringSpan srcPath, StringSpan destPath, FileSystemCopyFlags flags) { SC_TRY_MSG(Internal::validatePath(srcPath), "copyFile: Invalid source path"); SC_TRY_MSG(Internal::validatePath(destPath), "copyFile: Invalid destination path"); return Result(Internal::copyFile(srcPath, destPath, flags, false)); } SC::Result SC::FileSystem::Operations::copyDirectory(StringSpan srcPath, StringSpan destPath, FileSystemCopyFlags flags) { SC_TRY_MSG(Internal::validatePath(srcPath), "copyDirectory: Invalid source path"); SC_TRY_MSG(Internal::validatePath(destPath), "copyDirectory: Invalid destination path"); return Result(Internal::copyFile(srcPath, destPath, flags, true)); } #if __APPLE__ SC::Result SC::FileSystem::Operations::Internal::copyFile(StringSpan source, StringSpan destination, FileSystemCopyFlags options, bool isDirectory) { const char* sourceFile = source.getNullTerminatedNative(); const char* destinationFile = destination.getNullTerminatedNative(); // Try clonefile and fallback to copyfile in case it fails with ENOTSUP or EXDEV // https://www.manpagez.com/man/2/clonefile/ // https://www.manpagez.com/man/3/copyfile/ if (options.useCloneIfSupported) { int cloneRes = ::clonefile(sourceFile, destinationFile, CLONE_NOFOLLOW | CLONE_NOOWNERCOPY); if (cloneRes != 0) { if ((errno == EEXIST) and options.overwrite) { // TODO: We should probably renaming instead of deleting...and eventually rollback on failure if (isDirectory) { auto removeState = ::removefile_state_alloc(); auto removeFree = MakeDeferred([&] { ::removefile_state_free(removeState); }); SC_TRY_POSIX(::removefile(destinationFile, removeState, REMOVEFILE_RECURSIVE), "copyFile: Failed to remove file (removeRes == 0)"); } else { SC_TRY_POSIX(::remove(destinationFile), "copyFile: Failed to remove file"); } cloneRes = ::clonefile(sourceFile, destinationFile, CLONE_NOFOLLOW | CLONE_NOOWNERCOPY); } } if (cloneRes == 0) { return Result(true); } else if (errno != ENOTSUP and errno != EXDEV) { // We only fallback in case of ENOTSUP and EXDEV (cross-device link) return Result::Error("copyFile: Failed to clone file (errno != ENOTSUP and errno != EXDEV)"); } } uint32_t flags = COPYFILE_ALL; // We should not use COPYFILE_CLONE_FORCE as clonefile just failed if (options.overwrite) { flags |= COPYFILE_UNLINK; } if (isDirectory) { flags |= COPYFILE_RECURSIVE; } // TODO: Should define flags to decide if to follow symlinks on source and destination auto copyState = ::copyfile_state_alloc(); auto copyFree = MakeDeferred([&] { ::copyfile_state_free(copyState); }); SC_TRY_POSIX(::copyfile(sourceFile, destinationFile, copyState, flags), "copyFile: Failed to copy file"); return Result(true); } SC::Result SC::FileSystem::Operations::removeDirectoryRecursive(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeDirectoryRecursive: Invalid path"); auto state = ::removefile_state_alloc(); auto stateFree = MakeDeferred([&] { ::removefile_state_free(state); }); SC_TRY_POSIX(::removefile(path.getNullTerminatedNative(), state, REMOVEFILE_RECURSIVE), "removeDirectoryRecursive: Failed to remove directory"); return Result(true); } #if SC_XCTEST #include "Path.h" // OPTIONAL DEPENDENCY #include #endif SC::StringSpan SC::FileSystem::Operations::getExecutablePath(StringPath& executablePath) { #if SC_XCTEST Dl_info dlinfo; int res = dladdr((void*)Memory::allocate, &dlinfo); if (res != 0 and executablePath.assign(StringSpan::fromNullTerminated(dlinfo.dli_fname, StringEncoding::Utf8))) { return executablePath.view(); } #else uint32_t executableLength = static_cast(StringPath::MaxPath); if (::_NSGetExecutablePath(executablePath.writableSpan().data(), &executableLength) == 0) { (void)executablePath.resize(::strlen(executablePath.view().bytesIncludingTerminator())); return executablePath.view(); } #endif return {}; } SC::StringSpan SC::FileSystem::Operations::getCurrentWorkingDirectory(StringPath& currentWorkingDirectory) { if (::getcwd(currentWorkingDirectory.writableSpan().data(), StringPath::MaxPath) != nullptr) { (void)currentWorkingDirectory.resize(::strlen(currentWorkingDirectory.view().bytesIncludingTerminator())); return currentWorkingDirectory.view(); } return {}; } SC::StringSpan SC::FileSystem::Operations::getApplicationRootDirectory(StringPath& applicationRootDirectory) { #if SC_XCTEST StringView appDir = getExecutablePath(applicationRootDirectory); if (not appDir.isEmpty()) { if (applicationRootDirectory.assign(Path::dirname(appDir, Path::AsNative, 3))) { return applicationRootDirectory.view(); } } #else CFBundleRef mainBundle = CFBundleGetMainBundle(); if (mainBundle != nullptr) { CFURLRef bundleURL = CFBundleCopyBundleURL(mainBundle); if (bundleURL != nullptr) { uint8_t* path = reinterpret_cast<uint8_t*>(applicationRootDirectory.writableSpan().data()); if (CFURLGetFileSystemRepresentation(bundleURL, true, path, StringPath::MaxPath)) { (void)applicationRootDirectory.resize(::strlen(reinterpret_cast<char*>(path))); CFRelease(bundleURL); return applicationRootDirectory.view(); } CFRelease(bundleURL); } } #endif return {}; } #else SC::Result SC::FileSystem::Operations::Internal::copyFile(StringSpan source, StringSpan destination, FileSystemCopyFlags options, bool isDirectory) { if (isDirectory) { // For directories, first check if source exists and is a directory SC_TRY_MSG(existsAndIsDirectory(source), "copyFile: Source path is not a directory"); // Create destination directory if it doesn't exist if (not existsAndIsDirectory(destination)) { SC_TRY(makeDirectory(destination)); } else if (not options.overwrite) { return Result::Error("copyFile: Destination directory already exists and overwrite is not enabled"); } // Open source directory DIR* dir = ::opendir(source.getNullTerminatedNative()); if (dir == nullptr) { return Result::Error("copyFile: Failed to open source directory"); } auto closeDir = MakeDeferred([&] { ::closedir(dir); }); // Buffer for full path construction char fullSourcePath[PATH_MAX]; char fullDestPath[PATH_MAX]; struct dirent* entry; // Iterate through directory entries while ((entry = ::readdir(dir)) != nullptr) { // Skip . and .. entries if (::strcmp(entry->d_name, ".") == 0 or ::strcmp(entry->d_name, "..") == 0) continue; // Construct full paths if (::snprintf(fullSourcePath, sizeof(fullSourcePath), "%s/%s", source.getNullTerminatedNative(), entry->d_name) >= static_cast(sizeof(fullSourcePath)) or ::snprintf(fullDestPath, sizeof(fullDestPath), "%s/%s", destination.getNullTerminatedNative(), entry->d_name) >= static_cast(sizeof(fullDestPath))) { return Result::Error("copyFile: Path too long"); } struct stat statbuf; if (::lstat(fullSourcePath, &statbuf) != 0) { return Result::Error("copyFile: Failed to get file stats"); } // Recursively copy subdirectories and files SC_TRY(copyFile(StringSpan(fullSourcePath), StringSpan(fullDestPath), options, S_ISDIR(statbuf.st_mode))); } return Result(true); } // Original file copying logic for non-directory files if (not options.overwrite and existsAndIsFile(destination)) { return Result::Error("copyFile: Failed to copy file (destination file already exists)"); } int inputDescriptor = ::open(source.getNullTerminatedNative(), O_RDONLY); if (inputDescriptor < 0) { return Result::Error("copyFile: Failed to open source file"); } auto closeInput = MakeDeferred([&] { ::close(inputDescriptor); }); struct stat inputStat; SC_TRY_POSIX(::fstat(inputDescriptor, &inputStat), "copyFile: Failed to get file stats"); int outputDescriptor = ::open(destination.getNullTerminatedNative(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (outputDescriptor < 0) { return Result::Error("copyFile: Failed to open destination file"); } auto closeOutput = MakeDeferred([&] { ::close(outputDescriptor); }); const int sendRes = ::sendfile(outputDescriptor, inputDescriptor, nullptr, inputStat.st_size); if (sendRes < 0) { // Sendfile failed, fallback to traditional read/write constexpr size_t bufferSize = 4096; char buffer[bufferSize]; ssize_t bytesRead; while ((bytesRead = ::read(inputDescriptor, buffer, bufferSize)) > 0) { if (::write(outputDescriptor, buffer, static_cast(bytesRead)) < 0) { return Result::Error("copyFile: Failed to write to destination file"); } } if (bytesRead < 0) { return Result::Error("copyFile: Failed to read from source file"); } } return Result(true); } SC::Result SC::FileSystem::Operations::removeDirectoryRecursive(StringSpan path) { SC_TRY_MSG(Internal::validatePath(path), "removeDirectoryRecursive: Invalid path"); // Open directory DIR* dir = ::opendir(path.getNullTerminatedNative()); if (dir == nullptr) { return Result::Error("removeDirectoryRecursive: Failed to open directory"); } auto closeDir = MakeDeferred([&] { ::closedir(dir); }); // Buffer for full path construction char fullPath[PATH_MAX]; struct dirent* entry; // Iterate through directory entries while ((entry = ::readdir(dir)) != nullptr) { // Skip . and .. entries if (::strcmp(entry->d_name, ".") == 0 or ::strcmp(entry->d_name, "..") == 0) continue; // Construct full path if (::snprintf(fullPath, sizeof(fullPath), "%s/%s", path.getNullTerminatedNative(), entry->d_name) >= static_cast(sizeof(fullPath))) { return Result::Error("removeDirectoryRecursive: Path too long"); } struct stat statbuf; if (::lstat(fullPath, &statbuf) != 0) { return Result::Error("removeDirectoryRecursive: Failed to get file stats"); } if (S_ISDIR(statbuf.st_mode)) { // Recursively remove subdirectory SC_TRY(removeDirectoryRecursive(fullPath)); } else { // Remove file if (::unlink(fullPath) != 0) { return Result::Error("removeDirectoryRecursive: Failed to remove file"); } } } // Remove the now-empty directory if (::rmdir(path.getNullTerminatedNative()) != 0) { return Result::Error("removeDirectoryRecursive: Failed to remove directory"); } return Result(true); } SC::StringSpan SC::FileSystem::Operations::getExecutablePath(StringPath& executablePath) { const int pathLength = ::readlink("/proc/self/exe", executablePath.writableSpan().data(), StringPath::MaxPath); if (pathLength > 0) { (void)executablePath.resize(static_cast(pathLength)); return executablePath.view(); } return {}; } SC::StringSpan SC::FileSystem::Operations::getCurrentWorkingDirectory(StringPath& currentWorkingDirectory) { const int pathLength = syscall(SYS_getcwd, currentWorkingDirectory.view().bytesIncludingTerminator(), StringPath::MaxPath); if (pathLength > 0) { (void)currentWorkingDirectory.resize(static_cast(pathLength)); return currentWorkingDirectory.view(); } return {}; } SC::StringSpan SC::FileSystem::Operations::getApplicationRootDirectory(StringPath& applicationRootDirectory) { StringSpan executablePath = getExecutablePath(applicationRootDirectory); if (!executablePath.isEmpty()) { // Get the directory part of the executable path char* buffer = applicationRootDirectory.writableSpan().data(); const char* lastSlash = ::strrchr(buffer, '/'); if (lastSlash != nullptr) { const size_t newSize = static_cast(lastSlash - buffer); // Null-terminate the path ::memset(buffer + newSize, 0, StringPath::MaxPath - newSize); (void)applicationRootDirectory.resize(newSize); return applicationRootDirectory.view(); } } return {}; } #endif #endif #endif // SANE_CPP_FILESYSTEM_IMPLEMENTATION</char*></uint8_t*></decltype(tempres),></decltype(tempres),>