// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include #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 #endif #if ZEN_PLATFORM_MAC # 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* Dir) { // This may be suboptimal, in that it appears to try and create directories // from the root on up instead of from some directory which is known to // be present // // We should implement a smarter version at some point since this can be // pretty expensive in aggregate return std::filesystem::create_directories(Dir); } // Erase all files and directories in a given directory, leaving an empty directory // behind 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 = DeleteDirectories(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 DeleteDirectories(const wchar_t* DirPath) { const bool KeepDotFiles = false; return WipeDirectory(DirPath, KeepDotFiles) && RemoveDirectoryW(DirPath) == TRUE; } bool CleanDirectory(const wchar_t* DirPath) { if (std::filesystem::exists(DirPath)) { const bool KeepDotFiles = false; return WipeDirectory(DirPath, KeepDotFiles); } return CreateDirectories(DirPath); } bool CleanDirectory(const wchar_t* DirPath, bool KeepDotFiles) { if (std::filesystem::exists(DirPath)) { return WipeDirectory(DirPath, KeepDotFiles); } return CreateDirectories(DirPath); } #endif // ZEN_PLATFORM_WINDOWS bool CreateDirectories(const std::filesystem::path& Dir) { if (Dir.string().ends_with(":")) { return false; } while (!std::filesystem::is_directory(Dir)) { if (Dir.has_parent_path()) { CreateDirectories(Dir.parent_path()); } std::error_code ErrorCode; std::filesystem::create_directory(Dir, ErrorCode); if (ErrorCode) { throw std::system_error(ErrorCode, fmt::format("Failed to create directories for '{}'", Dir.string())); } return true; } return false; } bool DeleteDirectories(const std::filesystem::path& Dir) { #if ZEN_PLATFORM_WINDOWS return DeleteDirectories(Dir.c_str()); #else std::error_code ErrorCode; return std::filesystem::remove_all(Dir, ErrorCode); #endif } bool CleanDirectory(const std::filesystem::path& Dir) { #if ZEN_PLATFORM_WINDOWS return CleanDirectory(Dir.c_str()); #else if (std::filesystem::exists(Dir)) { bool Success = true; std::error_code ErrorCode; for (const auto& Item : std::filesystem::directory_iterator(Dir)) { Success &= std::filesystem::remove_all(Item, ErrorCode); } return Success; } return CreateDirectories(Dir); #endif } bool CleanDirectoryExceptDotFiles(const std::filesystem::path& Dir) { #if ZEN_PLATFORM_WINDOWS const bool KeepDotFiles = true; return CleanDirectory(Dir.c_str(), KeepDotFiles); #else ZEN_UNUSED(Dir); 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 } static bool CloneFile(std::filesystem::path FromPath, 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; } 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; } 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; } // 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(); TargetFile.Close(); DeleteFileW(ToPath.c_str()); 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; #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); ZEN_ERROR("CloneFile() is not implemented on this platform"); return false; #elif ZEN_PLATFORM_MAC /* clonefile() syscall if APFS */ ZEN_UNUSED(FromPath, ToPath); ZEN_ERROR("CloneFile() is not implemented on this platform"); return false; #endif // ZEN_PLATFORM_WINDOWS } void CopyFile(std::filesystem::path FromPath, 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(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options) { bool Success = false; if (Options.EnableClone) { Success = CloneFile(FromPath.native(), ToPath.native()); if (Success) { return true; } } if (Options.MustClone) { 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_EXCL | O_CLOEXEC, 0666); if (ToFd < 0) { ThrowLastError(fmt::format("failed to create file {}", ToPath)); } fchmod(ToFd, 0666); ScopedFd $To = {ToFd}; // Copy impl static const size_t BufferSize = 64 << 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) != BufferSize) { Success = false; break; } } free(Buffer); #endif // ZEN_PLATFORM_WINDOWS if (!Success) { ThrowLastError("file copy failed"sv); } return true; } void CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options) { // Validate arguments if (FromPath.empty() || !std::filesystem::is_directory(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 (std::filesystem::exists(ToPath)) { if (!std::filesystem::is_directory(ToPath)) { throw std::runtime_error(fmt::format("specified CopyTree target '{}' is not a directory", ToPath)); } } else { std::filesystem::create_directories(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) 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&) 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(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; std::filesystem::remove(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; std::filesystem::remove(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); } } Memory::Free(RenameInfo); if (!Success) { return false; } #elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC std::filesystem::path SourcePath = PathFromHandle(FileRef.FileHandle); int Ret = link(SourcePath.c_str(), Path.c_str()); if (Ret < 0) { int32_t err = errno; if (err == ENOENT) { zen::CreateDirectories(Path.parent_path()); Ret = link(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(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; } ZENCORE_API 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 uint64_t 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 (hRes == ERROR_FILE_NOT_FOUND || 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, sizeof FileInfoBuffer); 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) + 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); 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); } 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)) { TraverseFileSystem(FullPath, Visitor); } } else if (S_ISREG(Stat.st_mode)) { Visitor.VisitFile(RootDir, FileName, Stat.st_size); } 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 } 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 } std::filesystem::path PathFromHandle(void* NativeHandle) { std::error_code Ec; std::filesystem::path Result = PathFromHandle(NativeHandle, Ec); if (Ec) { throw std::system_error(Ec, fmt::format("failed to get path from file handle '{}'", NativeHandle)); } return Result; } uint64_t FileSizeFromHandle(void* NativeHandle) { uint64_t FileSize = ~0ull; #if ZEN_PLATFORM_WINDOWS BY_HANDLE_FILE_INFORMATION Bhfh = {}; if (GetFileInformationByHandle(NativeHandle, &Bhfh)) { FileSize = uint64_t(Bhfh.nFileSizeHigh) << 32 | Bhfh.nFileSizeLow; } #else int Fd = int(intptr_t(NativeHandle)); struct stat Stat; fstat(Fd, &Stat); FileSize = size_t(Stat.st_size); #endif return FileSize; } 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 } void GetDirectoryContent(const std::filesystem::path& RootDir, uint8_t Flags, DirectoryContent& OutContent) { FileSystemTraversal Traversal; struct Visitor : public FileSystemTraversal::TreeVisitor { Visitor(uint8_t Flags, DirectoryContent& OutContent) : Flags(Flags), Content(OutContent) {} virtual void VisitFile([[maybe_unused]] const std::filesystem::path& Parent, [[maybe_unused]] const path_view& File, [[maybe_unused]] uint64_t FileSize) override { if (Flags & DirectoryContent::IncludeFilesFlag) { Content.Files.push_back(Parent / File); } } virtual bool VisitDirectory([[maybe_unused]] const std::filesystem::path& Parent, const path_view& DirectoryName) override { if (Flags & DirectoryContent::IncludeDirsFlag) { Content.Directories.push_back(Parent / DirectoryName); } return (Flags & DirectoryContent::RecursiveFlag) != 0; } const uint8_t Flags; DirectoryContent& Content; } Visit(Flags, OutContent); Traversal.TraverseFileSystem(RootDir, Visit); } 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 = std::filesystem::exists(Path, Ec); if (Ec) { return false; } if (!Exists) { return true; } uintmax_t Size = std::filesystem::file_size(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 (!std::filesystem::exists(src)) { continue; } std::error_code DummyEc; std::filesystem::path target = GetFileName(i); if (std::filesystem::exists(target, DummyEc)) { std::filesystem::remove(target, DummyEc); } std::filesystem::rename(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 (std::filesystem::exists(SourcePath)) { std::filesystem::path TargetPath = GetPathForIndex(i); std::error_code DummyEc; if (std::filesystem::exists(TargetPath, DummyEc)) { std::filesystem::remove_all(TargetPath, DummyEc); } std::filesystem::rename(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 } ////////////////////////////////////////////////////////////////////////// // // 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(is_regular_file(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 auto FromHandle = PathFromHandle((void*)uintptr_t(Handle)); 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) override { bFoundExpected |= std::filesystem::equivalent(Parent / File, Expected); } virtual bool VisitDirectory(const std::filesystem::path&, const path_view&) override { return true; } bool bFoundExpected = false; std::filesystem::path Expected; } Visitor; 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("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); } std::filesystem::remove(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"; printf("%ls\n", Path.ToPath().c_str()); 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); 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(std::filesystem::exists(RotateDir)); RotateDirectories(RotateDir, RotateMax); CHECK(!std::filesystem::exists(RotateDir)); CHECK(std::filesystem::exists(DirWithSuffix(1))); NewDir(); CHECK(std::filesystem::exists(RotateDir)); RotateDirectories(RotateDir, RotateMax); CHECK(!std::filesystem::exists(RotateDir)); CHECK(std::filesystem::exists(DirWithSuffix(1))); CHECK(std::filesystem::exists(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(!std::filesystem::exists(RotateDir)); for (int i = 0; i < RotateMax; ++i) { CHECK(std::filesystem::exists(DirWithSuffix(i + 1))); } for (int i = RotateMax; i < RotateMax + 5; ++i) { CHECK(!std::filesystem::exists(DirWithSuffix(RotateMax + i + 1))); } } #endif } // namespace zen