Skip to content

FileManager: Implement contentsEqual(atPath:andPath:) #1510

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 4 commits into from
Apr 18, 2018
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
9 changes: 9 additions & 0 deletions CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,15 @@ static inline int _direntNameLength(struct dirent *entry) {
#endif
}

// major() and minor() might be implemented as macros or functions.
static inline unsigned int _dev_major(dev_t rdev) {
return major(rdev);
}

static inline unsigned int _dev_minor(dev_t rdev) {
return minor(rdev);
}

_CF_EXPORT_SCOPE_END

#endif /* __COREFOUNDATION_FORSWIFTFOUNDATIONONLY__ */
156 changes: 154 additions & 2 deletions Foundation/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -637,11 +637,163 @@ open class FileManager : NSObject {
open func isDeletableFile(atPath path: String) -> Bool {
NSUnimplemented()
}


private func _compareFiles(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size: Int64, bufSize: Int) -> Bool {
let fd1 = open(file1Rep, O_RDONLY)
guard fd1 >= 0 else {
return false
}
defer { close(fd1) }

let fd2 = open(file2Rep, O_RDONLY)
guard fd2 >= 0 else {
return false
}
defer { close(fd2) }

let buffer1 = UnsafeMutablePointer<UInt8>.allocate(capacity: bufSize)
let 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 read(fd1, buffer1, bytesToRead) == bytesToRead else {
return false
}
guard read(fd2, buffer2, bytesToRead) == 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: Int(bufSize))
let buffer2 = UnsafeMutablePointer<CChar>.allocate(capacity: Int(bufSize))

let size1 = readlink(file1Rep, buffer1, bufSize)
let size2 = readlink(file2Rep, buffer2, bufSize)

let compare: Bool
if size1 < 0 || size2 < 0 || size1 != size || size1 != size2 {
compare = false
} else {
compare = memcmp(buffer1, buffer2, size1) == 0
}

buffer1.deallocate()
buffer2.deallocate()
return compare
}

private func _compareDirectories(atPath path1: String, andPath path2: String) -> Bool {
guard let enumerator1 = enumerator(atPath: path1) else {
return false
}

guard let enumerator2 = enumerator(atPath: path2) else {
return false
}

var path1entries = Set<String>()
while let item = enumerator1.nextObject() as? String {
path1entries.insert(item)
}

while let item = enumerator2.nextObject() as? String {
if path1entries.remove(item) == nil {
return false
}
if contentsEqual(atPath: NSString(string: path1).appendingPathComponent(item), andPath: NSString(string: path2).appendingPathComponent(item)) == false {
return false
}
}
return path1entries.isEmpty
}

private func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat {
let _fsRep: UnsafePointer<Int8>
if fsRep == nil {
_fsRep = fileSystemRepresentation(withPath: path)
defer { _fsRep.deallocate() }
} else {
_fsRep = fsRep!
}
var statInfo = stat()
guard lstat(_fsRep, &statInfo) == 0 else {
throw _NSErrorWithErrno(errno, reading: true, path: path)
}
return statInfo
}

/* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes.
*/
open func contentsEqual(atPath path1: String, andPath path2: String) -> Bool {
NSUnimplemented()
let fsRep1 = fileSystemRepresentation(withPath: path1)
defer { fsRep1.deallocate() }

guard let file1 = try? _lstatFile(atPath: path1, withFileSystemRepresentation: fsRep1) else {
return false
}
let file1Type = file1.st_mode & S_IFMT

// Dont use access() for symlinks as only the contents should be checked even
// if the symlink doesnt point to an actual file, but access() will always try
// to resolve the link and fail if the destination is not found
if path1 == path2 && file1Type != S_IFLNK {
return access(fsRep1, R_OK) == 0
}

let fsRep2 = fileSystemRepresentation(withPath: path2)
defer { fsRep2.deallocate() }
guard let file2 = try? _lstatFile(atPath: path2, withFileSystemRepresentation: fsRep2) else {
return false
}
let file2Type = file2.st_mode & S_IFMT

// Are paths the same type: file, directory, symbolic link etc.
guard file1Type == file2Type else {
return false
}

if file1Type == S_IFCHR || file1Type == S_IFBLK {
// For character devices, just check the major/minor pair is the same.
return _dev_major(file1.st_rdev) == _dev_major(file2.st_rdev)
&& _dev_minor(file1.st_rdev) == _dev_minor(file2.st_rdev)
}

// If both paths point to the same device/inode or they are both zero length
// then they are considered equal so just check readability.
if (file1.st_dev == file2.st_dev && file1.st_ino == file2.st_ino)
|| (file1.st_size == 0 && file2.st_size == 0) {
return access(fsRep1, R_OK) == 0 && access(fsRep2, R_OK) == 0
}

if file1Type == S_IFREG {
// Regular files and symlinks should at least have the same filesize if contents are equal.
guard file1.st_size == file2.st_size else {
return false
}
return _compareFiles(withFileSystemRepresentation: path1, andFileSystemRepresentation: path2, size: Int64(file1.st_size), bufSize: Int(file1.st_blksize))
}
else if file1Type == S_IFLNK {
return _compareSymlinks(withFileSystemRepresentation: fsRep1, andFileSystemRepresentation: fsRep2, size: Int64(file1.st_size))
}
else if file1Type == S_IFDIR {
return _compareDirectories(atPath: path1, andPath: path2)
}

// Dont know how to compare other file types.
return false
}

/* 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.
Expand Down
135 changes: 134 additions & 1 deletion TestFoundation/TestFileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class TestFileManager : XCTestCase {
("test_homedirectoryForUser", test_homedirectoryForUser),
("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser),
("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath),
("test_mountedVolumeURLs", test_mountedVolumeURLs)
("test_mountedVolumeURLs", test_mountedVolumeURLs),
("test_contentsEqual", test_contentsEqual)
]
}

Expand Down Expand Up @@ -620,4 +621,136 @@ class TestFileManager : XCTestCase {
XCTAssertTrue(visibleVolumes.count < volumes.count)
#endif
}

func test_contentsEqual() {
let fm = FileManager.default
let tmpParentDirURL = URL(fileURLWithPath: NSTemporaryDirectory() + "test_contentsEqualdir", isDirectory: true)
let testDir1 = tmpParentDirURL.appendingPathComponent("testDir1")
let testDir2 = tmpParentDirURL.appendingPathComponent("testDir2")
let testDir3 = testDir1.appendingPathComponent("subDir/anotherDir/extraDir/lastDir")

defer { try? fm.removeItem(atPath: tmpParentDirURL.path) }

func testFileURL(_ name: String, _ ext: String) -> URL? {
guard let url = testBundle().url(forResource: name, withExtension: ext) else {
XCTFail("Cant open \(name).\(ext)")
return nil
}
return url
}

guard let testFile1URL = testFileURL("NSStringTestData", "txt") else { return }
guard let testFile2URL = testFileURL("NSURLTestData", "plist") else { return }
guard let testFile3URL = testFileURL("NSString-UTF32-BE-data", "txt") else { return }
guard let testFile4URL = testFileURL("NSString-UTF32-LE-data", "txt") else { return }
let symlink = testDir1.appendingPathComponent("testlink").path

// Setup test directories
do {
// Clean out and leftover test data
try? fm.removeItem(atPath: tmpParentDirURL.path)

// testDir1
try fm.createDirectory(atPath: testDir1.path, withIntermediateDirectories: true)
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("null1").path, withDestinationPath: "/dev/null")
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("zero1").path, withDestinationPath: "/dev/zero")
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")
let unreadable = testDir1.appendingPathComponent("unreadable_file").path
try "unreadable".write(toFile: unreadable, atomically: false, encoding: .ascii)
try fm.setAttributes([.posixPermissions: NSNumber(value: 0)], ofItemAtPath: unreadable)
try Data().write(to: testDir1.appendingPathComponent("empty_file"))
try fm.createSymbolicLink(atPath: symlink, withDestinationPath: testFile1URL.path)

try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("thisDir").path, withDestinationPath: ".")
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("parentDir").path, withDestinationPath: "..")
try fm.createSymbolicLink(atPath: testDir1.appendingPathComponent("rootDir").path, withDestinationPath: "/")

// testDir2
try fm.createDirectory(atPath: testDir2.path, withIntermediateDirectories: true)
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)
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))
}

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"))
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_existant_file", andPath: "/non_existant_file"))

let emptyFile = testDir1.appendingPathComponent("empty_file")
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: "/dev/null"))
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("null1").path))
XCTAssertFalse(fm.contentsEqual(atPath: emptyFile.path, andPath: testDir1.appendingPathComponent("unreadable_file").path))

XCTAssertTrue(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile1URL.path))
XCTAssertFalse(fm.contentsEqual(atPath: testFile1URL.path, andPath: testFile2URL.path))
XCTAssertFalse(fm.contentsEqual(atPath: testFile3URL.path, andPath: testFile4URL.path))
XCTAssertFalse(fm.contentsEqual(atPath: symlink, andPath: testFile1URL.path))

XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir1.path))
XCTAssertTrue(fm.contentsEqual(atPath: testDir2.path, andPath: testDir3.path))
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))

// Copy everything in testDir1 to testDir2 to make them equal
do {
for entry in try fm.subpathsOfDirectory(atPath: testDir1.path) {
// Skip entries that already exist
if entry == "bar2" || entry == "unreadable_file" {
continue
}
let srcPath = testDir1.appendingPathComponent(entry).path
let dstPath = testDir2.appendingPathComponent(entry).path
if let attrs = try? fm.attributesOfItem(atPath: srcPath),
let fileType = attrs[.type] as? FileAttributeType, fileType == .typeDirectory {
try fm.createDirectory(atPath: dstPath, withIntermediateDirectories: false, attributes: nil)
} else {
try fm.copyItem(atPath: srcPath, toPath: dstPath)
}
}
} catch {
XCTFail("Failed to copy \(testDir1.path) to \(testDir2.path), \(error)")
return
}
// This will still fail due to unreadable files and a file in testDir2 not in testDir1
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
do {
try fm.copyItem(atPath: testDir2.appendingPathComponent("foo2").path, toPath: testDir1.appendingPathComponent("foo2").path)
try fm.removeItem(atPath: testDir1.appendingPathComponent("unreadable_file").path)
} catch {
XCTFail(String(describing: error))
return
}
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))

let dataFile1 = testDir1.appendingPathComponent("dataFile")
let dataFile2 = testDir2.appendingPathComponent("dataFile")
do {
try Data(count: 100_000).write(to: dataFile1)
try fm.copyItem(atPath: dataFile1.path, toPath: dataFile2.path)
} catch {
XCTFail("Could not create test data files: \(error)")
return
}
XCTAssertTrue(fm.contentsEqual(atPath: dataFile1.path, andPath: dataFile2.path))
XCTAssertTrue(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
var data = Data(count: 100_000)
data[99_999] = 1
try? data.write(to: dataFile1)
XCTAssertFalse(fm.contentsEqual(atPath: dataFile1.path, andPath: dataFile2.path))
XCTAssertFalse(fm.contentsEqual(atPath: testDir1.path, andPath: testDir2.path))
}
}