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