Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/ContainerizationArchive/ArchiveError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
42 changes: 38 additions & 4 deletions Sources/ContainerizationArchive/ArchiveWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions Sources/ContainerizationArchive/ArchiveWriterConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
) {
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
31 changes: 0 additions & 31 deletions Sources/ContainerizationArchive/ArchiveWriterDelegate.swift

This file was deleted.

13 changes: 1 addition & 12 deletions Sources/ContainerizationArchive/FileArchiveWriterDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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))
}
}
12 changes: 11 additions & 1 deletion Sources/ContainerizationArchive/Reader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerizationArchive/TempDir.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading