Add docs for ContainerizationArchive (#28)

Also removes some dead code.

Signed-off-by: Aditya Ramani <a_ramani@apple.com>
This commit is contained in:
Aditya Ramani
2025-06-06 16:29:11 -07:00
committed by GitHub
parent 9959810656
commit 5f29918ecb
8 changed files with 89 additions and 55 deletions
@@ -18,6 +18,7 @@
import CArchive
import Foundation
/// An enumeration of the errors that can be thrown while interacting with an archive.
public enum ArchiveError: Error, CustomStringConvertible {
case unableToCreateArchive
case noUnderlyingArchive
@@ -37,6 +38,7 @@ public enum ArchiveError: Error, CustomStringConvertible {
case failedToDetectFormat
case failedToExtractArchive(String)
/// Description of the error
public var description: String {
switch self {
case .unableToCreateArchive:
@@ -18,10 +18,13 @@
import CArchive
import Foundation
/// A class responsible for writing archives in various formats.
public final class ArchiveWriter {
var underlying: OpaquePointer!
var delegate: ArchiveWriterDelegate?
var delegate: FileArchiveWriterDelegate?
/// Initialize a new `ArchiveWriter` with the given configuration.
/// This method attempts to initialize an empty archive in memory, failing which it throws a `unableToCreateArchive` error.
public init(configuration: ArchiveWriterConfiguration) throws {
// because for some bizarre reason, UTF8 paths won't work unless this process explicitly sets a locale like en_US.UTF-8
try Self.attemptSetLocales(locales: configuration.locales)
@@ -34,24 +37,38 @@ public final class ArchiveWriter {
try setOptions(configuration.options)
}
public convenience init(configuration: ArchiveWriterConfiguration, delegate: ArchiveWriterDelegate) throws {
/// Initialize a new `ArchiveWriter` with the given configuration and specifed delegate.
private convenience init(configuration: ArchiveWriterConfiguration, delegate: FileArchiveWriterDelegate) throws {
try self.init(configuration: configuration)
self.delegate = delegate
try self.open()
}
private convenience init(configuration: ArchiveWriterConfiguration, file: URL) throws {
try self.init(configuration: configuration, delegate: FileArchiveWriterDelegate(url: file))
}
/// Initialize a new `ArchiveWriter` for writing into the specified file with the given configuration options.
public convenience init(format: Format, filter: Filter, options: [Options] = [], file: URL) throws {
try self.init(
configuration: .init(format: format, filter: filter), delegate: FileArchiveWriterDelegate(url: file))
}
/// Opens the given file for writing data into
public func open(file: URL) throws {
guard let underlying = underlying else { throw ArchiveError.noUnderlyingArchive }
let res = archive_write_open_filename(underlying, file.path)
try wrap(res, ArchiveError.unableToOpenArchive, underlying: underlying)
}
/// Opens the given fd for writing data into
public func open(fileDescriptor: Int32) throws {
guard let underlying = underlying else { throw ArchiveError.noUnderlyingArchive }
let res = archive_write_open_fd(underlying, fileDescriptor)
try wrap(res, ArchiveError.unableToOpenArchive, underlying: underlying)
}
/// Performs any necessary finalizations on the archive and releases resources.
public func finishEncoding() throws {
if let u = underlying {
let r = archive_free(u)
@@ -72,7 +89,7 @@ public final class ArchiveWriter {
}
}
public static func attemptSetLocales(locales: [String]) throws {
private static func attemptSetLocales(locales: [String]) throws {
for locale in locales {
if setlocale(LC_ALL, locale) != nil {
return
@@ -195,12 +212,29 @@ extension ArchiveWriter {
ArchiveWriterTransaction(writer: self)
}
/// Create a new entry in the archive with the given properties.
/// - Parameters:
/// - entry: A `WriteEntry` object describing the metadata of the entry to be created
/// (e.g., name, modification date, permissions).
/// - data: The `Data` object containing the content for the new entry.
public func writeEntry(entry: WriteEntry, data: Data) throws {
try data.withUnsafeBytes { bytes in
try writeEntry(entry: entry, data: bytes)
}
}
/// Creates a new entry in the archive with the given properties.
///
/// This method performs the following:
/// 1. Writes the archive header using the provided `WriteEntry` metadata.
/// 2. Writes the content from the `UnsafeRawBufferPointer` into the archive.
/// 3. Finalizes the entry in the archive.
///
/// - Parameters:
/// - entry: A `WriteEntry` object describing the metadata of the entry to be created
/// (e.g., name, modification date, permissions, type).
/// - data: An optional `UnsafeRawBufferPointer` containing the raw bytes for the new entry's
/// content. Pass `nil` for entries that do not have content data (e.g., directories, symlinks).
public func writeEntry(entry: WriteEntry, data: UnsafeRawBufferPointer?) throws {
try writeHeader(entry: entry)
if let data = data {
@@ -234,7 +268,7 @@ extension ArchiveWriter {
}
extension ArchiveWriter {
/// Recursively archives the content of a directory. Regular files, symlinks and directories are added to the archive.
/// Recursively archives the content of a directory. Regular files, symlinks and directories are added into the archive.
/// Note: Symlinks are added to the archive if both the source and target for the symlink are both contained in the top level directory.
public func archiveDirectory(_ dir: URL) throws {
let fm = FileManager.default
@@ -17,12 +17,24 @@
import CArchive
/// Represents the configuration settings for an `ArchiveWriter`.
///
/// This struct allows specifying the archive format, compression filter,
/// various format-specific options, and preferred locales for string encoding.
public struct ArchiveWriterConfiguration {
/// The desired archive format
public var format: Format
/// The compression filter to apply to the archive
public var filter: Filter
/// An array of format-specific options to apply to the archive.
/// This includes options like compression level and extended attribute format.
public var options: [Options]
/// An array of preferred locale identifiers for string encoding
public var locales: [String]
/// Initializes a new `ArchiveWriterConfiguration`.
///
/// Sets up the configuration with the specified format, filter, options, and locales.
public init(
format: Format, filter: Filter, options: [Options] = [], locales: [String] = ["en_US.UTF-8", "C.UTF-8"]
) {
@@ -34,19 +46,19 @@ public struct ArchiveWriterConfiguration {
}
extension ArchiveWriter {
func setFormat(_ format: Format) throws {
internal func setFormat(_ format: Format) throws {
guard let underlying = self.underlying else { throw ArchiveError.noUnderlyingArchive }
let r = archive_write_set_format(underlying, format.code)
guard r == ARCHIVE_OK else { throw ArchiveError.unableToSetFormat(r, format) }
}
func addFilter(_ filter: Filter) throws {
internal func addFilter(_ filter: Filter) throws {
guard let underlying = self.underlying else { throw ArchiveError.noUnderlyingArchive }
let r = archive_write_add_filter(underlying, filter.code)
guard r == ARCHIVE_OK else { throw ArchiveError.unableToAddFilter(r, filter) }
}
func setOptions(_ options: [Options]) throws {
internal func setOptions(_ options: [Options]) throws {
try options.forEach {
switch $0 {
case .compressionLevel(let level):
@@ -99,6 +111,7 @@ public enum Options {
}
}
/// An enumeration of the supported archive formats.
public enum Format: String, Sendable {
/// POSIX-standard `ustar` archives
case ustar
@@ -126,7 +139,7 @@ public enum Format: String, Sendable {
/// XAR archives
case xar
var code: CInt {
internal var code: CInt {
switch self {
case .ustar: return ARCHIVE_FORMAT_TAR_USTAR
case .pax: return ARCHIVE_FORMAT_TAR_PAX_INTERCHANGE
@@ -147,7 +160,7 @@ public enum Format: String, Sendable {
}
}
/// A filter (compression / encoding) to use when writing.
/// An enumreration of the supported filters (compression / encoding standards) for an archive.
public enum Filter: String, Sendable {
case none
case gzip
@@ -163,7 +176,7 @@ public enum Filter: String, Sendable {
case grzip
case lz4
var code: CInt {
internal var code: CInt {
switch self {
case .none: return ARCHIVE_FILTER_NONE
case .gzip: return ARCHIVE_FILTER_GZIP
@@ -1,31 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import CArchive
public protocol ArchiveWriterDelegate: AnyObject {
/// The open callback is invoked by archive_write_open(). It should return ARCHIVE_OK if the underlying file or data source is successfully opened. If the open fails, it should call archive_set_error() to register an error code and message and return ARCHIVE_FATAL. Please note that
/// if open fails, close is not called and resources must be freed inside the open callback or with the free callback.
func open(archive: ArchiveWriter) throws
/// returns number of bytes written
func write(archive: ArchiveWriter, buffer: UnsafeRawBufferPointer) throws -> Int
/// The close callback is invoked by archive_close when the archive processing is complete. If the open callback fails, the close callback is not invoked. The callback should return ARCHIVE_OK on success. On failure, the callback should invoke archive_set_error() to register an
/// error code and message and return
func close(archive: ArchiveWriter) throws
/// The free callback is always invoked on archive_free. The return code of this callback is not processed.
func free(archive: ArchiveWriter)
}
@@ -18,8 +18,7 @@
import Foundation
import SystemPackage
public final class FileArchiveWriterDelegate: ArchiveWriterDelegate {
internal final class FileArchiveWriterDelegate {
public let path: FilePath
private var fd: FileDescriptor!
@@ -54,13 +53,3 @@ public final class FileArchiveWriterDelegate: ArchiveWriterDelegate {
}
}
}
extension ArchiveWriter {
public convenience init(configuration: ArchiveWriterConfiguration, file: URL) throws {
try self.init(configuration: configuration, delegate: FileArchiveWriterDelegate(url: file))
}
public convenience init(format: Format, filter: Filter, options: [Options] = [], file: URL) throws {
try self.init(
configuration: .init(format: format, filter: filter), delegate: FileArchiveWriterDelegate(url: file))
}
}
+11 -1
View File
@@ -18,15 +18,24 @@
import CArchive
import Foundation
/// A class responsible for reading entries from an archive file.
public final class ArchiveReader {
/// A pointer to the underlying `archive` C structure.
var underlying: OpaquePointer?
/// The file handle associated with the archive file being read.
let fileHandle: FileHandle?
/// Initializes an `ArchiveReader` to read from a specified file URL with an explicit `Format` and `Filter`.
/// Note: This method must be used when it is known that the archive at the specified URL follows the specifed
/// `Format` and `Filter`.
public convenience init(format: Format, filter: Filter, file: URL) throws {
let fileHandle = try FileHandle(forReadingFrom: file)
try self.init(format: format, filter: filter, fileHandle: fileHandle)
}
/// Initializes an `ArchiveReader` to read from the provided file descriptor with an explicit `Format` and `Filter`.
/// Note: This method must be used when it is known that the archive pointed to by the file descriptor follows the specifed
/// `Format` and `Filter`.
public init(format: Format, filter: Filter, fileHandle: FileHandle) throws {
self.underlying = archive_read_new()
self.fileHandle = fileHandle
@@ -41,7 +50,8 @@ public final class ArchiveReader {
.checkOk(elseThrow: { .unableToOpenArchive($0) })
}
// Initialize the archive reader by trying to auto detect the archive and compression format
/// Initialize the `ArchiveReader` to read from a specified file URL
/// by trying to auto determine the archives `Format` and `Filter`.
public init(file: URL) throws {
self.underlying = archive_read_new()
let fileHandle = try FileHandle(forReadingFrom: file)
@@ -18,7 +18,7 @@
import ContainerizationExtras
import Foundation
func createTemporaryDirectory(baseName: String) -> URL? {
internal func createTemporaryDirectory(baseName: String) -> URL? {
let url = FileManager.default.uniqueTemporaryDirectory().appendingPathComponent(
"\(baseName).XXXXXX")
guard let templatePathData = (url.absoluteURL.path as NSString).utf8String else {
@@ -18,6 +18,8 @@
import CArchive
import Foundation
/// Represents a single entry (e.g., a file, directory, symbolic link)
/// that is to be read/written into an archive.
public final class WriteEntry {
let underlying: OpaquePointer
@@ -35,6 +37,7 @@ public final class WriteEntry {
}
extension WriteEntry {
/// The size of the entry in bytes.
public var size: Int64? {
get {
guard archive_entry_size_is_set(underlying) != 0 else { return nil }
@@ -49,6 +52,7 @@ extension WriteEntry {
}
}
/// The mode of the entry.
public var permissions: mode_t {
get {
archive_entry_perm(underlying)
@@ -58,6 +62,7 @@ extension WriteEntry {
}
}
/// The owner id of the entry.
public var owner: uid_t? {
get {
uid_t(exactly: archive_entry_uid(underlying))
@@ -67,6 +72,7 @@ extension WriteEntry {
}
}
/// The group id of the entry
public var group: gid_t? {
get {
gid_t(exactly: archive_entry_gid(underlying))
@@ -76,6 +82,7 @@ extension WriteEntry {
}
}
/// The path of file this entry hardlinks to
public var hardlink: String? {
get {
guard let cstr = archive_entry_hardlink(underlying) else {
@@ -94,6 +101,7 @@ extension WriteEntry {
}
}
/// The UTF-8 encoded path of file this entry hardlinks to
public var hardlinkUtf8: String? {
get {
guard let cstr = archive_entry_hardlink_utf8(underlying) else {
@@ -112,6 +120,7 @@ extension WriteEntry {
}
}
/// The string representation of the permissions of the entry
public var strmode: String? {
if let cstr = archive_entry_strmode(underlying) {
return String(cString: cstr)
@@ -119,6 +128,7 @@ extension WriteEntry {
return nil
}
/// The type of file this entry represents.
public var fileType: URLFileResourceType {
get {
switch archive_entry_filetype(underlying) {
@@ -146,6 +156,7 @@ extension WriteEntry {
}
}
/// The date that the entry was last accessed
public var contentAccessDate: Date? {
get {
Date(
@@ -162,6 +173,7 @@ extension WriteEntry {
}
}
/// The date that the entry was created
public var creationDate: Date? {
get {
Date(
@@ -178,6 +190,7 @@ extension WriteEntry {
}
}
/// The date that the entry was modified
public var modificationDate: Date? {
get {
Date(
@@ -194,6 +207,7 @@ extension WriteEntry {
}
}
/// The file path of the entry
public var path: String? {
get {
guard let pathname = archive_entry_pathname(underlying) else {
@@ -212,6 +226,7 @@ extension WriteEntry {
}
}
/// The UTF-8 encoded file path of the entry
public var pathUtf8: String? {
get {
guard let pathname = archive_entry_pathname_utf8(underlying) else {
@@ -230,6 +245,7 @@ extension WriteEntry {
}
}
/// The symlink target that the entry points to
public var symlinkTarget: String? {
get {
guard let target = archive_entry_symlink(underlying) else {
@@ -248,6 +264,7 @@ extension WriteEntry {
}
}
/// The extended attributes of the entry
public var xattrs: [String: Data] {
get {
archive_entry_xattr_reset(self.underlying)