// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include # include # pragma comment(lib, "shell32.lib") # pragma comment(lib, "ole32.lib") #endif #if ZEN_PLATFORM_WINDOWS ZEN_THIRD_PARTY_INCLUDES_START # include # include ZEN_THIRD_PARTY_INCLUDES_END #endif #if ZEN_PLATFORM_LINUX # include # include # include # include # include # include # include #endif #if ZEN_PLATFORM_MAC # include # include # include # include # include # include # include # include # include #endif #include #include #include namespace zen { using namespace std::literals; #if ZEN_PLATFORM_WINDOWS static bool DeleteReparsePoint(const wchar_t* Path, DWORD dwReparseTag) { windows::Handle hDir(CreateFileW(Path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr)); if (hDir != INVALID_HANDLE_VALUE) { REPARSE_GUID_DATA_BUFFER Rgdb = {}; Rgdb.ReparseTag = dwReparseTag; DWORD dwBytes; const BOOL bOK = DeviceIoControl(hDir, FSCTL_DELETE_REPARSE_POINT, &Rgdb, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, nullptr, 0, &dwBytes, nullptr); return bOK == TRUE; } return false; } bool CreateDirectories(const wchar_t* Path) { return CreateDirectories(std::filesystem::path(Path)); } // Erase all files and directories in a given directory, leaving an empty directory // behind bool DeleteDirectoriesInternal(const wchar_t* DirPath); static bool WipeDirectory(const wchar_t* DirPath, bool KeepDotFiles) { ExtendableWideStringBuilder<128> Pattern; Pattern.Append(DirPath); Pattern.Append(L"\\*"); WIN32_FIND_DATAW FindData; HANDLE hFind = FindFirstFileW(Pattern.c_str(), &FindData); bool Success = true; if (hFind != nullptr) { do { bool AttemptDelete = true; if (FindData.cFileName[0] == L'.') { if (FindData.cFileName[1] == L'.') { if (FindData.cFileName[2] == L'\0') { AttemptDelete = false; } } else if (FindData.cFileName[1] == L'\0') { AttemptDelete = false; } if (KeepDotFiles) { AttemptDelete = false; } } if (AttemptDelete) { ExtendableWideStringBuilder<128> Path; Path.Append(DirPath); Path.Append(L'\\'); Path.Append(FindData.cFileName); // if (fd.dwFileAttributes & FILE_ATTRIBUTE_RECALL_ON_OPEN) // deleteReparsePoint(path.c_str(), fd.dwReserved0); if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (FindData.dwFileAttributes & FILE_ATTRIBUTE_RECALL_ON_OPEN) { if (!DeleteReparsePoint(Path.c_str(), FindData.dwReserved0)) { Success = false; } } if (FindData.dwFileAttributes & FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS) { if (!DeleteReparsePoint(Path.c_str(), FindData.dwReserved0)) { Success = false; } } bool Succeeded = DeleteDirectoriesInternal(Path.c_str()); if (!Succeeded) { if (FindData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { if (!DeleteReparsePoint(Path.c_str(), FindData.dwReserved0)) { Success = false; } } } } else { BOOL Succeeded = DeleteFileW(Path.c_str()); if (!Succeeded) { // We should emit a warning here, but this is quite low level so care // needs to be taken. Success = false; } } } } while (FindNextFileW(hFind, &FindData) == TRUE); FindClose(hFind); } return true; } bool DeleteDirectoriesInternal(const wchar_t* DirPath) { const bool KeepDotFiles = false; return WipeDirectory(DirPath, KeepDotFiles) && RemoveDirectoryW(DirPath) == TRUE; } bool CleanDirectory(const wchar_t* DirPath, bool KeepDotFiles) { if (IsDir(DirPath)) { return WipeDirectory(DirPath, KeepDotFiles); } return CreateDirectories(DirPath); } #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_WINDOWS const uint32_t FileAttributesSystemReadOnlyFlag = FILE_ATTRIBUTE_READONLY; #else const uint32_t FileAttributesSystemReadOnlyFlag = 0x00000001; #endif // ZEN_PLATFORM_WINDOWS const uint32_t FileModeWriteEnableFlags = 0222; bool IsFileAttributeReadOnly(uint32_t FileAttributes) { #if ZEN_PLATFORM_WINDOWS return (FileAttributes & FileAttributesSystemReadOnlyFlag) != 0; #else return (FileAttributes & 0x00000001) != 0; #endif // ZEN_PLATFORM_WINDOWS } bool IsFileModeReadOnly(uint32_t FileMode) { return (FileMode & FileModeWriteEnableFlags) == 0; } uint32_t MakeFileAttributeReadOnly(uint32_t FileAttributes, bool ReadOnly) { return ReadOnly ? (FileAttributes | FileAttributesSystemReadOnlyFlag) : (FileAttributes & ~FileAttributesSystemReadOnlyFlag); } uint32_t MakeFileModeReadOnly(uint32_t FileMode, bool ReadOnly) { return ReadOnly ? (FileMode & ~FileModeWriteEnableFlags) : (FileMode | FileModeWriteEnableFlags); } #if ZEN_PLATFORM_WINDOWS static DWORD WinGetFileAttributes(const std::filesystem::path& Path, std::error_code& Ec) { DWORD Attributes = ::GetFileAttributes(Path.native().c_str()); if (Attributes == INVALID_FILE_ATTRIBUTES) { DWORD LastError = GetLastError(); switch (LastError) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_BAD_NETPATH: case ERROR_INVALID_DRIVE: break; case ERROR_ACCESS_DENIED: { WIN32_FIND_DATA FindData; HANDLE FindHandle = ::FindFirstFile(Path.native().c_str(), &FindData); if (FindHandle == INVALID_HANDLE_VALUE) { DWORD LastFindError = GetLastError(); if (LastFindError != ERROR_FILE_NOT_FOUND) { Ec = MakeErrorCode(LastError); } } else { FindClose(FindHandle); Attributes = FindData.dwFileAttributes; } } break; default: Ec = MakeErrorCode(LastError); break; } } return Attributes; } #endif // ZEN_PLATFORM_WINDOWS bool RemoveDirNative(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS BOOL Success = ::RemoveDirectory(Path.native().c_str()); if (!Success) { DWORD LastError = GetLastError(); switch (LastError) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: break; default: Ec = MakeErrorCode(LastError); break; } return false; } return true; #else return std::filesystem::remove(Path, Ec); #endif // ZEN_PLATFORM_WINDOWS } bool RemoveFileNative(const std::filesystem::path& Path, bool ForceRemoveReadOnlyFiles, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS const std::filesystem::path::value_type* NativePath = Path.native().c_str(); BOOL Success = ::DeleteFile(NativePath); if (!Success) { if (ForceRemoveReadOnlyFiles) { DWORD FileAttributes = WinGetFileAttributes(NativePath, Ec); if (Ec) { return false; } if ((FileAttributes != INVALID_FILE_ATTRIBUTES) && IsFileAttributeReadOnly(FileAttributes) != 0) { ::SetFileAttributes(NativePath, MakeFileAttributeReadOnly(FileAttributes, false)); Success = ::DeleteFile(NativePath); } } if (!Success) { DWORD LastError = GetLastError(); switch (LastError) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: break; default: Ec = MakeErrorCode(LastError); break; } return false; } } return true; #else if (!ForceRemoveReadOnlyFiles) { struct stat Stat; int err = stat(Path.native().c_str(), &Stat); if (err != 0) { int32_t err = errno; if (err == ENOENT) { Ec.clear(); return false; } } const uint32_t Mode = (uint32_t)Stat.st_mode; if (IsFileModeReadOnly(Mode)) { Ec = MakeErrorCode(EACCES); return false; } } return std::filesystem::remove(Path, Ec); #endif // ZEN_PLATFORM_WINDOWS } static void WipeDirectoryContentInternal(const std::filesystem::path& Path, bool ForceRemoveReadOnlyFiles, std::error_code& Ec) { DirectoryContent LocalDirectoryContent; GetDirectoryContent(Path, DirectoryContentFlags::IncludeDirs | DirectoryContentFlags::IncludeFiles, LocalDirectoryContent); for (const std::filesystem::path& LocalFilePath : LocalDirectoryContent.Files) { RemoveFileNative(LocalFilePath, ForceRemoveReadOnlyFiles, Ec); for (size_t Retries = 0; Ec && Retries < 3; Retries++) { Sleep(100 + int(Retries * 50)); Ec.clear(); if (IsFile(LocalFilePath)) { RemoveFileNative(LocalFilePath, ForceRemoveReadOnlyFiles, Ec); } } if (Ec) { return; } } for (std::filesystem::path& LocalDirPath : LocalDirectoryContent.Directories) { WipeDirectoryContentInternal(LocalDirPath, ForceRemoveReadOnlyFiles, Ec); if (Ec) { return; } RemoveDirNative(LocalDirPath, Ec); for (size_t Retries = 0; Ec && Retries < 3; Retries++) { Sleep(100 + int(Retries * 50)); Ec.clear(); if (IsDir(LocalDirPath)) { RemoveDirNative(LocalDirPath, Ec); } } if (Ec) { return; } } } bool CreateDirectory(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS BOOL Success = ::CreateDirectory(Path.native().c_str(), nullptr); if (!Success) { DWORD LastError = GetLastError(); switch (LastError) { case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: break; default: Ec = MakeErrorCode(LastError); break; } return false; } return Success; #else return std::filesystem::create_directory(Path, Ec); #endif // ZEN_PLATFORM_WINDOWS } bool CreateDirectories(const std::filesystem::path& Path) { std::error_code Ec; bool Success = CreateDirectories(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to create directories for '{}'", Path.string())); } return Success; } bool CreateDirectories(const std::filesystem::path& Path, std::error_code& Ec) { if (Path.string().ends_with(":")) { return false; } bool Exists = IsDir(Path, Ec); if (Ec) { return false; } if (Exists) { return false; } if (Path.has_parent_path()) { bool Result = CreateDirectories(Path.parent_path(), Ec); if (Ec) { return Result; } } return CreateDirectory(Path, Ec); } bool CleanDirectory(const std::filesystem::path& Path, bool ForceRemoveReadOnlyFiles) { std::error_code Ec; bool Result = CleanDirectory(Path, ForceRemoveReadOnlyFiles, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to clean directory for '{}'", Path.string())); } return Result; } bool CleanDirectory(const std::filesystem::path& Path, bool ForceRemoveReadOnlyFiles, std::error_code& Ec) { bool Exists = IsDir(Path, Ec); if (Ec) { return Exists; } if (Exists) { WipeDirectoryContentInternal(Path, ForceRemoveReadOnlyFiles, Ec); return false; } return CreateDirectory(Path, Ec); } bool DeleteDirectories(const std::filesystem::path& Path) { std::error_code Ec; bool Result = DeleteDirectories(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to delete directories for '{}'", Path.string())); } return Result; } bool DeleteDirectories(const std::filesystem::path& Path, std::error_code& Ec) { bool Exists = IsDir(Path, Ec); if (Ec) { return Exists; } if (Exists) { WipeDirectoryContentInternal(Path, false, Ec); if (Ec) { return false; } bool Result = RemoveDirNative(Path, Ec); for (size_t Retries = 0; Ec && Retries < 3; Retries++) { Sleep(100 + int(Retries * 50)); Ec.clear(); if (IsDir(Path)) { Result = RemoveDirNative(Path, Ec); } } return Result; } return false; } bool CleanDirectoryExceptDotFiles(const std::filesystem::path& Path) { #if ZEN_PLATFORM_WINDOWS const bool KeepDotFiles = true; return CleanDirectory(Path.c_str(), KeepDotFiles); #else ZEN_UNUSED(Path); ZEN_NOT_IMPLEMENTED(); #endif } ////////////////////////////////////////////////////////////////////////// bool SupportsBlockRefCounting(std::filesystem::path Path) { #if ZEN_PLATFORM_WINDOWS windows::Handle Handle(CreateFileW(Path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr)); if (Handle == INVALID_HANDLE_VALUE) { Handle.Detach(); return false; } ULONG FileSystemFlags = 0; if (!GetVolumeInformationByHandleW(Handle, nullptr, 0, nullptr, nullptr, /* lpFileSystemFlags */ &FileSystemFlags, nullptr, 0)) { return false; } if (!(FileSystemFlags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) { return false; } return true; #else ZEN_UNUSED(Path); return false; #endif // ZEN_PLATFORM_WINDOWS } #if ZEN_PLATFORM_WINDOWS class WindowsCloneQueryInterface : public CloneQueryInterface { public: WindowsCloneQueryInterface(uint64_t AlignmentSize, DWORD TargetVolumeSerialNumber) : m_AlignmentSize(AlignmentSize) , m_TargetVolumeSerialNumber(TargetVolumeSerialNumber) { } virtual bool CanClone(void* SourceNativeHandle) override { HANDLE SourceFile = (HANDLE)SourceNativeHandle; DWORD SourceVolumeSerialNumber = 0; if (GetVolumeInformationByHandleW(SourceFile, nullptr, MAX_PATH, &SourceVolumeSerialNumber, nullptr, nullptr, nullptr, 0) == FALSE) { return false; } if (SourceVolumeSerialNumber != m_TargetVolumeSerialNumber) { return false; } return true; } virtual uint64_t GetClonableRange(uint64_t SourceOffset, uint64_t TargetOffset, uint64_t Size, uint64_t& OutPreBytes, uint64_t& OutPostBytes) override { if (Size < m_AlignmentSize) { return 0; } uint64_t PreBytes = (m_AlignmentSize - (SourceOffset % m_AlignmentSize)) % m_AlignmentSize; uint64_t PostBytes = (SourceOffset + Size) % m_AlignmentSize; ZEN_ASSERT(Size >= PreBytes + PostBytes); if (Size - (PreBytes + PostBytes) < m_AlignmentSize) { return 0; } ZEN_ASSERT((PreBytes < Size && PostBytes < Size && Size >= PreBytes + PostBytes + m_AlignmentSize)); const uint64_t DestCloneOffset = TargetOffset + PreBytes; if (DestCloneOffset % m_AlignmentSize != 0) { return 0; } OutPreBytes = PreBytes; OutPostBytes = PostBytes; uint64_t CloneSize = Size - (PreBytes + PostBytes); ZEN_ASSERT(CloneSize % m_AlignmentSize == 0); return CloneSize; } virtual bool TryClone(void* SourceNativeHandle, void* TargetNativeHandle, uint64_t AlignedSourceOffset, uint64_t AlignedTargetOffset, uint64_t AlignedSize, uint64_t TargetFinalSize) { ZEN_ASSERT_SLOW(CanClone(SourceNativeHandle)); ZEN_ASSERT((AlignedSourceOffset % m_AlignmentSize) == 0); ZEN_ASSERT((AlignedTargetOffset % m_AlignmentSize) == 0); ZEN_ASSERT(AlignedSize > 0); ZEN_ASSERT((AlignedSize % m_AlignmentSize) == 0); HANDLE SourceFile = (HANDLE)SourceNativeHandle; ZEN_ASSERT(SourceFile != INVALID_HANDLE_VALUE); HANDLE TargetFile = (HANDLE)TargetNativeHandle; ZEN_ASSERT(TargetFile != INVALID_HANDLE_VALUE); FILE_BASIC_INFO SourceFileInfo{}; if (GetFileInformationByHandleEx(SourceFile, FileBasicInfo, &SourceFileInfo, sizeof(SourceFileInfo)) == FALSE) { std::error_code DummyEc; ZEN_DEBUG("Failed to get file information for file {}", PathFromHandle(SourceFile, DummyEc)); return false; } FILE_BASIC_INFO DestFileInfo{}; if (GetFileInformationByHandleEx(TargetFile, FileBasicInfo, &DestFileInfo, sizeof(DestFileInfo)) == FALSE) { std::error_code DummyEc; ZEN_DEBUG("Failed to get file information for file {}", PathFromHandle(TargetFile, DummyEc)); return false; } if ((SourceFileInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != (DestFileInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) { const bool SourceIsSparse = (SourceFileInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0; FILE_SET_SPARSE_BUFFER Sparse{}; Sparse.SetSparse = SourceIsSparse ? TRUE : FALSE; DWORD BytesReturned{}; if (!DeviceIoControl(TargetFile, FSCTL_SET_SPARSE, &Sparse, sizeof(Sparse), nullptr, 0, &BytesReturned, nullptr)) { std::error_code DummyEc; ZEN_DEBUG("Failed setting sparse status for file {} to {}", PathFromHandle(TargetFile, DummyEc), SourceIsSparse); return false; } } { const uint64_t TargetFileSize = FileSizeFromHandle(TargetFile); if (TargetFileSize != TargetFinalSize) { FILE_END_OF_FILE_INFO EOFInfo{}; EOFInfo.EndOfFile.QuadPart = TargetFinalSize; if (!SetFileInformationByHandle(TargetFile, FileEndOfFileInfo, &EOFInfo, sizeof(EOFInfo))) { std::error_code DummyEc; ZEN_DEBUG("Failed setting final size {} for file {}", TargetFinalSize, PathFromHandle(TargetFile, DummyEc)); return false; } } } { DUPLICATE_EXTENTS_DATA Extent{}; Extent.FileHandle = SourceFile; Extent.SourceFileOffset.QuadPart = AlignedSourceOffset; Extent.TargetFileOffset.QuadPart = AlignedTargetOffset; Extent.ByteCount.QuadPart = AlignedSize; DWORD BytesReturned{}; if (DeviceIoControl(TargetFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &Extent, sizeof(Extent), NULL, 0, &BytesReturned, NULL) != TRUE) { std::error_code DummyEc; ZEN_DEBUG("Failed cloning {} bytes from file {} at {} to file {} at {}", Extent.ByteCount.QuadPart, PathFromHandle(SourceFile, DummyEc), Extent.SourceFileOffset.QuadPart, PathFromHandle(TargetFile, DummyEc), Extent.TargetFileOffset.QuadPart); return false; } } return true; } private: uint64_t m_AlignmentSize; DWORD m_TargetVolumeSerialNumber; }; #endif // ZEN_PLATFORM_WINDOWS std::unique_ptr GetCloneQueryInterface(const std::filesystem::path& TargetDirectory) { #if ZEN_PLATFORM_WINDOWS std::filesystem::path TargetFilePath = TargetDirectory / ".cloneinfo"; windows::Handle TargetFile(CreateFileW(TargetFilePath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, 0, nullptr)); if (TargetFile == INVALID_HANDLE_VALUE) { TargetFile.Detach(); ThrowLastError(fmt::format("Failed opening file '{}' for write", TargetFilePath)); } DWORD DestVolumeSerialNumber = 0; DWORD DestVolumeFlags = 0; if (GetVolumeInformationByHandleW(TargetFile, nullptr, 0, &DestVolumeSerialNumber, nullptr, &DestVolumeFlags, nullptr, 0) == FALSE) { ZEN_DEBUG("Failed to get volume information for path {}", TargetFilePath); return {}; } TargetFile.Close(); std::error_code Dummy; RemoveFile(TargetFilePath, Dummy); if ((DestVolumeFlags & FILE_SUPPORTS_BLOCK_REFCOUNTING) == 0) { return {}; } std::filesystem::path::string_type NativePath = TargetFilePath; if (NativePath.find(TEXT("\\\\?\\")) == 0) { NativePath = NativePath.substr(4); } if (std::filesystem::path::string_type::size_type DelimiterPos = NativePath.find(TEXT(":\\")); DelimiterPos != std::filesystem::path::string_type::npos) { NativePath = NativePath.substr(0, DelimiterPos + 2); DWORD SectorsPerCluster = 0; DWORD BytesPerSector = 0; if (GetDiskFreeSpace(NativePath.c_str(), &SectorsPerCluster, &BytesPerSector, nullptr, nullptr) == TRUE) { return std::make_unique(SectorsPerCluster * BytesPerSector, DestVolumeSerialNumber); } } #else // ZEN_PLATFORM_WINDOWS ZEN_UNUSED(TargetDirectory); #endif // ZEN_PLATFORM_WINDOWS return {}; } #if ZEN_PLATFORM_WINDOWS static bool TryCloneFile(void* SourceNativeHandle, void* TargetNativeHandle) { HANDLE FromFile = (HANDLE)SourceNativeHandle; HANDLE TargetFile = (HANDLE)TargetNativeHandle; ULONG FileSystemFlags; if (!GetVolumeInformationByHandleW(FromFile, nullptr, 0, nullptr, nullptr, /* lpFileSystemFlags */ &FileSystemFlags, nullptr, 0)) { return false; } if (!(FileSystemFlags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) { SetLastError(ERROR_NOT_CAPABLE); return false; } FILE_END_OF_FILE_INFO FileSize; if (!GetFileSizeEx(FromFile, &FileSize.EndOfFile)) { return false; } FILE_BASIC_INFO BasicInfo; if (!GetFileInformationByHandleEx(FromFile, FileBasicInfo, &BasicInfo, sizeof BasicInfo)) { return false; } DWORD dwBytesReturned = 0; FSCTL_GET_INTEGRITY_INFORMATION_BUFFER GetIntegrityInfoBuffer; if (!DeviceIoControl(FromFile, FSCTL_GET_INTEGRITY_INFORMATION, nullptr, 0, &GetIntegrityInfoBuffer, sizeof GetIntegrityInfoBuffer, &dwBytesReturned, nullptr)) { return false; } // Delete target file when handle is closed (we only reset this if the copy succeeds) FILE_DISPOSITION_INFO FileDisposition = {TRUE}; if (!SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition)) { const DWORD ErrorCode = ::GetLastError(); SetLastError(ErrorCode); return false; } // Make file sparse so we don't end up allocating space when we change the file size if (!DeviceIoControl(TargetFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &dwBytesReturned, nullptr)) { return false; } // Copy integrity checking information FSCTL_SET_INTEGRITY_INFORMATION_BUFFER IntegritySet = {GetIntegrityInfoBuffer.ChecksumAlgorithm, GetIntegrityInfoBuffer.Reserved, GetIntegrityInfoBuffer.Flags}; if (!DeviceIoControl(TargetFile, FSCTL_SET_INTEGRITY_INFORMATION, &IntegritySet, sizeof IntegritySet, nullptr, 0, nullptr, nullptr)) { return false; } // Resize file - note that the file is sparse at this point so no additional data will be written if (!SetFileInformationByHandle(TargetFile, FileEndOfFileInfo, &FileSize, sizeof FileSize)) { return false; } constexpr auto RoundToClusterSize = [](LONG64 FileSize, ULONG ClusterSize) -> LONG64 { return (FileSize + ClusterSize - 1) / ClusterSize * ClusterSize; }; static_assert(RoundToClusterSize(5678, 4 * 1024) == 8 * 1024); // Loop for cloning file contents. This is necessary as the API has a 32-bit size // limit for some reason const LONG64 SplitThreshold = (1LL << 32) - GetIntegrityInfoBuffer.ClusterSizeInBytes; DUPLICATE_EXTENTS_DATA DuplicateExtentsData{.FileHandle = FromFile}; for (LONG64 CurrentByteOffset = 0, RemainingBytes = RoundToClusterSize(FileSize.EndOfFile.QuadPart, GetIntegrityInfoBuffer.ClusterSizeInBytes); RemainingBytes > 0; CurrentByteOffset += SplitThreshold, RemainingBytes -= SplitThreshold) { DuplicateExtentsData.SourceFileOffset.QuadPart = CurrentByteOffset; DuplicateExtentsData.TargetFileOffset.QuadPart = CurrentByteOffset; DuplicateExtentsData.ByteCount.QuadPart = std::min(SplitThreshold, RemainingBytes); if (!DeviceIoControl(TargetFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &DuplicateExtentsData, sizeof DuplicateExtentsData, nullptr, 0, &dwBytesReturned, nullptr)) { return false; } } // Make the file not sparse again now that we have populated the contents if (!(BasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) { FILE_SET_SPARSE_BUFFER SetSparse = {FALSE}; if (!DeviceIoControl(TargetFile, FSCTL_SET_SPARSE, &SetSparse, sizeof SetSparse, nullptr, 0, &dwBytesReturned, nullptr)) { return false; } } // Update timestamps (but don't lie about the creation time) BasicInfo.CreationTime.QuadPart = 0; if (!SetFileInformationByHandle(TargetFile, FileBasicInfo, &BasicInfo, sizeof BasicInfo)) { return false; } if (!FlushFileBuffers(TargetFile)) { return false; } // Finally now everything is done - make sure the file is not deleted on close! FileDisposition = {FALSE}; const bool AllOk = (TRUE == SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition)); return AllOk; } #endif // ZEN_PLATFORM_WINDOWS bool TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath) { #if ZEN_PLATFORM_WINDOWS windows::Handle FromFile(CreateFileW(FromPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr)); if (FromFile == INVALID_HANDLE_VALUE) { FromFile.Detach(); return false; } SetFileAttributesW(ToPath.c_str(), FILE_ATTRIBUTE_NORMAL); windows::Handle TargetFile(CreateFileW(ToPath.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, /* no sharing */ FILE_SHARE_READ, nullptr, OPEN_ALWAYS, 0, /* hTemplateFile */ FromFile)); if (TargetFile == INVALID_HANDLE_VALUE) { TargetFile.Detach(); return false; } return TryCloneFile((void*)FromFile.m_Handle, (void*)TargetFile.m_Handle); #elif ZEN_PLATFORM_LINUX # if 0 struct ScopedFd { ~ScopedFd() { close(Fd); } int Fd; }; // The 'from' file int FromFd = open(FromPath.c_str(), O_RDONLY|O_CLOEXEC); if (FromFd < 0) { return false; } ScopedFd $From = { FromFd }; // The 'to' file int ToFd = open(ToPath.c_str(), O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0666); if (ToFd < 0) { return false; } fchmod(ToFd, 0666); ScopedFd $To = { FromFd }; ioctl(ToFd, FICLONE, FromFd); return false; # endif // 0 ZEN_UNUSED(FromPath, ToPath); return false; #elif ZEN_PLATFORM_MAC /* clonefile() syscall if APFS */ ZEN_UNUSED(FromPath, ToPath); return false; #endif // ZEN_PLATFORM_WINDOWS } void CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath, const CopyFileOptions& Options, std::error_code& OutErrorCode) { OutErrorCode.clear(); bool Success = CopyFile(FromPath, ToPath, Options); if (!Success) { OutErrorCode = MakeErrorCodeFromLastError(); } } bool CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath, const CopyFileOptions& Options) { bool Success = false; if (Options.EnableClone) { Success = TryCloneFile(FromPath.native(), ToPath.native()); if (Success) { return true; } } if (Options.MustClone) { #if ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX ZEN_ERROR("CloneFile() is not implemented on this platform"); #endif // ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX return false; } #if ZEN_PLATFORM_WINDOWS BOOL CancelFlag = FALSE; Success = !!::CopyFileExW(FromPath.c_str(), ToPath.c_str(), /* lpProgressRoutine */ nullptr, /* lpData */ nullptr, &CancelFlag, /* dwCopyFlags */ 0); #else struct ScopedFd { ~ScopedFd() { close(Fd); } int Fd; }; // From file int FromFd = open(FromPath.c_str(), O_RDONLY | O_CLOEXEC); if (FromFd < 0) { ThrowLastError(fmt::format("failed to open file {}", FromPath)); } ScopedFd $From = {FromFd}; // To file int ToFd = open(ToPath.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, 0666); if (ToFd < 0) { ThrowLastError(fmt::format("failed to create file {}", ToPath)); } fchmod(ToFd, 0666); ScopedFd $To = {ToFd}; struct stat Stat; fstat(FromFd, &Stat); size_t FileSizeBytes = Stat.st_size; fchown(ToFd, Stat.st_uid, Stat.st_gid); // Copy impl const size_t BufferSize = Min(FileSizeBytes, 64u << 10); void* Buffer = malloc(BufferSize); while (true) { int BytesRead = read(FromFd, Buffer, BufferSize); if (BytesRead <= 0) { Success = (BytesRead == 0); break; } if (write(ToFd, Buffer, BytesRead) != BytesRead) { Success = false; break; } } free(Buffer); #endif // ZEN_PLATFORM_WINDOWS if (!Success) { ThrowLastError(fmt::format("file copy from {} to {} failed", FromPath, ToPath)); } return true; } void CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options) { // Validate arguments if (FromPath.empty() || !IsDir(FromPath)) throw std::runtime_error("invalid CopyTree source directory specified"); if (ToPath.empty()) throw std::runtime_error("no CopyTree target specified"); if (Options.MustClone && !SupportsBlockRefCounting(FromPath)) throw std::runtime_error(fmt::format("cloning not possible from '{}'", FromPath)); if (IsFile(ToPath)) { throw std::runtime_error(fmt::format("specified CopyTree target '{}' is not a directory", ToPath)); } if (!IsDir(ToPath)) { CreateDirectories(ToPath); } if (Options.MustClone && !SupportsBlockRefCounting(ToPath)) throw std::runtime_error(fmt::format("cloning not possible from '{}'", ToPath)); // Verify source/target relationships std::error_code Ec; std::filesystem::path FromCanonical = std::filesystem::canonical(FromPath, Ec); if (!Ec) { std::filesystem::path ToCanonical = std::filesystem::canonical(ToPath, Ec); if (!Ec) { if (FromCanonical == ToCanonical) { throw std::runtime_error("Target and source must be distinct files or directories"); } if (ToCanonical.generic_string().starts_with(FromCanonical.generic_string()) || FromCanonical.generic_string().starts_with(ToCanonical.generic_string())) { throw std::runtime_error("Invalid parent/child relationship for source/target directories"); } } } struct CopyVisitor : public FileSystemTraversal::TreeVisitor { CopyVisitor(std::filesystem::path InBasePath, zen::CopyFileOptions InCopyOptions) : BasePath(InBasePath), CopyOptions(InCopyOptions) { } virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t FileSize, uint32_t, uint64_t) override { std::error_code Ec; const std::filesystem::path Relative = std::filesystem::relative(Parent, BasePath, Ec); if (Ec) { FailedFileCount++; } else { const std::filesystem::path FromPath = Parent / File; std::filesystem::path ToPath; if (Relative.compare(".")) { zen::CreateDirectories(TargetPath / Relative); ToPath = TargetPath / Relative / File; } else { ToPath = TargetPath / File; } try { if (zen::CopyFile(FromPath, ToPath, CopyOptions)) { ++FileCount; ByteCount += FileSize; } else { throw std::runtime_error("CopyFile failed in an unexpected way"); } } catch (const std::exception& Ex) { ++FailedFileCount; throw std::runtime_error(fmt::format("failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what())); } } } virtual bool VisitDirectory(const std::filesystem::path&, const path_view&, uint32_t) override { return true; } std::filesystem::path BasePath; std::filesystem::path TargetPath; zen::CopyFileOptions CopyOptions; int FileCount = 0; uint64_t ByteCount = 0; int FailedFileCount = 0; }; CopyVisitor Visitor{FromPath, Options}; Visitor.TargetPath = ToPath; FileSystemTraversal Traversal; Traversal.TraverseFileSystem(FromPath, Visitor); if (Visitor.FailedFileCount) { throw std::runtime_error(fmt::format("{} file copy operations FAILED", Visitor.FailedFileCount)); } } void WriteFile(void* NativeHandle, const void* Data, uint64_t Size, uint64_t FileOffset, uint64_t ChunkSize, std::error_code& Ec) { ZEN_ASSERT(NativeHandle != nullptr); Ec.clear(); while (Size) { const uint64_t NumberOfBytesToWrite = Min(Size, ChunkSize); #if ZEN_PLATFORM_WINDOWS OVERLAPPED Ovl{}; Ovl.Offset = DWORD(FileOffset & 0xffff'ffffu); Ovl.OffsetHigh = DWORD(FileOffset >> 32); DWORD dwNumberOfBytesWritten = 0; BOOL Success = ::WriteFile(NativeHandle, Data, DWORD(NumberOfBytesToWrite), &dwNumberOfBytesWritten, &Ovl); #else static_assert(sizeof(off_t) >= sizeof(uint64_t), "sizeof(off_t) does not support large files"); int Fd = int(uintptr_t(NativeHandle)); int BytesWritten = pwrite(Fd, Data, NumberOfBytesToWrite, FileOffset); bool Success = (BytesWritten > 0); #endif if (!Success) { Ec = MakeErrorCodeFromLastError(); return; } Size -= NumberOfBytesToWrite; FileOffset += NumberOfBytesToWrite; Data = reinterpret_cast(Data) + NumberOfBytesToWrite; } } void ReadFile(void* NativeHandle, void* Data, uint64_t Size, uint64_t FileOffset, uint64_t ChunkSize, std::error_code& Ec) { while (Size) { const uint64_t NumberOfBytesToRead = Min(Size, ChunkSize); size_t BytesRead = 0; #if ZEN_PLATFORM_WINDOWS OVERLAPPED Ovl{}; Ovl.Offset = DWORD(FileOffset & 0xffff'ffffu); Ovl.OffsetHigh = DWORD(FileOffset >> 32); DWORD dwNumberOfBytesRead = 0; BOOL Success = ::ReadFile(NativeHandle, Data, DWORD(NumberOfBytesToRead), &dwNumberOfBytesRead, &Ovl); if (Success) { BytesRead = size_t(dwNumberOfBytesRead); } else { Ec = MakeErrorCodeFromLastError(); return; } #else static_assert(sizeof(off_t) >= sizeof(uint64_t), "sizeof(off_t) does not support large files"); int Fd = int(uintptr_t(NativeHandle)); ssize_t ReadResult = pread(Fd, Data, NumberOfBytesToRead, FileOffset); if (ReadResult != -1) { BytesRead = size_t(ReadResult); } else { Ec = MakeErrorCodeFromLastError(); return; } #endif Size -= BytesRead; FileOffset += BytesRead; Data = reinterpret_cast(Data) + BytesRead; } } void WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t BufferCount) { #if ZEN_PLATFORM_WINDOWS windows::FileHandle Outfile; HRESULT hRes = Outfile.Create(Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS); if (hRes == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)) { CreateDirectories(Path.parent_path()); hRes = Outfile.Create(Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS); } if (FAILED(hRes)) { ThrowSystemException(hRes, fmt::format("File open failed for '{}'", Path).c_str()); } #else int OpenFlags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC; int Fd = open(Path.c_str(), OpenFlags, 0666); if (Fd < 0) { zen::CreateDirectories(Path.parent_path()); Fd = open(Path.c_str(), OpenFlags, 0666); } if (Fd < 0) { ThrowLastError(fmt::format("File open failed for '{}'", Path)); } fchmod(Fd, 0666); #endif // TODO: this should be block-enlightened for (size_t i = 0; i < BufferCount; ++i) { uint64_t WriteSize = Data[i]->Size(); const void* DataPtr = Data[i]->Data(); while (WriteSize) { const uint64_t ChunkSize = Min(WriteSize, uint64_t(2) * 1024 * 1024 * 1024); #if ZEN_PLATFORM_WINDOWS hRes = Outfile.Write(DataPtr, gsl::narrow_cast(WriteSize)); if (FAILED(hRes)) { Outfile.Close(); std::error_code DummyEc; RemoveFile(Path, DummyEc); ThrowSystemException(hRes, fmt::format("File write failed for '{}'", Path).c_str()); } #else if (write(Fd, DataPtr, WriteSize) != int64_t(WriteSize)) { close(Fd); std::error_code DummyEc; RemoveFile(Path, DummyEc); ThrowLastError(fmt::format("File write failed for '{}'", Path)); } #endif // ZEN_PLATFORM_WINDOWS WriteSize -= ChunkSize; DataPtr = reinterpret_cast(DataPtr) + ChunkSize; } } #if ZEN_PLATFORM_WINDOWS Outfile.Close(); #else close(Fd); #endif } void WriteFile(std::filesystem::path Path, IoBuffer Data) { const IoBuffer* const DataPtr = &Data; WriteFile(Path, &DataPtr, 1); } void WriteFile(std::filesystem::path Path, CompositeBuffer InData) { std::vector DataVec; for (const SharedBuffer& Segment : InData.GetSegments()) { DataVec.push_back(Segment.AsIoBuffer()); } std::vector DataPtrs; for (IoBuffer& Data : DataVec) { DataPtrs.push_back(&Data); } WriteFile(Path, DataPtrs.data(), DataPtrs.size()); } bool MoveToFile(std::filesystem::path Path, IoBuffer Data) { if (!Data.IsWholeFile()) { return false; } IoBufferFileReference FileRef; if (!Data.GetFileReference(/* out */ FileRef)) { return false; } #if ZEN_PLATFORM_WINDOWS const HANDLE ChunkFileHandle = FileRef.FileHandle; std::wstring FileName = Path.native(); const DWORD BufferSize = sizeof(FILE_RENAME_INFO) + gsl::narrow(FileName.size() * sizeof(WCHAR)); FILE_RENAME_INFO* RenameInfo = reinterpret_cast(Memory::Alloc(BufferSize)); memset(RenameInfo, 0, BufferSize); RenameInfo->ReplaceIfExists = TRUE; RenameInfo->FileNameLength = gsl::narrow(FileName.size()); memcpy(RenameInfo->FileName, FileName.c_str(), FileName.size() * sizeof(WCHAR)); RenameInfo->FileName[FileName.size()] = 0; // Try to move file into place BOOL Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize); if (!Success) { DWORD LastError = GetLastError(); if (LastError == ERROR_PATH_NOT_FOUND) { zen::CreateDirectories(Path.parent_path()); Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize); } if (!Success && (LastError == ERROR_ACCESS_DENIED)) { // Fallback to regular rename std::error_code Ec; std::filesystem::path SourcePath = PathFromHandle(FileRef.FileHandle, Ec); if (!Ec) { auto NativeSourcePath = SourcePath.native().c_str(); auto NativeTargetPath = Path.native().c_str(); Success = ::MoveFile(NativeSourcePath, NativeTargetPath); if (!Success) { LastError = GetLastError(); if (LastError == ERROR_PATH_NOT_FOUND) { zen::CreateDirectories(Path.parent_path()); Success = ::MoveFile(NativeSourcePath, NativeTargetPath); } } } } } Memory::Free(RenameInfo); if (!Success) { return false; } #elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC std::error_code Ec; std::filesystem::path SourcePath = PathFromHandle(FileRef.FileHandle, Ec); if (Ec) { return false; } int Ret = rename(SourcePath.c_str(), Path.c_str()); if (Ret < 0) { int32_t err = errno; if (err == ENOENT) { zen::CreateDirectories(Path.parent_path()); Ret = rename(SourcePath.c_str(), Path.c_str()); } } if (Ret < 0) { return false; } #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC Data.SetDeleteOnClose(false); return true; } IoBuffer FileContents::Flatten() { if (Data.size() == 1) { return Data[0]; } else if (Data.empty()) { return {}; } else { ZEN_NOT_IMPLEMENTED(); } } FileContents ReadStdIn() { BinaryWriter Writer; do { uint8_t ReadBuffer[1024]; size_t BytesRead = fread(ReadBuffer, 1, sizeof ReadBuffer, stdin); Writer.Write(ReadBuffer, BytesRead); } while (!feof(stdin)); FileContents Contents; Contents.Data.emplace_back(IoBuffer(IoBuffer::Clone, Writer.GetData(), Writer.GetSize())); return Contents; } FileContents ReadFile(const std::filesystem::path& Path) { uint64_t FileSizeBytes; void* Handle; #if ZEN_PLATFORM_WINDOWS windows::Handle FromFile(CreateFileW(Path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr)); if (FromFile == INVALID_HANDLE_VALUE) { FromFile.Detach(); return FileContents{.ErrorCode = std::error_code(::GetLastError(), std::system_category())}; } FILE_END_OF_FILE_INFO FileSize; if (!GetFileSizeEx(FromFile, &FileSize.EndOfFile)) { return FileContents{.ErrorCode = std::error_code(::GetLastError(), std::system_category())}; } FileSizeBytes = FileSize.EndOfFile.QuadPart; Handle = FromFile.Detach(); #else int Fd = open(Path.c_str(), O_RDONLY | O_CLOEXEC); if (Fd < 0) { FileContents Ret; Ret.ErrorCode = std::error_code(zen::GetLastError(), std::system_category()); return Ret; } static_assert(sizeof(decltype(stat::st_size)) == sizeof(uint64_t), "fstat() doesn't support large files"); struct stat Stat; fstat(Fd, &Stat); FileSizeBytes = Stat.st_size; Handle = (void*)uintptr_t(Fd); #endif FileContents Contents; Contents.Data.emplace_back(IoBuffer(IoBuffer::File, Handle, 0, FileSizeBytes, /*IsWholeFile*/ true)); return Contents; } void ScanFile(void* NativeHandle, uint64_t Offset, uint64_t Size, uint64_t ChunkSize, std::function&& ProcessFunc) { ZEN_ASSERT(NativeHandle != nullptr); uint64_t BufferSize = Min(ChunkSize, Size); std::vector ReadBuffer(BufferSize); uint64_t ReadOffset = 0; while (ReadOffset < Size) { const uint64_t NumberOfBytesToRead = Min(Size - ReadOffset, BufferSize); uint64_t FileOffset = Offset + ReadOffset; #if ZEN_PLATFORM_WINDOWS OVERLAPPED Ovl{}; Ovl.Offset = DWORD(FileOffset & 0xffff'ffffu); Ovl.OffsetHigh = DWORD(FileOffset >> 32); DWORD BytesRead = 0; BOOL Success = ::ReadFile(NativeHandle, ReadBuffer.data(), DWORD(NumberOfBytesToRead), &BytesRead, &Ovl); if (!Success) { throw std::system_error(std::error_code(::GetLastError(), std::system_category()), "file scan failed"); } #else int BytesRead = pread(int(intptr_t(NativeHandle)), ReadBuffer.data(), size_t(NumberOfBytesToRead), off_t(FileOffset)); if (BytesRead < 0) { throw std::system_error(std::error_code(errno, std::system_category()), "file scan failed"); } #endif ProcessFunc(ReadBuffer.data(), (size_t)BytesRead); ReadOffset += (uint64_t)BytesRead; } } bool ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function&& ProcessFunc) { #if ZEN_PLATFORM_WINDOWS windows::Handle FromFile(CreateFileW(Path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr)); if (FromFile == INVALID_HANDLE_VALUE) { FromFile.Detach(); return false; } std::vector ReadBuffer(ChunkSize); for (;;) { DWORD dwBytesRead = 0; BOOL Success = ::ReadFile(FromFile, ReadBuffer.data(), (DWORD)ReadBuffer.size(), &dwBytesRead, nullptr); if (!Success) { throw std::system_error(std::error_code(::GetLastError(), std::system_category()), "file scan failed"); } if (dwBytesRead == 0) break; ProcessFunc(ReadBuffer.data(), dwBytesRead); } #else int Fd = open(Path.c_str(), O_RDONLY | O_CLOEXEC); if (Fd < 0) { return false; } bool Success = true; void* Buffer = malloc(ChunkSize); while (true) { int BytesRead = read(Fd, Buffer, ChunkSize); if (BytesRead < 0) { Success = false; break; } if (BytesRead == 0) { break; } ProcessFunc(Buffer, BytesRead); } free(Buffer); close(Fd); if (!Success) { ThrowLastError("file scan failed"); } #endif // ZEN_PLATFORM_WINDOWS return true; } void PathToUtf8(const std::filesystem::path& Path, StringBuilderBase& Out) { #if ZEN_PLATFORM_WINDOWS WideToUtf8(Path.native().c_str(), Out); #else Out << Path.c_str(); #endif } std::string PathToUtf8(const std::filesystem::path& Path) { #if ZEN_PLATFORM_WINDOWS return WideToUtf8(Path.native().c_str()); #else return Path.string(); #endif } DiskSpace DiskSpaceInfo(std::filesystem::path Directory, std::error_code& Error) { using namespace std::filesystem; space_info SpaceInfo = space(Directory, Error); if (Error) { return {}; } return { .Free = uint64_t(SpaceInfo.available), .Total = uint64_t(SpaceInfo.capacity), }; } void FileSystemTraversal::TraverseFileSystem(const std::filesystem::path& RootDir, TreeVisitor& Visitor) { #if ZEN_PLATFORM_WINDOWS std::vector FileInfoBuffer(8 * 1024); FILE_INFO_BY_HANDLE_CLASS FibClass = FileIdBothDirectoryRestartInfo; bool Continue = true; windows::FileHandle RootDirHandle; HRESULT hRes = RootDirHandle.Create(RootDir.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS); if (FAILED(hRes)) { if (HRESULT_CODE(hRes) == ERROR_FILE_NOT_FOUND || HRESULT_CODE(hRes) == ERROR_PATH_NOT_FOUND) { // Directory no longer exist, treat it as empty return; } ThrowSystemException(hRes, fmt::format("Failed to open handle to '{}'", RootDir)); } while (Continue) { BOOL Success = GetFileInformationByHandleEx(RootDirHandle, FibClass, FileInfoBuffer.data(), (DWORD)(FileInfoBuffer.size() * sizeof(uint64_t))); FibClass = FileIdBothDirectoryInfo; // Set up for next iteration uint64_t EntryOffset = 0; if (!Success) { DWORD LastError = GetLastError(); if (LastError == ERROR_NO_MORE_FILES) { break; } throw std::system_error(std::error_code(LastError, std::system_category()), "file system traversal error"); } for (;;) { const FILE_ID_BOTH_DIR_INFO* DirInfo = reinterpret_cast(reinterpret_cast(FileInfoBuffer.data()) + EntryOffset); std::wstring_view FileName(DirInfo->FileName, DirInfo->FileNameLength / sizeof(wchar_t)); if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (FileName == L"."sv || FileName == L".."sv) { // Not very interesting } else { const bool ShouldDescend = Visitor.VisitDirectory(RootDir, FileName, gsl::narrow(DirInfo->FileAttributes)); if (ShouldDescend) { // Note that this recursion combined with the buffer could // blow the stack, we should consider a different strategy std::filesystem::path FullPath = RootDir / FileName; TraverseFileSystem(FullPath, Visitor); } } } else if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DEVICE) { ZEN_WARN("encountered device node during file system traversal: '{}' found in '{}'", WideToUtf8(FileName), RootDir); } else { Visitor.VisitFile(RootDir, FileName, DirInfo->EndOfFile.QuadPart, gsl::narrow(DirInfo->FileAttributes), (uint64_t)DirInfo->LastWriteTime.QuadPart); } const uint64_t NextOffset = DirInfo->NextEntryOffset; if (NextOffset == 0) { break; } EntryOffset += NextOffset; } } #else /* Could also implement this using Linux's getdents() syscall */ DIR* Dir = opendir(RootDir.c_str()); if (Dir == nullptr) { int Err = errno; if (Err == ENOENT) { // Directory no longer exist, treat it as empty return; } ThrowLastError(fmt::format("Failed to open directory for traversal: {}", RootDir.c_str())); } for (struct dirent* Entry; (Entry = readdir(Dir));) { const char* FileName = Entry->d_name; struct stat Stat; std::filesystem::path FullPath = RootDir / FileName; stat(FullPath.c_str(), &Stat); if (S_ISDIR(Stat.st_mode)) { if (strcmp(FileName, ".") == 0 || strcmp(FileName, "..") == 0) { /* nop */ } else if (Visitor.VisitDirectory(RootDir, FileName, gsl::narrow(Stat.st_mode))) { TraverseFileSystem(FullPath, Visitor); } } else if (S_ISREG(Stat.st_mode)) { Visitor.VisitFile(RootDir, FileName, Stat.st_size, gsl::narrow(Stat.st_mode), gsl::narrow(Stat.st_mtime)); } else { ZEN_WARN("encountered non-regular file during file system traversal ({}): {} found in {}", Stat.st_mode, FileName, RootDir.c_str()); } } closedir(Dir); #endif // ZEN_PLATFORM_WINDOWS } std::filesystem::path CanonicalPath(std::filesystem::path InPath, std::error_code& Ec) { ZEN_UNUSED(Ec); #if ZEN_PLATFORM_WINDOWS windows::FileHandle Handle; HRESULT hRes = Handle.Create(InPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS); if (FAILED(hRes)) { Ec = MakeErrorCodeFromLastError(); return {}; } return PathFromHandle(Handle, Ec); #else return InPath; #endif } bool IsFile(const std::filesystem::path& Path) { std::error_code Ec; bool Result = IsFile(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to test if path '{}' is a file", Path.string())); } return Result; } bool IsFile(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS DWORD Attributes = WinGetFileAttributes(Path, Ec); if (Ec) { return false; } if (Attributes == INVALID_FILE_ATTRIBUTES) { return false; } return (Attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; #else struct stat Stat; int err = stat(Path.native().c_str(), &Stat); if (err != 0) { int32_t err = errno; if (err == ENOENT) { Ec.clear(); return false; } } if (S_ISREG(Stat.st_mode)) { return true; } return false; #endif // ZEN_PLATFORM_WINDOWS } bool IsDir(const std::filesystem::path& Path) { std::error_code Ec; bool Result = IsDir(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to test if path '{}' is a directory", Path.string())); } return Result; } bool IsDir(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS DWORD Attributes = WinGetFileAttributes(Path, Ec); if (Ec) { return false; } if (Attributes == INVALID_FILE_ATTRIBUTES) { return false; } return (Attributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; #else struct stat Stat; int err = stat(Path.native().c_str(), &Stat); if (err != 0) { int32_t err = errno; if (err == ENOENT) { Ec.clear(); return false; } } if (S_ISDIR(Stat.st_mode)) { return true; } return false; #endif // ZEN_PLATFORM_WINDOWS } bool RemoveFile(const std::filesystem::path& Path) { std::error_code Ec; bool Success = RemoveFile(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to remove file '{}'", Path.string())); } return Success; } bool RemoveFile(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS return RemoveFileNative(Path, false, Ec); #else bool IsDirectory = std::filesystem::is_directory(Path, Ec); if (IsDirectory) { Ec = MakeErrorCode(EPERM); return false; } Ec.clear(); return RemoveFileNative(Path, false, Ec); #endif // ZEN_PLATFORM_WINDOWS } bool RemoveDir(const std::filesystem::path& Path) { std::error_code Ec; bool Success = RemoveDir(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to remove directory '{}'", Path.string())); } return Success; } bool RemoveDir(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS return RemoveDirNative(Path, Ec); #else bool IsFile = std::filesystem::is_regular_file(Path, Ec); if (IsFile) { Ec = MakeErrorCode(EPERM); return false; } Ec.clear(); return RemoveDirNative(Path, Ec); #endif // ZEN_PLATFORM_WINDOWS } std::filesystem::path PathFromHandle(void* NativeHandle, std::error_code& Ec) { if (NativeHandle == nullptr) { return ""; } #if ZEN_PLATFORM_WINDOWS if (NativeHandle == INVALID_HANDLE_VALUE) { return ""; } auto GetFinalPathNameByHandleWRetry = [&Ec](HANDLE hFile, LPWSTR lpszFilePath, DWORD cchFilePath, DWORD dwFlags, DWORD& OutRequiredLength) -> DWORD { size_t MaxTries = 5; while (MaxTries > 0) { MaxTries--; DWORD Res = GetFinalPathNameByHandleW(hFile, lpszFilePath, cchFilePath, dwFlags); if (Res == 0) { DWORD LastError = zen::GetLastError(); // Under heavy concurrent loads we might get access denied on a file handle while trying to get path name. // Retry if that is the case. if (LastError != ERROR_ACCESS_DENIED) { Sleep(2); return LastError; } // Retry continue; } ZEN_ASSERT(Res != 1); // We don't accept empty path names OutRequiredLength = Res; return ERROR_SUCCESS; } return ERROR_ACCESS_DENIED; }; static const DWORD PathDataSize = 512; wchar_t PathData[PathDataSize]; DWORD RequiredLengthIncludingNul = 0; DWORD Error = GetFinalPathNameByHandleWRetry(NativeHandle, PathData, PathDataSize, FILE_NAME_OPENED, RequiredLengthIncludingNul); if (Error != ERROR_SUCCESS) { Ec = MakeErrorCodeFromLastError(); return fmt::format("", Ec.message()); } if (RequiredLengthIncludingNul < PathDataSize) { std::wstring FullPath(PathData, gsl::narrow(RequiredLengthIncludingNul)); return FullPath; } std::wstring FullPath; FullPath.resize(RequiredLengthIncludingNul - 1); DWORD FinalLength = 0; Error = GetFinalPathNameByHandleWRetry(NativeHandle, FullPath.data(), RequiredLengthIncludingNul, FILE_NAME_OPENED, FinalLength); if (Error != ERROR_SUCCESS) { Ec = MakeErrorCodeFromLastError(); return fmt::format("", Ec.message()); } ZEN_UNUSED(FinalLength); return FullPath; #elif ZEN_PLATFORM_LINUX char Link[PATH_MAX]; char Path[64]; sprintf(Path, "/proc/self/fd/%d", int(uintptr_t(NativeHandle))); ssize_t BytesRead = readlink(Path, Link, sizeof(Link) - 1); if (BytesRead <= 0) { Ec = MakeErrorCodeFromLastError(); return fmt::format("", Ec.message()); } Link[BytesRead] = '\0'; return Link; #elif ZEN_PLATFORM_MAC int Fd = int(uintptr_t(NativeHandle)); char Path[MAXPATHLEN]; if (fcntl(Fd, F_GETPATH, Path) < 0) { Ec = MakeErrorCodeFromLastError(); return fmt::format("", Ec.message()); } return Path; #endif // ZEN_PLATFORM_WINDOWS } uint64_t FileSizeFromPath(const std::filesystem::path& Path) { std::error_code Ec; uint64_t Size = FileSizeFromPath(Path, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to get file size for path '{}'", Path.string())); } return Size; } uint64_t FileSizeFromPath(const std::filesystem::path& Path, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS void* Handle = ::CreateFile(Path.native().c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); if (Handle == INVALID_HANDLE_VALUE) { DWORD LastError = GetLastError(); Ec = MakeErrorCode(LastError); return 0; } auto _ = MakeGuard([Handle]() { CloseHandle(Handle); }); LARGE_INTEGER FileSize; BOOL Success = GetFileSizeEx(Handle, &FileSize); if (!Success) { Ec = MakeErrorCodeFromLastError(); return 0; } return FileSize.QuadPart; #else return std::filesystem::file_size(Path, Ec); #endif // ZEN_PLATFORM_WINDOWS } uint64_t FileSizeFromHandle(void* NativeHandle, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS BY_HANDLE_FILE_INFORMATION Bhfh = {}; if (GetFileInformationByHandle(NativeHandle, &Bhfh)) { return uint64_t(Bhfh.nFileSizeHigh) << 32 | Bhfh.nFileSizeLow; } else { Ec = MakeErrorCodeFromLastError(); return 0; } #else int Fd = int(intptr_t(NativeHandle)); static_assert(sizeof(decltype(stat::st_size)) == sizeof(uint64_t), "fstat() doesn't support large files"); struct stat Stat; if (fstat(Fd, &Stat) == -1) { Ec = MakeErrorCodeFromLastError(); return 0; } return uint64_t(Stat.st_size); #endif } uint64_t FileSizeFromHandle(void* NativeHandle) { std::error_code Ec; uint64_t FileSize = FileSizeFromHandle(NativeHandle, Ec); if (Ec) { return ~0ull; } return FileSize; } uint64_t GetModificationTickFromHandle(void* NativeHandle, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS FILETIME LastWriteTime; BOOL OK = GetFileTime((HANDLE)NativeHandle, NULL, NULL, &LastWriteTime); if (OK) { return ((uint64_t(LastWriteTime.dwHighDateTime) << 32) | LastWriteTime.dwLowDateTime); } #elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC int Fd = int(uintptr_t(NativeHandle)); struct stat Stat; if (0 == fstat(Fd, &Stat)) { return gsl::narrow(Stat.st_mtime); } #endif Ec = MakeErrorCodeFromLastError(); return 0; } uint64_t GetModificationTickFromPath(const std::filesystem::path& Filename) { // PathFromHandle void* Handle; #if ZEN_PLATFORM_WINDOWS Handle = CreateFileW(Filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); if (Handle == INVALID_HANDLE_VALUE) { ThrowLastError(fmt::format("Failed to open file {} to check modification tick.", Filename)); } auto _ = MakeGuard([Handle]() { CloseHandle(Handle); }); std::error_code Ec; uint64_t ModificatonTick = GetModificationTickFromHandle(Handle, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to get modification tick for path '{}'", Filename.string())); } return ModificatonTick; #else struct stat Stat; int err = stat(Filename.native().c_str(), &Stat); if (err) { ThrowLastError(fmt::format("Failed to get mode of file {}", Filename)); } return gsl::narrow(Stat.st_mtime); #endif } bool TryGetFileProperties(const std::filesystem::path& Path, uint64_t& OutSize, uint64_t& OutModificationTick, uint32_t& OutNativeModeOrAttributes) { #if ZEN_PLATFORM_WINDOWS const std::filesystem::path::value_type* NativePath = Path.native().c_str(); { void* Handle = CreateFileW(NativePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); if (Handle == INVALID_HANDLE_VALUE) { return false; } auto _ = MakeGuard([Handle]() { CloseHandle(Handle); }); BY_HANDLE_FILE_INFORMATION Bhfh = {}; if (!GetFileInformationByHandle(Handle, &Bhfh)) { return false; } OutSize = uint64_t(Bhfh.nFileSizeHigh) << 32 | Bhfh.nFileSizeLow; OutModificationTick = ((uint64_t(Bhfh.ftLastWriteTime.dwHighDateTime) << 32) | Bhfh.ftLastWriteTime.dwLowDateTime); OutNativeModeOrAttributes = Bhfh.dwFileAttributes; return true; } #else struct stat Stat; int err = stat(Path.native().c_str(), &Stat); if (err) { return false; } OutModificationTick = gsl::narrow(Stat.st_mtime); OutSize = size_t(Stat.st_size); OutNativeModeOrAttributes = (uint32_t)Stat.st_mode; return true; #endif } void RenameFile(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath) { std::error_code Ec; RenameFile(SourcePath, TargetPath, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to rename path from '{}' to '{}'", SourcePath.string(), TargetPath.string())); } } void RenameFile(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS BOOL Success = ::MoveFileEx(SourcePath.native().c_str(), TargetPath.native().c_str(), MOVEFILE_REPLACE_EXISTING); if (!Success) { Ec = MakeErrorCodeFromLastError(); } #else return std::filesystem::rename(SourcePath, TargetPath, Ec); #endif // ZEN_PLATFORM_WINDOWS } void RenameDirectory(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath) { std::error_code Ec; RenameDirectory(SourcePath, TargetPath, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to rename directory from '{}' to '{}'", SourcePath.string(), TargetPath.string())); } } void RenameDirectory(const std::filesystem::path& SourcePath, const std::filesystem::path& TargetPath, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS BOOL Success = ::MoveFile(SourcePath.native().c_str(), TargetPath.native().c_str()); if (!Success) { Ec = MakeErrorCodeFromLastError(); } #else return std::filesystem::rename(SourcePath, TargetPath, Ec); #endif // ZEN_PLATFORM_WINDOWS } std::filesystem::path GetRunningExecutablePath() { #if ZEN_PLATFORM_WINDOWS // TODO: make this long path aware TCHAR ExePath[MAX_PATH]; DWORD PathLength = GetModuleFileName(NULL, ExePath, ZEN_ARRAY_COUNT(ExePath)); return {std::wstring_view(ExePath, PathLength)}; #elif ZEN_PLATFORM_LINUX char Link[256]; ssize_t BytesRead = readlink("/proc/self/exe", Link, sizeof(Link) - 1); if (BytesRead < 0) return {}; Link[BytesRead] = '\0'; return Link; #elif ZEN_PLATFORM_MAC char Buffer[PROC_PIDPATHINFO_MAXSIZE]; int SelfPid = GetCurrentProcessId(); if (proc_pidpath(SelfPid, Buffer, sizeof(Buffer)) <= 0) return {}; return Buffer; #endif // ZEN_PLATFORM_WINDOWS } void MaximizeOpenFileCount() { #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC struct rlimit Limit; int Error = getrlimit(RLIMIT_NOFILE, &Limit); if (Error) { ZEN_WARN("failed getting rlimit RLIMIT_NOFILE, reason '{}'", zen::MakeErrorCode(Error).message()); } else { struct rlimit NewLimit = Limit; NewLimit.rlim_cur = NewLimit.rlim_max; ZEN_DEBUG("changing RLIMIT_NOFILE from rlim_cur = {}, rlim_max {} to rlim_cur = {}, rlim_max {}", Limit.rlim_cur, Limit.rlim_max, NewLimit.rlim_cur, NewLimit.rlim_max); Error = setrlimit(RLIMIT_NOFILE, &NewLimit); if (Error != 0) { ZEN_WARN("failed to set RLIMIT_NOFILE limits from rlim_cur = {}, rlim_max {} to rlim_cur = {}, rlim_max {}, reason '{}'", Limit.rlim_cur, Limit.rlim_max, NewLimit.rlim_cur, NewLimit.rlim_max, zen::MakeErrorCode(Error).message()); } } #endif } bool PrepareFileForScatteredWrite(void* FileHandle, uint64_t FinalSize) { bool Result = true; #if ZEN_PLATFORM_WINDOWS BY_HANDLE_FILE_INFORMATION Information; if (GetFileInformationByHandle(FileHandle, &Information)) { if ((Information.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) == 0) { DWORD _ = 0; BOOL Ok = DeviceIoControl(FileHandle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &_, nullptr); if (!Ok) { std::error_code DummyEc; ZEN_DEBUG("Unable to set sparse mode for file '{}'", PathFromHandle(FileHandle, DummyEc)); Result = false; } } } FILE_ALLOCATION_INFO AllocationInfo = {}; AllocationInfo.AllocationSize.QuadPart = LONGLONG(FinalSize); if (!SetFileInformationByHandle(FileHandle, FileAllocationInfo, &AllocationInfo, DWORD(sizeof(AllocationInfo)))) { std::error_code DummyEc; ZEN_DEBUG("Unable to set file allocation size to {} for file '{}'", FinalSize, PathFromHandle(FileHandle, DummyEc)); Result = false; } #else // ZEN_PLATFORM_WINDOWS ZEN_UNUSED(FileHandle, FinalSize); #endif // ZEN_PLATFORM_WINDOWS return Result; } void GetDirectoryContent(const std::filesystem::path& RootDir, DirectoryContentFlags Flags, DirectoryContent& OutContent) { ZEN_ASSERT(EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::IncludeDirs)); ZEN_ASSERT(EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFiles) ? true : (!EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFileSizes))); FileSystemTraversal Traversal; struct Visitor : public FileSystemTraversal::TreeVisitor { Visitor(DirectoryContentFlags Flags, DirectoryContent& OutContent) : Flags(Flags), Content(OutContent) {} virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t FileSize, uint32_t NativeModeOrAttributes, uint64_t NativeModificationTick) override { if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFiles)) { Content.Files.push_back(Parent / File); if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFileSizes)) { Content.FileSizes.push_back(FileSize); } if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeAttributes)) { Content.FileAttributes.push_back(NativeModeOrAttributes); } if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeModificationTick)) { Content.FileModificationTicks.push_back(NativeModificationTick); } } } virtual bool VisitDirectory([[maybe_unused]] const std::filesystem::path& Parent, const path_view& DirectoryName, uint32_t NativeModeOrAttributes) override { if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeDirs)) { Content.Directories.push_back(Parent / DirectoryName); if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeAttributes)) { Content.DirectoryAttributes.push_back(NativeModeOrAttributes); } } return EnumHasAnyFlags(Flags, DirectoryContentFlags::Recursive); } const DirectoryContentFlags Flags; DirectoryContent& Content; } Visit(Flags, OutContent); Traversal.TraverseFileSystem(RootDir, Visit); } void GetDirectoryContent(const std::filesystem::path& RootDir, DirectoryContentFlags Flags, GetDirectoryContentVisitor& Visitor, WorkerThreadPool& WorkerPool, Latch& PendingWorkCount) { ZEN_ASSERT(EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::IncludeDirs)); ZEN_ASSERT(EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFiles) ? true : (!EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFileSizes))); struct MultithreadedVisitor : public FileSystemTraversal::TreeVisitor { MultithreadedVisitor(WorkerThreadPool& InWorkerPool, Latch& InOutPendingWorkCount, const std::filesystem::path& InRelativeRoot, DirectoryContentFlags InFlags, GetDirectoryContentVisitor* InVisitor) : WorkerPool(InWorkerPool) , PendingWorkCount(InOutPendingWorkCount) , RelativeRoot(InRelativeRoot) , Flags(InFlags) , Visitor(InVisitor) { } virtual void VisitFile(const std::filesystem::path&, const path_view& File, uint64_t FileSize, uint32_t NativeModeOrAttributes, uint64_t NativeModificationTick) override { if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFiles)) { Content.FileNames.push_back(File); if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeFileSizes)) { Content.FileSizes.push_back(FileSize); } if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeAttributes)) { Content.FileAttributes.push_back(NativeModeOrAttributes); } if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeModificationTick)) { Content.FileModificationTicks.push_back(NativeModificationTick); } } } virtual bool VisitDirectory(const std::filesystem::path& Parent, const path_view& DirectoryName, uint32_t NativeModeOrAttributes) override { std::filesystem::path Path = Parent / DirectoryName; if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeDirs)) { Content.DirectoryNames.push_back(DirectoryName); if (EnumHasAnyFlags(Flags, DirectoryContentFlags::IncludeAttributes)) { Content.DirectoryAttributes.push_back(NativeModeOrAttributes); } } if (EnumHasAnyFlags(Flags, DirectoryContentFlags::Recursive)) { if (Visitor->AsyncAllowDirectory(Parent, DirectoryName)) { PendingWorkCount.AddCount(1); try { WorkerPool.ScheduleWork( [WorkerPool = &WorkerPool, PendingWorkCount = &PendingWorkCount, Visitor = Visitor, Flags = Flags, Path = std::move(Path), RelativeRoot = RelativeRoot / DirectoryName]() { ZEN_ASSERT(Visitor); auto _ = MakeGuard([&]() { PendingWorkCount->CountDown(); }); try { MultithreadedVisitor SubVisitor(*WorkerPool, *PendingWorkCount, RelativeRoot, Flags, Visitor); FileSystemTraversal Traversal; Traversal.TraverseFileSystem(Path, SubVisitor); Visitor->AsyncVisitDirectory(SubVisitor.RelativeRoot, std::move(SubVisitor.Content)); } catch (const std::exception& Ex) { ZEN_ERROR("Failed scheduling work to scan subfolder '{}'. Reason: '{}'", Path / RelativeRoot, Ex.what()); } }, WorkerThreadPool::EMode::DisableBacklog); } catch (const std::exception& Ex) { ZEN_ERROR("Failed scheduling work to scan folder '{}'. Reason: '{}'", Path, Ex.what()); PendingWorkCount.CountDown(); } } } return false; } WorkerThreadPool& WorkerPool; Latch& PendingWorkCount; const std::filesystem::path RelativeRoot; const DirectoryContentFlags Flags; GetDirectoryContentVisitor::DirectoryContent Content; GetDirectoryContentVisitor* Visitor; } WrapperVisitor(WorkerPool, PendingWorkCount, {}, Flags, &Visitor); FileSystemTraversal Traversal; Traversal.TraverseFileSystem(RootDir, WrapperVisitor); Visitor.AsyncVisitDirectory(WrapperVisitor.RelativeRoot, std::move(WrapperVisitor.Content)); } std::string GetEnvVariable(std::string_view VariableName) { ZEN_ASSERT(!VariableName.empty()); #if ZEN_PLATFORM_WINDOWS std::vector EnvVariableBuffer(1023 + 1); DWORD RESULT = GetEnvironmentVariableA(std::string(VariableName).c_str(), EnvVariableBuffer.data(), (DWORD)EnvVariableBuffer.size()); if (RESULT == 0) { return ""; } if (RESULT <= EnvVariableBuffer.size()) { return std::string(EnvVariableBuffer.data(), size_t(RESULT)); } EnvVariableBuffer.resize(size_t(RESULT)); RESULT = GetEnvironmentVariableA(std::string(VariableName).c_str(), EnvVariableBuffer.data(), (DWORD)EnvVariableBuffer.size()); if (RESULT == 0) { return ""; } if (RESULT <= EnvVariableBuffer.size()) { return std::string(EnvVariableBuffer.data(), size_t(RESULT)); } #endif #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC char* EnvVariable = getenv(std::string(VariableName).c_str()); if (EnvVariable) { return std::string(EnvVariable); } #endif return ""; } std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles) { const std::filesystem::path BasePath(Filename.parent_path()); const std::string Stem(Filename.stem().string()); const std::string Extension(Filename.extension().string()); std::error_code Result; auto GetFileName = [&](size_t Index) -> std::filesystem::path { if (Index == 0) { return BasePath / (Stem + Extension); } return BasePath / fmt::format("{}.{}{}", Stem, Index, Extension); }; auto IsEmpty = [](const std::filesystem::path& Path, std::error_code& Ec) -> bool { bool Exists = IsFile(Path, Ec); if (Ec) { return false; } if (!Exists) { return true; } uintmax_t Size = FileSizeFromPath(Path, Ec); if (Ec) { return false; } return Size == 0; }; bool BaseIsEmpty = IsEmpty(GetFileName(0), Result); if (Result) { return Result; } if (!BaseIsEmpty) { // We try our best to rotate the logs, if we fail we fail and will try to open the base log file anyway for (auto i = MaxFiles; i > 0; i--) { std::filesystem::path src = GetFileName(i - 1); if (!IsFile(src)) { continue; } std::error_code DummyEc; std::filesystem::path target = GetFileName(i); if (IsFile(target, DummyEc)) { RemoveFile(target, DummyEc); } RenameFile(src, target, DummyEc); } } return Result; } std::error_code RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories) { const std::filesystem::path BasePath(DirectoryName.parent_path()); const std::string Stem(DirectoryName.stem().string()); auto GetPathForIndex = [&](size_t Index) -> std::filesystem::path { if (Index == 0) { return BasePath / Stem; } return BasePath / fmt::format("{}.{}", Stem, Index); }; auto IsEmpty = [](const std::filesystem::path& Path, std::error_code& Ec) -> bool { return std::filesystem::is_empty(Path, Ec); }; std::error_code Result; const bool BaseIsEmpty = IsEmpty(GetPathForIndex(0), Result); if (Result) { return Result; } if (BaseIsEmpty) return Result; for (std::size_t i = MaxDirectories; i > 0; i--) { const std::filesystem::path SourcePath = GetPathForIndex(i - 1); if (IsDir(SourcePath)) { std::filesystem::path TargetPath = GetPathForIndex(i); std::error_code DummyEc; if (IsDir(TargetPath, DummyEc)) { DeleteDirectories(TargetPath, DummyEc); } RenameDirectory(SourcePath, TargetPath, DummyEc); } } return Result; } std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName) { #if ZEN_PLATFORM_WINDOWS std::wstring Executable(Utf8ToWide(ExecutableName)); DWORD Result = SearchPathW(nullptr, Executable.c_str(), L".exe", 0, nullptr, nullptr); if (!Result) return ExecutableName; auto PathBuffer = std::make_unique_for_overwrite(Result); Result = SearchPathW(nullptr, Executable.c_str(), L".exe", Result, PathBuffer.get(), nullptr); if (!Result) return ExecutableName; return PathBuffer.get(); #else return ExecutableName; #endif } std::filesystem::path PickDefaultSystemRootDirectory() { #if ZEN_PLATFORM_WINDOWS // Pick sensible default PWSTR ProgramDataDir = nullptr; HRESULT hRes = SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &ProgramDataDir); if (SUCCEEDED(hRes)) { std::filesystem::path FinalPath(ProgramDataDir); FinalPath /= L"Epic\\Zen"; ::CoTaskMemFree(ProgramDataDir); return FinalPath; } return L""; #else // ZEN_PLATFORM_WINDOWS int UserId = getuid(); const passwd* Passwd = getpwuid(UserId); return std::filesystem::path(Passwd->pw_dir) / ".zen"; #endif // ZEN_PLATFORM_WINDOWS } #if ZEN_PLATFORM_WINDOWS uint32_t GetFileAttributesFromPath(const std::filesystem::path& Filename, std::error_code& Ec) { return WinGetFileAttributes(Filename, Ec); } uint32_t GetFileAttributesFromPath(const std::filesystem::path& Filename) { std::error_code Ec; uint32_t Result = GetFileAttributesFromPath(Filename, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("failed to get attributes of file '{}'", Filename.string())); } return Result; } void SetFileAttributesToPath(const std::filesystem::path& Filename, uint32_t Attributes, std::error_code& Ec) { if (::SetFileAttributes(Filename.native().c_str(), Attributes) == 0) { Ec = MakeErrorCodeFromLastError(); } } void SetFileAttributesToPath(const std::filesystem::path& Filename, uint32_t Attributes) { std::error_code Ec; zen::SetFileAttributesToPath(Filename, Attributes, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("failed to set attributes of file {}", Filename.string())); } } #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC uint32_t GetFileMode(const std::filesystem::path& Filename) { std::error_code Ec; uint32_t Result = GetFileMode(Filename, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to get mode of file {}", Filename)); } return Result; } uint32_t GetFileMode(const std::filesystem::path& Filename, std::error_code& Ec) { struct stat Stat; int err = stat(Filename.native().c_str(), &Stat); if (err) { Ec = MakeErrorCodeFromLastError(); return 0; } return (uint32_t)Stat.st_mode; } void SetFileMode(const std::filesystem::path& Filename, uint32_t Mode) { std::error_code Ec; SetFileMode(Filename, Mode, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("Failed to set mode of file {}", Filename)); } } void SetFileMode(const std::filesystem::path& Filename, uint32_t Mode, std::error_code& Ec) { int err = chmod(Filename.native().c_str(), (mode_t)Mode); if (err) { Ec = MakeErrorCodeFromLastError(); } } #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC bool SetFileReadOnly(const std::filesystem::path& Filename, bool ReadOnly, std::error_code& Ec) { #if ZEN_PLATFORM_WINDOWS uint32_t CurrentAttributes = GetFileAttributesFromPath(Filename, Ec); if (Ec) { return false; } if (CurrentAttributes == INVALID_FILE_ATTRIBUTES) { Ec = MakeErrorCode(ERROR_FILE_NOT_FOUND); return false; } uint32_t NewAttributes = MakeFileAttributeReadOnly(CurrentAttributes, ReadOnly); if (CurrentAttributes != NewAttributes) { SetFileAttributesToPath(Filename, NewAttributes, Ec); if (Ec) { return false; } return true; } #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC uint32_t CurrentMode = GetFileMode(Filename, Ec); if (Ec) { return false; } uint32_t NewMode = MakeFileModeReadOnly(CurrentMode, ReadOnly); if (CurrentMode != NewMode) { SetFileMode(Filename, NewMode, Ec); if (Ec) { return false; } return true; } #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC return false; } bool SetFileReadOnly(const std::filesystem::path& Filename, bool ReadOnly) { std::error_code Ec; bool Result = SetFileReadOnly(Filename, ReadOnly, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("failed to set read only mode of file '{}'", Filename.string())); } return Result; } void MakeSafeAbsolutePathInPlace(std::filesystem::path& Path) { if (!Path.empty()) { std::filesystem::path AbsolutePath = std::filesystem::absolute(Path).make_preferred(); #if ZEN_PLATFORM_WINDOWS const std::string_view Prefix = "\\\\?\\"; const std::u8string PrefixU8(Prefix.begin(), Prefix.end()); std::u8string PathString = AbsolutePath.u8string(); if (!PathString.empty() && !PathString.starts_with(PrefixU8)) { PathString.insert(0, PrefixU8); Path = PathString; } #endif // ZEN_PLATFORM_WINDOWS } } std::filesystem::path MakeSafeAbsolutePath(const std::filesystem::path& Path) { std::filesystem::path Tmp(Path); MakeSafeAbsolutePathInPlace(Tmp); return Tmp; } class SharedMemoryImpl : public SharedMemory { public: struct Data { void* Handle = nullptr; void* DataPtr = nullptr; size_t Size = 0; std::string Name; }; static Data Open(std::string_view Name, size_t Size, bool SystemGlobal) { #if ZEN_PLATFORM_WINDOWS std::wstring InstanceMapName = Utf8ToWide(fmt::format("{}\\{}", SystemGlobal ? "Global" : "Local", Name)); HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, InstanceMapName.c_str()); if (hMap == NULL) { return {}; } void* pBuf = MapViewOfFile(hMap, // handle to map object FILE_MAP_ALL_ACCESS, // read/write permission 0, // offset high 0, // offset low DWORD(Size)); // size if (pBuf == NULL) { CloseHandle(hMap); } return Data{.Handle = hMap, .DataPtr = pBuf, .Size = Size, .Name = std::string(Name)}; #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC ZEN_UNUSED(SystemGlobal); std::string InstanceMapName = fmt::format("/{}", Name); int Fd = shm_open(InstanceMapName.c_str(), O_RDWR, 0666); if (Fd < 0) { return {}; } void* hMap = (void*)intptr_t(Fd); struct stat Stat; fstat(Fd, &Stat); if (size_t(Stat.st_size) < Size) { close(Fd); return {}; } void* pBuf = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_SHARED, Fd, 0); if (pBuf == MAP_FAILED) { close(Fd); return {}; } return Data{.Handle = hMap, .DataPtr = pBuf, .Size = Size, .Name = std::string(Name)}; #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC } static Data Create(std::string_view Name, size_t Size, bool SystemGlobal) { #if ZEN_PLATFORM_WINDOWS std::wstring InstanceMapName = Utf8ToWide(fmt::format("{}\\{}", SystemGlobal ? "Global" : "Local", Name)); SECURITY_ATTRIBUTES m_Attributes{}; SECURITY_DESCRIPTOR m_Sd{}; m_Attributes.nLength = sizeof m_Attributes; m_Attributes.bInheritHandle = false; // Disable inheritance const BOOL Success = InitializeSecurityDescriptor(&m_Sd, SECURITY_DESCRIPTOR_REVISION); if (Success) { if (!SetSecurityDescriptorDacl(&m_Sd, TRUE, (PACL)NULL, FALSE)) { ThrowLastError("SetSecurityDescriptorDacl failed"); } m_Attributes.lpSecurityDescriptor = &m_Sd; } HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, // use paging file &m_Attributes, // allow anyone to access PAGE_READWRITE, // read/write access 0, // maximum object size (high-order DWORD) DWORD(Size), // maximum object size (low-order DWORD) InstanceMapName.c_str()); if (hMap == NULL) { return {}; } void* pBuf = MapViewOfFile(hMap, // handle to map object FILE_MAP_ALL_ACCESS, // read/write permission 0, // offset high 0, // offset low DWORD(Size)); // size if (pBuf == NULL) { CloseHandle(hMap); return {}; } return Data{.Handle = hMap, .DataPtr = pBuf, .Size = Size, .Name = std::string(Name)}; #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC ZEN_UNUSED(SystemGlobal); std::string InstanceMapName = fmt::format("/{}", Name); int Fd = shm_open(InstanceMapName.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666); if (Fd < 0) { return {}; } fchmod(Fd, 0666); void* hMap = (void*)intptr_t(Fd); int Result = ftruncate(Fd, Size); ZEN_UNUSED(Result); void* pBuf = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_SHARED, Fd, 0); if (pBuf == MAP_FAILED) { close(Fd); return {}; } return Data{.Handle = hMap, .DataPtr = pBuf, .Size = Size, .Name = std::string(Name)}; #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC } static void Close(Data&& MemMap, bool Delete) { #if ZEN_PLATFORM_WINDOWS ZEN_UNUSED(Delete); if (MemMap.DataPtr != nullptr) { UnmapViewOfFile(MemMap.DataPtr); MemMap.DataPtr = nullptr; } if (MemMap.Handle != nullptr) { CloseHandle(MemMap.Handle); MemMap.Handle = nullptr; } #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC if (MemMap.DataPtr != nullptr) { munmap(MemMap.DataPtr, MemMap.Size); MemMap.DataPtr = nullptr; } if (MemMap.Handle != nullptr) { int Fd = int(intptr_t(MemMap.Handle)); close(Fd); MemMap.Handle = nullptr; } if (Delete) { std::string InstanceMapName = fmt::format("/{}", MemMap.Name); shm_unlink(InstanceMapName.c_str()); } #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC } SharedMemoryImpl(Data&& MemMap, bool IsOwned) : m_MemMap(std::move(MemMap)), m_IsOwned(IsOwned) {} virtual ~SharedMemoryImpl() { try { Close(std::move(m_MemMap), /*Delete*/ m_IsOwned); } catch (const std::exception& Ex) { ZEN_ERROR("SharedMemoryImpl::~SharedMemoryImpl threw exception: {}", Ex.what()); } } virtual void* GetData() override { return m_MemMap.DataPtr; } private: Data m_MemMap; const bool m_IsOwned = false; }; std::unique_ptr OpenSharedMemory(std::string_view Name, size_t Size, bool SystemGlobal) { SharedMemoryImpl::Data MemMap = SharedMemoryImpl::Open(Name, Size, SystemGlobal); if (MemMap.DataPtr) { return std::make_unique(std::move(MemMap), /*IsOwned*/ false); } return {}; } std::unique_ptr CreateSharedMemory(std::string_view Name, size_t Size, bool SystemGlobal) { SharedMemoryImpl::Data MemMap = SharedMemoryImpl::Create(Name, Size, SystemGlobal); if (MemMap.DataPtr) { return std::make_unique(std::move(MemMap), /*IsOwned*/ true); } return {}; } ////////////////////////////////////////////////////////////////////////// // // Testing related code follows... // #if ZEN_WITH_TESTS void filesystem_forcelink() { } TEST_CASE("filesystem") { using namespace std::filesystem; // GetExePath -- this is not a great test as it's so dependent on where the this code gets linked in path BinPath = GetRunningExecutablePath(); const bool ExpectedExe = PathToUtf8(BinPath.stem().native()).ends_with("-test"sv) || BinPath.stem() == "zenserver"; CHECK(ExpectedExe); CHECK(IsFile(BinPath)); // PathFromHandle void* Handle; # if ZEN_PLATFORM_WINDOWS Handle = CreateFileW(BinPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); CHECK(Handle != INVALID_HANDLE_VALUE); # else int Fd = open(BinPath.c_str(), O_RDONLY | O_CLOEXEC); CHECK(Fd >= 0); Handle = (void*)uintptr_t(Fd); # endif std::error_code Ec; auto FromHandle = PathFromHandle((void*)uintptr_t(Handle), Ec); CHECK(!Ec); CHECK(equivalent(FromHandle, BinPath)); # if ZEN_PLATFORM_WINDOWS CloseHandle(Handle); # else close(int(uintptr_t(Handle))); # endif // Traversal struct : public FileSystemTraversal::TreeVisitor { virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t, uint32_t, uint64_t) override { // std::filesystem::equivalent is *very* expensive on Windows, filter out unlikely candidates if (ExpectedFilename == ToLower(std::filesystem::path(File).string())) { bFoundExpected |= std::filesystem::equivalent(Parent / File, Expected); } } virtual bool VisitDirectory(const std::filesystem::path&, const path_view&, uint32_t) override { return true; } bool bFoundExpected = false; std::string ExpectedFilename; std::filesystem::path Expected; } Visitor; Visitor.ExpectedFilename = ToLower(BinPath.filename().string()); Visitor.Expected = BinPath; FileSystemTraversal().TraverseFileSystem(BinPath.parent_path().parent_path(), Visitor); CHECK(Visitor.bFoundExpected); // Scan/read file FileContents BinRead = ReadFile(BinPath); std::vector BinScan; ScanFile(BinPath, 16 << 10, [&](const void* Data, size_t Size) { const auto* Ptr = (uint8_t*)Data; BinScan.insert(BinScan.end(), Ptr, Ptr + Size); }); CHECK_EQ(BinRead.Data.size(), 1); CHECK_EQ(BinScan.size(), BinRead.Data[0].GetSize()); } TEST_CASE("Filesystem.Basics") { std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".test"; CleanDirectory(TestBaseDir, true); DeleteDirectories(TestBaseDir); CHECK(!IsDir(TestBaseDir)); CHECK(CleanDirectory(TestBaseDir, false)); CHECK(IsDir(TestBaseDir)); CHECK(!CleanDirectory(TestBaseDir, false)); CHECK(!IsDir(TestBaseDir / "no_such_thing")); CHECK(!IsDir("hgjda/cev_/q12")); CHECK(!IsFile(TestBaseDir)); CHECK(!IsFile(TestBaseDir / "no_such_thing")); CHECK(!IsFile("hgjda/cev_/q12")); CHECK_THROWS(FileSizeFromPath(TestBaseDir) == 0); CHECK_THROWS(FileSizeFromPath(TestBaseDir / "no_such_file")); CHECK(!CreateDirectories(TestBaseDir)); CHECK(CreateDirectories(TestBaseDir / "nested" / "a" / "bit" / "deep")); CHECK(!CreateDirectories(TestBaseDir / "nested" / "a" / "bit" / "deep")); CHECK(IsDir(TestBaseDir / "nested" / "a" / "bit" / "deep")); CHECK(IsDir(TestBaseDir / "nested" / "a" / "bit")); CHECK(!IsDir(TestBaseDir / "nested" / "a" / "bit" / "deep" / "no")); CHECK_THROWS(WriteFile(TestBaseDir / "nested" / "a", IoBuffer(20))); CHECK_NOTHROW(WriteFile(TestBaseDir / "nested" / "a" / "yo", IoBuffer(20))); CHECK(IsFile(TestBaseDir / "nested" / "a" / "yo")); CHECK(FileSizeFromPath(TestBaseDir / "nested" / "a" / "yo") == 20); CHECK(!IsFile(TestBaseDir / "nested" / "a")); CHECK(DeleteDirectories(TestBaseDir / "nested" / "a" / "bit")); CHECK(IsFile(TestBaseDir / "nested" / "a" / "yo")); CHECK(!IsDir(TestBaseDir / "nested" / "a" / "bit")); CHECK(!DeleteDirectories(TestBaseDir / "nested" / "a" / "bit")); CHECK(IsDir(TestBaseDir / "nested" / "a")); CHECK(DeleteDirectories(TestBaseDir / "nested")); CHECK(!IsFile(TestBaseDir / "nested" / "a" / "yo")); CHECK(CreateDirectories(TestBaseDir / "nested" / "deeper")); CHECK_NOTHROW(WriteFile(TestBaseDir / "nested" / "deeper" / "yo", IoBuffer(20))); CHECK_NOTHROW(RenameDirectory(TestBaseDir / "nested" / "deeper", TestBaseDir / "new_place")); CHECK(IsFile(TestBaseDir / "new_place" / "yo")); CHECK(FileSizeFromPath(TestBaseDir / "new_place" / "yo") == 20); CHECK(IsDir(TestBaseDir / "new_place")); CHECK(!IsFile(TestBaseDir / "new_place")); CHECK_THROWS(RenameDirectory(TestBaseDir / "nested" / "deeper", TestBaseDir / "new_place")); CHECK(!RemoveDir(TestBaseDir / "nested" / "deeper")); CHECK(RemoveFile(TestBaseDir / "new_place" / "yo")); CHECK(!IsFile(TestBaseDir / "new_place" / "yo")); CHECK_THROWS(FileSizeFromPath(TestBaseDir / "new_place" / "yo")); CHECK(!RemoveFile(TestBaseDir / "new_place" / "yo")); CHECK_THROWS(RemoveFile(TestBaseDir / "nested")); CHECK_THROWS(RemoveDir(TestBaseDir)); CHECK_NOTHROW(WriteFile(TestBaseDir / "yo", IoBuffer(20))); CHECK_NOTHROW(RenameFile(TestBaseDir / "yo", TestBaseDir / "new_place" / "yo")); CHECK(!IsFile(TestBaseDir / "yo")); CHECK(IsFile(TestBaseDir / "new_place" / "yo")); CHECK(FileSizeFromPath(TestBaseDir / "new_place" / "yo") == 20); CHECK_THROWS(RemoveDir(TestBaseDir / "new_place" / "yo")); CHECK(DeleteDirectories(TestBaseDir)); CHECK(!IsFile(TestBaseDir / "new_place" / "yo")); CHECK(!IsDir(TestBaseDir)); CHECK(!IsDir(TestBaseDir / "nested")); CHECK(CreateDirectories(TestBaseDir / "nested")); CHECK_NOTHROW(WriteFile(TestBaseDir / "nested" / "readonly", IoBuffer(20))); CHECK(SetFileReadOnly(TestBaseDir / "nested" / "readonly", true)); CHECK_THROWS(RemoveFile(TestBaseDir / "nested" / "readonly")); CHECK_THROWS(CleanDirectory(TestBaseDir, false)); CHECK(SetFileReadOnly(TestBaseDir / "nested" / "readonly", false)); CHECK(RemoveFile(TestBaseDir / "nested" / "readonly")); CHECK(!CleanDirectory(TestBaseDir, false)); CHECK_NOTHROW(WriteFile(TestBaseDir / "nested" / "readonly", IoBuffer(20))); CHECK(SetFileReadOnly(TestBaseDir / "nested" / "readonly", true)); CHECK(!CleanDirectory(TestBaseDir / "nested", true)); CHECK(!CleanDirectory(TestBaseDir, false)); CHECK(RemoveDir(TestBaseDir)); } TEST_CASE("WriteFile") { std::filesystem::path TempFile = GetRunningExecutablePath().parent_path(); TempFile /= "write_file_test"; uint64_t Magics[] = { 0x0'a9e'a9e'a9e'a9e'a9e, 0x0'493'493'493'493'493, }; struct { const void* Data; size_t Size; } MagicTests[] = { { Magics, sizeof(Magics), }, { Magics + 1, sizeof(Magics[0]), }, }; for (auto& MagicTest : MagicTests) { WriteFile(TempFile, IoBuffer(IoBuffer::Wrap, MagicTest.Data, MagicTest.Size)); FileContents MagicsReadback = ReadFile(TempFile); CHECK_EQ(MagicsReadback.Data.size(), 1); CHECK_EQ(MagicsReadback.Data[0].GetSize(), MagicTest.Size); CHECK_EQ(memcmp(MagicTest.Data, MagicsReadback.Data[0].Data(), MagicTest.Size), 0); } RemoveFile(TempFile); } TEST_CASE("DiskSpaceInfo") { std::filesystem::path BinPath = GetRunningExecutablePath(); DiskSpace Space = {}; std::error_code Error; Space = DiskSpaceInfo(BinPath, Error); CHECK(!Error); bool Okay = DiskSpaceInfo(BinPath, Space); CHECK(Okay); CHECK(int64_t(Space.Total) > 0); CHECK(int64_t(Space.Free) > 0); // Hopefully there's at least one byte free } TEST_CASE("PathBuilder") { # if ZEN_PLATFORM_WINDOWS const char* foo_bar = "/foo\\bar"; # else const char* foo_bar = "/foo/bar"; # endif ExtendablePathBuilder<32> Path; for (const char* Prefix : {"/foo", "/foo/"}) { Path.Reset(); Path.Append(Prefix); Path /= "bar"; CHECK(Path.ToPath() == foo_bar); } using fspath = std::filesystem::path; Path.Reset(); Path.Append(fspath("/foo/")); Path /= (fspath("bar")); CHECK(Path.ToPath() == foo_bar); # if ZEN_PLATFORM_WINDOWS Path.Reset(); Path.Append(fspath(L"/\u0119oo/")); Path /= L"bar"; CHECK(Path.ToView() == L"/\u0119oo/bar"); CHECK(Path.ToPath() == L"\\\u0119oo\\bar"); # endif } TEST_CASE("RotateDirectories") { std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".test"; CleanDirectory(TestBaseDir, false); std::filesystem::path RotateDir = TestBaseDir / "rotate_dir" / "dir_to_rotate"; IoBuffer DummyFileData = IoBufferBuilder::MakeCloneFromMemory("blubb", 5); auto NewDir = [&] { CreateDirectories(RotateDir); WriteFile(RotateDir / ".placeholder", DummyFileData); }; auto DirWithSuffix = [&](int Index) -> std::filesystem::path { return RotateDir.generic_string().append(fmt::format(".{}", Index)); }; const int RotateMax = 10; NewDir(); CHECK(IsDir(RotateDir)); RotateDirectories(RotateDir, RotateMax); CHECK(!IsDir(RotateDir)); CHECK(IsDir(DirWithSuffix(1))); NewDir(); CHECK(IsDir(RotateDir)); RotateDirectories(RotateDir, RotateMax); CHECK(!IsDir(RotateDir)); CHECK(IsDir(DirWithSuffix(1))); CHECK(IsDir(DirWithSuffix(2))); for (int i = 0; i < RotateMax; ++i) { NewDir(); std::error_code Ec = RotateDirectories(RotateDir, 10); const bool IsError = !!Ec; CHECK_EQ(IsError, false); } CHECK(!IsDir(RotateDir)); for (int i = 0; i < RotateMax; ++i) { CHECK(IsDir(DirWithSuffix(i + 1))); } for (int i = RotateMax; i < RotateMax + 5; ++i) { CHECK(!IsDir(DirWithSuffix(RotateMax + i + 1))); } } TEST_CASE("SharedMemory") { CHECK(!OpenSharedMemory("SharedMemoryTest0", 482, false)); CHECK(!OpenSharedMemory("SharedMemoryTest0", 482, true)); { auto Mem0 = CreateSharedMemory("SharedMemoryTest0", 482, false); CHECK(Mem0); strcpy((char*)Mem0->GetData(), "this is the string we are looking for"); auto Mem1 = OpenSharedMemory("SharedMemoryTest0", 482, false); CHECK_EQ(std::string((char*)Mem0->GetData()), std::string((char*)Mem1->GetData())); } CHECK(!OpenSharedMemory("SharedMemoryTest0", 482, false)); } #endif } // namespace zen