@@ -1046,6 +1046,85 @@ private struct FileManagerTests {
10461046 }
10471047 }
10481048
1049+ #if !os(Windows) && !os(WASI) && !os(OpenBSD) && !canImport(Android)
1050+ @Test func extendedAttributesDoNotFollowSymlinksWhenSetting( ) async throws {
1051+ let xattrKey = FileAttributeKey ( " NSFileExtendedAttributes " )
1052+ #if canImport(Darwin)
1053+ let attrName = " com.swiftfoundation.symlinktest "
1054+ let probeName = " com.swiftfoundation.symlinkprobe "
1055+ #elseif os(Linux)
1056+ // Linux requires the user.* namespace prefix for regular files
1057+ let attrName = " user.swiftfoundation.symlinktest "
1058+ let probeName = " user.swiftfoundation.symlinkprobe "
1059+ #else
1060+ let attrName = " swiftfoundation.symlinktest "
1061+ let probeName = " swiftfoundation.symlinkprobe "
1062+ #endif
1063+ let attrValue = Data ( [ 0xAA , 0xBB , 0xCC ] )
1064+ let probeValue = Data ( [ 0x11 , 0x22 , 0x33 ] )
1065+
1066+ try await FilePlayground {
1067+ File ( " target " , contents: Data ( " payload " . utf8) )
1068+ SymbolicLink ( " link " , destination: " target " )
1069+ } . test { fileManager in
1070+ // First, prove that this environment supports xattrs on regular files and that we
1071+ // have permission to set them. If this fails, the symlink behavior isn't meaningful.
1072+ do {
1073+ try fileManager. setAttributes ( [ xattrKey: [ probeName: probeValue] ] , ofItemAtPath: " target " )
1074+ } catch let error as CocoaError {
1075+ if error. code == . featureUnsupported { return }
1076+ guard let posix = error. underlying as? POSIXError else { throw error }
1077+ #if canImport(Darwin)
1078+ guard posix. code == . ENOTSUP || posix. code. rawValue == EOPNOTSUPP || posix. code == . EPERM else { throw error }
1079+ #else
1080+ guard posix. code. rawValue == EOPNOTSUPP || posix. code == . EPERM else { throw error }
1081+ #endif
1082+ return
1083+ }
1084+
1085+ // Attempt to set xattrs on the symlink.
1086+ var setSucceeded = false
1087+ do {
1088+ try fileManager. setAttributes (
1089+ [ xattrKey: [ attrName: attrValue] ] , ofItemAtPath: " link " )
1090+ setSucceeded = true
1091+ } catch let error as CocoaError {
1092+ if error. code == . featureUnsupported { return }
1093+ guard let posix = error. underlying as? POSIXError else { throw error }
1094+ #if canImport(Darwin)
1095+ guard posix. code == . ENOTSUP || posix. code. rawValue == EOPNOTSUPP || posix. code == . EPERM else { throw error }
1096+ #else
1097+ // .EPERM can occur on Linux if xattrs on symlinks aren't permitted.
1098+ // If we can set xattrs on the target file (probe above), EPERM here strongly suggests
1099+ // we tried to set xattrs on the symlink itself (non-following), not the target.
1100+ guard posix. code. rawValue == EOPNOTSUPP || posix. code == . EPERM else { throw error }
1101+ #endif
1102+ // Fall through to verify target wasn't modified
1103+ }
1104+
1105+ let targetAttrs = try fileManager. attributesOfItem ( atPath: " target " )
1106+ let targetXattrs = targetAttrs [ xattrKey] as? [ String : Data ]
1107+
1108+ // The target file must NOT have the xattr - this is the key assertion.
1109+ // If setAttributes incorrectly followed the symlink, the xattr would be on the target.
1110+ #expect(
1111+ targetXattrs ? [ attrName] == nil ,
1112+ " setAttributes must not follow symlinks when setting extended attributes " )
1113+
1114+ if setSucceeded {
1115+ // If setting on symlink succeeded, verify it's actually on the symlink
1116+ let linkAttrs = try fileManager. attributesOfItem ( atPath: " link " )
1117+ let linkXattrs = try #require(
1118+ linkAttrs [ xattrKey] as? [ String : Data ] ,
1119+ " Expected extended attributes on symlink after setAttributes call " )
1120+ #expect(
1121+ linkXattrs [ attrName] == attrValue,
1122+ " xattr should be applied to the symlink itself " )
1123+ }
1124+ }
1125+ }
1126+ #endif
1127+
10491128 #if !canImport(Darwin) || os(macOS)
10501129 @Test func currentUserHomeDirectory( ) async throws {
10511130 let userName = ProcessInfo . processInfo. userName
0 commit comments