Skip to content

Commit ea46535

Browse files
authored
Merge pull request swiftlang#2411 from gmittert/WhichOneOfYouIsTheOriginal
Implement _contentsEqual on Windows
2 parents 9e8cb54 + 6bc00b2 commit ea46535

File tree

4 files changed

+151
-42
lines changed

4 files changed

+151
-42
lines changed

Foundation/FileManager+POSIX.swift

-28
Original file line numberDiff line numberDiff line change
@@ -853,34 +853,6 @@ extension FileManager {
853853
}
854854
}
855855

856-
private func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
857-
guard let file1 = FileHandle(fileSystemRepresentation: file1Rep, flags: O_RDONLY, createMode: 0) else { return false }
858-
guard let file2 = FileHandle(fileSystemRepresentation: file2Rep, flags: O_RDONLY, createMode: 0) else { return false }
859-
860-
var buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
861-
var buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
862-
defer {
863-
buffer1.deallocate()
864-
buffer2.deallocate()
865-
}
866-
var bytesLeft = size
867-
while bytesLeft > 0 {
868-
let bytesToRead = Int(min(Int64(bufSize), bytesLeft))
869-
870-
guard let file1BytesRead = try? file1._readBytes(into: buffer1, length: bytesToRead), file1BytesRead == bytesToRead else {
871-
return false
872-
}
873-
guard let file2BytesRead = try? file2._readBytes(into: buffer2, length: bytesToRead), file2BytesRead == bytesToRead else {
874-
return false
875-
}
876-
guard memcmp(buffer1, buffer2, bytesToRead) == 0 else {
877-
return false
878-
}
879-
bytesLeft -= Int64(bytesToRead)
880-
}
881-
return true
882-
}
883-
884856
private func _compareSymlinks(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64) -> Bool {
885857
let bufSize = Int(size)
886858
let buffer1 = UnsafeMutablePointer<CChar>.allocate(capacity: bufSize)

Foundation/FileManager+Win32.swift

+89-13
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ extension FileManager {
589589

590590
if ffd.dwFileAttributes & DWORD(FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY {
591591
let readableAttributes = ffd.dwFileAttributes & DWORD(bitPattern: ~FILE_ATTRIBUTE_READONLY)
592-
guard file.withCString(encodedAs: UTF16.self, { SetFileAttributesW($0, readableAttributes) }) else {
592+
guard itemPath.withCString(encodedAs: UTF16.self, { SetFileAttributesW($0, readableAttributes) }) else {
593593
throw _NSErrorWithWindowsError(GetLastError(), reading: false, paths: [file])
594594
}
595595
}
@@ -684,10 +684,6 @@ extension FileManager {
684684
return true
685685
}
686686

687-
internal func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
688-
NSUnimplemented()
689-
}
690-
691687
internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
692688
let _fsRep: UnsafePointer<Int8>
693689
if fsRep == nil {
@@ -749,7 +745,86 @@ extension FileManager {
749745
}
750746

751747
internal func _contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
752-
NSUnimplemented()
748+
guard let path1Handle = path1.withCString(encodedAs: UTF16.self, {
749+
CreateFileW($0, GENERIC_READ, DWORD(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), nil,
750+
DWORD(OPEN_EXISTING), DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS), nil)
751+
}), path1Handle != INVALID_HANDLE_VALUE else {
752+
return false
753+
}
754+
755+
defer { CloseHandle(path1Handle) }
756+
757+
guard let path2Handle = path2.withCString(encodedAs: UTF16.self, {
758+
CreateFileW($0, GENERIC_READ, DWORD(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), nil,
759+
DWORD(OPEN_EXISTING), DWORD(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS), nil)
760+
}), path2Handle != INVALID_HANDLE_VALUE else {
761+
return false
762+
}
763+
defer { CloseHandle(path2Handle) }
764+
765+
let file1Type = GetFileType(path1Handle)
766+
guard GetLastError() == NO_ERROR else {
767+
return false
768+
}
769+
let file2Type = GetFileType(path2Handle)
770+
guard GetLastError() == NO_ERROR else {
771+
return false
772+
}
773+
774+
guard file1Type == FILE_TYPE_DISK, file2Type == FILE_TYPE_DISK else {
775+
return false
776+
}
777+
778+
var path1FileInfo = BY_HANDLE_FILE_INFORMATION()
779+
var path2FileInfo = BY_HANDLE_FILE_INFORMATION()
780+
guard GetFileInformationByHandle(path1Handle, &path1FileInfo),
781+
GetFileInformationByHandle(path2Handle, &path2FileInfo) else {
782+
return false
783+
}
784+
785+
// If both paths point to the same volume/filenumber or they are both zero length
786+
// then they are considered equal
787+
if path1FileInfo.nFileIndexHigh == path2FileInfo.nFileIndexHigh
788+
&& path1FileInfo.nFileIndexLow == path2FileInfo.nFileIndexLow
789+
&& path1FileInfo.dwVolumeSerialNumber == path2FileInfo.dwVolumeSerialNumber {
790+
return true
791+
}
792+
793+
let path1Attrs = path1FileInfo.dwFileAttributes
794+
let path2Attrs = path2FileInfo.dwFileAttributes
795+
if path1Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
796+
|| path2Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT {
797+
guard path1Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
798+
&& path2Attrs & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT else {
799+
return false
800+
}
801+
guard let pathDest1 = try? _destinationOfSymbolicLink(atPath: path1),
802+
let pathDest2 = try? _destinationOfSymbolicLink(atPath: path2) else {
803+
return false
804+
}
805+
return pathDest1 == pathDest2
806+
} else if DWORD(FILE_ATTRIBUTE_DIRECTORY) & path1Attrs == DWORD(FILE_ATTRIBUTE_DIRECTORY)
807+
|| DWORD(FILE_ATTRIBUTE_DIRECTORY) & path2Attrs == DWORD(FILE_ATTRIBUTE_DIRECTORY) {
808+
guard DWORD(FILE_ATTRIBUTE_DIRECTORY) & path1Attrs == DWORD(FILE_ATTRIBUTE_DIRECTORY)
809+
&& DWORD(FILE_ATTRIBUTE_DIRECTORY) & path2Attrs == FILE_ATTRIBUTE_DIRECTORY else {
810+
return false
811+
}
812+
return _compareDirectories(atPath: path1, andPath: path2)
813+
} else {
814+
if path1FileInfo.nFileSizeHigh == 0 && path1FileInfo.nFileSizeLow == 0
815+
&& path2FileInfo.nFileSizeHigh == 0 && path2FileInfo.nFileSizeLow == 0 {
816+
return true
817+
}
818+
819+
let path1Fsr = fileSystemRepresentation(withPath: path1)
820+
defer { path1Fsr.deallocate() }
821+
let path2Fsr = fileSystemRepresentation(withPath: path2)
822+
defer { path2Fsr.deallocate() }
823+
return _compareFiles(withFileSystemRepresentation: path1Fsr,
824+
andFileSystemRepresentation: path2Fsr,
825+
size: (Int64(path1FileInfo.nFileSizeHigh) << 32) | Int64(path1FileInfo.nFileSizeLow),
826+
bufSize: 0x1000)
827+
}
753828
}
754829

755830
internal func _appendSymlinkDestination(_ dest: String, toPath: String) -> String {
@@ -810,7 +885,7 @@ extension FileManager {
810885
override func nextObject() -> Any? {
811886
func firstValidItem() -> URL? {
812887
while let url = _stack.popLast() {
813-
if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
888+
if !FileManager.default.fileExists(atPath: url.path) {
814889
guard let handler = _errorHandler,
815890
handler(url, _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [url.path]))
816891
else { return nil }
@@ -823,15 +898,16 @@ extension FileManager {
823898
}
824899

825900
// If we most recently returned a directory, decend into it
826-
var isDir: ObjCBool = false
827-
guard FileManager.default.fileExists(atPath: _lastReturned.path, isDirectory: &isDir) else {
828-
guard let handler = _errorHandler,
901+
guard let attrs = try? FileManager.default.windowsFileAttributes(atPath: _lastReturned.path) else {
902+
guard let handler = _errorHandler,
829903
handler(_lastReturned, _NSErrorWithWindowsError(GetLastError(), reading: true, paths: [_lastReturned.path]))
830-
else { return nil }
831-
return firstValidItem()
904+
else { return nil }
905+
return firstValidItem()
832906
}
833907

834-
if isDir.boolValue && (level == 0 || !_options.contains(.skipsSubdirectoryDescendants)) {
908+
let isDir = attrs.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY) == DWORD(FILE_ATTRIBUTE_DIRECTORY)
909+
&& attrs.dwFileAttributes & DWORD(FILE_ATTRIBUTE_REPARSE_POINT) == 0
910+
if isDir && (level == 0 || !_options.contains(.skipsSubdirectoryDescendants)) {
835911
var ffd = WIN32_FIND_DATAW()
836912
let dirPath = joinPath(prefix: _lastReturned.path, suffix: "*")
837913
let handle = dirPath.withCString(encodedAs: UTF16.self) {

Foundation/FileManager.swift

+30-1
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,8 @@ open class FileManager : NSObject {
825825
guard let enumerator2 = enumerator(atPath: path2) else {
826826
return false
827827
}
828+
enumerator1.skipDescendants()
829+
enumerator2.skipDescendants()
828830

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

876+
internal func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
877+
guard let file1 = FileHandle(fileSystemRepresentation: file1Rep, flags: O_RDONLY, createMode: 0) else { return false }
878+
guard let file2 = FileHandle(fileSystemRepresentation: file2Rep, flags: O_RDONLY, createMode: 0) else { return false }
879+
880+
var buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
881+
var buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
882+
defer {
883+
buffer1.deallocate()
884+
buffer2.deallocate()
885+
}
886+
var bytesLeft = size
887+
while bytesLeft > 0 {
888+
let bytesToRead = Int(min(Int64(bufSize), bytesLeft))
889+
890+
guard let file1BytesRead = try? file1._readBytes(into: buffer1, length: bytesToRead), file1BytesRead == bytesToRead else {
891+
return false
892+
}
893+
guard let file2BytesRead = try? file2._readBytes(into: buffer2, length: bytesToRead), file2BytesRead == bytesToRead else {
894+
return false
895+
}
896+
guard memcmp(buffer1, buffer2, bytesToRead) == 0 else {
897+
return false
898+
}
899+
bytesLeft -= Int64(bytesToRead)
900+
}
901+
return true
902+
}
903+
874904
/* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes.
875905
*/
876-
@available(Windows, deprecated, message: "Not Yet Implemented")
877906
open func contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
878907
return _contentsEqual(atPath: path1, andPath: path2)
879908
}

TestFoundation/TestFileManager.swift

+32
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,9 @@ class TestFileManager : XCTestCase {
841841
let srcLink = srcPath + "/testlink"
842842
let destLink = destPath + "/testlink"
843843
do {
844+
#if os(Windows)
845+
fm.createFile(atPath: srcPath.appendingPathComponent("linkdest"), contents: Data(), attributes: nil)
846+
#endif
844847
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
845848
try fm.copyItem(atPath: srcLink, toPath: destLink)
846849
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
@@ -925,6 +928,9 @@ class TestFileManager : XCTestCase {
925928
let srcLink = srcPath + "/testlink"
926929
let destLink = destPath + "/testlink"
927930
do {
931+
#if os(Windows)
932+
fm.createFile(atPath: srcPath.appendingPathComponent("linkdest"), contents: Data(), attributes: nil)
933+
#endif
928934
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
929935
try fm.linkItem(atPath: srcLink, toPath: destLink)
930936
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
@@ -1009,8 +1015,10 @@ class TestFileManager : XCTestCase {
10091015

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

10271035
// testDir2
10281036
try fm.createDirectory(atPath: testDir2.path, withIntermediateDirectories: true)
1037+
#if os(Windows)
1038+
try "foo".write(toFile: testDir2.appendingPathComponent("foo1").path, atomically: false, encoding: .ascii)
1039+
try fm.createDirectory(atPath: testDir2.appendingPathComponent("../testDir1").path, withIntermediateDirectories: true)
1040+
try "foo".write(toFile: testDir2.appendingPathComponent("../testDir1/foo.txt").path, atomically: false, encoding: .ascii)
1041+
#endif
10291042
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
10301043
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
10311044

10321045
// testDir3
10331046
try fm.createDirectory(atPath: testDir3.path, withIntermediateDirectories: true)
1047+
#if os(Windows)
1048+
try fm.createDirectory(atPath: testDir3.appendingPathComponent("../testDir1").path, withIntermediateDirectories: true)
1049+
try "foo".write(toFile: testDir3.appendingPathComponent("../testDir1/foo.txt").path, atomically: false, encoding: .ascii)
1050+
try "foo".write(toFile: testDir3.appendingPathComponent("foo1").path, atomically: false, encoding: .ascii)
1051+
#endif
10341052
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
10351053
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
10361054
} catch {
10371055
XCTFail(String(describing: error))
10381056
}
10391057

1058+
#if os(Windows)
1059+
XCTAssertFalse(fm.contentsEqual(atPath: "NUL", andPath: "NUL"))
1060+
#else
10401061
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/null"))
10411062
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/urandom", andPath: "/dev/urandom"))
10421063
XCTAssertFalse(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/zero"))
10431064
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("null1").path, andPath: "/dev/null"))
10441065
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("zero").path, andPath: "/dev/zero"))
1066+
#endif
10451067
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo1").path))
10461068
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo2").path))
10471069
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.appendingPathComponent("bar2").path, andPath: testDir2.appendingPathComponent("bar2").path))
10481070
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo1").path, andPath: testDir2.appendingPathComponent("foo2").path))
10491071
XCTAssertFalse(fm.contentsEqual(atPath: "/non_existent_file", andPath: "/non_existent_file"))
10501072

10511073
let emptyFile = testDir1.appendingPathComponent("empty_file")
1074+
#if os(Windows)
1075+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "NUL"))
1076+
#else
10521077
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "/dev/null"))
1078+
#endif
10531079
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("null1").path))
1080+
#if os(Windows)
1081+
// A file cannot be unreadable on Windows
1082+
XCTAssertTrue(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))
1083+
#else
10541084
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))
1085+
#endif
10551086

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

0 commit comments

Comments
 (0)