Skip to content

Commit

Permalink
FoundationEssentials: implement file diffing support for Windows
Browse files Browse the repository at this point in the history
Windows does not have the same Unix file system APIs (e.g. FTS).
Implement a platform specific implementation to compare files on
Windows.
  • Loading branch information
compnerd committed Apr 16, 2024
1 parent 32b136e commit 43478a9
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 2 deletions.
155 changes: 153 additions & 2 deletions Sources/FoundationEssentials/FileManager/FileManager+Basics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,28 @@ import Darwin
import Glibc
#elseif os(Windows)
import CRT
import WinSDK
#endif

extension _FILE_ID_128: Equatable {
public static func == (_ lhs: _FILE_ID_128, _ rhs: _FILE_ID_128) -> Bool {
return withUnsafePointer(to: lhs.Identifier.0) { pLHS in
let pLHSBuffer = UnsafeBufferPointer(start: pLHS, count: 16)
return withUnsafePointer(to: rhs.Identifier.0) { pRHS in
let pRHSBuffer = UnsafeBufferPointer(start: pRHS, count: 16)

return pLHSBuffer.elementsEqual(pRHSBuffer)
}
}
}
}

extension LARGE_INTEGER: Equatable {
public static func == (_ lhs: LARGE_INTEGER, _ rhs: LARGE_INTEGER) -> Bool {
return lhs.QuadPart == rhs.QuadPart
}
}

internal struct _FileManagerImpl {
weak var _manager: FileManager?
weak var delegate: FileManagerDelegate?
Expand Down Expand Up @@ -60,6 +80,134 @@ internal struct _FileManagerImpl {
atPath path: String,
andPath other: String
) -> Bool {
#if os(Windows)
return (try? path.withNTPathRepresentation {
let hLHS = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, 0, nil)
if hLHS == INVALID_HANDLE_VALUE {
return false
}
defer { CloseHandle(hLHS) }

return (try? other.withNTPathRepresentation {
let hRHS = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, 0, nil)
if hRHS == INVALID_HANDLE_VALUE {
return false
}
defer { CloseHandle(hRHS) }

let dwLHSFileType: DWORD = GetFileType(hLHS)
let dwRHSFileType: DWORD = GetFileType(hRHS)

guard dwLHSFileType == FILE_TYPE_DISK, dwRHSFileType == FILE_TYPE_DISK else {
return CompareObjectHandles(hLHS, hRHS)
}

var fiLHS: FILE_ID_INFO = .init()
guard GetFileInformationByHandleEx(hLHS, FileIdInfo, &fiLHS, DWORD(MemoryLayout.size(ofValue: fiLHS))) else {
return false
}

var fiRHS: FILE_ID_INFO = .init()
guard GetFileInformationByHandleEx(hRHS, FileIdInfo, &fiRHS, DWORD(MemoryLayout.size(ofValue: fiRHS))) else {
return false
}

if fiLHS.VolumeSerialNumber == fiRHS.VolumeSerialNumber, fiLHS.FileId == fiRHS.FileId {
return true
}

var fbiLHS: FILE_BASIC_INFO = .init()
guard GetFileInformationByHandleEx(hLHS, FileBasicInfo, &fbiLHS, DWORD(MemoryLayout.size(ofValue: fbiLHS))) else {
return false
}

var fbiRHS: FILE_BASIC_INFO = .init()
guard GetFileInformationByHandleEx(hRHS, FileBasicInfo, &fbiRHS, DWORD(MemoryLayout.size(ofValue: fbiRHS))) else {
return false
}

if fbiLHS.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT,
fbiRHS.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT {
return (try? destinationOfSymbolicLink(atPath: path) == destinationOfSymbolicLink(atPath: other)) ?? false
} else if fbiLHS.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY,
fbiRHS.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == FILE_ATTRIBUTE_DIRECTORY {
// We immediately convert the contents to a set to allow us
// to compare the two sets in a single pass. This is
// computationally less expensive (O(n) vs O(n^2)) than
// comparing each item in the array.
guard let aLHSItems = try? Set(fileManager.contentsOfDirectory(atPath: path)),
let aRHSItems = try? Set(fileManager.contentsOfDirectory(atPath: other)),
aLHSItems.count == aRHSItems.count, aRHSItems.intersection(aLHSItems) == aLHSItems else {
return false
}

for item in aLHSItems {
var hr: HRESULT

var pszLHS: PWSTR? = nil
hr = PathAllocCombine(path, item, PATHCCH_ALLOW_LONG_PATHS, &pszLHS)
guard hr == S_OK else { return false }
defer { LocalFree(pszLHS) }

var pszRHS: PWSTR? = nil
hr = PathAllocCombine(other, item, PATHCCH_ALLOW_LONG_PATHS, &pszRHS)
guard hr == S_OK else { return false }
defer { LocalFree(pszRHS) }

let lhs: String = String(decodingCString: pszLHS!, as: UTF16.self)
let rhs: String = String(decodingCString: pszRHS!, as: UTF16.self)
guard contentsEqual(atPath: lhs, andPath: rhs) else { return false }
}

return true
} else if fbiLHS.FileAttributes & FILE_ATTRIBUTE_NORMAL == FILE_ATTRIBUTE_NORMAL,
fbiRHS.FileAttributes & FILE_ATTRIBUTE_NORMAL == FILE_ATTRIBUTE_NORMAL {
var liLHSSize: LARGE_INTEGER = .init()
var liRHSSize: LARGE_INTEGER = .init()
guard GetFileSizeEx(hLHS, &liLHSSize), GetFileSizeEx(hRHS, &liRHSSize), liLHSSize == liRHSSize else {
return false
}

let kBufferSize = 0x1000
var dwBytesRemaining: UInt64 = UInt64(liLHSSize.QuadPart)
return withUnsafeTemporaryAllocation(of: CChar.self, capacity: kBufferSize) { pLHSBuffer in
return withUnsafeTemporaryAllocation(of: CChar.self, capacity: kBufferSize) { pRHSBuffer in
repeat {
let dwBytesToRead: DWORD = DWORD(min(UInt64(pLHSBuffer.count), dwBytesRemaining))

var dwLHSRead: DWORD = 0
guard ReadFile(hLHS, pLHSBuffer.baseAddress, dwBytesToRead, &dwLHSRead, nil) else {
return false
}

var dwRHSRead: DWORD = 0
guard ReadFile(hRHS, pRHSBuffer.baseAddress, dwBytesToRead, &dwRHSRead, nil) else {
return false
}

guard dwLHSRead == dwRHSRead else {
return false
}

guard memcmp(pLHSBuffer.baseAddress, pRHSBuffer.baseAddress, Int(dwLHSRead)) == 0 else {
return false
}

if dwLHSRead < dwBytesToRead {
break
}

dwBytesRemaining -= UInt64(dwLHSRead)
} while dwBytesRemaining > 0
return dwBytesRemaining == 0
}
}
}

return false
}) ?? false
}) ?? false
#else
func _openFD(_ path: UnsafePointer<CChar>) -> Int32? {
var statBuf = stat()
let fd = open(path, 0, 0)
Expand Down Expand Up @@ -148,6 +296,7 @@ internal struct _FileManagerImpl {
}

fatalError("Unknown file type 0x\(String(myInfo.st_mode, radix: 16)) for file \(path)")
#endif
}

func fileSystemRepresentation(withPath path: String) -> UnsafePointer<CChar>? {
Expand Down Expand Up @@ -210,7 +359,8 @@ extension FileManager {
return try path.withFileSystemRepresentation(body)
}
#endif


#if !os(Windows)
@nonobjc
func _fileStat(_ path: String) -> stat? {
let result = self.withFileSystemRepresentation(for: path) { rep -> stat? in
Expand All @@ -224,7 +374,8 @@ extension FileManager {
guard let result else { return nil }
return result
}

#endif

@nonobjc
static var MAX_PATH_SIZE: Int { 1026 }
}
16 changes: 16 additions & 0 deletions Sources/FoundationEssentials/WinSDK+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ package var PAGE_READONLY: DWORD {
DWORD(WinSDK.PAGE_READONLY)
}

package var PATHCCH_ALLOW_LONG_PATHS: ULONG {
ULONG(WinSDK.PATHCCH_ALLOW_LONG_PATHS.rawValue)
}

package var RRF_RT_REG_SZ: DWORD {
DWORD(WinSDK.RRF_RT_REG_SZ)
}

package var SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD {
DWORD(WinSDK.SYMBOLIC_LINK_FLAG_DIRECTORY)
}
Expand All @@ -157,4 +165,12 @@ package var VOLUME_NAME_DOS: DWORD {
DWORD(WinSDK.VOLUME_NAME_DOS)
}

package func PathAllocCombine(_ pszPathIn: String, _ pszMore: String, _ dwFlags: ULONG, _ ppszPathOut: UnsafeMutablePointer<PWSTR?>?) -> HRESULT {
pszPathIn.withCString(encodedAs: UTF16.self) { pszPathIn in
pszMore.withCString(encodedAs: UTF16.self) { pszMore in
WinSDK.PathAllocCombine(pszPathIn, pszMore, dwFlags, ppszPathOut)
}
}
}

#endif

0 comments on commit 43478a9

Please sign in to comment.