Skip to content

Commit 80093d6

Browse files
authored
Ensure that FileManager.copyItem cannot copy directory metadata to files (#1081) (#1083)
* (135575520) Ensure that FileManager.copyItem cannot copy directory metadata to files * Fix whitespacing * Fix Windows test failure
1 parent ef651ce commit 80093d6

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
@@ -908,6 +908,91 @@ enum _FileOperations {
908908
#endif
909909
}
910910
#endif
911+
912+
#if !canImport(Darwin)
913+
private static func _copyDirectoryMetadata(srcFD: CInt, srcPath: @autoclosure () -> String, dstFD: CInt, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws {
914+
// Copy extended attributes
915+
var size = flistxattr(srcFD, nil, 0)
916+
if size > 0 {
917+
try withUnsafeTemporaryAllocation(of: CChar.self, capacity: size) { keyList in
918+
size = flistxattr(srcFD, keyList.baseAddress!, size)
919+
if size > 0 {
920+
var current = keyList.baseAddress!
921+
let end = keyList.baseAddress!.advanced(by: keyList.count)
922+
while current < end {
923+
var valueSize = fgetxattr(srcFD, current, nil, 0)
924+
if valueSize >= 0 {
925+
try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: valueSize) { valueBuffer in
926+
valueSize = fgetxattr(srcFD, current, valueBuffer.baseAddress!, valueSize)
927+
if valueSize >= 0 {
928+
if fsetxattr(dstFD, current, valueBuffer.baseAddress!, valueSize, 0) != 0 {
929+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
930+
}
931+
}
932+
}
933+
}
934+
current = current.advanced(by: strlen(current) + 1) /* pass null byte */
935+
}
936+
}
937+
}
938+
}
939+
var statInfo = stat()
940+
if fstat(srcFD, &statInfo) == 0 {
941+
// Copy owner/group
942+
if fchown(dstFD, statInfo.st_uid, statInfo.st_gid) != 0 {
943+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
944+
}
945+
946+
// Copy modification date
947+
let value = timeval(tv_sec: statInfo.st_mtim.tv_sec, tv_usec: statInfo.st_mtim.tv_nsec / 1000)
948+
var tv = (value, value)
949+
try withUnsafePointer(to: &tv) {
950+
try $0.withMemoryRebound(to: timeval.self, capacity: 2) {
951+
if futimes(dstFD, $0) != 0 {
952+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
953+
}
954+
}
955+
}
956+
957+
// Copy permissions
958+
if fchmod(dstFD, statInfo.st_mode) != 0 {
959+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
960+
}
961+
} else {
962+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
963+
}
964+
}
965+
#endif
966+
967+
private static func _openDirectoryFD(_ ptr: UnsafePointer<CChar>, srcPath: @autoclosure () -> String, dstPath: @autoclosure () -> String, delegate: some LinkOrCopyDelegate) throws -> CInt? {
968+
let fd = open(ptr, O_RDONLY | O_NOFOLLOW | O_DIRECTORY)
969+
guard fd >= 0 else {
970+
try delegate.throwIfNecessary(errno, srcPath(), dstPath())
971+
return nil
972+
}
973+
return fd
974+
}
975+
976+
// Safely copies metadata from one directory to another ensuring that both paths are directories and cannot be swapped for files before/while copying metadata
977+
private static func _safeCopyDirectoryMetadata(src: UnsafePointer<CChar>, dst: UnsafePointer<CChar>, delegate: some LinkOrCopyDelegate, extraFlags: Int32 = 0) throws {
978+
guard let srcFD = try _openDirectoryFD(src, srcPath: String(cString: src), dstPath: String(cString: dst), delegate: delegate) else {
979+
return
980+
}
981+
defer { close(srcFD) }
982+
983+
guard let dstFD = try _openDirectoryFD(dst, srcPath: String(cString: src), dstPath: String(cString: dst), delegate: delegate) else {
984+
return
985+
}
986+
defer { close(dstFD) }
987+
988+
#if canImport(Darwin)
989+
if fcopyfile(srcFD, dstFD, nil, copyfile_flags_t(COPYFILE_METADATA | COPYFILE_NOFOLLOW | extraFlags)) != 0 {
990+
try delegate.throwIfNecessary(errno, String(cString: src), String(cString: dst))
991+
}
992+
#else
993+
try _copyDirectoryMetadata(srcFD: srcFD, srcPath: String(cString: src), dstFD: dstFD, dstPath: String(cString: dst), delegate: delegate)
994+
#endif
995+
}
911996

912997
#if os(WASI)
913998
private static func _linkOrCopyFile(_ srcPtr: UnsafePointer<CChar>, _ dstPtr: UnsafePointer<CChar>, with fileManager: FileManager, delegate: some LinkOrCopyDelegate) throws {
@@ -1000,18 +1085,7 @@ enum _FileOperations {
10001085

10011086
case FTS_DP:
10021087
// Directory being visited in post-order - copy the permissions over.
1003-
#if canImport(Darwin)
1004-
if copyfile(fts_path, buffer.baseAddress!, nil, copyfile_flags_t(COPYFILE_METADATA | COPYFILE_NOFOLLOW | extraFlags)) != 0 {
1005-
try delegate.throwIfNecessary(errno, String(cString: fts_path), String(cString: buffer.baseAddress!))
1006-
}
1007-
#else
1008-
do {
1009-
let attributes = try fileManager.attributesOfItem(atPath: String(cString: fts_path))
1010-
try fileManager.setAttributes(attributes, ofItemAtPath: String(cString: buffer.baseAddress!))
1011-
} catch {
1012-
try delegate.throwIfNecessary(error, String(cString: fts_path), String(cString: buffer.baseAddress!))
1013-
}
1014-
#endif
1088+
try Self._safeCopyDirectoryMetadata(src: fts_path, dst: buffer.baseAddress!, delegate: delegate, extraFlags: extraFlags)
10151089

10161090
case FTS_SL: fallthrough // Symlink.
10171091
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)