archive single file to .aar file in Swift

Apple provides sample code for compressing a single file here, but the "aa" command and Finder cannot decompress these files. I needed to write a custom decompresser using Apple's sample code here.

Apple provides sample code for creating an "aar" file for directories here and a single string here, and the aa command and Finder can deal with these.

However, I have struggled creating an ".aar" file for a single file.

The file could be quite large, so reading it into memory and writing it as a blob is not an option.

Does anyone have suggestions or can point me to Apple documentation that can create a ".aar" file for a single file?

Answered by DTS Engineer in 877173022

Coming at this from the perspective of the C API, the equivalent of writeDirectoryContents(archiveFrom:path:keySet:selectUsing:flags:threadCount:) is the combination of AAPathListCreateWithDirectoryContents and AAArchiveStreamWritePathList. And to write a single file you can create the path list with AAPathListCreateWithPath.

The Swift API combines these two into a single step, so there’s no Swift equivalent of AAPathListCreateWithPath. However, it’s not clear whether you need that. If you put a file in a directory and then point writeDirectoryContents(…) at that directory, you end up with an archive that contains just that file. For example, with this hierarchy:

/Users/
    quinn/
        Test/
            somefile.text

passing /Users/quinn/Test to writeDirectoryContents(…) I end up with this:

% aa list -v -i output.aar
Operation: list
  worker threads: 14
  verbosity level: 1
  input file: output.aar
  entry types: bcdfhlmps
D PAT= UID=502 GID=20 MOD=00755 FLG=0x00000000 CTM=1663853978.872477459 MTM=1771930096.106661813
F PAT=somefile.text UID=502 GID=20 MOD=00644 FLG=0x00000000 CTM=1771867732.937172055 MTM=1771868833.352496645 DAT[3971]
        0.00 total time (s)

which seems pretty reasonable.

Is that what you’re looking for? Is the presence of that directory (D) entry a problem?

Share and Enjoy

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

The following code is working for me. Hopefully, Apple has a better example posted somewhere and someone will post it here.

It is based on Apple's string compression code with some code at the end to write the source file as a sequence of blobs to the encodeStream.

func compressFile(at sourceURL: URL, to destURL: URL, deleteSource: Bool = true) async throws {
        
    let sourceLastComponent = sourceURL.lastPathComponent
    let destPath = FilePath(destURL.path)
    
    guard let sourceFileSize = getFileSize(url: sourceURL) else {
        return
    }
    
    
    // writeFileStream
    guard let writeFileStream =  ArchiveByteStream.fileStream(
        path: destPath,
        mode: .writeOnly,
        options: [.create],
        permissions: FilePermissions(rawValue: 0o644)
    ) else {
        return
    }
    defer { try? writeFileStream.close() }
    
    
    // compressStream
    guard let compressStream = ArchiveByteStream.compressionStream(
        using: .lzfse,
        writingTo: writeFileStream
    ) else {
        return
    }
    defer { try? compressStream.close() }
    
    
    // encodeStream
    guard let encodeStream = ArchiveStream.encodeStream(writingTo: compressStream) else {
        return
    }
    defer {
        try? encodeStream.close()
    }
    
    // Create header and send it to the encodeStream
    let header = ArchiveHeader()
    header.append(.string(key: ArchiveHeader.FieldKey("PAT"),
                          value: sourceLastComponent))
    header.append(.uint(key: ArchiveHeader.FieldKey("TYP"),
                        value:  UInt64(ArchiveHeader.EntryType.regularFile.rawValue)))
    header.append(.blob(key: ArchiveHeader.FieldKey("DAT"),
                        size: UInt64(sourceFileSize)))
    do {
        try encodeStream.writeHeader(header)
    } catch {
        print("Failed to write header.")
        return
    }
    
    // Open source file, reading in chunks up to 64000 bytes and writing
    // them as blobs to the encodeStream
    let fileHandle = try FileHandle(forReadingFrom: sourceURL)
    defer { try? fileHandle.close() }

    let chunkSize = 64000

    while true {
        guard let data = try fileHandle.read(upToCount: chunkSize) else { break }
        if data.isEmpty { break }
        
        try data.withUnsafeBytes { rawBufferPointer in
            try encodeStream.writeBlob(key: ArchiveHeader.FieldKey("DAT"), from: rawBufferPointer)
        }
    }
    
    try fileHandle.close()
    
    if deleteSource {
        do {
            try FileManager.default.removeItem(at: sourceURL)
        } catch {
            print("ERROR: Could not remove source file at \(sourceURL.path)")
        }
    }
}

func getFileSize(url: URL) -> Int64? {
    do {
        let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey])
        return Int64(resourceValues.fileSize ?? 0)
    } catch {
        print("Error getting file size: \(error.localizedDescription)")
        return nil
    }
}
Accepted Answer

Coming at this from the perspective of the C API, the equivalent of writeDirectoryContents(archiveFrom:path:keySet:selectUsing:flags:threadCount:) is the combination of AAPathListCreateWithDirectoryContents and AAArchiveStreamWritePathList. And to write a single file you can create the path list with AAPathListCreateWithPath.

The Swift API combines these two into a single step, so there’s no Swift equivalent of AAPathListCreateWithPath. However, it’s not clear whether you need that. If you put a file in a directory and then point writeDirectoryContents(…) at that directory, you end up with an archive that contains just that file. For example, with this hierarchy:

/Users/
    quinn/
        Test/
            somefile.text

passing /Users/quinn/Test to writeDirectoryContents(…) I end up with this:

% aa list -v -i output.aar
Operation: list
  worker threads: 14
  verbosity level: 1
  input file: output.aar
  entry types: bcdfhlmps
D PAT= UID=502 GID=20 MOD=00755 FLG=0x00000000 CTM=1663853978.872477459 MTM=1771930096.106661813
F PAT=somefile.text UID=502 GID=20 MOD=00644 FLG=0x00000000 CTM=1771867732.937172055 MTM=1771868833.352496645 DAT[3971]
        0.00 total time (s)

which seems pretty reasonable.

Is that what you’re looking for? Is the presence of that directory (D) entry a problem?

Share and Enjoy

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

It’s better to reply as a reply, rather than in the comments; see Quinn’s Top Ten DevForums Tips for this and other titbits.

I had to switch to Objective-C for the language to find the list

Right. This is because Swift isn’t importing the C API directly, but rather the C APIs are marked as refined for Swift and there’s a Swift API that wraps them. If you rummage around in the headers you can see how this actually works, via the APPLE_ARCHIVE_SWIFT_PRIVATE macro.

Curiously, you can access all this stuff from Swift by adding a double underscore prefix:

guard let pathList = __AAPathListCreateWithDirectoryContents("/foo/bar", nil, nil, nil, 0, 0) else {
    … handle the error …
}

Not that I recommend that you do that. Using low-level C APIs like this from Swift isn’t much fun. Moreover, it puts you way off the beaten path.

So, if you find something that you can’t do from Swift but can do from C, I recommend that you:

  • Do it from C, and then call that C code from Swift.
  • File an enhancement request against the Swift API explaining what you’re doing and why the Swift API doesn’t work for you.

And if you file any such bugs, please post the bug numbers, just for the record.

Share and Enjoy

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

archive single file to .aar file in Swift
 
 
Q