diff --git a/Sources/ContainerizationArchive/ArchiveError.swift b/Sources/ContainerizationArchive/ArchiveError.swift index 3002d0a5..277a93c6 100644 --- a/Sources/ContainerizationArchive/ArchiveError.swift +++ b/Sources/ContainerizationArchive/ArchiveError.swift @@ -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: diff --git a/Sources/ContainerizationArchive/ArchiveWriter.swift b/Sources/ContainerizationArchive/ArchiveWriter.swift index c814c39a..f8990798 100644 --- a/Sources/ContainerizationArchive/ArchiveWriter.swift +++ b/Sources/ContainerizationArchive/ArchiveWriter.swift @@ -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 diff --git a/Sources/ContainerizationArchive/ArchiveWriterConfiguration.swift b/Sources/ContainerizationArchive/ArchiveWriterConfiguration.swift index cfcd45f2..c92d5358 100644 --- a/Sources/ContainerizationArchive/ArchiveWriterConfiguration.swift +++ b/Sources/ContainerizationArchive/ArchiveWriterConfiguration.swift @@ -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 diff --git a/Sources/ContainerizationArchive/ArchiveWriterDelegate.swift b/Sources/ContainerizationArchive/ArchiveWriterDelegate.swift deleted file mode 100644 index d3aaeaef..00000000 --- a/Sources/ContainerizationArchive/ArchiveWriterDelegate.swift +++ /dev/null @@ -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) -} diff --git a/Sources/ContainerizationArchive/FileArchiveWriterDelegate.swift b/Sources/ContainerizationArchive/FileArchiveWriterDelegate.swift index c6ae5907..4e7b3fc7 100644 --- a/Sources/ContainerizationArchive/FileArchiveWriterDelegate.swift +++ b/Sources/ContainerizationArchive/FileArchiveWriterDelegate.swift @@ -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)) - } -} diff --git a/Sources/ContainerizationArchive/Reader.swift b/Sources/ContainerizationArchive/Reader.swift index d01d9614..1c8ddc03 100644 --- a/Sources/ContainerizationArchive/Reader.swift +++ b/Sources/ContainerizationArchive/Reader.swift @@ -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) diff --git a/Sources/ContainerizationArchive/TempDir.swift b/Sources/ContainerizationArchive/TempDir.swift index 02fa1c5f..c6794fa5 100644 --- a/Sources/ContainerizationArchive/TempDir.swift +++ b/Sources/ContainerizationArchive/TempDir.swift @@ -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 { diff --git a/Sources/ContainerizationArchive/WriteEntry.swift b/Sources/ContainerizationArchive/WriteEntry.swift index 7b0c531d..46dd8478 100644 --- a/Sources/ContainerizationArchive/WriteEntry.swift +++ b/Sources/ContainerizationArchive/WriteEntry.swift @@ -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)