21

I am looking for a solution to add extended file attributes for a file in swift. I checked this link Write extended file attributes, but the solutions are in objective c and I need a solution for swift.

Community
  • 1
  • 1
prabhu
  • 1,158
  • 1
  • 12
  • 27

1 Answers1

53

Here is a possible implementation in Swift 5 as an extension for URL, with methods to get, set, list, and remove extended attributes of a file. (Swift 2, 3, and 4 code can be found in the edit history.)

extension URL {

    /// Get extended attribute.
    func extendedAttribute(forName name: String) throws -> Data  {

        let data = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> Data in

            // Determine attribute size:
            let length = getxattr(fileSystemPath, name, nil, 0, 0, 0)
            guard length >= 0 else { throw URL.posixError(errno) }

            // Create buffer with required size:
            var data = Data(count: length)

            // Retrieve attribute:
            let result =  data.withUnsafeMutableBytes { [count = data.count] in
                getxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0)
            }
            guard result >= 0 else { throw URL.posixError(errno) }
            return data
        }
        return data
    }

    /// Set extended attribute.
    func setExtendedAttribute(data: Data, forName name: String) throws {

        try self.withUnsafeFileSystemRepresentation { fileSystemPath in
            let result = data.withUnsafeBytes {
                setxattr(fileSystemPath, name, $0.baseAddress, data.count, 0, 0)
            }
            guard result >= 0 else { throw URL.posixError(errno) }
        }
    }

    /// Remove extended attribute.
    func removeExtendedAttribute(forName name: String) throws {

        try self.withUnsafeFileSystemRepresentation { fileSystemPath in
            let result = removexattr(fileSystemPath, name, 0)
            guard result >= 0 else { throw URL.posixError(errno) }
        }
    }

    /// Get list of all extended attributes.
    func listExtendedAttributes() throws -> [String] {

        let list = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> [String] in
            let length = listxattr(fileSystemPath, nil, 0, 0)
            guard length >= 0 else { throw URL.posixError(errno) }

            // Create buffer with required size:
            var namebuf = Array<CChar>(repeating: 0, count: length)

            // Retrieve attribute list:
            let result = listxattr(fileSystemPath, &namebuf, namebuf.count, 0)
            guard result >= 0 else { throw URL.posixError(errno) }

            // Extract attribute names:
            let list = namebuf.split(separator: 0).compactMap {
                $0.withUnsafeBufferPointer {
                    $0.withMemoryRebound(to: UInt8.self) {
                        String(bytes: $0, encoding: .utf8)
                    }
                }
            }
            return list
        }
        return list
    }

    /// Helper function to create an NSError from a Unix errno.
    private static func posixError(_ err: Int32) -> NSError {
        return NSError(domain: NSPOSIXErrorDomain, code: Int(err),
                       userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(err))])
    }
}

Example usage:

let fileURL = URL(fileURLWithPath: "/path/to/file")

let attr1 = "com.myCompany.myAttribute"
let attr2 = "com.myCompany.otherAttribute"

let data1 = Data([1, 2, 3, 4])
let data2 = Data([5, 6, 7, 8, 9])

do {
    // Set attributes:
    try fileURL.setExtendedAttribute(data: data1, forName: attr1)
    try fileURL.setExtendedAttribute(data: data2, forName: attr2)

    // List attributes:
    let list = try fileURL.listExtendedAttributes()
    print(list)
    // ["com.myCompany.myAttribute", "com.myCompany.otherAttribute", "other"]

    let data1a = try fileURL.extendedAttribute(forName: attr1)
    print(data1a as NSData)
    // <01020304>

    // Remove attributes
    for attr in list {
        try fileURL.removeExtendedAttribute(forName: attr)
    }

} catch let error {
    print(error.localizedDescription)
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks for the answer. I just copy pasted your code and ran the application. No errors printed but i could not confirm the attribute set from command line. I got "No such xattr: com.myCompany.myAttribute" error – prabhu Jul 13 '16 at 06:57
  • @prabhu: Have you double-checked that the attribute name in the code is the same as the attribute name on the command line? With `xattr /path/to/file` you can list all existing attributes of the file. – It worked for me, I tested the code before posting :) – Martin R Jul 13 '16 at 07:02
  • yes it is same as in your example. I am not sure if I am missing anything. But I just copy pasted all you posted. No changes – prabhu Jul 13 '16 at 07:06
  • @prabhu: Hopefully you replaced `/path/to/file` by the real path of an existing file? – Martin R Jul 13 '16 at 07:07
  • Hey it is working. All I need to do is to set the attribute after i write contents to the file and "close it". I will accept the answer. Thanks – prabhu Jul 13 '16 at 07:10
  • Curious to know if i can write a bool value to an attribute. It would be helpful if you could show a sample to read the attribute from a file. Thanks in advance – prabhu Jul 13 '16 at 07:12
  • that is vey helpful. Thanks for your time (y) – prabhu Jul 14 '16 at 06:29
  • Hi Martin. I checked this in my simulator and it is working fine. But in device, I exported the file and mail it. Then I tried to check if the file contains the attribute in command line and it says "No such xattr: com.myCompany.myAttribute" error. Any idea? – prabhu Jul 14 '16 at 08:07
  • waiting for your response :) – prabhu Jul 19 '16 at 05:01
  • 1
    @prabhu: In what format did you export and mail the file? You would have to choose a format which preserves extended attributes (if such a format exists). – Martin R Jul 19 '16 at 09:43
  • Is there a function to remove all the stuff I added with "setExtendedAttribute"? I don't want to write 0 bytes, but keep the name in use. – Peter71 Sep 02 '16 at 11:45
  • @Peter71: The `removexattr` system call removes an attribute and its value. If you want to keep the attribute name then you have to set its value to zero bytes. – Martin R Sep 02 '16 at 11:50
  • I used: func removeExtendedAttribute(forName name: String) -> Bool { var fileSystemPath = [Int8](count: Int(MAXPATHLEN), repeatedValue: 0) guard self.getFileSystemRepresentation(&fileSystemPath, maxLength: fileSystemPath.count) else { return false } let result = removexattr(&fileSystemPath, name, 0) return result == 0 } but it delivers an error. – Peter71 Sep 02 '16 at 18:37
  • @Peter71: I have tested it and it works. (The `&` is not necessary.) – I have taken the liberty and added that to the answer :) – Martin R Sep 02 '16 at 18:50
  • Ok, I will test it again. Thanks. – Peter71 Sep 02 '16 at 19:28
  • how can I use this for removing xattr from a directory and all it its containing sub-directories, files – moghya Mar 11 '19 at 07:11
  • 2
    To fix the "`withUnsafeMutableBytes` is deprecated" warning in Swift 5, change `$0` to `$0.baseAddress`. – Marián Černý Apr 02 '19 at 09:17
  • 1
    @MariánČerný: Thanks for the notice, I have updated the code for Swift 5. – Martin R Apr 02 '19 at 09:43
  • 1
    @ayaio: I had recently updated the code for Swift 5, the Swift 4 version is in the [edit history](https://stackoverflow.com/revisions/09b23c4b-c7dd-45b8-bbdd-fc5334eb845c/view-source) – Martin R Apr 03 '19 at 20:28
  • @MartinR Hello Martin, I'm trying to solve https://stackoverflow.com/q/55497492/2227743 but I'm out of my comfort zone. I was trying to use your extension (with $0 for Swift 4 instead of $0.baseAddress) but I only get an empty array when listing attributes. Would you mind looking at this person's question? I'm interested by your take on this. :) (sorry about the previous comment, I didn't read the comments and edits before asking). Also, sorry if I'm bothering you, I'll understand if you're not interested. :) – Eric Aya Apr 03 '19 at 20:30
  • @ayaio: No problem, but I do not have an answer at the moment. – Martin R Apr 03 '19 at 20:46
  • @MartinR I managed to get the attributes from the file if it's in the filesystem (doesn't work if the file is copied in the app bundle, not sure why). I get the resource fork data with `try url.extendedAttribute(forName: "com.apple.ResourceFork")`, thanks to your extension. Now, to find how to decode this data and extract the custom icon from it... // Thank you for your work. :) – Eric Aya Apr 04 '19 at 07:23
  • When running the code and adding an attribute to a file there's automatically another attribute added: "com.apple.quarantine" – Mike Nathas Jan 16 '20 at 12:28
  • @MikeNathas: Sorry, I could not reproduce that behavior. – The com.apple.quarantine attribute is added to files downloaded from the Internet, see for example https://apple.stackexchange.com/q/104712/30895. – Martin R Jan 20 '20 at 20:19
  • I know that's why I'm wondering why this attribute is added. This code will show the attributes before (no attribute) and after adding an attribute with your code (2 new attributes): com.apple.quarantine and newAttr. https://pastebin.com/BGbqeaAH – Mike Nathas Jan 22 '20 at 15:16
  • @MikeNathas: Does that also happen if you add attributes with the “xattr” command-line tool? – Martin R Jan 22 '20 at 15:35
  • No, with `xattr -w newAttr "test" test.txt` only the newAttr attribute is added – Mike Nathas Jan 22 '20 at 15:40
  • @MikeNathas: Sorry, but I still cannot reproduce that behaviour. I have tried that code with various files in the Documents folder, and no com.apple.quarantine attribute was added for files which did not have that attribute before. – Martin R Jan 22 '20 at 19:48
  • @MikeNathas: Xcode 11.3.1 (Swift 5) on macOS 10.14 and 10.15. – Martin R Jan 22 '20 at 21:21
  • OK it seems only to happen when you run the code in playground – Mike Nathas Jan 23 '20 at 10:34
  • @martin Thanks for this, it was really useful. Do you happen to know if any way to get a list of all tags, like the one in the Finder side bar or in the Files app? – mralexhay Apr 12 '20 at 13:57
  • 1
    I used this script in order to get a list of all the tags of a file (or folder) and it worked fine. Example : "let fileURL = URL(fileURLWithPath: "/Volumes/testVolume", isDirectory: true)" AND : "let attr1 = "com.apple.metadata:_kMDItemUserTags". After that, you 'll need to cast the result to utf8 to make it readable : "let resultatdata1a = (String(decoding: data1a, as: UTF8.self))". I am using swift version 5. I had to disable sandbox to allow this operation. – Fredo Oct 10 '21 at 23:35
  • Thanks a lot, this works fine! The only note that there's "native" (Swift) POSIXError in Foundation: – Grigory Entin Jan 15 '22 at 15:50
  • https://developer.apple.com/documentation/foundation/posixerror – Grigory Entin Jan 15 '22 at 15:50
  • @GrigoryEntin: Thanks for the notice. My posixError is just a helper method to create an NSError. In order to use Swift.POSIXError I ended up with something like `POSIXError(POSIXErrorCode(rawValue: errno)!, userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(errno))])` with forced unwrapping. I am not sure if that is the best way ... – Martin R Jan 15 '22 at 18:16
  • @MartinR I see/get your point. Just to mention it, I feel like userInfo is not necessary there - POSIXError provides the proper description automatically (from what I can see). – Grigory Entin Jan 15 '22 at 19:01
  • @GrigoryEntin: You are right, `POSIXError(POSIXErrorCode(rawValue: errno)!)` would be sufficient. But I don't like the forced unwrapping. Of course one can provide a default value `POSIXError(POSIXErrorCode(rawValue: errno) ?? .EINVAL)`, but why? We *know* that `errno` is an error code from a system call. – Martin R Jan 15 '22 at 19:04
  • @MartinR I agreed with the argument for force unwrapping. Just thinking out loud, I wonder why (Swift) POSIXError exists at all. I can imagine that it would be used on the caller side, e.g. for matching/analysis of the errors... Even with this very code, I match against POSIXError.ENOATTR to catch non-existing attributes (so far I use POSIXError with fallback as you proposed). All in all, yes, it looks like a deficiency from API perspective that we have to use .Code instead of just Int for construction, still, it's not all black and white to me... – Grigory Entin Jan 15 '22 at 19:25