// Copyright Epic Games, Inc. All Rights Reserved. #include "UnsyncFile.h" #include "UnsyncCore.h" #include "UnsyncFilter.h" #include "UnsyncHash.h" #include "UnsyncMemory.h" #include "UnsyncScheduler.h" #include "UnsyncThread.h" #include #if UNSYNC_PLATFORM_UNIX # include # include # include # include #endif // UNSYNC_PLATFORM_UNIX #if UNSYNC_PLATFORM_WINDOWS UNSYNC_THIRD_PARTY_INCLUDES_START # include UNSYNC_THIRD_PARTY_INCLUDES_END #endif // UNSYNC_PLATFORM_WINDOWS namespace unsync { bool GForceBufferedFiles = false; // Windows epoch : 1601-01-01T00:00:00Z // Unix epoch : 1970-01-01T00:00:00Z static constexpr uint64 SECONDS_BETWEEN_WINDOWS_AND_UNIX = 11'644'473'600ull; static constexpr uint64 NANOS_PER_WINDOWS_TICK = 100ull; static constexpr uint64 WINDOWS_TICKS_PER_SECOND = 1'000'000'000ull / NANOS_PER_WINDOWS_TICK; // each tick is 100ns // Returns extended absolute path of a form \\?\D:\verylongpath or \\?\UNC\servername\verylongpath // Expects an absolute path input. Returns original path on non-Windows. // https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation FPath MakeExtendedAbsolutePath(const FPath& InAbsolutePath) { if (InAbsolutePath.empty()) { return FPath(); } #if UNSYNC_PLATFORM_WINDOWS UNSYNC_ASSERTF(InAbsolutePath.is_absolute(), L"Input path '%ls' must be absolute", InAbsolutePath.wstring().c_str()); const std::wstring& InFilenameString = InAbsolutePath.native(); if (InFilenameString.starts_with(L"\\\\?\\")) { return InAbsolutePath; } else if (InFilenameString.starts_with(L"\\\\")) { return std::wstring(L"\\\\?\\UNC\\") + InFilenameString.substr(2); } else { return std::wstring(L"\\\\?\\") + InFilenameString; } #else // UNSYNC_PLATFORM_WINDOWS return InAbsolutePath; #endif // UNSYNC_PLATFORM_WINDOWS } // Removes \\, \\?\UNC\, \\.\UNC\, \\.\ or \\?\ prefix from a path. // \\server\foo\bar -> server\foo\bar // \\?\d:\foo\bar -> d:\foo\bar // d:\foo\bar -> d:\foo\bar // Returns original path on non-Windows. static inline FPathStringView RemoveUNCPrefix(const FPath& InPath) { FPathStringView InPathString = InPath.native(); #if UNSYNC_PLATFORM_WINDOWS if (InPathString.starts_with(L"\\\\?\\UNC\\")) { return InPathString.substr(8); } else if (InPathString.starts_with(L"\\\\?\\")) { return InPathString.substr(4); } else if (InPathString.starts_with(L"\\\\")) { return InPathString.substr(2); } else #endif { return InPathString; } } FPath RemoveExtendedPathPrefix(const FPath& InPath) { FPathStringView InPathString = InPath.native(); #if UNSYNC_PLATFORM_WINDOWS if (InPathString.starts_with(L"\\\\?\\UNC\\")) { FPathStringView Remainder = InPathString.substr(8); std::wstring Result; Result.reserve(Remainder.length() + 2); Result += L"\\\\"; Result += Remainder; return FPath(Result); } else if (InPathString.starts_with(L"\\\\?\\")) { return InPathString.substr(4); } else { return InPathString; } #else // UNSYNC_PLATFORM_WINDOWS return InPathString; #endif // UNSYNC_PLATFORM_WINDOWS } std::filesystem::file_time_type FromWindowsFileTime(uint64 Ticks) { using FileTimeDuration = std::filesystem::file_time_type::duration; uint64 RawSeconds = Ticks / WINDOWS_TICKS_PER_SECOND; uint64 RawSubsecondTicks = Ticks - (RawSeconds * WINDOWS_TICKS_PER_SECOND); uint64 RawSubsecondNanos = RawSubsecondTicks * NANOS_PER_WINDOWS_TICK; #if UNSYNC_PLATFORM_WINDOWS FileTimeDuration Seconds = std::chrono::duration_cast(std::chrono::seconds(RawSeconds)); #else // UNSYNC_PLATFORM_WINDOWS FileTimeDuration Seconds = std::chrono::seconds(RawSeconds - SECONDS_BETWEEN_WINDOWS_AND_UNIX); #endif // UNSYNC_PLATFORM_WINDOWS FileTimeDuration SubsecondNanos = std::chrono::duration_cast(std::chrono::nanoseconds(RawSubsecondNanos)); FileTimeDuration DurationFromNativeEpoch = Seconds + SubsecondNanos; std::filesystem::file_time_type Result(DurationFromNativeEpoch); return Result; } FPath GetRelativePath(const FPath& Path, const FPath& Base) { FPathStringView ResultView = GetRelativePathView(Path, Base); return ResultView; } FPathStringView GetRelativePathView(const FPath& Path, const FPath& Base) { // Try a trivial case first, without touching the filesystem FPathStringView PathView = RemoveUNCPrefix(Path); FPathStringView BaseView = RemoveUNCPrefix(Base); if (PathView.starts_with(BaseView)) { FPathStringView PathViewRemainder = PathView.substr(BaseView.length()); if (PathViewRemainder.starts_with(FPath::preferred_separator)) { FPathStringView RelativePath = PathView.substr(BaseView.length()); while (RelativePath.starts_with(FPath::preferred_separator)) { RelativePath = RelativePath.substr(1); } return RelativePath; } } return {}; } void ConvertDirectorySeparatorsToNative(std::string& Path) { std::replace_if(Path.begin(), Path.end(), [](char C) { return C == '/' || C == '\\'; }, PATH_SEPARATOR); } void ConvertDirectorySeparatorsToUnix(std::string& Path) { std::replace_if(Path.begin(), Path.end(), [](char C) { return C == '\\'; }, char('/')); } void ConvertDirectorySeparatorsToNative(std::wstring& Path) { std::replace_if(Path.begin(), Path.end(), [](wchar_t C) { return C == '/' || C == '\\'; }, wchar_t(FPath::preferred_separator)); } void ConvertDirectorySeparatorsToUnix(std::wstring& Path) { std::replace_if(Path.begin(), Path.end(), [](wchar_t C) { return C == '\\'; }, wchar_t('/')); } std::error_code CopyFileIfNewer(const FPath& Source, const FPath& Target) { FFileAttributes SourceAttr = GetFileAttrib(Source); FFileAttributes TargetAttr = GetFileAttrib(Target); std::error_code Ec; if (SourceAttr.Size != TargetAttr.Size || SourceAttr.Mtime != TargetAttr.Mtime) { FileCopyOverwrite(Source, Target, Ec); } return Ec; } bool IsNonCaseSensitiveFileSystem(const FPath& ExistingPath) { UNSYNC_ASSERTF(PathExists(ExistingPath), L"IsCaseSensitiveFileSystem must be called with a path that exists on disk"); // Assume file system is case-sensitive if all-upper and all-lower versions of the path exist and resolve to the same FS entry. // This is not 100% robust due to symlinks, but is good enough for most practical purposes. FPath PathUpper = StringToUpper(ExistingPath.wstring()); FPath PathLower = StringToLower(ExistingPath.wstring()); if (PathExists(PathUpper) && PathExists(PathLower)) { return std::filesystem::equivalent(ExistingPath, PathUpper) && std::filesystem::equivalent(PathLower, PathUpper); } else { return false; } } bool IsCaseSensitiveFileSystem(const FPath& ExistingPath) { return !IsNonCaseSensitiveFileSystem(ExistingPath); } FFileAttributes GetCachedFileAttrib(const FPath& Path, FFileAttributeCache& AttribCache) { FFileAttributes Result; FPath ExtendedPath = MakeExtendedAbsolutePath(Path); auto It = AttribCache.Map.find(ExtendedPath); if (It != AttribCache.Map.end()) { Result = It->second; } return Result; } #if UNSYNC_PLATFORM_WINDOWS inline uint64 MakeU64(FILETIME Ft) { return MakeU64(Ft.dwHighDateTime, Ft.dwLowDateTime); } struct FCreateFileInfo { DWORD FileAccess = 0; DWORD Share = 0; DWORD Disposition = 0; DWORD Protection = 0; DWORD MapAccess = 0; DWORD FileFlags = FILE_ATTRIBUTE_NORMAL; FCreateFileInfo(EFileMode Mode) { switch (Mode & EFileMode::CommonModeMask) { default: case EFileMode::ReadOnly: case EFileMode::ReadOnlyUnbuffered: FileAccess = GENERIC_READ; Share = FILE_SHARE_READ; Disposition = OPEN_EXISTING; Protection = PAGE_READONLY; MapAccess = FILE_MAP_READ; break; case EFileMode::CreateReadWrite: case EFileMode::CreateWriteOnly: UNSYNC_ASSERT(!GDryRun || EnumHasAnyFlags(Mode, EFileMode::IgnoreDryRun)); FileAccess = GENERIC_READ | GENERIC_WRITE; Share = FILE_SHARE_WRITE; Disposition = CREATE_ALWAYS; Protection = PAGE_READWRITE; MapAccess = FILE_MAP_ALL_ACCESS; break; } } }; struct FWindowsAsyncFileReader final : FAsyncReader { UNSYNC_DISALLOW_COPY_ASSIGN(FWindowsAsyncFileReader) struct FOverlappedCommand { OVERLAPPED Overlapped = {}; uint64 RequestedOffset = 0; uint64 RequestedSize = 0; uint64 AlignedOffset = 0; uint64 AlignedSize = 0; uint64 Transferred = 0; uint64 UserData = 0; uint32 ErrorCode = 0; bool bIoActive = false; bool bComplete = true; FIOBuffer Buffer; IOCallback Callback = {}; }; FWindowsAsyncFileReader(FWindowsFile& InReader, uint32 InMaxPipelineDepth); virtual ~FWindowsAsyncFileReader(); virtual uint64 GetSize() override { return FileSize; } virtual bool IsValid() override { return !ErrorCode && !bClosed; } virtual bool EnqueueRead(uint64 SourceOffset, uint64 Size, uint64 UserData, IOCallback Callback); virtual void Flush() override; bool BeginReadingNextSegment(FOverlappedCommand& Cmd); bool FinishReadingSegment(FOverlappedCommand& Cmd); void CompleteReadCommand(FOverlappedCommand& Cmd); FWindowsFile& Inner; const uint32 MaxQueueDepth; static constexpr uint32 MAX_OVERLAPPED_COMMANDS = MAX_IO_PIPELINE_DEPTH; HANDLE OverlappedEvents[MAX_OVERLAPPED_COMMANDS] = {}; FOverlappedCommand Commands[MAX_OVERLAPPED_COMMANDS] = {}; uint64 NumCommandsIssued = 0; HANDLE FileHandle = INVALID_HANDLE_VALUE; uint64 FileSize = 0; FAtomicError ErrorCode; std::atomic_bool bClosed; }; FWindowsAsyncFileReader::FWindowsAsyncFileReader(FWindowsFile& InReader, uint32 InMaxPipelineDepth) : Inner(InReader) , FileHandle(InReader.FileHandle) , FileSize(InReader.GetSize()) , MaxQueueDepth(InMaxPipelineDepth) { UNSYNC_ASSERT(IsReadOnly(InReader.Mode)); if (!InReader.IsValid()) { ErrorCode.Set(SystemError(L"FWindowsAsyncFileReader source file is invalid", InReader.GetError())); InReader.GetError(); } for (uint32 I = 0; I < MaxQueueDepth; ++I) { OverlappedEvents[I] = CreateEvent(nullptr, true, true, nullptr); Commands[I].Overlapped.hEvent = OverlappedEvents[I]; } Inner.AddAsyncReader(this); } FWindowsAsyncFileReader::~FWindowsAsyncFileReader() { Flush(); for (HANDLE EventHandle : OverlappedEvents) { if (EventHandle) { CloseHandle(EventHandle); } } Inner.RemoveAsyncReader(this); } FWindowsFile::FWindowsFile(const FPath& InFilename, EFileMode InMode, uint64 InSize) : Mode(InMode) { Filename = MakeExtendedAbsolutePath(InFilename); bool bOpenedOk = OpenFileHandle(InMode); if (bOpenedOk) { if (IsReadOnly(InMode)) { LARGE_INTEGER LiFileSize = {}; bool bSizeOk = GetFileSizeEx(FileHandle, &LiFileSize); if (!bSizeOk) { LastError = GetLastError(); return; } FileSize = LiFileSize.QuadPart; } else if (IsWritable(InMode) && InSize) { DWORD BytesReturned = 0; BOOL SparseFileOk = DeviceIoControl(FileHandle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &BytesReturned, nullptr); if (!SparseFileOk) { UNSYNC_WARNING(L"Failed to mark file '%ls' as sparse.", Filename.wstring().c_str()); } LARGE_INTEGER LiFileSize = {}; LiFileSize.QuadPart = InSize; BOOL SizeOk = SetFilePointerEx(FileHandle, LiFileSize, nullptr, FILE_BEGIN); if (!SizeOk) { CloseHandle(FileHandle); LastError = GetLastError(); return; } BOOL EndOfFileOk = SetEndOfFile(FileHandle); if (!EndOfFileOk) { CloseHandle(FileHandle); LastError = GetLastError(); return; } FileSize = LiFileSize.QuadPart; } else if (IsWritable(InMode) && (InSize == 0)) { // nothing to do when creating an empty file } else { UNSYNC_ERROR(L"Unexpected file mode %d", (int)InMode); } } } FWindowsFile::~FWindowsFile() { Close(); std::lock_guard LockGuard(Mutex); UNSYNC_ASSERT(AsyncReaders.empty()); } void FWindowsFile::AddAsyncReader(FWindowsAsyncFileReader* Reader) { std::lock_guard LockGuard(Mutex); AsyncReaders.push_back(Reader); } void FWindowsFile::RemoveAsyncReader(FWindowsAsyncFileReader* Reader) { std::lock_guard LockGuard(Mutex); AsyncReaders.erase(std::remove(AsyncReaders.begin(), AsyncReaders.end(), Reader), AsyncReaders.end()); } bool FWindowsFile::OpenFileHandle(EFileMode InMode) { FCreateFileInfo Info(InMode); Info.FileFlags |= FILE_FLAG_OVERLAPPED; if (EnumHasAnyFlags(InMode, EFileMode::Unbuffered) && !GForceBufferedFiles) { Info.FileFlags |= FILE_FLAG_NO_BUFFERING; } FileHandle = CreateFileW(Filename.c_str(), Info.FileAccess, Info.Share, nullptr, Info.Disposition, Info.FileFlags, nullptr); if (FileHandle == INVALID_HANDLE_VALUE) { LastError = GetLastError(); return false; } else { return true; } } bool FWindowsFile::IsValid() { std::lock_guard LockGuard(Mutex); return FileHandle != INVALID_HANDLE_VALUE; } void FWindowsFile::Close() { std::lock_guard LockGuard(Mutex); FlushAsyncReaders(); if (FileHandle != INVALID_HANDLE_VALUE) { CloseHandle(FileHandle); FileHandle = INVALID_HANDLE_VALUE; } for (FWindowsAsyncFileReader* It : AsyncReaders) { It->bClosed = true; } } inline void SetOverlappedOffset(OVERLAPPED& Overlapped, uint64 Offset) { LARGE_INTEGER Pos; Pos.QuadPart = Offset; Overlapped.Offset = Pos.LowPart; Overlapped.OffsetHigh = Pos.HighPart; } bool FWindowsAsyncFileReader::FinishReadingSegment(FOverlappedCommand& Cmd) { UNSYNC_ASSERT(Cmd.bIoActive); DWORD ReadBytes = 0; const BOOL OverlappedResultOk = GetOverlappedResult(FileHandle, &Cmd.Overlapped, &ReadBytes, true); Cmd.bIoActive = false; Cmd.Transferred += ReadBytes; if (OverlappedResultOk) { return true; } else { Cmd.ErrorCode = GetLastError(); ErrorCode.Set(SystemError(L"GetOverlappedResult failed", Cmd.ErrorCode)); return false; } } bool FWindowsAsyncFileReader::BeginReadingNextSegment(FOverlappedCommand& Cmd) { UNSYNC_ASSERT(!Cmd.bIoActive); if (Cmd.Transferred >= Cmd.RequestedSize) { return false; } if (EnumHasAnyFlags(Inner.Mode, EFileMode::Unbuffered)) { Cmd.Transferred = AlignDownToMultiplePow2(Cmd.Transferred, FWindowsFile::UNBUFFERED_READ_ALIGNMENT); } const uint64 NextReadSize = Cmd.AlignedSize - Cmd.Transferred; uint8* BufferMemory = reinterpret_cast(Cmd.Buffer.GetMemory()); UNSYNC_ASSERT(Cmd.Transferred + NextReadSize <= Cmd.Buffer.GetMemorySize()); ResetEvent(Cmd.Overlapped.hEvent); SetOverlappedOffset(Cmd.Overlapped, Cmd.AlignedOffset + Cmd.Transferred); if (!ReadFile(FileHandle, BufferMemory + Cmd.Transferred, CheckedNarrow(NextReadSize), nullptr, &Cmd.Overlapped)) { const DWORD LastError = GetLastError(); if (LastError != ERROR_IO_PENDING) { Cmd.ErrorCode = LastError; ErrorCode.Set(SystemError(L"ReadFile failed", LastError)); return false; } } Cmd.bIoActive = true; return true; } void FWindowsAsyncFileReader::CompleteReadCommand(FOverlappedCommand& Cmd) { UNSYNC_ASSERT(!Cmd.bComplete); while (Cmd.bIoActive) { if (FinishReadingSegment(Cmd)) { BeginReadingNextSegment(Cmd); } } UNSYNC_ASSERTF( Cmd.RequestedSize <= Cmd.Transferred, L"Expected to read at least %llu bytes, but read %llu [FileSize=%llu, Cmd.AlignedOffset=%llu, Cmd.AlignedSize=%llu, Cmd.ErrorCode=%u]", llu(Cmd.RequestedSize), llu(Cmd.Transferred), llu(FileSize), llu(Cmd.AlignedOffset), llu(Cmd.AlignedSize), Cmd.ErrorCode); const uint64 ReadBytesClamped = std::min(Cmd.Buffer.GetSize(), Cmd.Transferred); if (Cmd.Callback) { Cmd.Callback(std::move(Cmd.Buffer), Cmd.RequestedOffset, ReadBytesClamped, Cmd.UserData); } Cmd.bComplete = true; } uint64 FWindowsFile::Write(const void* Data, uint64 DestOffset, uint64 TotalSize) { // TODO: !!!!! fire-and-forget asynchronous writes !!!!! std::lock_guard LockGuard(Mutex); UNSYNC_ASSERT(IsWritable(Mode)); if (!IsWriteOnly(Mode)) { FlushAsyncReaders(); // flush any outstanding read requests before writing } LARGE_INTEGER Pos; Pos.QuadPart = DestOffset; uint64 WrittenBytes = 0; static constexpr uint64 ChunkSize = 128_MB; uint64 NumChunks = DivUp(TotalSize, ChunkSize); uint64 SourceOffset = 0; for (uint64 I = 0; I < NumChunks; ++I) { int32 ThisChunkSize = CheckedNarrow(CalcChunkSize(I, ChunkSize, TotalSize)); OVERLAPPED Overlapped = {}; Overlapped.Offset = Pos.LowPart; Overlapped.OffsetHigh = Pos.HighPart; BOOL WriteOk = WriteFile(FileHandle, reinterpret_cast(Data) + SourceOffset, ThisChunkSize, nullptr, &Overlapped); if (!WriteOk && GetLastError() != ERROR_IO_PENDING) { LastError = GetLastError(); return 0; } DWORD ChunkWrittenBytes = 0; BOOL OverlappedResultOk = TRUE; uint32 MaxAttempts = 100000; uint32 Attempt = 0; for (Attempt = 0; Attempt < MaxAttempts; ++Attempt) { OverlappedResultOk = GetOverlappedResult(FileHandle, &Overlapped, &ChunkWrittenBytes, true); if (!OverlappedResultOk || ChunkWrittenBytes != 0) { break; } if (ChunkWrittenBytes == 0) { SchedulerSleep(1); } } if (Attempt == MaxAttempts) { UNSYNC_ERROR(L"Overlapped file write timed out"); } if (!OverlappedResultOk) { LastError = GetLastError(); break; } WrittenBytes += ChunkWrittenBytes; Pos.QuadPart += ChunkWrittenBytes; SourceOffset += ChunkWrittenBytes; } return WrittenBytes; } uint64 FWindowsFile::Read(void* Dest, uint64 SourceOffset, uint64 ReadSize) { std::lock_guard LockGuard(Mutex); UNSYNC_ASSERTF(((Mode & EFileMode::Unbuffered) == 0) || ((SourceOffset % UNBUFFERED_READ_ALIGNMENT == 0) && (ReadSize % UNBUFFERED_READ_ALIGNMENT == 0)), L"Unbuffered files only support Read when offset and size are aligned to 4KB"); UNSYNC_ASSERT(IsReadable(Mode)); LARGE_INTEGER Pos; Pos.QuadPart = SourceOffset; uint64 ReadBytes = 0; static constexpr uint64 ChunkSize = 128_MB; uint64 NumChunks = DivUp(ReadSize, ChunkSize); uint64 DestOffset = 0; for (uint64 I = 0; I < NumChunks; ++I) { uint32 ThisChunkSize = CheckedNarrow(CalcChunkSize(I, ChunkSize, ReadSize)); OVERLAPPED Overlapped = {}; Overlapped.Offset = Pos.LowPart; Overlapped.OffsetHigh = Pos.HighPart; BOOL ReadOk = ReadFile(FileHandle, reinterpret_cast(Dest) + DestOffset + I * ChunkSize, ThisChunkSize, nullptr, &Overlapped); if (!ReadOk && GetLastError() != ERROR_IO_PENDING) { LastError = GetLastError(); return 0; } DWORD ChunkReadBytes = 0; BOOL OverlappedResultOk = GetOverlappedResult(FileHandle, &Overlapped, &ChunkReadBytes, true); if (!OverlappedResultOk) { LastError = GetLastError(); break; } ReadBytes += ChunkReadBytes; Pos.QuadPart += ChunkReadBytes; SourceOffset += ChunkReadBytes; } return ReadBytes; } bool FWindowsAsyncFileReader::EnqueueRead(uint64 SourceOffset, uint64 Size, uint64 UserData, IOCallback Callback) { if (!IsValid()) { return false; } // Clamp size to the end of the file Size = std::min(FileSize, SourceOffset + Size) - SourceOffset; const EFileMode FileMode = Inner.Mode; UNSYNC_ASSERT(IsReadable(FileMode)); uint32 CmdIdx = ~0u; // Async commands are always strictly ordered const uint32 WaitSlotIndex = uint32(NumCommandsIssued % MaxQueueDepth); const DWORD WaitResult = WaitForSingleObject(OverlappedEvents[WaitSlotIndex], INFINITE); UNSYNC_ASSERT(WaitResult == WAIT_OBJECT_0); CmdIdx = WaitSlotIndex; UNSYNC_ASSERT(CmdIdx < MaxQueueDepth); FOverlappedCommand& Cmd = Commands[CmdIdx]; if (!Cmd.bComplete) { CompleteReadCommand(Cmd); } Cmd.RequestedOffset = SourceOffset; Cmd.RequestedSize = Size; Cmd.UserData = UserData; Cmd.Callback = Callback; Cmd.Transferred = 0; Cmd.ErrorCode = 0; Cmd.bComplete = false; if (EnumHasAnyFlags(FileMode, EFileMode::Unbuffered)) { uint64 OriginalSize = Size; uint64 OriginalBegin = SourceOffset; uint64 OriginalEnd = SourceOffset + Size; uint64 AlignedBegin = AlignDownToMultiplePow2(OriginalBegin, FWindowsFile::UNBUFFERED_READ_ALIGNMENT); uint64 AlignedEnd = AlignUpToMultiplePow2(OriginalEnd, FWindowsFile::UNBUFFERED_READ_ALIGNMENT); uint64 AlignedSize = AlignedEnd - AlignedBegin; Cmd.Buffer = FIOBuffer::Alloc(AlignedSize, L"WindowsFile::ReadAsync_aligned"); Cmd.Buffer.SetDataRange(OriginalBegin - AlignedBegin, OriginalSize); Cmd.AlignedOffset = AlignedBegin; Cmd.AlignedSize = AlignedSize; } else { Cmd.Buffer = FIOBuffer::Alloc(Size, L"WindowsFile::ReadAsync"); Cmd.AlignedOffset = SourceOffset; Cmd.AlignedSize = Size; } if (BeginReadingNextSegment(Cmd)) { ++NumCommandsIssued; return true; } else { return false; } } void FWindowsAsyncFileReader::Flush() { for (uint32 I = 0; I < MaxQueueDepth; ++I) { const uint64 CommandIndex = (NumCommandsIssued + I) % MaxQueueDepth; if (!Commands[CommandIndex].bComplete) { WaitForSingleObject(OverlappedEvents[CommandIndex], INFINITE); CompleteReadCommand(Commands[CommandIndex]); } } } void FWindowsFile::FlushAsyncReaders() { for (FWindowsAsyncFileReader* It : AsyncReaders) { It->Flush(); } } std::unique_ptr FWindowsFile::CreateAsyncReader(uint32 MaxPipelineDepth) { UNSYNC_ASSERT(IsValid()); MaxPipelineDepth = std::min(MaxPipelineDepth, FWindowsAsyncFileReader::MAX_OVERLAPPED_COMMANDS); return std::unique_ptr(new FWindowsAsyncFileReader(*this, MaxPipelineDepth)); } FFileAttributes GetFileAttrib(const FPath& Path, FFileAttributeCache* AttribCache) { FFileAttributes Result; FPath ExtendedPath = MakeExtendedAbsolutePath(Path); if (AttribCache) { auto It = AttribCache->Map.find(ExtendedPath); if (It != AttribCache->Map.end()) { Result = It->second; return Result; } } WIN32_FILE_ATTRIBUTE_DATA AttributeData; BOOL Ok = GetFileAttributesExW(ExtendedPath.c_str(), GetFileExInfoStandard, &AttributeData); if (Ok) { Result.bDirectory = !!(AttributeData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY); Result.Size = MakeU64(AttributeData.nFileSizeHigh, AttributeData.nFileSizeLow); Result.Mtime = MakeU64(AttributeData.ftLastWriteTime); Result.bReadOnly = (AttributeData.dwFileAttributes & FILE_ATTRIBUTE_READONLY); Result.bValid = true; } return Result; } uint64 ToWindowsFileTime(const std::filesystem::file_time_type& T) { return T.time_since_epoch().count(); } uint64 GetAvailableDiskSpace(const FPath& Path) { ULARGE_INTEGER AvailableBytes = {}; ULARGE_INTEGER TotalBytes = {}; ULARGE_INTEGER FreeBytes = {}; BOOL bOk = GetDiskFreeSpaceExW(Path.native().c_str(), &AvailableBytes, &TotalBytes, &FreeBytes); if (bOk) { return AvailableBytes.QuadPart; } else { return ~0ull; } } #endif // UNSYNC_PLATFORM_WINDOWS #if UNSYNC_PLATFORM_UNIX FUnixFile::FUnixFile(const FPath& InFilename, EFileMode InMode, uint64 in_size) : Filename(InFilename), Mode(InMode) { FileHandle = fopen(InFilename.native().c_str(), IsReadOnly(Mode) ? "rb" : "w+b"); if (FileHandle == nullptr) { return; } FileDescriptor = fileno(FileHandle); if (IsReadOnly(Mode)) { struct stat stat_buf = {}; LastError = fstat(FileDescriptor, &stat_buf); UNSYNC_ASSERT(LastError == 0); FileSize = stat_buf.st_size; } else { LastError = ftruncate(FileDescriptor, in_size); UNSYNC_ASSERT(LastError == 0); FileSize = in_size; } } FUnixFile::~FUnixFile() { Close(); } void FUnixFile::Close() { if (FileHandle) { fclose(FileHandle); FileHandle = nullptr; } } uint64 FUnixFile::Read(void* dest, uint64 SourceOffset, uint64 ReadSize) { // TODO: handle partial reads (pread returning 0 < x < ReadSize) uint64 read_bytes = pread(FileDescriptor, dest, ReadSize, SourceOffset); if (read_bytes != ReadSize) { LastError = errno; } return read_bytes; } uint64 FUnixFile::Write(const void* data, uint64 DestOffset, uint64 WriteSize) { UNSYNC_ASSERT(IsWritable(Mode)); // TODO: handle partial writes (pwrite returning 0 < x < WriteSize) uint64 wrote_bytes = pwrite(FileDescriptor, data, WriteSize, DestOffset); if (wrote_bytes != WriteSize) { LastError = errno; } return wrote_bytes; } FFileAttributes GetFileAttrib(const FPath& Path, FFileAttributeCache* AttribCache) { FFileAttributes Result; if (AttribCache) { auto it = AttribCache->Map.find(Path); if (it != AttribCache->Map.end()) { Result = it->second; return Result; } } // TODO: could potentially use std::filesystem::directory_entry for this on all platforms std::error_code ErrorCode = {}; auto Entry = std::filesystem::directory_entry(Path, ErrorCode); if (!ErrorCode) { std::filesystem::file_status Status = Entry.status(ErrorCode); if (ErrorCode) { return Result; } std::filesystem::perms Perms = Status.permissions(); Result.bDirectory = Entry.is_directory(); Result.Size = Result.bDirectory ? 0 : Entry.file_size(); Result.Mtime = ToWindowsFileTime(Entry.last_write_time()); Result.bReadOnly = IsReadOnly(Perms); Result.bIsExecutable = IsExecutable(Perms); Result.bValid = true; } return Result; } uint64 ToWindowsFileTime(const std::filesystem::file_time_type& FileTime) { std::chrono::duration FullDuration = FileTime.time_since_epoch(); uint64 FullSeconds = std::chrono::floor(FullDuration).count(); auto SubsecondDuration = FullDuration - std::chrono::seconds(FullSeconds); auto SubsecondNanos = std::chrono::duration_cast(SubsecondDuration).count(); uint64 Ticks = (FullSeconds + SECONDS_BETWEEN_WINDOWS_AND_UNIX) * WINDOWS_TICKS_PER_SECOND + (SubsecondNanos / NANOS_PER_WINDOWS_TICK); return Ticks; } uint64 GetAvailableDiskSpace(const FPath& Path) { return ~0ull; // TODO: query available space via statvfs() } #endif // UNSYNC_PLATFORM_UNIX bool SetFileMtime(const FPath& Path, uint64 Mtime, bool bAllowInDryRun) { UNSYNC_ASSERT(!GDryRun || bAllowInDryRun); UNSYNC_ASSERT(Mtime != 0); FPath ExtendedPath = MakeExtendedAbsolutePath(Path); std::filesystem::file_time_type FileTime = FromWindowsFileTime(Mtime); std::error_code ErrorCode; std::filesystem::last_write_time(ExtendedPath, FileTime, ErrorCode); return !ErrorCode; } bool SetFileReadOnly(const FPath& Path, bool bReadOnly) { UNSYNC_ASSERT(!GDryRun); FPath ExtendedPath = MakeExtendedAbsolutePath(Path); std::error_code ErrorCode; if (bReadOnly) { std::filesystem::permissions( ExtendedPath, std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write, std::filesystem::perm_options::remove, ErrorCode); } else { std::filesystem::permissions(ExtendedPath, std::filesystem::perms::owner_write, std::filesystem::perm_options::add, ErrorCode); } return !ErrorCode; } bool SetFileExecutable(const FPath& Path, bool Executable) { UNSYNC_ASSERT(!GDryRun); FPath ExtendedPath = MakeExtendedAbsolutePath(Path); std::error_code ErrorCode; // TODO: In general if the +x bit is set, it set for all, this isn't always true though // so we should at some point respect what the original file had set. For now though, // this is sufficient. if (Executable) { std::filesystem::permissions( ExtendedPath, std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, ErrorCode); } else { std::filesystem::permissions( ExtendedPath, std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::remove, ErrorCode); } return !ErrorCode; } FBuffer ReadFileToBuffer(const FPath& Filename) { FBuffer Result; FNativeFile File(Filename, EFileMode::ReadOnly); if (File.IsValid()) { Result.Resize(File.GetSize()); uint64 ReadBytes = File.Read(Result.Data(), 0, Result.Size()); Result.Resize(ReadBytes); } return Result; } bool WriteBufferToFile(const FPath& Filename, const uint8* Data, uint64 Size, EFileMode FileMode) { UNSYNC_LOG_INDENT; if (Data == nullptr) { UNSYNC_ERROR(L"WriteBufferToFile called with null buffer"); return false; } if (Size == 0) { UNSYNC_ERROR(L"WriteBufferToFile called with zero size buffer"); return false; } if (GDryRun && !EnumHasAnyFlags(FileMode, EFileMode::IgnoreDryRun)) { UNSYNC_ERROR(L"WriteBufferToFile called in dry run mode"); return false; } FNativeFile File(Filename, FileMode, Size); if (File.IsValid()) { uint64 WroteBytes = File.Write(Data, 0, Size); if (WroteBytes != Size) { UNSYNC_ERROR(L"Failed to write complete file '%ls'. Expected to write %llu bytes, actually written %llu bytes", Filename.wstring().c_str(), llu(Size), llu(WroteBytes)); } return WroteBytes == Size; } else { UNSYNC_ERROR(L"Failed to open file '%ls' for writing. %hs", Filename.wstring().c_str(), FormatSystemErrorMessage(File.GetError()).c_str()); return false; } } bool WriteBufferToFile(const FPath& Filename, const FBuffer& Buffer, EFileMode FileMode) { return WriteBufferToFile(Filename, Buffer.Data(), Buffer.Size(), FileMode); } bool WriteBufferToFile(const FPath& Filename, const std::string& Buffer, EFileMode FileMode) { return WriteBufferToFile(Filename, (const uint8*)Buffer.data(), Buffer.length(), FileMode); } struct FIOBufferCache { std::mutex Mutex; struct FAllocation { const wchar_t* DebugName = nullptr; uint8* Memory; uint64 Size; }; std::vector AllocatedBlocks; // todo: hash table (though current numbers are very low, so...) std::vector AvailableBlocks; uint64 CurrentCacheSize = 0; uint64 CurrentAllocatedSize = 0; static constexpr uint64 MAX_CACHED_ALLOC_SIZE = 32_MB; static constexpr uint64 MAX_TOTAL_CACHE_SIZE = 4_GB; ~FIOBufferCache() { for (FAllocation& X : AllocatedBlocks) { UnsyncFree(X.Memory); } for (FAllocation& X : AvailableBlocks) { UnsyncFree(X.Memory); } } uint8* Alloc(uint64 Size, const wchar_t* DebugName) { std::lock_guard LockGuard(Mutex); uint64 BestBlockIndex = ~0ull; uint64 BestBlockSize = ~0ull; if (Size <= MAX_CACHED_ALLOC_SIZE) { Size = NextPow2(CheckedNarrow(Size)); for (uint64 I = 0; I < AvailableBlocks.size(); ++I) { FAllocation& Candidate = AvailableBlocks[I]; if (Candidate.Size < BestBlockSize && Candidate.Size >= Size) { BestBlockSize = Candidate.Size; BestBlockIndex = I; } } } FAllocation Allocation = {}; if (BestBlockSize != ~0ull) { FAllocation Candidate = AvailableBlocks[BestBlockIndex]; AllocatedBlocks.push_back(Candidate); AvailableBlocks[BestBlockIndex] = AvailableBlocks.back(); AvailableBlocks.pop_back(); Allocation = Candidate; } else { CurrentAllocatedSize += Size; Allocation.Memory = (uint8*)UnsyncMalloc(Size); UNSYNC_ASSERT(Allocation.Memory); Allocation.Size = Size; AllocatedBlocks.push_back(Allocation); if (Size <= MAX_CACHED_ALLOC_SIZE) { CurrentCacheSize += Allocation.Size; } } return Allocation.Memory; } void Free(uint8* Ptr) { std::lock_guard LockGuard(Mutex); uint64 AllocationIndex = ~0u; for (uint64 I = 0; I < AllocatedBlocks.size(); ++I) { if (AllocatedBlocks[I].Memory == Ptr) { AllocationIndex = I; break; } } UNSYNC_ASSERTF(AllocationIndex != ~0u, L"Trying to free an unknown IOBuffer."); FAllocation FreedBlock = AllocatedBlocks[AllocationIndex]; if (FreedBlock.Size <= MAX_CACHED_ALLOC_SIZE) { AvailableBlocks.push_back(FreedBlock); } else { UnsyncFree(FreedBlock.Memory); CurrentAllocatedSize -= FreedBlock.Size; } while (CurrentCacheSize > MAX_TOTAL_CACHE_SIZE && !AvailableBlocks.empty()) { FAllocation& LastBlock = AvailableBlocks.back(); UnsyncFree(LastBlock.Memory); UNSYNC_ASSERT(CurrentCacheSize >= LastBlock.Size); CurrentCacheSize -= LastBlock.Size; CurrentAllocatedSize -= LastBlock.Size; AvailableBlocks.pop_back(); } AllocatedBlocks[AllocationIndex] = AllocatedBlocks.back(); AllocatedBlocks.pop_back(); } uint64 GetCurrentSize() { std::lock_guard LockGuard(Mutex); return CurrentCacheSize; } }; static FIOBufferCache GIoBufferCache; uint8* AllocIoBuffer(uint64 Size, const wchar_t* DebugName) { return GIoBufferCache.Alloc(Size, DebugName); } void FreeIoBuffer(uint8* Ptr) { GIoBufferCache.Free(Ptr); } uint64 GetCurrentIoCacheSize() { return GIoBufferCache.GetCurrentSize(); } FFileAttributeCache CreateFileAttributeCache(const FPath& Root, const FSyncFilter* SyncFilter) { FFileAttributeCache Result; FTimePoint NextProgressLogTime = TimePointNow() + std::chrono::seconds(1); auto ReportProgress = [&NextProgressLogTime, &Result]() { FTimePoint TimeNow = TimePointNow(); if (TimeNow >= NextProgressLogTime) { LogPrintf(ELogLevel::Debug, L"Found files: %d\r", (int)Result.Map.size()); NextProgressLogTime = TimeNow + std::chrono::seconds(1); } }; FPath ResolvedRoot = SyncFilter ? SyncFilter->Resolve(Root) : Root; for (const std::filesystem::directory_entry& Dir : RecursiveDirectoryScan(ResolvedRoot)) { if (Dir.is_directory()) { continue; } if (SyncFilter && !SyncFilter->ShouldSync(Dir.path().native())) { continue; } FFileAttributes Attr = {}; std::filesystem::perms Perms = Dir.status().permissions(); Attr.Mtime = ToWindowsFileTime(Dir.last_write_time()); Attr.Size = Dir.file_size(); Attr.bValid = true; Attr.bReadOnly = IsReadOnly(Perms); Attr.bIsExecutable = IsExecutable(Perms); Result.Map[Dir.path().native()] = Attr; ReportProgress(); } ReportProgress(); return Result; } bool IsDirectory(const FPath& Path) { FFileAttributes Attr = GetFileAttrib(Path); return Attr.bValid && Attr.bDirectory; } bool PathExists(const FPath& Path) { FPath ExtendedPath = MakeExtendedAbsolutePath(Path); return std::filesystem::exists(ExtendedPath); } bool PathExists(const FPath& Path, std::error_code& OutErrorCode) { FPath ExtendedPath = MakeExtendedAbsolutePath(Path); return std::filesystem::exists(ExtendedPath, OutErrorCode); } bool CreateDirectories(const FPath& Path) { FPath ExtendedPath = MakeExtendedAbsolutePath(Path); return std::filesystem::create_directories(ExtendedPath); } bool EnsureDirectoryExists(const FPath& Path) { return (PathExists(Path) && IsDirectory(Path)) || CreateDirectories(Path); } bool FileRename(const FPath& From, const FPath& To, std::error_code& OutErrorCode) { FPath ExtendedFrom = MakeExtendedAbsolutePath(From); FPath ExtendedTo = MakeExtendedAbsolutePath(To); std::filesystem::rename(ExtendedFrom, ExtendedTo, OutErrorCode); return OutErrorCode.value() == 0; } bool FileCopy(const FPath& From, const FPath& To, std::error_code& OutErrorCode) { FPath ExtendedFrom = MakeExtendedAbsolutePath(From); FPath ExtendedTo = MakeExtendedAbsolutePath(To); return std::filesystem::copy_file(ExtendedFrom, ExtendedTo, OutErrorCode); } bool FileCopyOverwrite(const FPath& From, const FPath& To, std::error_code& OutErrorCode) { FPath ExtendedFrom = MakeExtendedAbsolutePath(From); FPath ExtendedTo = MakeExtendedAbsolutePath(To); return std::filesystem::copy_file(ExtendedFrom, ExtendedTo, std::filesystem::copy_options::overwrite_existing, OutErrorCode); } bool FileRemove(const FPath& Path, std::error_code& OutErrorCode) { FPath ExtendedPath = MakeExtendedAbsolutePath(Path); return std::filesystem::remove(ExtendedPath, OutErrorCode); } std::filesystem::recursive_directory_iterator RecursiveDirectoryScan(const FPath& Path) { FPath ExtendedPath = MakeExtendedAbsolutePath(Path); return std::filesystem::recursive_directory_iterator(ExtendedPath); } std::filesystem::directory_iterator DirectoryScan(const FPath& Path) { FPath ExtendedPath = MakeExtendedAbsolutePath(Path); return std::filesystem::directory_iterator(ExtendedPath); } FMemReader::FMemReader(const uint8* InData, uint64 InDataSize) : Data(InData), Size(InDataSize) { } uint64 FMemReader::Read(void* Dest, uint64 SourceOffset, uint64 ReadSize) { uint64 ReadEndOffset = std::min(SourceOffset + ReadSize, Size); uint64 ClampedReadSize = ReadEndOffset - SourceOffset; memcpy(Dest, Data + SourceOffset, ClampedReadSize); return ClampedReadSize; } FMemReaderWriter::FMemReaderWriter(uint8* InData, uint64 InDataSize) : FMemReader(InData, InDataSize), DataRw(InData) { } uint64 FMemReaderWriter::Write(const void* InData, uint64 DestOffset, uint64 WriteSize) { uint64 WriteEndOffset = std::min(DestOffset + WriteSize, Size); uint64 ClampedWriteSize = WriteEndOffset - DestOffset; if (ClampedWriteSize && DataRw) { memcpy(DataRw + DestOffset, InData, ClampedWriteSize); } return ClampedWriteSize; } FIOBuffer FIOBuffer::Alloc(uint64 Size, const wchar_t* DebugName) { UNSYNC_ASSERT(Size); FIOBuffer Result; Result.MemoryPtr = AllocIoBuffer(Size, DebugName); Result.MemorySize = Size; Result.DataPtr = Result.MemoryPtr; Result.DataSize = Size; Result.DebugName = DebugName; return Result; } FIOBuffer::~FIOBuffer() { Clear(); UNSYNC_CLOBBER(Canary); } FIOBuffer::FIOBuffer(FIOBuffer&& Rhs) { UNSYNC_ASSERT(Rhs.Canary == CANARY); std::swap(MemoryPtr, Rhs.MemoryPtr); std::swap(MemorySize, Rhs.MemorySize); std::swap(DataPtr, Rhs.DataPtr); std::swap(DataSize, Rhs.DataSize); } void FIOBuffer::Clear() { UNSYNC_ASSERT(Canary == CANARY); if (MemoryPtr) { FreeIoBuffer(MemoryPtr); MemoryPtr = nullptr; MemorySize = 0; DataPtr = nullptr; DataSize = 0; } } FIOBuffer& FIOBuffer::operator=(FIOBuffer&& Rhs) { UNSYNC_ASSERT(Canary == CANARY); UNSYNC_ASSERT(Rhs.Canary == CANARY); if (this != &Rhs) { std::swap(MemoryPtr, Rhs.MemoryPtr); std::swap(MemorySize, Rhs.MemorySize); std::swap(DataPtr, Rhs.DataPtr); std::swap(DataSize, Rhs.DataSize); Rhs.Clear(); } return *this; } void TestFileTime() { UNSYNC_LOG(L"TestFileTime()"); UNSYNC_LOG_INDENT; // 20231024004826Z - 2023 October 24 12:48:26 // unix 1698108506 // windows 133425821060000000 const uint64 BaseExpectedWindowsTime = 133425821060000000ull; // Check basic conversion functionality at maximum { UNSYNC_LOG(L"File time precision estimate:"); UNSYNC_LOG_INDENT; uint64 ExpectedWindowsTime = BaseExpectedWindowsTime + 9999999; std::filesystem::file_time_type FileTime = FromWindowsFileTime(ExpectedWindowsTime); uint64 RoundTripWindowsTime = ToWindowsFileTime(FileTime); uint64 NativeCount = FileTime.time_since_epoch().count(); uint64 Delta = ExpectedWindowsTime > RoundTripWindowsTime ? ExpectedWindowsTime - RoundTripWindowsTime : RoundTripWindowsTime - ExpectedWindowsTime; UNSYNC_LOG(L"ExpectedWindowsTime = %llu", llu(ExpectedWindowsTime)); UNSYNC_LOG(L"RoundTripWindowsTime = %llu", llu(RoundTripWindowsTime)); UNSYNC_LOG(L"NativeCount = %llu, Delta = %llu", llu(NativeCount), llu(Delta)); } // Check basic conversion functionality at 1 second precision { uint64 ExpectedWindowsTime = BaseExpectedWindowsTime; std::filesystem::file_time_type FileTime = FromWindowsFileTime(ExpectedWindowsTime); uint64 RoundTripWindowsTime = ToWindowsFileTime(FileTime); uint64 NativeCount = FileTime.time_since_epoch().count(); UNSYNC_ASSERTF(RoundTripWindowsTime == ExpectedWindowsTime, L"RoundTripWindowsTime is %llu, but expected to be %llu. Native count: %llu", llu(RoundTripWindowsTime), llu(ExpectedWindowsTime), llu(NativeCount)); } } uint64 BlockingReadLarge(FIOReader& InReader, uint64 Offset, uint64 Size, uint8* OutputBuffer, uint64 OutputBufferSize) { const uint64 BytesPerRead = 2_MB; const uint64 ReadEnd = std::min(Offset + Size, InReader.GetSize()); const uint64 ClampedSize = ReadEnd - Offset; std::unique_ptr AsyncReader = InReader.CreateAsyncReader(); std::atomic TotalReadSize = 0; if (ClampedSize == 0) { return TotalReadSize; } FSchedulerSemaphore IoSemaphore(*GScheduler, 16); FTaskGroup CopyTasks = GScheduler->CreateTaskGroup(&IoSemaphore); uint64 NumReads = DivUp(ClampedSize, BytesPerRead); for (uint64 ReadIndex = 0; ReadIndex < NumReads; ++ReadIndex) { const uint64 ThisBatchSize = CalcChunkSize(ReadIndex, BytesPerRead, ClampedSize); const uint64 OutputOffset = BytesPerRead * ReadIndex; const uint64 ThisReadOffset = Offset + OutputOffset; auto ReadCallback = [OutputBuffer, OutputBufferSize, &TotalReadSize, &CopyTasks](FIOBuffer CmdBuffer, uint64 CmdSourceOffset, uint64 CmdReadSize, uint64 OutputOffset) { UNSYNC_ASSERT(OutputOffset + CmdReadSize <= OutputBufferSize); CopyTasks.run( [OutputBuffer, OutputOffset, CmdReadSize, CmdBuffer = MakeShared(std::move(CmdBuffer)), &TotalReadSize]() { memcpy(OutputBuffer + OutputOffset, CmdBuffer->GetData(), CmdReadSize); TotalReadSize += CmdReadSize; }); }; AsyncReader->EnqueueRead(ThisReadOffset, ThisBatchSize, OutputOffset, ReadCallback); } AsyncReader->Flush(); CopyTasks.wait(); return TotalReadSize; } void TestFileAttrib() { UNSYNC_LOG(L"TestFileAttrib()"); UNSYNC_LOG_INDENT; FPath TempDirPath = std::filesystem::temp_directory_path() / "unsync_test"; CreateDirectories(TempDirPath); const bool bDirectoryExists = PathExists(TempDirPath) && IsDirectory(TempDirPath); UNSYNC_ASSERT(bDirectoryExists); const FPath TestFilename = TempDirPath / "attrib.txt"; UNSYNC_LOG(L"Test file name: %ls", TestFilename.wstring().c_str()); if (PathExists(TestFilename)) { SetFileReadOnly(TestFilename, false); } const bool bFileWritten = WriteBufferToFile(TestFilename, "unsync test file"); UNSYNC_ASSERT(bFileWritten); const uint64 ExpectedFileTime = 133425821060000000ull; const bool bMtimeSet = SetFileMtime(TestFilename, ExpectedFileTime); UNSYNC_ASSERT(bMtimeSet); const FFileAttributes FileAttrib = GetFileAttrib(TestFilename); UNSYNC_ASSERT(!FileAttrib.bReadOnly); UNSYNC_ASSERT(FileAttrib.Mtime == ExpectedFileTime); const bool bReadOnlySet = SetFileReadOnly(TestFilename, true); UNSYNC_ASSERT(bReadOnlySet); const FFileAttributes FileAttribReadOnly = GetFileAttrib(TestFilename); UNSYNC_ASSERT(FileAttribReadOnly.bReadOnly); const bool bReadOnlyReset = SetFileReadOnly(TestFilename, false); UNSYNC_ASSERT(bReadOnlyReset); const FFileAttributes FileAttribNonReadOnly = GetFileAttrib(TestFilename); UNSYNC_ASSERT(!FileAttribNonReadOnly.bReadOnly); #if UNSYNC_PLATFORM_UNIX const bool bIsExecutable = SetFileExecutable(TestFilename, true); UNSYNC_ASSERT(bIsExecutable); const FFileAttributes FileAttribExecutable = GetFileAttrib(TestFilename); UNSYNC_ASSERT(FileAttribExecutable.bIsExecutable); const bool bIsNotExecutable = SetFileExecutable(TestFilename, false); UNSYNC_ASSERT(bIsNotExecutable); // This part of the test will fail on Windows platforms as the +x bit // means nothing there, so bIsExecutable will never be set to true. const FFileAttributes FileAttribNotExecutable = GetFileAttrib(TestFilename); UNSYNC_ASSERT(!FileAttribNotExecutable.bIsExecutable); #endif // UNSYNC_PLATFORM_UNIX std::error_code ErrorCode; const bool bFileDeleted = FileRemove(TestFilename, ErrorCode); UNSYNC_ASSERT(bFileDeleted); } void TestPathUtil() { #if UNSYNC_PLATFORM_WINDOWS UNSYNC_LOG(L"TestPathUtil()"); UNSYNC_LOG_INDENT; // Test path manipulation helpers { FPath Simple = FPath("\\\\?\\UNC\\server\\subdir\\a\\b\\c"); FPath Extended = MakeExtendedAbsolutePath(Simple); UNSYNC_ASSERT(Simple == Extended); } { FPath Simple = FPath("\\\\?\\d:\\local\\subdir\\a\\b\\c"); FPath Extended = MakeExtendedAbsolutePath(Simple); UNSYNC_ASSERT(Simple == Extended); } { FPath Simple = FPath("d:\\local\\subdir\\a\\b\\c"); FPath Extended = MakeExtendedAbsolutePath(Simple); FPath Stripped = RemoveExtendedPathPrefix(Extended); UNSYNC_ASSERT(Stripped == Simple); } { FPath Simple = FPath("\\\\server\\local\\subdir\\a\\b\\c"); FPath Extended = MakeExtendedAbsolutePath(Simple); FPath Stripped = RemoveExtendedPathPrefix(Extended); UNSYNC_ASSERT(Stripped == Simple); } { FPath Base = FPath("d:\\local\\subdir"); FPath Full = FPath("d:\\local\\subdir\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative == FPath("a\\b\\c")); } { FPath Base = FPath("\\\\server\\subdir"); FPath Full = FPath("\\\\server\\subdir\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative == FPath("a\\b\\c")); } { FPath Base = FPath("\\\\?\\d:\\local\\subdir"); FPath Full = FPath("\\\\?\\d:\\local\\subdir\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative == FPath("a\\b\\c")); } { FPath Base = FPath("\\\\?\\d:\\local\\subdir"); FPath Full = FPath("d:\\local\\subdir\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative == FPath("a\\b\\c")); } { FPath Base = FPath("d:\\local\\subdir"); FPath Full = FPath("\\\\?\\d:\\local\\subdir\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative == FPath("a\\b\\c")); } { FPath Base = FPath("d:\\local\\subdir"); FPath Full = FPath("\\\\?\\e:\\local\\subdir\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative.empty()); } { FPath Base = FPath("d:\\local\\subdir"); FPath Full = FPath("d:\\local\\a\\b\\c"); FPath Relative = GetRelativePath(Full, Base); UNSYNC_ASSERT(Relative.empty()); } #endif // UNSYNC_PLATFORM_WINDOWS } void DeleteOldFilesInDirectory(const FPath& Path, uint32 MaxFilesToKeep, bool bAllowInDryRun, const FPathFilterCallback& Filter) { struct FEntry { FPath Path; uint64 Mtime; }; std::vector Entries; FPath ExtendedPath = MakeExtendedAbsolutePath(Path); for (const std::filesystem::directory_entry& It : std::filesystem::directory_iterator(ExtendedPath)) { if (Filter && !Filter(It.path())) { continue; } if (It.is_regular_file()) { FEntry Entry; Entry.Mtime = ToWindowsFileTime(It.last_write_time()); Entry.Path = It.path(); Entries.push_back(Entry); } } // reverse sort std::sort(Entries.begin(), Entries.end(), [](const FEntry& A, const FEntry& B) { return A.Mtime > B.Mtime; }); while (Entries.size() > MaxFilesToKeep) { const FEntry& Oldest = Entries.back(); std::wstring PathStr = RemoveExtendedPathPrefix(Oldest.Path).wstring(); if (GDryRun && !bAllowInDryRun) { UNSYNC_VERBOSE(L"Deleting '%ls'(skipped due to dry run mode)", PathStr.c_str()); } else { UNSYNC_VERBOSE(L"Deleting '%ls'", PathStr.c_str()); std::error_code ErrorCode = {}; FileRemove(Oldest.Path, ErrorCode); } Entries.pop_back(); } } std::chrono::system_clock::time_point SystemTimeFromFileTime(std::filesystem::file_time_type FileTime) { #if 0 // TODO: use clock_cast() when it's supported in all target compilers return std::chrono::clock_cast(FileTime); #else # if UNSYNC_PLATFORM_WINDOWS std::chrono::seconds Offset(SECONDS_BETWEEN_WINDOWS_AND_UNIX); # else // UNSYNC_PLATFORM_WINDOWS std::chrono::seconds Offset(0); # endif // UNSYNC_PLATFORM_WINDOWS std::chrono::duration SinceEpoch = FileTime.time_since_epoch() - Offset; return std::chrono::system_clock::time_point(std::chrono::duration_cast(SinceEpoch)); #endif // 0 } FDummyAsyncReader::FDummyAsyncReader(FIOReader& InReader) : Inner(InReader) { } bool FDummyAsyncReader::EnqueueRead(uint64 SourceOffset, uint64 Size, uint64 UserData, IOCallback Callback) { if (Size != 0) { FIOBuffer Buffer = FIOBuffer::Alloc(Size, L"FDummyAsyncReader::ReadAsync"); uint64 ReadSize = Inner.Read(Buffer.GetData(), SourceOffset, Size); Callback(std::move(Buffer), SourceOffset, ReadSize, UserData); return true; } else { return false; } } std::unique_ptr FIOReader::CreateAsyncReader(uint32 MaxPipelineDepth) { return std::unique_ptr(new FDummyAsyncReader(*this)); } void TestFileAsyncRead() { UNSYNC_LOG(L"TestFileAsyncRead()"); UNSYNC_LOG_INDENT; UNSYNC_LOG(L"Initializing test data"); const FPath TempDirPath = std::filesystem::temp_directory_path() / "unsync_test"; CreateDirectories(TempDirPath); const bool bDirectoryExists = PathExists(TempDirPath) && IsDirectory(TempDirPath); UNSYNC_ASSERT(bDirectoryExists); const FPath TestFilename = TempDirPath / "ordered_integers.bin"; constexpr uint64 TestFileSize = 1_GB; FBuffer TempBuffer; TempBuffer.Resize(TestFileSize); uint32* TempBufferData = reinterpret_cast(TempBuffer.Data()); const uint32 NumElements = uint32(TestFileSize / sizeof(uint32)); for (uint32 i = 0; i < NumElements; ++i) { TempBufferData[i] = i; } const FHash256 ExpectedHash = HashBlake3Bytes(reinterpret_cast(TempBufferData), TestFileSize); if (!PathExists(TestFilename)) { UNSYNC_LOG(L"Writing test file '%ls'", TestFilename.wstring().c_str()); const bool bFileWritten = WriteBufferToFile(TestFilename, TempBuffer); UNSYNC_ASSERT(bFileWritten); } UNSYNC_LOG(L"ReadOnlyUnbuffered"); { UNSYNC_LOG_INDENT; memset(TempBufferData, 0, TestFileSize); FNativeFile TestFile(TestFilename, EFileMode::ReadOnlyUnbuffered); std::unique_ptr TestFileReader = TestFile.CreateAsyncReader(); constexpr uint64 ChunkSize = 8_MB; static_assert(TestFileSize % ChunkSize == 0); UNSYNC_LOG(L"Reading test data"); const FTimePoint ReadStartTime = TimePointNow(); const uint64 NumChunks = TestFileSize / ChunkSize; auto IoCallback = [TempBufferData](FIOBuffer Buffer, uint64 SourceOffset, uint64 ReadSize, uint64 UserData) { memcpy(reinterpret_cast(TempBufferData) + SourceOffset, Buffer.GetData(), Buffer.GetSize()); }; for (uint32 ChunkIndex = 0; ChunkIndex < NumChunks; ++ChunkIndex) { uint64 ChunkOffset = ChunkIndex * ChunkSize; TestFileReader->EnqueueRead(ChunkOffset, ChunkSize, 0, IoCallback); } TestFileReader->Flush(); const FTimePoint ReadDoneTime = TimePointNow(); UNSYNC_LOG(L"Hashing test data"); const FHash256 ActualHash = HashBlake3Bytes(reinterpret_cast(TempBufferData), TestFileSize); const FTimePoint HashDoneTime = TimePointNow(); const double ReadDuration = DurationSec(ReadStartTime, ReadDoneTime); const double HashDuration = DurationSec(ReadDoneTime, HashDoneTime); const double TotalDuration = DurationSec(ReadStartTime, HashDoneTime); UNSYNC_LOG(L"Read rate: %.2f MB / sec", SizeMb(TestFileSize) / ReadDuration); UNSYNC_LOG(L"Hash rate: %.2f MB / sec", SizeMb(TestFileSize) / HashDuration); UNSYNC_LOG(L"Total rate: %.2f MB / sec", SizeMb(TestFileSize) / TotalDuration); UNSYNC_LOG(L"Total time: %.3f sec", TotalDuration); UNSYNC_ASSERT(ActualHash == ExpectedHash); } UNSYNC_LOG(L"ReadOnlyUnbufferedStreaming"); { UNSYNC_LOG_INDENT; memset(TempBufferData, 0, TestFileSize); FNativeFile TestFile(TestFilename, EFileMode::ReadOnlyUnbuffered); std::unique_ptr TestFileReader = TestFile.CreateAsyncReader(); constexpr uint64 ChunkSize = 1_MB; static_assert(TestFileSize % ChunkSize == 0); UNSYNC_LOG(L"Reading test data"); const FTimePoint ReadStartTime = TimePointNow(); const uint64 NumChunks = TestFileSize / ChunkSize; FBlake3Hasher Hasher; uint64 CurrentOffset = 0; auto IoCallback = [TempBufferData, &Hasher, &CurrentOffset](FIOBuffer Buffer, uint64 SourceOffset, uint64 ReadSize, uint64 UserData) { UNSYNC_ASSERT(CurrentOffset == SourceOffset); UNSYNC_ASSERT(ReadSize == Buffer.GetSize()); CurrentOffset += ReadSize; Hasher.Update(Buffer.GetData(), Buffer.GetSize()); }; for (uint32 ChunkIndex = 0; ChunkIndex < NumChunks; ++ChunkIndex) { uint64 ChunkOffset = ChunkIndex * ChunkSize; TestFileReader->EnqueueRead(ChunkOffset, ChunkSize, 0, IoCallback); } TestFileReader->Flush(); const FTimePoint ReadDoneTime = TimePointNow(); FHash256 ActualHash = Hasher.Finalize(); const FTimePoint HashDoneTime = TimePointNow(); const double TotalDuration = DurationSec(ReadStartTime, HashDoneTime); UNSYNC_LOG(L"Read + Hash rate: %.2f MB / sec", SizeMb(TestFileSize) / TotalDuration); UNSYNC_LOG(L"Total time: %.3f sec", TotalDuration); UNSYNC_ASSERT(ActualHash == ExpectedHash); } } } // namespace unsync