Skip to content

Implement _contentsEqual on Windows #2411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions Foundation/FileManager+POSIX.swift
Original file line number Diff line number Diff line change
Expand Up @@ -853,34 +853,6 @@ extension FileManager {
}
}

private func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
guard let file1 = FileHandle(fileSystemRepresentation: file1Rep, flags: O_RDONLY, createMode: 0) else { return false }
guard let file2 = FileHandle(fileSystemRepresentation: file2Rep, flags: O_RDONLY, createMode: 0) else { return false }

var buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
var buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
defer {
buffer1.deallocate()
buffer2.deallocate()
}
var bytesLeft = size
while bytesLeft > 0 {
let bytesToRead = Int(min(Int64(bufSize), bytesLeft))

guard let file1BytesRead = try? file1._readBytes(into: buffer1, length: bytesToRead), file1BytesRead == bytesToRead else {
return false
}
guard let file2BytesRead = try? file2._readBytes(into: buffer2, length: bytesToRead), file2BytesRead == bytesToRead else {
return false
}
guard memcmp(buffer1, buffer2, bytesToRead) == 0 else {
return false
}
bytesLeft -= Int64(bytesToRead)
}
return true
}

private func _compareSymlinks(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64) -> Bool {
let bufSize = Int(size)
let buffer1 = UnsafeMutablePointer<CChar>.allocate(capacity: bufSize)
Expand Down
102 changes: 89 additions & 13 deletions Foundation/FileManager+Win32.swift
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ extension FileManager {

if ffd.dwFileAttributes & DWORD(FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY {
let readableAttributes = ffd.dwFileAttributes & DWORD(bitPattern: ~FILE_ATTRIBUTE_READONLY)
guard file.withCString(encodedAs: UTF16.self, { SetFileAttributesW($0, readableAttributes) }) else {
guard itemPath.withCString(encodedAs: UTF16.self, { SetFileAttributesW($0, readableAttributes) }) else {
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [file])
}
}
Expand Down Expand Up @@ -684,10 +684,6 @@ extension FileManager {
return true
}

internal func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
NSUnimplemented()
}

internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
let _fsRep: UnsafePointer<Int8>
if fsRep == nil {
Expand Down Expand Up @@ -749,7 +745,86 @@ extension FileManager {
}

internal func _contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
NSUnimplemented()
guard let path1Handle = path1.withCString(encodedAs: UTF16.self, {
CreateFileW($0, GENERIC_READ, DWORD(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), nil,
DWORD(OPEN_EXISTING), DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS), nil)
}), path1Handle != INVALID_HANDLE_VALUE else {
return false
}

defer { CloseHandle(path1Handle) }

guard let path2Handle = path2.withCString(encodedAs: UTF16.self, {
CreateFileW($0, GENERIC_READ, DWORD(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), nil,
DWORD(OPEN_EXISTING), DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS), nil)
}), path2Handle != INVALID_HANDLE_VALUE else {
return false
}
defer { CloseHandle(path2Handle) }

let file1Type = GetFileType(path1Handle)
guard GetLastError() == NO_ERROR else {
return false
}
let file2Type = GetFileType(path2Handle)
guard GetLastError() == NO_ERROR else {
return false
}

guard file1Type == FILE_TYPE_DISK, file2Type == FILE_TYPE_DISK else {
return false
}

var path1FileInfo = BY_HANDLE_FILE_INFORMATION()
var path2FileInfo = BY_HANDLE_FILE_INFORMATION()
guard GetFileInformationByHandle(path1Handle, &path1FileInfo),
GetFileInformationByHandle(path2Handle, &path2FileInfo) else {
return false
}

// If both paths point to the same volume/filenumber or they are both zero length
// then they are considered equal
if path1FileInfo.nFileIndexHigh == path2FileInfo.nFileIndexHigh
&& path1FileInfo.nFileIndexLow == path2FileInfo.nFileIndexLow
&& path1FileInfo.dwVolumeSerialNumber == path2FileInfo.dwVolumeSerialNumber {
return true
}

let path1Attrs = path1FileInfo.dwFileAttributes
let path2Attrs = path2FileInfo.dwFileAttributes
if path1Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
|| path2Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT {
guard path1Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
&& path2Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT else {
return false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh? What am I missing? Isn't this the condition that is in the if?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner one is &&, the outer is ||. If either is a reparse point, go into the if, but if one is not a reparse point, the immediately bail out. Sadly there's no logical XOR which might make that clearer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, does ^ not work here?

guard let pathDest1 = try? _destinationOfSymbolicLink(atPath: path1),
let pathDest2 = try? _destinationOfSymbolicLink(atPath: path2) else {
return false
}
return pathDest1 == pathDest2
} else if DWORD(FILE_ATTRIBUTE_DIRECTORY) & path1Attrs == DWORD(FILE_ATTRIBUTE_DIRECTORY)
|| DWORD(FILE_ATTRIBUTE_DIRECTORY) & path2Attrs == DWORD(FILE_ATTRIBUTE_DIRECTORY) {
guard DWORD(FILE_ATTRIBUTE_DIRECTORY) & path1Attrs == DWORD(FILE_ATTRIBUTE_DIRECTORY)
&& DWORD(FILE_ATTRIBUTE_DIRECTORY) & path2Attrs == FILE_ATTRIBUTE_DIRECTORY else {
return false
}
return _compareDirectories(atPath: path1, andPath: path2)
} else {
if path1FileInfo.nFileSizeHigh == 0 && path1FileInfo.nFileSizeLow == 0
&& path2FileInfo.nFileSizeHigh == 0 && path2FileInfo.nFileSizeLow == 0 {
return true
}

let path1Fsr = fileSystemRepresentation(withPath: path1)
defer { path1Fsr.deallocate() }
let path2Fsr = fileSystemRepresentation(withPath: path2)
defer { path2Fsr.deallocate() }
return _compareFiles(withFileSystemRepresentation: path1Fsr,
andFileSystemRepresentation: path2Fsr,
size: (Int64(path1FileInfo.nFileSizeHigh) << 32) | Int64(path1FileInfo.nFileSizeLow),
bufSize: 0x1000)
}
}

internal func _appendSymlinkDestination(_ dest: String, toPath: String) -> String {
Expand Down Expand Up @@ -810,7 +885,7 @@ extension FileManager {
override func nextObject() -> Any? {
func firstValidItem() -> URL? {
while let url = _stack.popLast() {
if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
if !FileManager.default.fileExists(atPath: url.path) {
guard let handler = _errorHandler,
handler(url, _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [url.path]))
else { return nil }
Expand All @@ -823,15 +898,16 @@ extension FileManager {
}

// If we most recently returned a directory, decend into it
var isDir: ObjCBool = false
guard FileManager.default.fileExists(atPath: _lastReturned.path, isDirectory: &isDir) else {
guard let handler = _errorHandler,
guard let attrs = try? FileManager.default.windowsFileAttributes(atPath: _lastReturned.path) else {
guard let handler = _errorHandler,
handler(_lastReturned, _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [_lastReturned.path]))
else { return nil }
return firstValidItem()
else { return nil }
return firstValidItem()
}

if isDir.boolValue && (level == 0 || !_options.contains(.skipsSubdirectoryDescendants)) {
let isDir = attrs.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY) == DWORD(FILE_ATTRIBUTE_DIRECTORY)
&& attrs.dwFileAttributes & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == 0
if isDir && (level == 0 || !_options.contains(.skipsSubdirectoryDescendants)) {
var ffd = WIN32_FIND_DATAW()
let dirPath = joinPath(prefix: _lastReturned.path, suffix: "*")
let handle = dirPath.withCString(encodedAs: UTF16.self) {
Expand Down
31 changes: 30 additions & 1 deletion Foundation/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,8 @@ open class FileManager : NSObject {
guard let enumerator2 = enumerator(atPath: path2) else {
return false
}
enumerator1.skipDescendants()
enumerator2.skipDescendants()

var path1entries = Set<String>()
while let item = enumerator1.nextObject() as? String {
Expand Down Expand Up @@ -871,9 +873,36 @@ open class FileManager : NSObject {
}()
#endif

internal func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
guard let file1 = FileHandle(fileSystemRepresentation: file1Rep, flags: O_RDONLY, createMode: 0) else { return false }
guard let file2 = FileHandle(fileSystemRepresentation: file2Rep, flags: O_RDONLY, createMode: 0) else { return false }

var buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
var buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
defer {
buffer1.deallocate()
buffer2.deallocate()
}
var bytesLeft = size
while bytesLeft > 0 {
let bytesToRead = Int(min(Int64(bufSize), bytesLeft))

guard let file1BytesRead = try? file1._readBytes(into: buffer1, length: bytesToRead), file1BytesRead == bytesToRead else {
return false
}
guard let file2BytesRead = try? file2._readBytes(into: buffer2, length: bytesToRead), file2BytesRead == bytesToRead else {
return false
}
guard memcmp(buffer1, buffer2, bytesToRead) == 0 else {
return false
}
bytesLeft -= Int64(bytesToRead)
}
return true
}

/* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes.
*/
@available(Windows, deprecated, message: "Not Yet Implemented")
open func contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
return _contentsEqual(atPath: path1, andPath: path2)
}
Expand Down
32 changes: 32 additions & 0 deletions TestFoundation/TestFileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,9 @@ class TestFileManager : XCTestCase {
let srcLink = srcPath + "/testlink"
let destLink = destPath + "/testlink"
do {
#if os(Windows)
fm.createFile(atPath: srcPath.appendingPathComponent("linkdest"), contents: Data(), attributes: nil)
#endif
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
try fm.copyItem(atPath: srcLink, toPath: destLink)
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
Expand Down Expand Up @@ -925,6 +928,9 @@ class TestFileManager : XCTestCase {
let srcLink = srcPath + "/testlink"
let destLink = destPath + "/testlink"
do {
#if os(Windows)
fm.createFile(atPath: srcPath.appendingPathComponent("linkdest"), contents: Data(), attributes: nil)
#endif
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
try fm.linkItem(atPath: srcLink, toPath: destLink)
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
Expand Down Expand Up @@ -1009,8 +1015,10 @@ class TestFileManager : XCTestCase {

// testDir1
try fm.createDirectory(atPath: testDir1.path, withIntermediateDirectories: true)
#if !os(Windows)
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("null1").path, withDestinationPath: "/dev/null")
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("zero1").path, withDestinationPath: "/dev/zero")
#endif
try "foo".write(toFile: testDir1.appendingPathComponent("foo.txt").path, atomically: false, encoding: .ascii)
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("foo1").path, withDestinationPath: "foo.txt")
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
Expand All @@ -1026,32 +1034,55 @@ class TestFileManager : XCTestCase {

// testDir2
try fm.createDirectory(atPath: testDir2.path, withIntermediateDirectories: true)
#if os(Windows)
try "foo".write(toFile: testDir2.appendingPathComponent("foo1").path, atomically: false, encoding: .ascii)
try fm.createDirectory(atPath: testDir2.appendingPathComponent("../testDir1").path, withIntermediateDirectories: true)
try "foo".write(toFile: testDir2.appendingPathComponent("../testDir1/foo.txt").path, atomically: false, encoding: .ascii)
#endif
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")

// testDir3
try fm.createDirectory(atPath: testDir3.path, withIntermediateDirectories: true)
#if os(Windows)
try fm.createDirectory(atPath: testDir3.appendingPathComponent("../testDir1").path, withIntermediateDirectories: true)
try "foo".write(toFile: testDir3.appendingPathComponent("../testDir1/foo.txt").path, atomically: false, encoding: .ascii)
try "foo".write(toFile: testDir3.appendingPathComponent("foo1").path, atomically: false, encoding: .ascii)
#endif
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
} catch {
XCTFail(String(describing: error))
}

#if os(Windows)
XCTAssertFalse(fm.contentsEqual(atPath: "NUL", andPath: "NUL"))
#else
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/null"))
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/urandom", andPath: "/dev/urandom"))
XCTAssertFalse(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/zero"))
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("null1").path, andPath: "/dev/null"))
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("zero").path, andPath: "/dev/zero"))
#endif
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo1").path))
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo2").path))
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.appendingPathComponent("bar2").path, andPath: testDir2.appendingPathComponent("bar2").path))
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo1").path, andPath: testDir2.appendingPathComponent("foo2").path))
XCTAssertFalse(fm.contentsEqual(atPath: "/non_existent_file", andPath: "/non_existent_file"))

let emptyFile = testDir1.appendingPathComponent("empty_file")
#if os(Windows)
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "NUL"))
#else
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "/dev/null"))
#endif
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("null1").path))
#if os(Windows)
// A file cannot be unreadable on Windows
XCTAssertTrue(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))
#else
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))
#endif

XCTAssertTrue(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile1URL.path))
XCTAssertFalse(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile2URL.path))
Expand Down Expand Up @@ -1774,6 +1805,7 @@ VIDEOS=StopgapVideos
("test_getRelationship", test_getRelationship),
("test_displayNames", test_displayNames),
("test_getItemReplacementDirectory", test_getItemReplacementDirectory),
("test_contentsEqual", test_contentsEqual),
/* ⚠️ */ ("test_replacement", testExpectedToFail(test_replacement,
/* ⚠️ */ "<https://bugs.swift.org/browse/SR-10819> Re-enable Foundation test TestFileManager.test_replacement")),
]
Expand Down