Skip to content

Commit bb3fccf

Browse files
authored
Ensure that FileManager.copyItem cannot copy directory metadata to files (#1081)
* (135575520) Ensure that FileManager.copyItem cannot copy directory metadata to files * Fix whitespacing * Fix Windows test failure
1 parent 8a95c62 commit bb3fccf

File tree

2 files changed

+90
-14
lines changed

2 files changed

+90
-14
lines changed

Sources/FoundationEssentials/FileManager/FileOperations.swift

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,91 @@ enum _FileOperations {
948948
#endif
949949
}
950950
#endif
951+
952+
#if !canImport(Darwin)
953+
private static func _copyDirectoryMetadata(srcFD: CInt, srcPath: @autoclosure () -> String, dstFD: CInt, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws {
954+
// Copy extended attributes
955+
var size = flistxattr(srcFD, nil, 0)
956+
if size > 0 {
957+
try withUnsafeTemporaryAllocation(of: CChar.self, capacity: size) { keyList in
958+
size = flistxattr(srcFD, keyList.baseAddress!, size)
959+
if size > 0 {
960+
var current = keyList.baseAddress!
961+
let end = keyList.baseAddress!.advanced(by: keyList.count)
962+
while current < end {
963+
var valueSize = fgetxattr(srcFD, current, nil, 0)
964+
if valueSize >= 0 {
965+
try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: valueSize) { valueBuffer in
966+
valueSize = fgetxattr(srcFD, current, valueBuffer.baseAddress!, valueSize)
967+
if valueSize >= 0 {
968+
if fsetxattr(dstFD, current, valueBuffer.baseAddress!, valueSize, 0) != 0 {
969+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
970+
}
971+
}
972+
}
973+
}
974+
current = current.advanced(by: strlen(current) + 1) /* pass null byte */
975+
}
976+
}
977+
}
978+
}
979+
var statInfo = stat()
980+
if fstat(srcFD, &statInfo) == 0 {
981+
// Copy owner/group
982+
if fchown(dstFD, statInfo.st_uid, statInfo.st_gid) != 0 {
983+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
984+
}
985+
986+
// Copy modification date
987+
let value = timeval(tv_sec: statInfo.st_mtim.tv_sec, tv_usec: statInfo.st_mtim.tv_nsec / 1000)
988+
var tv = (value, value)
989+
try withUnsafePointer(to: &tv) {
990+
try $0.withMemoryRebound(to: timeval.self, capacity: 2) {
991+
if futimes(dstFD, $0) != 0 {
992+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
993+
}
994+
}
995+
}
996+
997+
// Copy permissions
998+
if fchmod(dstFD, statInfo.st_mode) != 0 {
999+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
1000+
}
1001+
} else {
1002+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
1003+
}
1004+
}
1005+
#endif
1006+
1007+
private static func _openDirectoryFD(_ ptr: UnsafePointer<CChar>, srcPath: @autoclosure () -> String, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws -> CInt? {
1008+
let fd = open(ptr, O_RDONLY | O_NOFOLLOW | O_DIRECTORY)
1009+
guard fd >= 0 else {
1010+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
1011+
return nil
1012+
}
1013+
return fd
1014+
}
1015+
1016+
// Safely copies metadata from one directory to another ensuring that both paths are directories and cannot be swapped for files before/while copying metadata
1017+
private static func _safeCopyDirectoryMetadata(src: UnsafePointer<CChar>, dst: UnsafePointer<CChar>, delegate: some LinkOrCopyDelegate, extraFlags: Int32 = 0) throws {
1018+
guard let srcFD = try _openDirectoryFD(src, srcPath: String(cString: src), dstPath: String(cString: dst), delegate: delegate) else {
1019+
return
1020+
}
1021+
defer { close(srcFD) }
1022+
1023+
guard let dstFD = try _openDirectoryFD(dst, srcPath: String(cString: src), dstPath: String(cString: dst), delegate: delegate) else {
1024+
return
1025+
}
1026+
defer { close(dstFD) }
1027+
1028+
#if canImport(Darwin)
1029+
if fcopyfile(srcFD, dstFD, nil, copyfile_flags_t(COPYFILE_METADATA | COPYFILE_NOFOLLOW | extraFlags)) != 0 {
1030+
try delegate.throwIfNecessary(errno, String(cString: src), String(cString: dst))
1031+
}
1032+
#else
1033+
try _copyDirectoryMetadata(srcFD: srcFD, srcPath: String(cString: src), dstFD: dstFD, dstPath: String(cString: dst), delegate: delegate)
1034+
#endif
1035+
}
9511036

9521037
#if os(WASI)
9531038
private static func _linkOrCopyFile(_ srcPtr: UnsafePointer<CChar>, _ dstPtr: UnsafePointer<CChar>, with fileManager: FileManager, delegate: some LinkOrCopyDelegate) throws {
@@ -1040,18 +1125,7 @@ enum _FileOperations {
10401125

10411126
case FTS_DP:
10421127
// Directory being visited in post-order - copy the permissions over.
1043-
#if canImport(Darwin)
1044-
if copyfile(fts_path, buffer.baseAddress!, nil, copyfile_flags_t(COPYFILE_METADATA | COPYFILE_NOFOLLOW | extraFlags)) != 0 {
1045-
try delegate.throwIfNecessary(errno, String(cString: fts_path), String(cString: buffer.baseAddress!))
1046-
}
1047-
#else
1048-
do {
1049-
let attributes = try fileManager.attributesOfItem(atPath: String(cString: fts_path))
1050-
try fileManager.setAttributes(attributes, ofItemAtPath: String(cString: buffer.baseAddress!))
1051-
} catch {
1052-
try delegate.throwIfNecessary(error, String(cString: fts_path), String(cString: buffer.baseAddress!))
1053-
}
1054-
#endif
1128+
try Self._safeCopyDirectoryMetadata(src: fts_path, dst: buffer.baseAddress!, delegate: delegate, extraFlags: extraFlags)
10551129

10561130
case FTS_SL: fallthrough // Symlink.
10571131
case FTS_SLNONE: // Symlink with no target.

Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ final class FileManagerTests : XCTestCase {
495495
func testCopyItemAtPathToPath() throws {
496496
let data = randomData()
497497
try FileManagerPlayground {
498-
Directory("dir") {
498+
Directory("dir", attributes: [.posixPermissions : 0o777]) {
499499
File("foo", contents: data)
500500
"bar"
501501
}
@@ -510,8 +510,10 @@ final class FileManagerTests : XCTestCase {
510510
XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("dir", "dir2"), .init("dir/bar", "dir2/bar"), .init("dir/foo", "dir2/foo")])
511511
#else
512512
XCTAssertEqual($0.delegateCaptures.shouldCopy, [.init("dir", "dir2"), .init("dir/foo", "dir2/foo"), .init("dir/bar", "dir2/bar")])
513+
514+
// Specifically for non-Windows (where copying directory metadata takes a special path) double check that the metadata was copied exactly
515+
XCTAssertEqual(try $0.attributesOfItem(atPath: "dir2")[.posixPermissions] as? UInt, 0o777)
513516
#endif
514-
515517
XCTAssertThrowsError(try $0.copyItem(atPath: "does_not_exist", toPath: "dir3")) {
516518
XCTAssertEqual(($0 as? CocoaError)?.code, .fileReadNoSuchFile)
517519
}

0 commit comments

Comments
 (0)