Copy a file like ditto

How can I copy a file or a folder in the same manner as ditto, via Swift? What I mean is that all file attributes - extended attributes, permission settings, flags, timestamps, etc - are maintained on the copy?

I have tried the following:

Code Block
try FileManager.default.copyItem(atPath: srcPath, toPath: destPath)


However, in my testing, this maintains some attributes, but the extended attributes get mangled a bit. For example, I applied a quarantine flag to a test file, and here's what it looks like on the original and new files:

Code Block
$ xattr -l test.txt newtest.txt
test.txt: com.apple.quarantine: 0083;5991b778;Safari.app;BC4DFC58-0D26-460D-9688-81D119298642
newtest.txt: com.apple.quarantine: 0082;6051c84a;;


If I copy the file using ditto instead, I get this:

Code Block
$ ditto test.txt newtest.txt
$ xattr -l test.txt newtest.txt
test.txt: com.apple.quarantine: 0083;5991b778;Safari.app;BC4DFC58-0D26-460D-9688-81D119298642
newtest.txt: com.apple.quarantine: 0083;5991b778;Safari.app;BC4DFC58-0D26-460D-9688-81D119298642


I could simply call ditto from Swift, but that's a less ideal solution.

Replies

Copying files is way more complex than you think because it relies on a solid answer to “What is a file?”, and that’s a fundamentally squishy concept.

Why do you care about exactly replicating the quarantine attributes? What practical problems do you see with the attributes you get when copying with FileManager?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
What I'm working on is for file collection for forensic purposes, so being able to see things like a quarantine attribute, accurate timestamps, ownership info, etc, can give important information. Even capturing resource "forks" can be important, as some threats these days are known to hide malicious code there, where most security software won't find it.
Also, I didn't directly answer this question:

What practical problems do you see with the attributes you get when copying with FileManager?

In the example I cited above, both the UUID (for looking up the entry in the QuarantineEvents database) and downloading agent are missing, and the quarantine value and timestamp have been changed.

What I'm working on is for file collection for forensic purposes

Hmmm, and you’re not using asr?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hmmm, and you’re not using asr?

Not for this purpose. I'm just capturing specific files, not imaging the entire disk.

I'm just capturing specific files, not imaging the entire disk.

OK.

On the ditto front, I don’t have a complete answer for you. It’s possible that ditto has special privileges in this regard [1] but it’s also possible that it’s just using the file system APIs in a different way than FileManager. If you want a definitive answer, you should open a DTS tech support incident and one of my colleagues can dig into this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Consider this:

Code Block
% codesign -d --entitlements :- /usr/bin/ditto
<dict>
<key>com.apple.rootless.internal-installer-equivalent</key>
<true/>
</dict>
</plist>

Okay, thanks. Entitlements may be on the nose, as I'm seeing weird behavior when trying to run ditto from within Swift, as opposed to in the Terminal. I wanted to do a quick comparison, and running ditto via this code, in a Swift Playground, didn't work:

Code Block import Foundation
let srcPath = "/Users/thomas/Desktop/test.txt"
let destFolderPath = "/Users/thomas/Desktop/test-folder/"
func util_shell(_ command: String) -> String {
    let task = Process()
    task.launchPath = "/bin/bash"
    task.arguments = ["-c", command]
    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
    return output.trimmingCharacters(in: .whitespacesAndNewlines)
}
let theCommand = "ditto -v \(srcPath) \(destFolderPath)"
print(theCommand)
let theOutput = util_shell(theCommand)
print(theOutput)


This resulted in the following output:

Code Block ditto -v /Users/thomas/Desktop/test.txt /Users/thomas/Desktop/test-folder/
Copying /Users/thomas/Desktop/test.txt 
ditto: /Users/thomas/Desktop/test-folder/.BC.T_hHCsH6: Operation not permitted


Of course, maybe this isn't about entitlements and just means I screwed up the util_shell function somehow. It works for other shell commands, though.

I'm guessing I'll probably need to write more complex code, that captures important metadata about the source file(s) and writes that out to the destination. Sounds like a big job, though. I shudder at the thought of recursing through an entire folder tree and accurately copying everything inside...

Thanks for your suggestions!