Skip to content

Commit 76995e8

Browse files
authored
Merge pull request #1510 from spevans/pr_fm_contents_equal
2 parents 7d8bbf0 + 054ed7c commit 76995e8

File tree

3 files changed

+297
-3
lines changed

3 files changed

+297
-3
lines changed

CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,15 @@ static inline int _direntNameLength(struct dirent *entry) {
408408
#endif
409409
}
410410

411+
// major() and minor() might be implemented as macros or functions.
412+
static inline unsigned int _dev_major(dev_t rdev) {
413+
return major(rdev);
414+
}
415+
416+
static inline unsigned int _dev_minor(dev_t rdev) {
417+
return minor(rdev);
418+
}
419+
411420
_CF_EXPORT_SCOPE_END
412421

413422
#endif /* __COREFOUNDATION_FORSWIFTFOUNDATIONONLY__ */

Foundation/FileManager.swift

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -725,11 +725,163 @@ open class FileManager : NSObject {
725725
open func isDeletableFile(atPath path: String) -> Bool {
726726
NSUnimplemented()
727727
}
728-
728+
729+
private func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
730+
let fd1 = open(file1Rep, O_RDONLY)
731+
guard fd1 >= 0 else {
732+
return false
733+
}
734+
defer { close(fd1) }
735+
736+
let fd2 = open(file2Rep, O_RDONLY)
737+
guard fd2 >= 0 else {
738+
return false
739+
}
740+
defer { close(fd2) }
741+
742+
let buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
743+
let buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
744+
defer {
745+
buffer1.deallocate()
746+
buffer2.deallocate()
747+
}
748+
749+
var bytesLeft = size
750+
while bytesLeft > 0 {
751+
let bytesToRead = Int(min(Int64(bufSize), bytesLeft))
752+
guard read(fd1, buffer1, bytesToRead) == bytesToRead else {
753+
return false
754+
}
755+
guard read(fd2, buffer2, bytesToRead) == bytesToRead else {
756+
return false
757+
}
758+
guard memcmp(buffer1, buffer2, bytesToRead) == 0 else {
759+
return false
760+
}
761+
bytesLeft -= Int64(bytesToRead)
762+
}
763+
return true
764+
}
765+
766+
private func _compareSymlinks(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64) -> Bool {
767+
let bufSize = Int(size)
768+
let buffer1 = UnsafeMutablePointer<CChar>.allocate(capacity: Int(bufSize))
769+
let buffer2 = UnsafeMutablePointer<CChar>.allocate(capacity: Int(bufSize))
770+
771+
let size1 = readlink(file1Rep, buffer1, bufSize)
772+
let size2 = readlink(file2Rep, buffer2, bufSize)
773+
774+
let compare: Bool
775+
if size1 < 0 || size2 < 0 || size1 != size || size1 != size2 {
776+
compare = false
777+
} else {
778+
compare = memcmp(buffer1, buffer2, size1) == 0
779+
}
780+
781+
buffer1.deallocate()
782+
buffer2.deallocate()
783+
return compare
784+
}
785+
786+
private func _compareDirectories(atPath path1: String, andPath path2: String) -> Bool {
787+
guard let enumerator1 = enumerator(atPath: path1) else {
788+
return false
789+
}
790+
791+
guard let enumerator2 = enumerator(atPath: path2) else {
792+
return false
793+
}
794+
795+
var path1entries = Set<String>()
796+
while let item = enumerator1.nextObject() as? String {
797+
path1entries.insert(item)
798+
}
799+
800+
while let item = enumerator2.nextObject() as? String {
801+
if path1entries.remove(item) == nil {
802+
return false
803+
}
804+
if contentsEqual(atPath: NSString(string: path1).appendingPathComponent(item), andPath: NSString(string: path2).appendingPathComponent(item)) == false {
805+
return false
806+
}
807+
}
808+
return path1entries.isEmpty
809+
}
810+
811+
private func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
812+
let _fsRep: UnsafePointer<Int8>
813+
if fsRep == nil {
814+
_fsRep = fileSystemRepresentation(withPath: path)
815+
defer { _fsRep.deallocate() }
816+
} else {
817+
_fsRep = fsRep!
818+
}
819+
var statInfo = stat()
820+
guard lstat(_fsRep, &statInfo) == 0 else {
821+
throw _NSErrorWithErrno(errno, reading: true, path: path)
822+
}
823+
return statInfo
824+
}
825+
729826
/* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes.
730827
*/
731828
open func contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
732-
NSUnimplemented()
829+
let fsRep1 = fileSystemRepresentation(withPath: path1)
830+
defer { fsRep1.deallocate() }
831+
832+
guard let file1 = try? _lstatFile(atPath: path1, withFileSystemRepresentation: fsRep1) else {
833+
return false
834+
}
835+
let file1Type = file1.st_mode & S_IFMT
836+
837+
// Dont use access() for symlinks as only the contents should be checked even
838+
// if the symlink doesnt point to an actual file, but access() will always try
839+
// to resolve the link and fail if the destination is not found
840+
if path1 == path2 && file1Type != S_IFLNK {
841+
return access(fsRep1, R_OK) == 0
842+
}
843+
844+
let fsRep2 = fileSystemRepresentation(withPath: path2)
845+
defer { fsRep2.deallocate() }
846+
guard let file2 = try? _lstatFile(atPath: path2, withFileSystemRepresentation: fsRep2) else {
847+
return false
848+
}
849+
let file2Type = file2.st_mode & S_IFMT
850+
851+
// Are paths the same type: file, directory, symbolic link etc.
852+
guard file1Type == file2Type else {
853+
return false
854+
}
855+
856+
if file1Type == S_IFCHR || file1Type == S_IFBLK {
857+
// For character devices, just check the major/minor pair is the same.
858+
return _dev_major(file1.st_rdev) == _dev_major(file2.st_rdev)
859+
&& _dev_minor(file1.st_rdev) == _dev_minor(file2.st_rdev)
860+
}
861+
862+
// If both paths point to the same device/inode or they are both zero length
863+
// then they are considered equal so just check readability.
864+
if (file1.st_dev == file2.st_dev && file1.st_ino == file2.st_ino)
865+
|| (file1.st_size == 0 && file2.st_size == 0) {
866+
return access(fsRep1, R_OK) == 0 && access(fsRep2, R_OK) == 0
867+
}
868+
869+
if file1Type == S_IFREG {
870+
// Regular files and symlinks should at least have the same filesize if contents are equal.
871+
guard file1.st_size == file2.st_size else {
872+
return false
873+
}
874+
return _compareFiles(withFileSystemRepresentation: path1, andFileSystemRepresentation: path2, size: Int64(file1.st_size), bufSize: Int(file1.st_blksize))
875+
}
876+
else if file1Type == S_IFLNK {
877+
return _compareSymlinks(withFileSystemRepresentation: fsRep1, andFileSystemRepresentation: fsRep2, size: Int64(file1.st_size))
878+
}
879+
else if file1Type == S_IFDIR {
880+
return _compareDirectories(atPath: path1, andPath: path2)
881+
}
882+
883+
// Dont know how to compare other file types.
884+
return false
733885
}
734886

735887
/* displayNameAtPath: returns an NSString suitable for presentation to the user. For directories which have localization information, this will return the appropriate localized string. This string is not suitable for passing to anything that must interact with the filesystem.

TestFoundation/TestFileManager.swift

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class TestFileManager : XCTestCase {
3535
("test_homedirectoryForUser", test_homedirectoryForUser),
3636
("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser),
3737
("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath),
38-
("test_mountedVolumeURLs", test_mountedVolumeURLs)
38+
("test_mountedVolumeURLs", test_mountedVolumeURLs),
39+
("test_contentsEqual", test_contentsEqual)
3940
]
4041
}
4142

@@ -690,4 +691,136 @@ class TestFileManager : XCTestCase {
690691
XCTAssertTrue(visibleVolumes.count < volumes.count)
691692
#endif
692693
}
694+
695+
func test_contentsEqual() {
696+
let fm = FileManager.default
697+
let tmpParentDirURL = URL(fileURLWithPath: NSTemporaryDirectory() + "test_contentsEqualdir", isDirectory: true)
698+
let testDir1 = tmpParentDirURL.appendingPathComponent("testDir1")
699+
let testDir2 = tmpParentDirURL.appendingPathComponent("testDir2")
700+
let testDir3 = testDir1.appendingPathComponent("subDir/anotherDir/extraDir/lastDir")
701+
702+
defer { try? fm.removeItem(atPath: tmpParentDirURL.path) }
703+
704+
func testFileURL(_ name: String, _ ext: String) -> URL? {
705+
guard let url = testBundle().url(forResource: name, withExtension: ext) else {
706+
XCTFail("Cant open \(name).\(ext)")
707+
return nil
708+
}
709+
return url
710+
}
711+
712+
guard let testFile1URL = testFileURL("NSStringTestData", "txt") else { return }
713+
guard let testFile2URL = testFileURL("NSURLTestData", "plist") else { return }
714+
guard let testFile3URL = testFileURL("NSString-UTF32-BE-data", "txt") else { return }
715+
guard let testFile4URL = testFileURL("NSString-UTF32-LE-data", "txt") else { return }
716+
let symlink = testDir1.appendingPathComponent("testlink").path
717+
718+
// Setup test directories
719+
do {
720+
// Clean out and leftover test data
721+
try? fm.removeItem(atPath: tmpParentDirURL.path)
722+
723+
// testDir1
724+
try fm.createDirectory(atPath: testDir1.path, withIntermediateDirectories: true)
725+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("null1").path, withDestinationPath: "/dev/null")
726+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("zero1").path, withDestinationPath: "/dev/zero")
727+
try "foo".write(toFile: testDir1.appendingPathComponent("foo.txt").path, atomically: false, encoding: .ascii)
728+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("foo1").path, withDestinationPath: "foo.txt")
729+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
730+
let unreadable = testDir1.appendingPathComponent("unreadable_file").path
731+
try "unreadable".write(toFile: unreadable, atomically: false, encoding: .ascii)
732+
try fm.setAttributes([.posixPermissions: NSNumber(value: 0)], ofItemAtPath: unreadable)
733+
try Data().write(to: testDir1.appendingPathComponent("empty_file"))
734+
try fm.createSymbolicLink(atPath: symlink, withDestinationPath: testFile1URL.path)
735+
736+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("thisDir").path, withDestinationPath: ".")
737+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("parentDir").path, withDestinationPath: "..")
738+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("rootDir").path, withDestinationPath: "/")
739+
740+
// testDir2
741+
try fm.createDirectory(atPath: testDir2.path, withIntermediateDirectories: true)
742+
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
743+
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
744+
745+
// testDir3
746+
try fm.createDirectory(atPath: testDir3.path, withIntermediateDirectories: true)
747+
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
748+
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
749+
} catch {
750+
XCTFail(String(describing: error))
751+
}
752+
753+
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/null"))
754+
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/urandom", andPath: "/dev/urandom"))
755+
XCTAssertFalse(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/zero"))
756+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("null1").path, andPath: "/dev/null"))
757+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("zero").path, andPath: "/dev/zero"))
758+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo1").path))
759+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo2").path))
760+
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.appendingPathComponent("bar2").path, andPath: testDir2.appendingPathComponent("bar2").path))
761+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo1").path, andPath: testDir2.appendingPathComponent("foo2").path))
762+
XCTAssertFalse(fm.contentsEqual(atPath: "/non_existant_file", andPath: "/non_existant_file"))
763+
764+
let emptyFile = testDir1.appendingPathComponent("empty_file")
765+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "/dev/null"))
766+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("null1").path))
767+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))
768+
769+
XCTAssertTrue(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile1URL.path))
770+
XCTAssertFalse(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile2URL.path))
771+
XCTAssertFalse(fm.contentsEqual(atPath: testFile3URL.path, andPath: testFile4URL.path))
772+
XCTAssertFalse(fm.contentsEqual(atPath: symlink, andPath: testFile1URL.path))
773+
774+
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir1.path))
775+
XCTAssertTrue(fm.contentsEqual(atPath: testDir2.path, andPath: testDir3.path))
776+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
777+
778+
// Copy everything in testDir1 to testDir2 to make them equal
779+
do {
780+
for entry in try fm.subpathsOfDirectory(atPath: testDir1.path) {
781+
// Skip entries that already exist
782+
if entry == "bar2" || entry == "unreadable_file" {
783+
continue
784+
}
785+
let srcPath = testDir1.appendingPathComponent(entry).path
786+
let dstPath = testDir2.appendingPathComponent(entry).path
787+
if let attrs = try? fm.attributesOfItem(atPath: srcPath),
788+
let fileType = attrs[.type] as? FileAttributeType, fileType == .typeDirectory {
789+
try fm.createDirectory(atPath: dstPath, withIntermediateDirectories: false, attributes: nil)
790+
} else {
791+
try fm.copyItem(atPath: srcPath, toPath: dstPath)
792+
}
793+
}
794+
} catch {
795+
XCTFail("Failed to copy \(testDir1.path) to \(testDir2.path), \(error)")
796+
return
797+
}
798+
// This will still fail due to unreadable files and a file in testDir2 not in testDir1
799+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
800+
do {
801+
try fm.copyItem(atPath: testDir2.appendingPathComponent("foo2").path, toPath: testDir1.appendingPathComponent("foo2").path)
802+
try fm.removeItem(atPath: testDir1.appendingPathComponent("unreadable_file").path)
803+
} catch {
804+
XCTFail(String(describing: error))
805+
return
806+
}
807+
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
808+
809+
let dataFile1 = testDir1.appendingPathComponent("dataFile")
810+
let dataFile2 = testDir2.appendingPathComponent("dataFile")
811+
do {
812+
try Data(count: 100_000).write(to: dataFile1)
813+
try fm.copyItem(atPath: dataFile1.path, toPath: dataFile2.path)
814+
} catch {
815+
XCTFail("Could not create test data files: \(error)")
816+
return
817+
}
818+
XCTAssertTrue(fm.contentsEqual(atPath: dataFile1.path, andPath: dataFile2.path))
819+
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
820+
var data = Data(count: 100_000)
821+
data[99_999] = 1
822+
try? data.write(to: dataFile1)
823+
XCTAssertFalse(fm.contentsEqual(atPath: dataFile1.path, andPath: dataFile2.path))
824+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
825+
}
693826
}

0 commit comments

Comments
 (0)