@@ -1046,6 +1046,87 @@ 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+ // .ENOTSUP is Darwin-specific.
1079+ guard posix. code == . ENOTSUP || posix. code == . EOPNOTSUPP || posix. code == . EPERM else { throw error }
1080+ #else
1081+ guard posix. code == . EOPNOTSUPP || posix. code == . EPERM else { throw error }
1082+ #endif
1083+ return
1084+ }
1085+
1086+ // Attempt to set xattrs on the symlink.
1087+ var setSucceeded = false
1088+ do {
1089+ try fileManager. setAttributes (
1090+ [ xattrKey: [ attrName: attrValue] ] , ofItemAtPath: " link " )
1091+ setSucceeded = true
1092+ } catch let error as CocoaError {
1093+ if error. code == . featureUnsupported { return }
1094+ guard let posix = error. underlying as? POSIXError else { throw error }
1095+ #if canImport(Darwin)
1096+ // .ENOTSUP is Darwin-specific.
1097+ guard posix. code == . ENOTSUP || posix. code == . EOPNOTSUPP || posix. code == . EPERM else { throw error }
1098+ #else
1099+ // .EPERM can occur on Linux if xattrs on symlinks aren't permitted.
1100+ // If we can set xattrs on the target file (probe above), EPERM here strongly suggests
1101+ // we tried to set xattrs on the symlink itself (non-following), not the target.
1102+ guard posix. code == . EOPNOTSUPP || posix. code == . EPERM else { throw error }
1103+ #endif
1104+ // Fall through to verify target wasn't modified
1105+ }
1106+
1107+ let targetAttrs = try fileManager. attributesOfItem ( atPath: " target " )
1108+ let targetXattrs = targetAttrs [ xattrKey] as? [ String : Data ]
1109+
1110+ // The target file must NOT have the xattr - this is the key assertion.
1111+ // If setAttributes incorrectly followed the symlink, the xattr would be on the target.
1112+ #expect(
1113+ targetXattrs ? [ attrName] == nil ,
1114+ " setAttributes must not follow symlinks when setting extended attributes " )
1115+
1116+ if setSucceeded {
1117+ // If setting on symlink succeeded, verify it's actually on the symlink
1118+ let linkAttrs = try fileManager. attributesOfItem ( atPath: " link " )
1119+ let linkXattrs = try #require(
1120+ linkAttrs [ xattrKey] as? [ String : Data ] ,
1121+ " Expected extended attributes on symlink after setAttributes call " )
1122+ #expect(
1123+ linkXattrs [ attrName] == attrValue,
1124+ " xattr should be applied to the symlink itself " )
1125+ }
1126+ }
1127+ }
1128+ #endif
1129+
10491130 #if !canImport(Darwin) || os(macOS)
10501131 @Test func currentUserHomeDirectory( ) async throws {
10511132 let userName = ProcessInfo . processInfo. userName
0 commit comments