From 65c40d956d6223ab356b1c5a82c81820922a97a8 Mon Sep 17 00:00:00 2001 From: AZero13 Date: Fri, 5 Dec 2025 13:27:32 -0500 Subject: [PATCH] Fix FileManager's extended attribute symlink handling The if statement appears inverted: setxattr follows simlinks: lsetxattr does not. Additionally, _extendedAttributes was ignoring simlinks entirely. Both of these issues have been addressed. --- .../FileManager/FileManager+Files.swift | 12 ++--- .../FileManager/FileManager+Utilities.swift | 8 +-- .../FileManager/FileManagerTests.swift | 54 +++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index b2666ccbc..a341652f7 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -522,21 +522,21 @@ extension _FileManagerImpl { private func _extendedAttributes(at path: UnsafePointer, followSymlinks: Bool) throws -> [String : Data]? { #if canImport(Darwin) - var size = listxattr(path, nil, 0, 0) + var size = listxattr(path, nil, 0, followSymlinks ? 0 : XATTR_NOFOLLOW) #elseif os(FreeBSD) var size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0) #else - var size = listxattr(path, nil, 0) + var size = followSymlinks ? listxattr(path, nil, 0) : llistxattr(path, nil, 0) #endif guard size > 0 else { return nil } let keyList = UnsafeMutableBufferPointer.allocate(capacity: size) defer { keyList.deallocate() } #if canImport(Darwin) - size = listxattr(path, keyList.baseAddress!, size, 0) + size = listxattr(path, keyList.baseAddress!, size, followSymlinks ? 0 : XATTR_NOFOLLOW) #elseif os(FreeBSD) - size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, nil, 0) + size = (followSymlinks ? extattr_list_file : extattr_list_link)(path, EXTATTR_NAMESPACE_USER, keyList.baseAddress!, size) #else - size = listxattr(path, keyList.baseAddress!, size) + size = followSymlinks ? listxattr(path, keyList.baseAddress!, size) : llistxattr(path, keyList.baseAddress!, size) #endif guard size > 0 else { return nil } @@ -553,7 +553,7 @@ extension _FileManagerImpl { } #endif - if let value = try _extendedAttribute(current, at: path, followSymlinks: false) { + if let value = try _extendedAttribute(current, at: path, followSymlinks: followSymlinks) { extendedAttrs[currentKey] = value } } diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift index d882a67bb..24cb3ec0f 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Utilities.swift @@ -193,15 +193,15 @@ extension _FileManagerImpl { #else var result: Int32 if followSymLinks { - result = lsetxattr(path, key, buffer.baseAddress!, buffer.count, 0) - } else { result = setxattr(path, key, buffer.baseAddress!, buffer.count, 0) + } else { + result = lsetxattr(path, key, buffer.baseAddress!, buffer.count, 0) } #endif #if os(macOS) && FOUNDATION_FRAMEWORK - // if setxaddr failed and its a permission error for a sandbox app trying to set quaratine attribute, ignore it since its not - // permitted, the attribute will be put on the file by the quaratine MAC hook + // if setxattr failed and its a permission error for a sandbox app trying to set quarantine attribute, ignore it since its not + // permitted, the attribute will be put on the file by the quarantine MAC hook if result == -1 && errno == EPERM && _xpc_runtime_is_app_sandboxed() && strcmp(key, "com.apple.quarantine") == 0 { return } diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 44893c17e..30a1cfe33 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -1046,6 +1046,60 @@ private struct FileManagerTests { } } + // Extended attributes are not supported on all platforms + #if !os(Windows) && !os(WASI) && !os(OpenBSD) && !canImport(Android) + @Test func extendedAttributesOnSymlinks() async throws { + let xattrKey = FileAttributeKey("NSFileExtendedAttributes") + #if os(Linux) + // Linux requires the user.* namespace prefix for regular files + let attrName = "user.swift.foundation.symlinktest" + #else + let attrName = "org.swift.foundation.symlinktest" + #endif + let attrValue = Data([0xAA, 0xBB, 0xCC]) + let attrValue2 = Data([0xDD, 0xEE, 0xFF]) + + try await FilePlayground { + File("target", contents: Data("payload".utf8)) + SymbolicLink("link", destination: "target") + }.test { fileManager in + // First, validate that reading the attribute from the link does not read the value from the target + do { + try fileManager.setAttributes([xattrKey: [attrName: attrValue]], ofItemAtPath: "target") + let targetAttrs = try fileManager.attributesOfItem(atPath: "target") + let targetXAttrs = targetAttrs[xattrKey] as? [String: Data] + #expect(targetXAttrs?[attrName] == attrValue) + + let linkAttrs = try fileManager.attributesOfItem(atPath: "link") + let linkXAttrs = linkAttrs[xattrKey] as? [String: Data] + #expect(linkXAttrs?[attrName] == nil) + } + + // Attempt to set xattrs on the symlink + #if os(Linux) + // On Linux, user xattrs cannot be set on symlinks + #expect(throws: CocoaError.self) { + try fileManager.setAttributes([xattrKey: [attrName: attrValue2]], ofItemAtPath: "link") + } + let expectedValue: Data? = nil + #else + try fileManager.setAttributes([xattrKey: [attrName: attrValue2]], ofItemAtPath: "link") + let expectedValue: Data? = attrValue2 + #endif + + // Ensure that reading back the xattr of the link produces the expected value + let linkAttrs = try fileManager.attributesOfItem(atPath: "link") + let linkXAttrs = linkAttrs[xattrKey] as? [String: Data] + #expect(linkXAttrs?[attrName] == expectedValue) + + // Ensure that setting the xattr on the link did not set the xattr on the target + let targetAttrs = try fileManager.attributesOfItem(atPath: "target") + let targetXAttrs = targetAttrs[xattrKey] as? [String: Data] + #expect(targetXAttrs?[attrName] == attrValue) + } + } + #endif + #if !canImport(Darwin) || os(macOS) @Test func currentUserHomeDirectory() async throws { let userName = ProcessInfo.processInfo.userName