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"

archive single file to .aar file in Swift
 
 
Q