Skip to content

Commit 06add84

Browse files
committed
FileManager: Implement contentsEqual(atPath:andPath:)
1 parent 4f2ca47 commit 06add84

File tree

3 files changed

+242
-3
lines changed

3 files changed

+242
-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 _char_dev_major(dev_t rdev) {
413+
return major(rdev);
414+
}
415+
416+
static inline unsigned int _char_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: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,11 +637,161 @@ open class FileManager : NSObject {
637637
open func isDeletableFile(atPath path: String) -> Bool {
638638
NSUnimplemented()
639639
}
640-
640+
641+
private func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64) -> Bool {
642+
let bufSize = min(size, 1024 * 1024)
643+
let fd1 = open(file1Rep, O_RDONLY)
644+
guard fd1 >= 0 else {
645+
return false
646+
}
647+
defer { close(fd1) }
648+
649+
let fd2 = open(file2Rep, O_RDONLY)
650+
guard fd2 >= 0 else {
651+
return false
652+
}
653+
defer { close(fd2) }
654+
655+
let buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(bufSize))
656+
let buffer2 = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(bufSize))
657+
defer {
658+
buffer1.deallocate()
659+
buffer2.deallocate()
660+
}
661+
662+
var bytesLeft = size
663+
while bytesLeft > 0 {
664+
let bytesToRead = Int(min(bufSize, bytesLeft))
665+
guard read(fd1, buffer1, bytesToRead) == bytesToRead else {
666+
return false
667+
}
668+
guard read(fd2, buffer2, bytesToRead) == bytesToRead else {
669+
return false
670+
}
671+
guard memcmp(buffer1, buffer2, bytesToRead) == 0 else {
672+
return false
673+
}
674+
bytesLeft -= Int64(bytesToRead)
675+
}
676+
return true
677+
}
678+
679+
private func _compareSymlinks(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64) -> Bool {
680+
let bufSize = Int(size) + 1
681+
let buffer1 = UnsafeMutablePointer<CChar>.allocate(capacity: Int(bufSize))
682+
let buffer2 = UnsafeMutablePointer<CChar>.allocate(capacity: Int(bufSize))
683+
684+
let size1 = readlink(file1Rep, buffer1, bufSize)
685+
let size2 = readlink(file2Rep, buffer2, bufSize)
686+
687+
let compare: Bool
688+
if size1 < 0 || size2 < 0 || size1 != size || size1 != size2 {
689+
compare = false
690+
} else {
691+
compare = memcmp(buffer1, buffer2, size1) == 0
692+
}
693+
694+
buffer1.deallocate()
695+
buffer2.deallocate()
696+
return compare
697+
}
698+
699+
private func _compareDirectories(atPath path1: String, andPath path2: String) -> Bool {
700+
guard let enumerator1 = enumerator(atPath: path1) else {
701+
return false
702+
}
703+
704+
guard let enumerator2 = enumerator(atPath: path2) else {
705+
return false
706+
}
707+
708+
var path1entries = Set<String>()
709+
while let item = enumerator1.nextObject() as? String {
710+
path1entries.insert(item)
711+
}
712+
713+
while let item = enumerator2.nextObject() as? String {
714+
if path1entries.remove(item) == nil {
715+
return false
716+
}
717+
}
718+
return path1entries.isEmpty
719+
}
720+
721+
private func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
722+
let _fsRep: UnsafePointer<Int8>
723+
if fsRep == nil {
724+
_fsRep = fileSystemRepresentation(withPath: path)
725+
defer { _fsRep.deallocate() }
726+
} else {
727+
_fsRep = fsRep!
728+
}
729+
var statInfo = stat()
730+
guard lstat(_fsRep, &statInfo) == 0 else {
731+
throw _NSErrorWithErrno(errno, reading: true, path: path)
732+
}
733+
return statInfo
734+
}
735+
641736
/* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes.
642737
*/
643738
open func contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
644-
NSUnimplemented()
739+
let fsRep1 = fileSystemRepresentation(withPath: path1)
740+
defer { fsRep1.deallocate() }
741+
742+
guard let file1 = try? _lstatFile(atPath: path1, withFileSystemRepresentation: fsRep1) else {
743+
return false
744+
}
745+
let file1Type = file1.st_mode & S_IFMT
746+
747+
// Dont use access() for symlinks as only the contents should be checked even
748+
// if the symlink doesnt point to an actual file, but access() will always try
749+
// to resolve the link and fail if the destination is not found
750+
if path1 == path2 && file1Type != S_IFLNK {
751+
return access(fsRep1, R_OK) == 0
752+
}
753+
754+
let fsRep2 = fileSystemRepresentation(withPath: path2)
755+
defer { fsRep2.deallocate() }
756+
guard let file2 = try? _lstatFile(atPath: path2, withFileSystemRepresentation: fsRep2) else {
757+
return false
758+
}
759+
let file2Type = file2.st_mode & S_IFMT
760+
761+
// Are paths the same type: file, directory, symbolic link etc.
762+
guard file1Type == file2Type else {
763+
return false
764+
}
765+
766+
if file1Type == S_IFCHR {
767+
// For character devices, just check the major/minor pair is the same.
768+
return _char_dev_major(file1.st_rdev) == _char_dev_major(file2.st_rdev)
769+
&& _char_dev_minor(file1.st_rdev) == _char_dev_minor(file2.st_rdev)
770+
}
771+
772+
// If both paths point to the same device/inode or they are both zero length
773+
// then they are considered equal so just check readability.
774+
if (file1.st_dev == file2.st_dev && file1.st_ino == file2.st_ino)
775+
|| (file1.st_size == 0 && file2.st_size == 0) {
776+
return access(fsRep1, R_OK) == 0 && access(fsRep2, R_OK) == 0
777+
}
778+
779+
if file1Type == S_IFREG {
780+
// Regular files and symlinks should at least have the same filesize if contents are equal.
781+
guard file1.st_size == file2.st_size else {
782+
return false
783+
}
784+
return _compareFiles(withFileSystemRepresentation: path1, andFileSystemRepresentation: path2, size: Int64(file1.st_size))
785+
}
786+
else if file1Type == S_IFLNK {
787+
return _compareSymlinks(withFileSystemRepresentation: fsRep1, andFileSystemRepresentation: fsRep2, size: Int64(file1.st_size))
788+
}
789+
else if file1Type == S_IFDIR {
790+
return _compareDirectories(atPath: path1, andPath: path2)
791+
}
792+
793+
// Dont know how to compare other file types.
794+
return false
645795
}
646796

647797
/* 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: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class TestFileManager : XCTestCase {
3434
("test_homedirectoryForUser", test_homedirectoryForUser),
3535
("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser),
3636
("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath),
37-
("test_mountedVolumeURLs", test_mountedVolumeURLs)
37+
("test_mountedVolumeURLs", test_mountedVolumeURLs),
38+
("test_contentsEqual", test_contentsEqual)
3839
]
3940
}
4041

@@ -620,4 +621,83 @@ class TestFileManager : XCTestCase {
620621
XCTAssertTrue(visibleVolumes.count < volumes.count)
621622
#endif
622623
}
624+
625+
func test_contentsEqual() {
626+
let fm = FileManager.default
627+
let tmpParentDirURL = URL(fileURLWithPath: NSTemporaryDirectory() + "test_contentsEqualdir", isDirectory: true)
628+
let testDir1 = tmpParentDirURL.appendingPathComponent("testDir1")
629+
let testDir2 = tmpParentDirURL.appendingPathComponent("testDir2")
630+
let testDir3 = testDir1.appendingPathComponent("subDir")
631+
632+
defer { try? fm.removeItem(atPath: tmpParentDirURL.path) }
633+
634+
func testFileURL(_ name: String, _ ext: String) -> URL? {
635+
guard let url = testBundle().url(forResource: name, withExtension: ext) else {
636+
XCTFail("Cant open \(name).\(ext)")
637+
return nil
638+
}
639+
return url
640+
}
641+
642+
guard let testFile1URL = testFileURL("NSStringTestData", "txt") else { return }
643+
guard let testFile2URL = testFileURL("NSURLTestData", "plist") else { return }
644+
guard let testFile3URL = testFileURL("NSString-UTF32-BE-data", "txt") else { return }
645+
guard let testFile4URL = testFileURL("NSString-UTF32-LE-data", "txt") else { return }
646+
let symlink = testDir1.appendingPathComponent("testlink").path
647+
648+
// Setup test directories
649+
do {
650+
// Clean out and leftover test data
651+
try? fm.removeItem(atPath: tmpParentDirURL.path)
652+
653+
// testDir1
654+
try fm.createDirectory(atPath: testDir1.path, withIntermediateDirectories: true)
655+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("null1").path, withDestinationPath: "/dev/null")
656+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("zero1").path, withDestinationPath: "/dev/zero")
657+
try "foo".write(toFile: testDir1.appendingPathComponent("foo.txt").path, atomically: false, encoding: .ascii)
658+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("foo1").path, withDestinationPath: "foo.txt")
659+
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
660+
let unreadable = testDir1.appendingPathComponent("unreadable_file").path
661+
try "unreadable".write(toFile: unreadable, atomically: false, encoding: .ascii)
662+
try fm.setAttributes([.posixPermissions: NSNumber(value: 0)], ofItemAtPath: unreadable)
663+
try Data().write(to: testDir1.appendingPathComponent("empty_file"))
664+
try fm.createSymbolicLink(atPath: symlink, withDestinationPath: testFile1URL.path)
665+
666+
// testDir2
667+
try fm.createDirectory(atPath: testDir2.path, withIntermediateDirectories: true)
668+
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
669+
try fm.createSymbolicLink(atPath: testDir2.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
670+
671+
// testDir3
672+
try fm.createDirectory(atPath: testDir3.path, withIntermediateDirectories: true)
673+
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("bar2").path, withDestinationPath: "foo1")
674+
try fm.createSymbolicLink(atPath: testDir3.appendingPathComponent("foo2").path, withDestinationPath: "../testDir1/foo.txt")
675+
} catch {
676+
XCTFail(String(describing: error))
677+
}
678+
679+
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/null"))
680+
XCTAssertTrue(fm.contentsEqual(atPath: "/dev/urandom", andPath: "/dev/urandom"))
681+
XCTAssertFalse(fm.contentsEqual(atPath: "/dev/null", andPath: "/dev/zero"))
682+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("null1").path, andPath: "/dev/null"))
683+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("zero").path, andPath: "/dev/zero"))
684+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo1").path))
685+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo.txt").path, andPath: testDir1.appendingPathComponent("foo2").path))
686+
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.appendingPathComponent("bar2").path, andPath: testDir2.appendingPathComponent("bar2").path))
687+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.appendingPathComponent("foo1").path, andPath: testDir2.appendingPathComponent("foo2").path))
688+
XCTAssertFalse(fm.contentsEqual(atPath: "/non_existant_file", andPath: "/non_existant_file"))
689+
690+
let emptyFile = testDir1.appendingPathComponent("empty_file")
691+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "/dev/null"))
692+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("null1").path))
693+
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))
694+
695+
XCTAssertTrue(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile1URL.path))
696+
XCTAssertFalse(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile2URL.path))
697+
XCTAssertFalse(fm.contentsEqual(atPath: testFile3URL.path, andPath: testFile4URL.path))
698+
XCTAssertFalse(fm.contentsEqual(atPath: symlink, andPath: testFile1URL.path))
699+
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir1.path))
700+
XCTAssertTrue(fm.contentsEqual(atPath: testDir2.path, andPath: testDir3.path))
701+
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
702+
}
623703
}

0 commit comments

Comments
 (0)