chore: import upstream snapshot with attribution
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:30 +08:00
commit 680845cb1c
445 changed files with 103779 additions and 0 deletions
@@ -0,0 +1,103 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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
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
case noArchiveInCallback
case noDelegateConfigured
case delegateFreedBeforeCallback
case unableToSetFormat(CInt, Format)
case unableToAddFilter(CInt, Filter)
case unableToWriteEntryHeader(CInt)
case unableToWriteData(CLong)
case unableToCloseArchive(CInt)
case unableToOpenArchive(CInt)
case unableToSetOption(CInt)
case failedToSetLocale(locales: [String])
case failedToGetProperty(String, URLResourceKey)
case failedToDetectFilter
case failedToDetectFormat
case failedToExtractArchive(String)
case failedToCreateArchive(String)
case invalidBaseAddressArchiveWrite
/// Description of the error
public var description: String {
switch self {
case .unableToCreateArchive:
return "unable to create an archive."
case .noUnderlyingArchive:
return "no underlying archive was provided."
case .noArchiveInCallback:
return "no archive was provided in the callback."
case .noDelegateConfigured:
return "no delegate was configured."
case .delegateFreedBeforeCallback:
return "the delegate was freed before the callback was invoked."
case .unableToSetFormat(let code, let name):
return "unable to set the archive format \(name), code \(code)"
case .unableToAddFilter(let code, let name):
return "unable to set the archive filter \(name), code \(code)"
case .unableToWriteEntryHeader(let code):
return "unable to write the entry header to the archive, code \(code)"
case .unableToWriteData(let code):
return "unable to write data to the archive, code \(code)"
case .unableToCloseArchive(let code):
return "unable to close the archive, code \(code)"
case .unableToOpenArchive(let code):
return "unable to open the archive, code \(code)"
case .unableToSetOption(_):
return "unable to set an option on the archive."
case .failedToSetLocale(let locales):
return "failed to set locale to \(locales)"
case .failedToGetProperty(let path, let propertyName):
return "failed to read property \(propertyName) from file at path \(path)"
case .failedToDetectFilter:
return "failed to detect filter from archive."
case .failedToDetectFormat:
return "failed to detect format from archive."
case .failedToExtractArchive(let reason):
return "failed to extract archive: \(reason)"
case .failedToCreateArchive(let reason):
return "failed to create archive: \(reason)"
case .invalidBaseAddressArchiveWrite:
return "got an invalid base address for pointer when writing data to archive"
}
}
}
public struct LibArchiveError: Error {
public let source: ArchiveError
public let description: String
}
func wrap(_ f: @autoclosure () -> CInt, _ e: (CInt) -> ArchiveError, underlying: OpaquePointer? = nil) throws {
let result = f()
guard result == ARCHIVE_OK else {
let error = e(result)
guard let underlying = underlying,
let description = archive_error_string(underlying).map(String.init(cString:))
else {
throw error
}
throw LibArchiveError(source: error, description: description)
}
}
@@ -0,0 +1,428 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// 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
import ContainerizationError
import ContainerizationOS
import Foundation
import SystemPackage
/// A protocol for reading data in chunks, compatible with both `InputStream` and zero-allocation archive readers.
public protocol ReadableStream {
/// Reads up to `maxLength` bytes into the provided buffer.
/// Returns the number of bytes actually read, 0 for EOF, or -1 for error.
func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength: Int) -> Int
}
extension InputStream: ReadableStream {}
/// Small wrapper type to read data from an archive entry.
public struct ArchiveEntryReader: ReadableStream {
private weak var reader: ArchiveReader?
init(reader: ArchiveReader) {
self.reader = reader
}
/// Reads up to `maxLength` bytes into the provided buffer.
/// Returns the number of bytes actually read, 0 for EOF, or -1 for error.
public func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength: Int) -> Int {
guard let archive = reader?.underlying else { return -1 }
let bytesRead = archive_read_data(archive, buffer, maxLength)
return bytesRead < 0 ? -1 : bytesRead
}
}
/// A class responsible for reading entries from an archive file.
public final class ArchiveReader {
private static let chunkSize = 4 * 1024 * 1024
/// A pointer to the underlying `archive` C structure.
var underlying: OpaquePointer?
/// The file handle associated with the archive file being read.
let fileHandle: FileHandle?
/// Temporary decompressed file URL if the input was zstd-compressed
private var tempDecompressedFile: URL?
/// 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 specified
/// `Format` and `Filter`.
public convenience init(format: Format, filter: Filter, file: URL) throws {
// If filter is zstd, decompress it and use filter .none
let fileToRead: URL
let tempFile: URL?
let actualFilter: Filter
if filter == .zstd {
let decompressed = try Self.decompressZstd(file)
tempFile = decompressed
fileToRead = decompressed
actualFilter = .none
} else {
tempFile = nil
fileToRead = file
actualFilter = filter
}
do {
let fileHandle = try FileHandle(forReadingFrom: fileToRead)
try self.init(format: format, filter: actualFilter, fileHandle: fileHandle)
} catch {
if let tempFile {
try? FileManager.default.removeItem(at: tempFile.deletingLastPathComponent())
}
throw error
}
self.tempDecompressedFile = tempFile
}
/// 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 specified
/// `Format` and `Filter`.
public init(format: Format, filter: Filter, fileHandle: FileHandle) throws {
self.underlying = archive_read_new()
self.fileHandle = fileHandle
try archive_read_set_format(underlying, format.code)
.checkOk(elseThrow: .unableToSetFormat(format.code, format))
try archive_read_append_filter(underlying, filter.code)
.checkOk(elseThrow: .unableToAddFilter(filter.code, filter))
let fd = fileHandle.fileDescriptor
try archive_read_open_fd(underlying, fd, 4096)
.checkOk(elseThrow: { .unableToOpenArchive($0) })
}
/// 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()
// Try to decompress as zstd first, fall back to original if it fails
let fileToRead: URL
if let decompressed = try? Self.decompressZstd(file) {
self.tempDecompressedFile = decompressed
fileToRead = decompressed
} else {
fileToRead = file
}
let fileHandle = try FileHandle(forReadingFrom: fileToRead)
self.fileHandle = fileHandle
try archive_read_support_filter_all(underlying)
.checkOk(elseThrow: .failedToDetectFilter)
try archive_read_support_format_all(underlying)
.checkOk(elseThrow: .failedToDetectFormat)
let fd = fileHandle.fileDescriptor
try archive_read_open_fd(underlying, fd, 4096)
.checkOk(elseThrow: { .unableToOpenArchive($0) })
}
/// Decompress a zstd file to a temporary location
public static func decompressZstd(_ source: URL) throws -> URL {
guard let tempDir = createTemporaryDirectory(baseName: "zstd-decompress") else {
throw ArchiveError.failedToDetectFormat
}
let tempFile = tempDir.appendingPathComponent(
source.deletingPathExtension().lastPathComponent
)
do {
let srcPath = source.path
let srcFd = open(srcPath, O_RDONLY)
guard srcFd >= 0 else { throw ArchiveError.failedToDetectFormat }
defer { close(srcFd) }
let dstFd = open(tempFile.path, O_WRONLY | O_CREAT | O_TRUNC, 0o644)
guard dstFd >= 0 else { throw ArchiveError.failedToDetectFormat }
defer { close(dstFd) }
guard zstd_decompress_fd(srcFd, dstFd) == 0 else {
throw ArchiveError.failedToDetectFormat
}
} catch {
try? FileManager.default.removeItem(at: tempDir)
throw error
}
return tempFile
}
/// Clean up the temporary directory created by `decompressZstd`.
/// The decompressed file is placed inside a unique temporary directory,
/// so removing that directory cleans up everything.
public static func cleanUpDecompressedZstd(_ file: URL) {
try? FileManager.default.removeItem(at: file.deletingLastPathComponent())
}
deinit {
archive_read_free(underlying)
try? fileHandle?.close()
if let tempFile = tempDecompressedFile {
Self.cleanUpDecompressedZstd(tempFile)
}
}
}
extension CInt {
fileprivate func checkOk(elseThrow error: @autoclosure () -> ArchiveError) throws {
guard self == ARCHIVE_OK else { throw error() }
}
fileprivate func checkOk(elseThrow error: (CInt) -> ArchiveError) throws {
guard self == ARCHIVE_OK else { throw error(self) }
}
}
extension ArchiveReader: Sequence {
public func makeIterator() -> Iterator {
Iterator(reader: self)
}
public struct Iterator: IteratorProtocol {
var reader: ArchiveReader
public mutating func next() -> (WriteEntry, Data)? {
let entry = WriteEntry()
let result = archive_read_next_header2(reader.underlying, entry.underlying)
if result == ARCHIVE_EOF {
return nil
}
let data = reader.readDataForEntry(entry)
return (entry, data)
}
}
/// Returns an iterator that yields archive entries.
public func makeStreamingIterator() -> StreamingIterator {
StreamingIterator(reader: self)
}
public struct StreamingIterator: Sequence, IteratorProtocol {
var reader: ArchiveReader
public func makeIterator() -> StreamingIterator {
self
}
public mutating func next() -> (WriteEntry, ArchiveEntryReader)? {
let entry = WriteEntry()
let result = archive_read_next_header2(reader.underlying, entry.underlying)
if result == ARCHIVE_EOF {
return nil
}
let streamReader = ArchiveEntryReader(reader: reader)
return (entry, streamReader)
}
}
internal func readDataForEntry(_ entry: WriteEntry) -> Data {
let bufferSize = Int(Swift.min(entry.size ?? 4096, 4096))
var entry = Data()
var part = Data(count: bufferSize)
while true {
let c = part.withUnsafeMutableBytes { buffer in
guard let baseAddress = buffer.baseAddress else {
return 0
}
return archive_read_data(self.underlying, baseAddress, buffer.count)
}
guard c > 0 else { break }
part.count = c
entry.append(part)
}
return entry
}
}
extension ArchiveReader {
public convenience init(name: String, bundle: Data, tempDirectoryBaseName: String? = nil) throws {
let baseName = tempDirectoryBaseName ?? "Unarchiver"
guard let tempDir = createTemporaryDirectory(baseName: baseName) else {
throw ArchiveError.failedToExtractArchive("failed to create temporary directory")
}
let url = tempDir.appendingPathComponent(name)
do {
try bundle.write(to: url, options: .atomic)
try self.init(format: .zip, filter: .none, file: url)
} catch {
try? FileManager.default.removeItem(at: tempDir)
throw error
}
// Register for cleanup in deinit (only needed when the zstd path didn't already set it)
if self.tempDecompressedFile == nil {
self.tempDecompressedFile = url
}
}
/// Extracts the contents of an archive to the provided directory.
/// Rejects member paths that escape the root directory or traverse
/// symbolic links, and uses a "last entry wins" replacement policy
/// for an existing file at a path to be extracted.
public func extractContents(to directory: URL) throws -> [String] {
// Create the root directory with standard permissions
// and create a FileDescriptor for secure path traversal.
let fm = FileManager.default
let rootFilePath = FilePath(directory.path)
try fm.createDirectory(atPath: directory.path, withIntermediateDirectories: true)
let rootFileDescriptor = try FileDescriptor.open(rootFilePath, .readOnly)
defer { try? rootFileDescriptor.close() }
// Iterate and extract archive entries, collecting rejected paths.
var foundEntry = false
var rejectedPaths = [String]()
for (entry, dataReader) in self.makeStreamingIterator() {
guard let memberPath = (entry.path.map { FilePath($0) }) else {
continue
}
foundEntry = true
// Try to extract the entry, catching path validation errors
let extracted = try extractEntry(
entry: entry,
dataReader: dataReader,
memberPath: memberPath,
rootFileDescriptor: rootFileDescriptor
)
if !extracted {
rejectedPaths.append(memberPath.string)
}
}
guard foundEntry else {
throw ArchiveError.failedToExtractArchive("no entries found in archive")
}
return rejectedPaths
}
/// This method extracts a given file from the archive.
/// This operation modifies the underlying file descriptor's position within the archive,
/// meaning subsequent reads will start from a new location.
/// To reset the underlying file descriptor to the beginning of the archive, close and
/// reopen the archive.
public func extractFile(path: String) throws -> (WriteEntry, Data) {
let entry = WriteEntry()
while archive_read_next_header2(self.underlying, entry.underlying) != ARCHIVE_EOF {
guard let entryPath = entry.path else { continue }
let trimCharSet = CharacterSet(charactersIn: "./")
let trimmedEntry = entryPath.trimmingCharacters(in: trimCharSet)
let trimmedRequired = path.trimmingCharacters(in: trimCharSet)
guard trimmedEntry == trimmedRequired else { continue }
let data = readDataForEntry(entry)
return (entry, data)
}
throw ArchiveError.failedToExtractArchive(" \(path) not found in archive")
}
/// Extracts a single archive entry.
/// Returns false if the entry was rejected due to path validation errors.
/// Throws on system errors.
private func extractEntry(
entry: WriteEntry,
dataReader: ArchiveEntryReader,
memberPath: FilePath,
rootFileDescriptor: FileDescriptor
) throws -> Bool {
guard let lastComponent = memberPath.lastComponent else {
return false
}
let relativePath = memberPath.removingLastComponent()
let type = entry.fileType
do {
switch type {
case .regular:
try FileDescriptorOps.mkdir(rootFileDescriptor, relativePath, makeIntermediates: true) { fd in
// Remove existing entry if present (mimics containerd's "last entry wins" behavior)
try? FileDescriptorOps.unlinkRecursive(fd, filename: lastComponent)
// Open file for writing using openat with O_NOFOLLOW to prevent TOC-TOU attacks
let fileMode = entry.permissions & 0o777 // Mask to permission bits only
let fileFd = openat(fd.rawValue, lastComponent.string, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, fileMode)
guard fileFd >= 0 else {
throw ArchiveError.failedToExtractArchive("failed to create file: \(memberPath)")
}
defer { close(fileFd) }
try Self.copyDataReaderToFd(dataReader: dataReader, fileFd: fileFd, memberPath: memberPath)
setFileAttributes(fd: fileFd, entry: entry)
}
case .directory:
try FileDescriptorOps.mkdir(rootFileDescriptor, memberPath, makeIntermediates: true) { fd in
setFileAttributes(fd: fd.rawValue, entry: entry)
}
case .symbolicLink:
guard let targetPath = (entry.symlinkTarget.map { FilePath($0) }) else {
return false
}
var symlinkCreated = false
try FileDescriptorOps.mkdir(rootFileDescriptor, relativePath, makeIntermediates: true) { fd in
// Remove existing entry if present (mimics containerd's "last entry wins" behavior)
try? FileDescriptorOps.unlinkRecursive(fd, filename: lastComponent)
guard symlinkat(targetPath.string, fd.rawValue, lastComponent.string) == 0 else {
throw ArchiveError.failedToExtractArchive("failed to create symlink: \(targetPath) <- \(memberPath)")
}
symlinkCreated = true
}
return symlinkCreated
default:
return false
}
return true
} catch let error as FileDescriptorOps.Error {
// Just reject path validation errors, don't fail the extraction
switch error {
case .systemError:
// Fail for system errors
throw error
case .invalidRelativePath, .invalidPathComponent, .cannotFollowSymlink:
return false
}
}
}
private func setFileAttributes(fd: Int32, entry: WriteEntry) {
fchmod(fd, entry.permissions)
if let owner = entry.owner, let group = entry.group {
fchown(fd, owner, group)
}
}
private static func copyDataReaderToFd(dataReader: ArchiveEntryReader, fileFd: Int32, memberPath: FilePath) throws {
var buffer = [UInt8](repeating: 0, count: ArchiveReader.chunkSize)
while true {
let bytesRead = buffer.withUnsafeMutableBufferPointer { bufferPtr in
guard let baseAddress = bufferPtr.baseAddress else { return 0 }
return dataReader.read(baseAddress, maxLength: bufferPtr.count)
}
if bytesRead < 0 {
throw ArchiveError.failedToExtractArchive("failed to read data for: \(memberPath)")
}
if bytesRead == 0 {
break // EOF
}
let bytesWritten = write(fileFd, buffer, bytesRead)
guard bytesWritten == bytesRead else {
throw ArchiveError.failedToExtractArchive("failed to write data for: \(memberPath)")
}
}
}
}
@@ -0,0 +1,339 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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
import Foundation
import SystemPackage
/// A class responsible for writing archives in various formats.
public final class ArchiveWriter {
private static let chunkSize = 4 * 1024 * 1024
var underlying: OpaquePointer?
/// 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)
guard let underlying = archive_write_new() else { throw ArchiveError.unableToCreateArchive }
self.underlying = underlying
try setFormat(configuration.format)
try addFilter(configuration.filter)
try setOptions(configuration.options)
}
/// Initialize a new `ArchiveWriter` for writing into the specified file with the given configuration options.
public convenience init(format: Format, filter: Filter, options: [Options] = [], locales: [String] = ArchiveWriterConfiguration.defaultLocales, file: URL) throws {
let config = ArchiveWriterConfiguration(
format: format,
filter: filter,
options: options,
locales: locales
)
try self.init(configuration: config)
try self.open(file: 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 {
guard let u = underlying else { return }
underlying = nil
let r = archive_free(u)
guard r == ARCHIVE_OK else {
throw ArchiveError.unableToCloseArchive(r)
}
}
deinit {
if let u = underlying {
archive_free(u)
underlying = nil
}
}
private static func attemptSetLocales(locales: [String]) throws {
for locale in locales {
if setlocale(LC_ALL, locale) != nil {
return
}
}
throw ArchiveError.failedToSetLocale(locales: locales)
}
}
public class ArchiveWriterTransaction {
private let writer: ArchiveWriter
fileprivate init(writer: ArchiveWriter) {
self.writer = writer
}
public func writeHeader(entry: WriteEntry) throws {
try writer.writeHeader(entry: entry)
}
public func writeChunk(data: UnsafeRawBufferPointer) throws {
try writer.writeData(data: data)
}
public func finish() throws {
try writer.finishEntry()
}
}
extension ArchiveWriter {
public func makeTransactionWriter() -> ArchiveWriterTransaction {
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 {
try writeData(data: data)
}
try finishEntry()
}
fileprivate func writeHeader(entry: WriteEntry) throws {
guard let underlying = self.underlying else { throw ArchiveError.noUnderlyingArchive }
try wrap(
archive_write_header(underlying, entry.underlying), ArchiveError.unableToWriteEntryHeader,
underlying: underlying)
}
fileprivate func finishEntry() throws {
guard let underlying = self.underlying else { throw ArchiveError.noUnderlyingArchive }
archive_write_finish_entry(underlying)
}
fileprivate func writeData(data: UnsafeRawBufferPointer) throws {
guard let underlying = self.underlying else { throw ArchiveError.noUnderlyingArchive }
var offset = 0
while offset < data.count {
guard let baseAddress = data.baseAddress?.advanced(by: offset) else {
throw ArchiveError.invalidBaseAddressArchiveWrite
}
let result = archive_write_data(underlying, baseAddress, data.count - offset)
guard result > 0 else {
throw ArchiveError.unableToWriteData(result)
}
offset += Int(result)
}
}
}
extension ArchiveWriter {
private func archive(_ relativePath: FilePath, dirPath: FilePath) throws {
let fm = FileManager.default
let fullPath = dirPath.appending(relativePath.string)
var statInfo = stat()
guard lstat(fullPath.string, &statInfo) == 0 else {
let errNo = errno
let err = POSIXErrorCode(rawValue: errNo) ?? .EINVAL
throw ArchiveError.failedToCreateArchive("lstat failed for '\(fullPath)': \(POSIXError(err))")
}
let mode = statInfo.st_mode
let uid = statInfo.st_uid
let gid = statInfo.st_gid
var size: Int64 = 0
let type: URLFileResourceType
if (mode & S_IFMT) == S_IFREG {
type = .regular
size = Int64(statInfo.st_size)
} else if (mode & S_IFMT) == S_IFDIR {
type = .directory
} else if (mode & S_IFMT) == S_IFLNK {
type = .symbolicLink
} else {
return
}
#if os(macOS)
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctimespec.tv_sec))
let access = Date(timeIntervalSince1970: Double(statInfo.st_atimespec.tv_sec))
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtimespec.tv_sec))
#else
let created = Date(timeIntervalSince1970: Double(statInfo.st_ctim.tv_sec))
let access = Date(timeIntervalSince1970: Double(statInfo.st_atim.tv_sec))
let modified = Date(timeIntervalSince1970: Double(statInfo.st_mtim.tv_sec))
#endif
let entry = WriteEntry()
if type == .symbolicLink {
let targetPath = try fm.destinationOfSymbolicLink(atPath: fullPath.string)
// Resolve the target relative to the symlink's parent, not the archive root.
let symlinkParent = fullPath.removingLastComponent()
let resolvedFull = symlinkParent.appending(targetPath).lexicallyNormalized()
guard resolvedFull.starts(with: dirPath) else {
return
}
entry.symlinkTarget = targetPath
}
entry.path = relativePath.string
entry.size = size
entry.creationDate = created
entry.modificationDate = modified
entry.contentAccessDate = access
entry.fileType = type
entry.group = gid
entry.owner = uid
entry.permissions = mode
if type == .regular {
let buf = UnsafeMutableRawBufferPointer.allocate(byteCount: Self.chunkSize, alignment: 1)
guard let baseAddress = buf.baseAddress else {
throw ArchiveError.failedToCreateArchive("cannot create temporary buffer of size \(Self.chunkSize)")
}
defer { buf.deallocate() }
let fd = Foundation.open(fullPath.string, O_RDONLY)
guard fd >= 0 else {
let err = POSIXErrorCode(rawValue: errno) ?? .EINVAL
throw ArchiveError.failedToCreateArchive("cannot open file \(fullPath.string) for reading: \(err)")
}
defer { close(fd) }
try self.writeHeader(entry: entry)
while true {
let n = read(fd, baseAddress, Self.chunkSize)
if n == 0 { break }
if n < 0 {
let err = POSIXErrorCode(rawValue: errno) ?? .EIO
throw ArchiveError.failedToCreateArchive("failed to read from file \(fullPath.string): \(err)")
}
try self.writeData(data: UnsafeRawBufferPointer(start: baseAddress, count: n))
}
try self.finishEntry()
} else {
try self.writeEntry(entry: entry, data: nil)
}
}
/// 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
let dirPath = FilePath(dir.path)
guard let enumerator = fm.enumerator(atPath: dirPath.string) else {
throw POSIXError(.ENOTDIR)
}
// Emit a leading "./" entry for the root directory, matching GNU/BSD tar behavior.
var rootStat = stat()
guard lstat(dirPath.string, &rootStat) == 0 else {
let err = POSIXErrorCode(rawValue: errno) ?? .EINVAL
throw ArchiveError.failedToCreateArchive("lstat failed for '\(dirPath)': \(POSIXError(err))")
}
let rootEntry = WriteEntry()
rootEntry.path = "./"
rootEntry.size = 0
rootEntry.fileType = .directory
rootEntry.owner = rootStat.st_uid
rootEntry.group = rootStat.st_gid
rootEntry.permissions = rootStat.st_mode
#if os(macOS)
rootEntry.creationDate = Date(timeIntervalSince1970: Double(rootStat.st_ctimespec.tv_sec))
rootEntry.contentAccessDate = Date(timeIntervalSince1970: Double(rootStat.st_atimespec.tv_sec))
rootEntry.modificationDate = Date(timeIntervalSince1970: Double(rootStat.st_mtimespec.tv_sec))
#else
rootEntry.creationDate = Date(timeIntervalSince1970: Double(rootStat.st_ctim.tv_sec))
rootEntry.contentAccessDate = Date(timeIntervalSince1970: Double(rootStat.st_atim.tv_sec))
rootEntry.modificationDate = Date(timeIntervalSince1970: Double(rootStat.st_mtim.tv_sec))
#endif
try self.writeHeader(entry: rootEntry)
for case let relativePath as String in enumerator {
try archive(FilePath(relativePath), dirPath: dirPath)
}
}
public func archive(_ paths: [FilePath], base: FilePath) throws {
let fm = FileManager.default
let base = base.lexicallyNormalized()
for path in paths {
guard path.starts(with: base) else {
throw ArchiveError.failedToCreateArchive("'\(path.string)' is not under '\(base.string)'")
}
let relativePath = path.components.dropFirst(base.components.count)
.reduce(into: FilePath("")) { $0.append($1) }
var isDir: ObjCBool = false
_ = fm.fileExists(atPath: path.string, isDirectory: &isDir)
if isDir.boolValue {
guard let enumerator = fm.enumerator(atPath: path.string) else {
throw POSIXError(.ENOTDIR)
}
try archive(relativePath, dirPath: base)
for case let child as String in enumerator {
let childPath = relativePath.appending(child)
try archive(childPath, dirPath: base)
}
} else {
try archive(relativePath, dirPath: base)
}
}
}
}
@@ -0,0 +1,200 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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
/// 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 {
public static let defaultLocales = ["en_US.UTF-8", "C.UTF-8"]
/// 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] = Self.defaultLocales
) {
self.format = format
self.filter = filter
self.options = options
self.locales = locales
}
}
extension ArchiveWriter {
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) }
}
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) }
}
internal func setOptions(_ options: [Options]) throws {
guard let underlying = self.underlying else { throw ArchiveError.noUnderlyingArchive }
try options.forEach {
switch $0 {
case .compressionLevel(let level):
try wrap(
archive_write_set_option(underlying, nil, "compression-level", "\(level)"),
ArchiveError.unableToSetOption, underlying: self.underlying)
case .compression(.store):
try wrap(
archive_write_set_option(underlying, nil, "compression", "store"), ArchiveError.unableToSetOption,
underlying: self.underlying)
case .compression(.deflate):
try wrap(
archive_write_set_option(underlying, nil, "compression", "deflate"), ArchiveError.unableToSetOption,
underlying: self.underlying)
case .xattrformat(let value):
let v = value.description
try wrap(
archive_write_set_option(underlying, nil, "xattrheader", v), ArchiveError.unableToSetOption,
underlying: self.underlying)
}
}
}
}
public enum Options {
case compressionLevel(UInt32)
case compression(Compression)
case xattrformat(XattrFormat)
public enum Compression {
case store
case deflate
}
public enum XattrFormat: String, CustomStringConvertible {
case schily
case libarchive
case all
public var description: String {
switch self {
case .libarchive:
return "LIBARCHIVE"
case .schily:
return "SCHILY"
case .all:
return "ALL"
}
}
}
}
/// An enumeration of the supported archive formats.
public enum Format: String, Sendable {
/// POSIX-standard `ustar` archives
case ustar
case gnutar
/// POSIX `pax interchange format` archives
case pax
case paxRestricted
/// POSIX octet-oriented cpio archives
case cpio
case cpioNewc
/// Zip archive
case zip
/// two different variants of shar archives
case shar
case sharDump
/// ISO9660 CD images
case iso9660
/// 7-Zip archives
case sevenZip
/// ar archives
case arBSD
case arGNU
/// mtree file tree descriptions
case mtree
/// XAR archives
case xar
internal var code: CInt {
switch self {
case .ustar: return ARCHIVE_FORMAT_TAR_USTAR
case .pax: return ARCHIVE_FORMAT_TAR_PAX_INTERCHANGE
case .paxRestricted: return ARCHIVE_FORMAT_TAR_PAX_RESTRICTED
case .gnutar: return ARCHIVE_FORMAT_TAR_GNUTAR
case .cpio: return ARCHIVE_FORMAT_CPIO_POSIX
case .cpioNewc: return ARCHIVE_FORMAT_CPIO_AFIO_LARGE
case .zip: return ARCHIVE_FORMAT_ZIP
case .shar: return ARCHIVE_FORMAT_SHAR_BASE
case .sharDump: return ARCHIVE_FORMAT_SHAR_DUMP
case .iso9660: return ARCHIVE_FORMAT_ISO9660
case .sevenZip: return ARCHIVE_FORMAT_7ZIP
case .arBSD: return ARCHIVE_FORMAT_AR_BSD
case .arGNU: return ARCHIVE_FORMAT_AR_GNU
case .mtree: return ARCHIVE_FORMAT_MTREE
case .xar: return ARCHIVE_FORMAT_XAR
}
}
}
/// An enumeration of the supported filters (compression / encoding standards) for an archive.
public enum Filter: String, Sendable {
case none
case gzip
case bzip2
case compress
case lzma
case xz
case uu
case rpm
case lzip
case lrzip
case lzop
case grzip
case lz4
case zstd
internal var code: CInt {
switch self {
case .none: return ARCHIVE_FILTER_NONE
case .gzip: return ARCHIVE_FILTER_GZIP
case .bzip2: return ARCHIVE_FILTER_BZIP2
case .compress: return ARCHIVE_FILTER_COMPRESS
case .lzma: return ARCHIVE_FILTER_LZMA
case .xz: return ARCHIVE_FILTER_XZ
case .uu: return ARCHIVE_FILTER_UU
case .rpm: return ARCHIVE_FILTER_RPM
case .lzip: return ARCHIVE_FILTER_LZIP
case .lrzip: return ARCHIVE_FILTER_LRZIP
case .lzop: return ARCHIVE_FILTER_LZOP
case .grzip: return ARCHIVE_FILTER_GRZIP
case .lz4: return ARCHIVE_FILTER_LZ4
case .zstd: return ARCHIVE_FILTER_ZSTD
}
}
}
@@ -0,0 +1,65 @@
The libarchive distribution as a whole is Copyright by Tim Kientzle
and is subject to the copyright notice reproduced at the bottom of
this file.
Each individual file in this distribution should have a clear
copyright/licensing statement at the beginning of the file. If any do
not, please let me know and I will rectify it. The following is
intended to summarize the copyright status of the individual files;
the actual statements in the files are controlling.
* Except as listed below, all C sources (including .c and .h files)
and documentation files are subject to the copyright notice reproduced
at the bottom of this file.
* The following source files are also subject in whole or in part to
a 3-clause UC Regents copyright; please read the individual source
files for details:
libarchive/archive_read_support_filter_compress.c
libarchive/archive_write_add_filter_compress.c
libarchive/mtree.5
* The following source files are in the public domain:
libarchive/archive_getdate.c
* The following source files are triple-licensed with the ability to choose
from CC0 1.0 Universal, OpenSSL or Apache 2.0 licenses:
libarchive/archive_blake2.h
libarchive/archive_blake2_impl.h
libarchive/archive_blake2s_ref.c
libarchive/archive_blake2sp_ref.c
* The build files---including Makefiles, configure scripts,
and auxiliary scripts used as part of the compile process---have
widely varying licensing terms. Please check individual files before
distributing them to see if those restrictions apply to you.
I intend for all new source code to use the license below and hope over
time to replace code with other licenses with new implementations that
do use the license below. The varying licensing of the build scripts
seems to be an unavoidable mess.
Copyright (c) 2003-2018 <author(s)>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer
in this position and unchanged.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,68 @@
/*
* Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
*
* 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.
*/
#include "archive_bridge.h"
#include <zstd.h>
#include <stdlib.h>
#include <unistd.h>
void archive_set_error_wrapper(struct archive *a, int error_number, const char *error_string) {
archive_set_error(a, error_number, "%s", error_string);
}
int zstd_decompress_fd(int src_fd, int dst_fd) {
ZSTD_DStream *dstream = ZSTD_createDStream();
if (!dstream) return 1;
size_t init_result = ZSTD_initDStream(dstream);
if (ZSTD_isError(init_result)) {
ZSTD_freeDStream(dstream);
return 1;
}
size_t in_size = ZSTD_DStreamInSize();
size_t out_size = ZSTD_DStreamOutSize();
void *in_buf = malloc(in_size);
void *out_buf = malloc(out_size);
if (!in_buf || !out_buf) {
free(in_buf);
free(out_buf);
ZSTD_freeDStream(dstream);
return 1;
}
int rc = 0;
ssize_t bytes_read;
while ((bytes_read = read(src_fd, in_buf, in_size)) > 0) {
ZSTD_inBuffer input = { in_buf, (size_t)bytes_read, 0 };
while (input.pos < input.size) {
ZSTD_outBuffer output = { out_buf, out_size, 0 };
size_t result = ZSTD_decompressStream(dstream, &output, &input);
if (ZSTD_isError(result)) { rc = 1; goto done; }
if (output.pos > 0) {
ssize_t written = write(dst_fd, out_buf, output.pos);
if (written != (ssize_t)output.pos) { rc = 1; goto done; }
}
}
}
if (bytes_read < 0) rc = 1;
done:
free(in_buf);
free(out_buf);
ZSTD_freeDStream(dstream);
return rc;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
//
#pragma once
#include "archive.h"
#include <stdint.h>
void archive_set_error_wrapper(struct archive *a, int error_number, const char *error_string);
/// Decompress a zstd-compressed file at \p src_fd into \p dst_fd.
/// Returns 0 on success, or a non-zero error code on failure.
int zstd_decompress_fd(int src_fd, int dst_fd);
@@ -0,0 +1,731 @@
/*-
* Copyright (c) 2003-2008 Tim Kientzle
* Copyright (c) 2016 Martin Matuska
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef ARCHIVE_ENTRY_H_INCLUDED
#define ARCHIVE_ENTRY_H_INCLUDED
/* Note: Compiler will complain if this does not match archive.h! */
#define ARCHIVE_VERSION_NUMBER 3007007
/*
* Note: archive_entry.h is for use outside of libarchive; the
* configuration headers (config.h, archive_platform.h, etc.) are
* purely internal. Do NOT use HAVE_XXX configuration macros to
* control the behavior of this header! If you must conditionalize,
* use predefined compiler and/or platform macros.
*/
#include <sys/types.h>
#include <stddef.h> /* for wchar_t */
#include <stdint.h>
#include <time.h>
#if defined(_WIN32) && !defined(__CYGWIN__)
#include <windows.h>
#endif
/* Get a suitable 64-bit integer type. */
#if !defined(__LA_INT64_T_DEFINED)
# if ARCHIVE_VERSION_NUMBER < 4000000
#define __LA_INT64_T la_int64_t
# endif
#define __LA_INT64_T_DEFINED
# if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__WATCOMC__)
typedef __int64 la_int64_t;
# else
#include <unistd.h>
# if defined(_SCO_DS) || defined(__osf__)
typedef long long la_int64_t;
# else
typedef int64_t la_int64_t;
# endif
# endif
#endif
/* The la_ssize_t should match the type used in 'struct stat' */
#if !defined(__LA_SSIZE_T_DEFINED)
/* Older code relied on the __LA_SSIZE_T macro; after 4.0 we'll switch to the typedef exclusively. */
# if ARCHIVE_VERSION_NUMBER < 4000000
#define __LA_SSIZE_T la_ssize_t
# endif
#define __LA_SSIZE_T_DEFINED
# if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__WATCOMC__)
# if defined(_SSIZE_T_DEFINED) || defined(_SSIZE_T_)
typedef ssize_t la_ssize_t;
# elif defined(_WIN64)
typedef __int64 la_ssize_t;
# else
typedef long la_ssize_t;
# endif
# else
# include <unistd.h> /* ssize_t */
typedef ssize_t la_ssize_t;
# endif
#endif
/* Get a suitable definition for mode_t */
#if ARCHIVE_VERSION_NUMBER >= 3999000
/* Switch to plain 'int' for libarchive 4.0. It's less broken than 'mode_t' */
# define __LA_MODE_T int
#elif defined(_WIN32) && !defined(__CYGWIN__) && !defined(__BORLANDC__) && !defined(__WATCOMC__)
# define __LA_MODE_T unsigned short
#else
# define __LA_MODE_T mode_t
#endif
/* Large file support for Android */
#if defined(__LIBARCHIVE_BUILD) && defined(__ANDROID__)
#include "android_lf.h"
#endif
/*
* On Windows, define LIBARCHIVE_STATIC if you're building or using a
* .lib. The default here assumes you're building a DLL. Only
* libarchive source should ever define __LIBARCHIVE_BUILD.
*/
#if ((defined __WIN32__) || (defined _WIN32) || defined(__CYGWIN__)) && (!defined LIBARCHIVE_STATIC)
# ifdef __LIBARCHIVE_BUILD
# ifdef __GNUC__
# define __LA_DECL __attribute__((dllexport)) extern
# else
# define __LA_DECL __declspec(dllexport)
# endif
# else
# ifdef __GNUC__
# define __LA_DECL
# else
# define __LA_DECL __declspec(dllimport)
# endif
# endif
#elif defined __LIBARCHIVE_ENABLE_VISIBILITY
# define __LA_DECL __attribute__((visibility("default")))
#else
/* Static libraries on all platforms and shared libraries on non-Windows. */
# define __LA_DECL
#endif
#if defined(__GNUC__) && __GNUC__ >= 3 && __GNUC_MINOR__ >= 1
# define __LA_DEPRECATED __attribute__((deprecated))
#else
# define __LA_DEPRECATED
#endif
#ifdef __cplusplus
extern "C" {
#endif
/*
* Description of an archive entry.
*
* You can think of this as "struct stat" with some text fields added in.
*
* TODO: Add "comment", "charset", and possibly other entries that are
* supported by "pax interchange" format. However, GNU, ustar, cpio,
* and other variants don't support these features, so they're not an
* excruciatingly high priority right now.
*
* TODO: "pax interchange" format allows essentially arbitrary
* key/value attributes to be attached to any entry. Supporting
* such extensions may make this library useful for special
* applications (e.g., a package manager could attach special
* package-management attributes to each entry).
*/
struct archive;
struct archive_entry;
/*
* File-type constants. These are returned from archive_entry_filetype()
* and passed to archive_entry_set_filetype().
*
* These values match S_XXX defines on every platform I've checked,
* including Windows, AIX, Linux, Solaris, and BSD. They're
* (re)defined here because platforms generally don't define the ones
* they don't support. For example, Windows doesn't define S_IFLNK or
* S_IFBLK. Instead of having a mass of conditional logic and system
* checks to define any S_XXX values that aren't supported locally,
* I've just defined a new set of such constants so that
* libarchive-based applications can manipulate and identify archive
* entries properly even if the hosting platform can't store them on
* disk.
*
* These values are also used directly within some portable formats,
* such as cpio. If you find a platform that varies from these, the
* correct solution is to leave these alone and translate from these
* portable values to platform-native values when entries are read from
* or written to disk.
*/
/*
* In libarchive 4.0, we can drop the casts here.
* They're needed to work around Borland C's broken mode_t.
*/
#define AE_IFMT ((__LA_MODE_T)0170000)
#define AE_IFREG ((__LA_MODE_T)0100000)
#define AE_IFLNK ((__LA_MODE_T)0120000)
#define AE_IFSOCK ((__LA_MODE_T)0140000)
#define AE_IFCHR ((__LA_MODE_T)0020000)
#define AE_IFBLK ((__LA_MODE_T)0060000)
#define AE_IFDIR ((__LA_MODE_T)0040000)
#define AE_IFIFO ((__LA_MODE_T)0010000)
/*
* Symlink types
*/
#define AE_SYMLINK_TYPE_UNDEFINED 0
#define AE_SYMLINK_TYPE_FILE 1
#define AE_SYMLINK_TYPE_DIRECTORY 2
/*
* Basic object manipulation
*/
__LA_DECL struct archive_entry *archive_entry_clear(struct archive_entry *);
/* The 'clone' function does a deep copy; all of the strings are copied too. */
__LA_DECL struct archive_entry *archive_entry_clone(struct archive_entry *);
__LA_DECL void archive_entry_free(struct archive_entry *);
__LA_DECL struct archive_entry *archive_entry_new(void);
/*
* This form of archive_entry_new2() will pull character-set
* conversion information from the specified archive handle. The
* older archive_entry_new(void) form is equivalent to calling
* archive_entry_new2(NULL) and will result in the use of an internal
* default character-set conversion.
*/
__LA_DECL struct archive_entry *archive_entry_new2(struct archive *);
/*
* Retrieve fields from an archive_entry.
*
* There are a number of implicit conversions among these fields. For
* example, if a regular string field is set and you read the _w wide
* character field, the entry will implicitly convert narrow-to-wide
* using the current locale. Similarly, dev values are automatically
* updated when you write devmajor or devminor and vice versa.
*
* In addition, fields can be "set" or "unset." Unset string fields
* return NULL, non-string fields have _is_set() functions to test
* whether they've been set. You can "unset" a string field by
* assigning NULL; non-string fields have _unset() functions to
* unset them.
*
* Note: There is one ambiguity in the above; string fields will
* also return NULL when implicit character set conversions fail.
* This is usually what you want.
*/
__LA_DECL time_t archive_entry_atime(struct archive_entry *);
__LA_DECL long archive_entry_atime_nsec(struct archive_entry *);
__LA_DECL int archive_entry_atime_is_set(struct archive_entry *);
__LA_DECL time_t archive_entry_birthtime(struct archive_entry *);
__LA_DECL long archive_entry_birthtime_nsec(struct archive_entry *);
__LA_DECL int archive_entry_birthtime_is_set(struct archive_entry *);
__LA_DECL time_t archive_entry_ctime(struct archive_entry *);
__LA_DECL long archive_entry_ctime_nsec(struct archive_entry *);
__LA_DECL int archive_entry_ctime_is_set(struct archive_entry *);
__LA_DECL dev_t archive_entry_dev(struct archive_entry *);
__LA_DECL int archive_entry_dev_is_set(struct archive_entry *);
__LA_DECL dev_t archive_entry_devmajor(struct archive_entry *);
__LA_DECL dev_t archive_entry_devminor(struct archive_entry *);
__LA_DECL __LA_MODE_T archive_entry_filetype(struct archive_entry *);
__LA_DECL int archive_entry_filetype_is_set(struct archive_entry *);
__LA_DECL void archive_entry_fflags(struct archive_entry *,
unsigned long * /* set */,
unsigned long * /* clear */);
__LA_DECL const char *archive_entry_fflags_text(struct archive_entry *);
__LA_DECL la_int64_t archive_entry_gid(struct archive_entry *);
__LA_DECL int archive_entry_gid_is_set(struct archive_entry *);
__LA_DECL const char *archive_entry_gname(struct archive_entry *);
__LA_DECL const char *archive_entry_gname_utf8(struct archive_entry *);
__LA_DECL const wchar_t *archive_entry_gname_w(struct archive_entry *);
__LA_DECL void archive_entry_set_link_to_hardlink(struct archive_entry *);
__LA_DECL const char *archive_entry_hardlink(struct archive_entry *);
__LA_DECL const char *archive_entry_hardlink_utf8(struct archive_entry *);
__LA_DECL const wchar_t *archive_entry_hardlink_w(struct archive_entry *);
__LA_DECL int archive_entry_hardlink_is_set(struct archive_entry *);
__LA_DECL la_int64_t archive_entry_ino(struct archive_entry *);
__LA_DECL la_int64_t archive_entry_ino64(struct archive_entry *);
__LA_DECL int archive_entry_ino_is_set(struct archive_entry *);
__LA_DECL __LA_MODE_T archive_entry_mode(struct archive_entry *);
__LA_DECL time_t archive_entry_mtime(struct archive_entry *);
__LA_DECL long archive_entry_mtime_nsec(struct archive_entry *);
__LA_DECL int archive_entry_mtime_is_set(struct archive_entry *);
__LA_DECL unsigned int archive_entry_nlink(struct archive_entry *);
__LA_DECL const char *archive_entry_pathname(struct archive_entry *);
__LA_DECL const char *archive_entry_pathname_utf8(struct archive_entry *);
__LA_DECL const wchar_t *archive_entry_pathname_w(struct archive_entry *);
__LA_DECL __LA_MODE_T archive_entry_perm(struct archive_entry *);
__LA_DECL int archive_entry_perm_is_set(struct archive_entry *);
__LA_DECL int archive_entry_rdev_is_set(struct archive_entry *);
__LA_DECL dev_t archive_entry_rdev(struct archive_entry *);
__LA_DECL dev_t archive_entry_rdevmajor(struct archive_entry *);
__LA_DECL dev_t archive_entry_rdevminor(struct archive_entry *);
__LA_DECL const char *archive_entry_sourcepath(struct archive_entry *);
__LA_DECL const wchar_t *archive_entry_sourcepath_w(struct archive_entry *);
__LA_DECL la_int64_t archive_entry_size(struct archive_entry *);
__LA_DECL int archive_entry_size_is_set(struct archive_entry *);
__LA_DECL const char *archive_entry_strmode(struct archive_entry *);
__LA_DECL void archive_entry_set_link_to_symlink(struct archive_entry *);
__LA_DECL const char *archive_entry_symlink(struct archive_entry *);
__LA_DECL const char *archive_entry_symlink_utf8(struct archive_entry *);
__LA_DECL int archive_entry_symlink_type(struct archive_entry *);
__LA_DECL const wchar_t *archive_entry_symlink_w(struct archive_entry *);
__LA_DECL la_int64_t archive_entry_uid(struct archive_entry *);
__LA_DECL int archive_entry_uid_is_set(struct archive_entry *);
__LA_DECL const char *archive_entry_uname(struct archive_entry *);
__LA_DECL const char *archive_entry_uname_utf8(struct archive_entry *);
__LA_DECL const wchar_t *archive_entry_uname_w(struct archive_entry *);
__LA_DECL int archive_entry_is_data_encrypted(struct archive_entry *);
__LA_DECL int archive_entry_is_metadata_encrypted(struct archive_entry *);
__LA_DECL int archive_entry_is_encrypted(struct archive_entry *);
/*
* Set fields in an archive_entry.
*
* Note: Before libarchive 2.4, there were 'set' and 'copy' versions
* of the string setters. 'copy' copied the actual string, 'set' just
* stored the pointer. In libarchive 2.4 and later, strings are
* always copied.
*/
__LA_DECL void archive_entry_set_atime(struct archive_entry *, time_t, long);
__LA_DECL void archive_entry_unset_atime(struct archive_entry *);
#if defined(_WIN32) && !defined(__CYGWIN__)
__LA_DECL void archive_entry_copy_bhfi(struct archive_entry *, BY_HANDLE_FILE_INFORMATION *);
#endif
__LA_DECL void archive_entry_set_birthtime(struct archive_entry *, time_t, long);
__LA_DECL void archive_entry_unset_birthtime(struct archive_entry *);
__LA_DECL void archive_entry_set_ctime(struct archive_entry *, time_t, long);
__LA_DECL void archive_entry_unset_ctime(struct archive_entry *);
__LA_DECL void archive_entry_set_dev(struct archive_entry *, dev_t);
__LA_DECL void archive_entry_set_devmajor(struct archive_entry *, dev_t);
__LA_DECL void archive_entry_set_devminor(struct archive_entry *, dev_t);
__LA_DECL void archive_entry_set_filetype(struct archive_entry *, unsigned int);
__LA_DECL void archive_entry_set_fflags(struct archive_entry *,
unsigned long /* set */, unsigned long /* clear */);
/* Returns pointer to start of first invalid token, or NULL if none. */
/* Note that all recognized tokens are processed, regardless. */
__LA_DECL const char *archive_entry_copy_fflags_text(struct archive_entry *,
const char *);
__LA_DECL const char *archive_entry_copy_fflags_text_len(struct archive_entry *,
const char *, size_t);
__LA_DECL const wchar_t *archive_entry_copy_fflags_text_w(struct archive_entry *,
const wchar_t *);
__LA_DECL void archive_entry_set_gid(struct archive_entry *, la_int64_t);
__LA_DECL void archive_entry_set_gname(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_gname_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_gname(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_gname_w(struct archive_entry *, const wchar_t *);
__LA_DECL int archive_entry_update_gname_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_hardlink(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_hardlink_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_hardlink(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_hardlink_w(struct archive_entry *, const wchar_t *);
__LA_DECL int archive_entry_update_hardlink_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_ino(struct archive_entry *, la_int64_t);
__LA_DECL void archive_entry_set_ino64(struct archive_entry *, la_int64_t);
__LA_DECL void archive_entry_set_link(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_link_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_link(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_link_w(struct archive_entry *, const wchar_t *);
__LA_DECL int archive_entry_update_link_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_mode(struct archive_entry *, __LA_MODE_T);
__LA_DECL void archive_entry_set_mtime(struct archive_entry *, time_t, long);
__LA_DECL void archive_entry_unset_mtime(struct archive_entry *);
__LA_DECL void archive_entry_set_nlink(struct archive_entry *, unsigned int);
__LA_DECL void archive_entry_set_pathname(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_pathname_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_pathname(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_pathname_w(struct archive_entry *, const wchar_t *);
__LA_DECL int archive_entry_update_pathname_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_perm(struct archive_entry *, __LA_MODE_T);
__LA_DECL void archive_entry_set_rdev(struct archive_entry *, dev_t);
__LA_DECL void archive_entry_set_rdevmajor(struct archive_entry *, dev_t);
__LA_DECL void archive_entry_set_rdevminor(struct archive_entry *, dev_t);
__LA_DECL void archive_entry_set_size(struct archive_entry *, la_int64_t);
__LA_DECL void archive_entry_unset_size(struct archive_entry *);
__LA_DECL void archive_entry_copy_sourcepath(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_sourcepath_w(struct archive_entry *, const wchar_t *);
__LA_DECL void archive_entry_set_symlink(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_symlink_type(struct archive_entry *, int);
__LA_DECL void archive_entry_set_symlink_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_symlink(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_symlink_w(struct archive_entry *, const wchar_t *);
__LA_DECL int archive_entry_update_symlink_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_uid(struct archive_entry *, la_int64_t);
__LA_DECL void archive_entry_set_uname(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_uname_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_uname(struct archive_entry *, const char *);
__LA_DECL void archive_entry_copy_uname_w(struct archive_entry *, const wchar_t *);
__LA_DECL int archive_entry_update_uname_utf8(struct archive_entry *, const char *);
__LA_DECL void archive_entry_set_is_data_encrypted(struct archive_entry *, char is_encrypted);
__LA_DECL void archive_entry_set_is_metadata_encrypted(struct archive_entry *, char is_encrypted);
/*
* Routines to bulk copy fields to/from a platform-native "struct
* stat." Libarchive used to just store a struct stat inside of each
* archive_entry object, but this created issues when trying to
* manipulate archives on systems different than the ones they were
* created on.
*
* TODO: On Linux and other LFS systems, provide both stat32 and
* stat64 versions of these functions and all of the macro glue so
* that archive_entry_stat is magically defined to
* archive_entry_stat32 or archive_entry_stat64 as appropriate.
*/
__LA_DECL const struct stat *archive_entry_stat(struct archive_entry *);
__LA_DECL void archive_entry_copy_stat(struct archive_entry *, const struct stat *);
/*
* Storage for Mac OS-specific AppleDouble metadata information.
* Apple-format tar files store a separate binary blob containing
* encoded metadata with ACL, extended attributes, etc.
* This provides a place to store that blob.
*/
__LA_DECL const void * archive_entry_mac_metadata(struct archive_entry *, size_t *);
__LA_DECL void archive_entry_copy_mac_metadata(struct archive_entry *, const void *, size_t);
/*
* Digest routine. This is used to query the raw hex digest for the
* given entry. The type of digest is provided as an argument.
*/
#define ARCHIVE_ENTRY_DIGEST_MD5 0x00000001
#define ARCHIVE_ENTRY_DIGEST_RMD160 0x00000002
#define ARCHIVE_ENTRY_DIGEST_SHA1 0x00000003
#define ARCHIVE_ENTRY_DIGEST_SHA256 0x00000004
#define ARCHIVE_ENTRY_DIGEST_SHA384 0x00000005
#define ARCHIVE_ENTRY_DIGEST_SHA512 0x00000006
__LA_DECL const unsigned char * archive_entry_digest(struct archive_entry *, int /* type */);
/*
* ACL routines. This used to simply store and return text-format ACL
* strings, but that proved insufficient for a number of reasons:
* = clients need control over uname/uid and gname/gid mappings
* = there are many different ACL text formats
* = would like to be able to read/convert archives containing ACLs
* on platforms that lack ACL libraries
*
* This last point, in particular, forces me to implement a reasonably
* complete set of ACL support routines.
*/
/*
* Permission bits.
*/
#define ARCHIVE_ENTRY_ACL_EXECUTE 0x00000001
#define ARCHIVE_ENTRY_ACL_WRITE 0x00000002
#define ARCHIVE_ENTRY_ACL_READ 0x00000004
#define ARCHIVE_ENTRY_ACL_READ_DATA 0x00000008
#define ARCHIVE_ENTRY_ACL_LIST_DIRECTORY 0x00000008
#define ARCHIVE_ENTRY_ACL_WRITE_DATA 0x00000010
#define ARCHIVE_ENTRY_ACL_ADD_FILE 0x00000010
#define ARCHIVE_ENTRY_ACL_APPEND_DATA 0x00000020
#define ARCHIVE_ENTRY_ACL_ADD_SUBDIRECTORY 0x00000020
#define ARCHIVE_ENTRY_ACL_READ_NAMED_ATTRS 0x00000040
#define ARCHIVE_ENTRY_ACL_WRITE_NAMED_ATTRS 0x00000080
#define ARCHIVE_ENTRY_ACL_DELETE_CHILD 0x00000100
#define ARCHIVE_ENTRY_ACL_READ_ATTRIBUTES 0x00000200
#define ARCHIVE_ENTRY_ACL_WRITE_ATTRIBUTES 0x00000400
#define ARCHIVE_ENTRY_ACL_DELETE 0x00000800
#define ARCHIVE_ENTRY_ACL_READ_ACL 0x00001000
#define ARCHIVE_ENTRY_ACL_WRITE_ACL 0x00002000
#define ARCHIVE_ENTRY_ACL_WRITE_OWNER 0x00004000
#define ARCHIVE_ENTRY_ACL_SYNCHRONIZE 0x00008000
#define ARCHIVE_ENTRY_ACL_PERMS_POSIX1E \
(ARCHIVE_ENTRY_ACL_EXECUTE \
| ARCHIVE_ENTRY_ACL_WRITE \
| ARCHIVE_ENTRY_ACL_READ)
#define ARCHIVE_ENTRY_ACL_PERMS_NFS4 \
(ARCHIVE_ENTRY_ACL_EXECUTE \
| ARCHIVE_ENTRY_ACL_READ_DATA \
| ARCHIVE_ENTRY_ACL_LIST_DIRECTORY \
| ARCHIVE_ENTRY_ACL_WRITE_DATA \
| ARCHIVE_ENTRY_ACL_ADD_FILE \
| ARCHIVE_ENTRY_ACL_APPEND_DATA \
| ARCHIVE_ENTRY_ACL_ADD_SUBDIRECTORY \
| ARCHIVE_ENTRY_ACL_READ_NAMED_ATTRS \
| ARCHIVE_ENTRY_ACL_WRITE_NAMED_ATTRS \
| ARCHIVE_ENTRY_ACL_DELETE_CHILD \
| ARCHIVE_ENTRY_ACL_READ_ATTRIBUTES \
| ARCHIVE_ENTRY_ACL_WRITE_ATTRIBUTES \
| ARCHIVE_ENTRY_ACL_DELETE \
| ARCHIVE_ENTRY_ACL_READ_ACL \
| ARCHIVE_ENTRY_ACL_WRITE_ACL \
| ARCHIVE_ENTRY_ACL_WRITE_OWNER \
| ARCHIVE_ENTRY_ACL_SYNCHRONIZE)
/*
* Inheritance values (NFS4 ACLs only); included in permset.
*/
#define ARCHIVE_ENTRY_ACL_ENTRY_INHERITED 0x01000000
#define ARCHIVE_ENTRY_ACL_ENTRY_FILE_INHERIT 0x02000000
#define ARCHIVE_ENTRY_ACL_ENTRY_DIRECTORY_INHERIT 0x04000000
#define ARCHIVE_ENTRY_ACL_ENTRY_NO_PROPAGATE_INHERIT 0x08000000
#define ARCHIVE_ENTRY_ACL_ENTRY_INHERIT_ONLY 0x10000000
#define ARCHIVE_ENTRY_ACL_ENTRY_SUCCESSFUL_ACCESS 0x20000000
#define ARCHIVE_ENTRY_ACL_ENTRY_FAILED_ACCESS 0x40000000
#define ARCHIVE_ENTRY_ACL_INHERITANCE_NFS4 \
(ARCHIVE_ENTRY_ACL_ENTRY_FILE_INHERIT \
| ARCHIVE_ENTRY_ACL_ENTRY_DIRECTORY_INHERIT \
| ARCHIVE_ENTRY_ACL_ENTRY_NO_PROPAGATE_INHERIT \
| ARCHIVE_ENTRY_ACL_ENTRY_INHERIT_ONLY \
| ARCHIVE_ENTRY_ACL_ENTRY_SUCCESSFUL_ACCESS \
| ARCHIVE_ENTRY_ACL_ENTRY_FAILED_ACCESS \
| ARCHIVE_ENTRY_ACL_ENTRY_INHERITED)
/* We need to be able to specify combinations of these. */
#define ARCHIVE_ENTRY_ACL_TYPE_ACCESS 0x00000100 /* POSIX.1e only */
#define ARCHIVE_ENTRY_ACL_TYPE_DEFAULT 0x00000200 /* POSIX.1e only */
#define ARCHIVE_ENTRY_ACL_TYPE_ALLOW 0x00000400 /* NFS4 only */
#define ARCHIVE_ENTRY_ACL_TYPE_DENY 0x00000800 /* NFS4 only */
#define ARCHIVE_ENTRY_ACL_TYPE_AUDIT 0x00001000 /* NFS4 only */
#define ARCHIVE_ENTRY_ACL_TYPE_ALARM 0x00002000 /* NFS4 only */
#define ARCHIVE_ENTRY_ACL_TYPE_POSIX1E (ARCHIVE_ENTRY_ACL_TYPE_ACCESS \
| ARCHIVE_ENTRY_ACL_TYPE_DEFAULT)
#define ARCHIVE_ENTRY_ACL_TYPE_NFS4 (ARCHIVE_ENTRY_ACL_TYPE_ALLOW \
| ARCHIVE_ENTRY_ACL_TYPE_DENY \
| ARCHIVE_ENTRY_ACL_TYPE_AUDIT \
| ARCHIVE_ENTRY_ACL_TYPE_ALARM)
/* Tag values mimic POSIX.1e */
#define ARCHIVE_ENTRY_ACL_USER 10001 /* Specified user. */
#define ARCHIVE_ENTRY_ACL_USER_OBJ 10002 /* User who owns the file. */
#define ARCHIVE_ENTRY_ACL_GROUP 10003 /* Specified group. */
#define ARCHIVE_ENTRY_ACL_GROUP_OBJ 10004 /* Group who owns the file. */
#define ARCHIVE_ENTRY_ACL_MASK 10005 /* Modify group access (POSIX.1e only) */
#define ARCHIVE_ENTRY_ACL_OTHER 10006 /* Public (POSIX.1e only) */
#define ARCHIVE_ENTRY_ACL_EVERYONE 10107 /* Everyone (NFS4 only) */
/*
* Set the ACL by clearing it and adding entries one at a time.
* Unlike the POSIX.1e ACL routines, you must specify the type
* (access/default) for each entry. Internally, the ACL data is just
* a soup of entries. API calls here allow you to retrieve just the
* entries of interest. This design (which goes against the spirit of
* POSIX.1e) is useful for handling archive formats that combine
* default and access information in a single ACL list.
*/
__LA_DECL void archive_entry_acl_clear(struct archive_entry *);
__LA_DECL int archive_entry_acl_add_entry(struct archive_entry *,
int /* type */, int /* permset */, int /* tag */,
int /* qual */, const char * /* name */);
__LA_DECL int archive_entry_acl_add_entry_w(struct archive_entry *,
int /* type */, int /* permset */, int /* tag */,
int /* qual */, const wchar_t * /* name */);
/*
* To retrieve the ACL, first "reset", then repeatedly ask for the
* "next" entry. The want_type parameter allows you to request only
* certain types of entries.
*/
__LA_DECL int archive_entry_acl_reset(struct archive_entry *, int /* want_type */);
__LA_DECL int archive_entry_acl_next(struct archive_entry *, int /* want_type */,
int * /* type */, int * /* permset */, int * /* tag */,
int * /* qual */, const char ** /* name */);
/*
* Construct a text-format ACL. The flags argument is a bitmask that
* can include any of the following:
*
* Flags only for archive entries with POSIX.1e ACL:
* ARCHIVE_ENTRY_ACL_TYPE_ACCESS - Include POSIX.1e "access" entries.
* ARCHIVE_ENTRY_ACL_TYPE_DEFAULT - Include POSIX.1e "default" entries.
* ARCHIVE_ENTRY_ACL_STYLE_MARK_DEFAULT - Include "default:" before each
* default ACL entry.
* ARCHIVE_ENTRY_ACL_STYLE_SOLARIS - Output only one colon after "other" and
* "mask" entries.
*
* Flags only for archive entries with NFSv4 ACL:
* ARCHIVE_ENTRY_ACL_STYLE_COMPACT - Do not output the minus character for
* unset permissions and flags in NFSv4 ACL permission and flag fields
*
* Flags for for archive entries with POSIX.1e ACL or NFSv4 ACL:
* ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID - Include extra numeric ID field in
* each ACL entry.
* ARCHIVE_ENTRY_ACL_STYLE_SEPARATOR_COMMA - Separate entries with comma
* instead of newline.
*/
#define ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID 0x00000001
#define ARCHIVE_ENTRY_ACL_STYLE_MARK_DEFAULT 0x00000002
#define ARCHIVE_ENTRY_ACL_STYLE_SOLARIS 0x00000004
#define ARCHIVE_ENTRY_ACL_STYLE_SEPARATOR_COMMA 0x00000008
#define ARCHIVE_ENTRY_ACL_STYLE_COMPACT 0x00000010
__LA_DECL wchar_t *archive_entry_acl_to_text_w(struct archive_entry *,
la_ssize_t * /* len */, int /* flags */);
__LA_DECL char *archive_entry_acl_to_text(struct archive_entry *,
la_ssize_t * /* len */, int /* flags */);
__LA_DECL int archive_entry_acl_from_text_w(struct archive_entry *,
const wchar_t * /* wtext */, int /* type */);
__LA_DECL int archive_entry_acl_from_text(struct archive_entry *,
const char * /* text */, int /* type */);
/* Deprecated constants */
#define OLD_ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID 1024
#define OLD_ARCHIVE_ENTRY_ACL_STYLE_MARK_DEFAULT 2048
/* Deprecated functions */
__LA_DECL const wchar_t *archive_entry_acl_text_w(struct archive_entry *,
int /* flags */) __LA_DEPRECATED;
__LA_DECL const char *archive_entry_acl_text(struct archive_entry *,
int /* flags */) __LA_DEPRECATED;
/* Return bitmask of ACL types in an archive entry */
__LA_DECL int archive_entry_acl_types(struct archive_entry *);
/* Return a count of entries matching 'want_type' */
__LA_DECL int archive_entry_acl_count(struct archive_entry *, int /* want_type */);
/* Return an opaque ACL object. */
/* There's not yet anything clients can actually do with this... */
struct archive_acl;
__LA_DECL struct archive_acl *archive_entry_acl(struct archive_entry *);
/*
* extended attributes
*/
__LA_DECL void archive_entry_xattr_clear(struct archive_entry *);
__LA_DECL void archive_entry_xattr_add_entry(struct archive_entry *,
const char * /* name */, const void * /* value */,
size_t /* size */);
/*
* To retrieve the xattr list, first "reset", then repeatedly ask for the
* "next" entry.
*/
__LA_DECL int archive_entry_xattr_count(struct archive_entry *);
__LA_DECL int archive_entry_xattr_reset(struct archive_entry *);
__LA_DECL int archive_entry_xattr_next(struct archive_entry *,
const char ** /* name */, const void ** /* value */, size_t *);
/*
* sparse
*/
__LA_DECL void archive_entry_sparse_clear(struct archive_entry *);
__LA_DECL void archive_entry_sparse_add_entry(struct archive_entry *,
la_int64_t /* offset */, la_int64_t /* length */);
/*
* To retrieve the xattr list, first "reset", then repeatedly ask for the
* "next" entry.
*/
__LA_DECL int archive_entry_sparse_count(struct archive_entry *);
__LA_DECL int archive_entry_sparse_reset(struct archive_entry *);
__LA_DECL int archive_entry_sparse_next(struct archive_entry *,
la_int64_t * /* offset */, la_int64_t * /* length */);
/*
* Utility to match up hardlinks.
*
* The 'struct archive_entry_linkresolver' is a cache of archive entries
* for files with multiple links. Here's how to use it:
* 1. Create a lookup object with archive_entry_linkresolver_new()
* 2. Tell it the archive format you're using.
* 3. Hand each archive_entry to archive_entry_linkify().
* That function will return 0, 1, or 2 entries that should
* be written.
* 4. Call archive_entry_linkify(resolver, NULL) until
* no more entries are returned.
* 5. Call archive_entry_linkresolver_free(resolver) to free resources.
*
* The entries returned have their hardlink and size fields updated
* appropriately. If an entry is passed in that does not refer to
* a file with multiple links, it is returned unchanged. The intention
* is that you should be able to simply filter all entries through
* this machine.
*
* To make things more efficient, be sure that each entry has a valid
* nlinks value. The hardlink cache uses this to track when all links
* have been found. If the nlinks value is zero, it will keep every
* name in the cache indefinitely, which can use a lot of memory.
*
* Note that archive_entry_size() is reset to zero if the file
* body should not be written to the archive. Pay attention!
*/
struct archive_entry_linkresolver;
/*
* There are three different strategies for marking hardlinks.
* The descriptions below name them after the best-known
* formats that rely on each strategy:
*
* "Old cpio" is the simplest, it always returns any entry unmodified.
* As far as I know, only cpio formats use this. Old cpio archives
* store every link with the full body; the onus is on the dearchiver
* to detect and properly link the files as they are restored.
* "tar" is also pretty simple; it caches a copy the first time it sees
* any link. Subsequent appearances are modified to be hardlink
* references to the first one without any body. Used by all tar
* formats, although the newest tar formats permit the "old cpio" strategy
* as well. This strategy is very simple for the dearchiver,
* and reasonably straightforward for the archiver.
* "new cpio" is trickier. It stores the body only with the last
* occurrence. The complication is that we might not
* see every link to a particular file in a single session, so
* there's no easy way to know when we've seen the last occurrence.
* The solution here is to queue one link until we see the next.
* At the end of the session, you can enumerate any remaining
* entries by calling archive_entry_linkify(NULL) and store those
* bodies. If you have a file with three links l1, l2, and l3,
* you'll get the following behavior if you see all three links:
* linkify(l1) => NULL (the resolver stores l1 internally)
* linkify(l2) => l1 (resolver stores l2, you write l1)
* linkify(l3) => l2, l3 (all links seen, you can write both).
* If you only see l1 and l2, you'll get this behavior:
* linkify(l1) => NULL
* linkify(l2) => l1
* linkify(NULL) => l2 (at end, you retrieve remaining links)
* As the name suggests, this strategy is used by newer cpio variants.
* It's noticeably more complex for the archiver, slightly more complex
* for the dearchiver than the tar strategy, but makes it straightforward
* to restore a file using any link by simply continuing to scan until
* you see a link that is stored with a body. In contrast, the tar
* strategy requires you to rescan the archive from the beginning to
* correctly extract an arbitrary link.
*/
__LA_DECL struct archive_entry_linkresolver *archive_entry_linkresolver_new(void);
__LA_DECL void archive_entry_linkresolver_set_strategy(
struct archive_entry_linkresolver *, int /* format_code */);
__LA_DECL void archive_entry_linkresolver_free(struct archive_entry_linkresolver *);
__LA_DECL void archive_entry_linkify(struct archive_entry_linkresolver *,
struct archive_entry **, struct archive_entry **);
__LA_DECL struct archive_entry *archive_entry_partial_links(
struct archive_entry_linkresolver *res, unsigned int *links);
#ifdef __cplusplus
}
#endif
/* This is meaningless outside of this header. */
#undef __LA_DECL
#endif /* !ARCHIVE_ENTRY_H_INCLUDED */
@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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 ContainerizationExtras
import Foundation
internal func createTemporaryDirectory(baseName: String) -> URL? {
let url = FileManager.default.uniqueTemporaryDirectory().appendingPathComponent(
"\(baseName).XXXXXX")
var path = url.absoluteURL.path
return path.withUTF8 { utf8Bytes in
var mutablePath = Array(utf8Bytes) + [0]
return mutablePath.withUnsafeMutableBufferPointer { buffer -> URL? in
guard let baseAddress = buffer.baseAddress else { return nil }
mkdtemp(baseAddress)
let resultPath = String(decoding: buffer[..<(buffer.count - 1)], as: UTF8.self)
return URL(fileURLWithPath: resultPath, isDirectory: true)
}
}
}
@@ -0,0 +1,318 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// 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
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
public init(_ archive: ArchiveWriter) {
underlying = archive_entry_new2(archive.underlying)
}
public init() {
underlying = archive_entry_new()
}
deinit {
archive_entry_free(underlying)
}
}
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 }
return archive_entry_size(underlying)
}
set {
if let s = newValue {
archive_entry_set_size(underlying, s)
} else {
archive_entry_unset_size(underlying)
}
}
}
/// The mode of the entry.
public var permissions: mode_t {
get {
archive_entry_perm(underlying)
}
set {
archive_entry_set_perm(underlying, newValue)
}
}
/// The owner id of the entry.
public var owner: uid_t? {
get {
uid_t(exactly: archive_entry_uid(underlying))
}
set {
archive_entry_set_uid(underlying, Int64(newValue ?? 0))
}
}
/// The group id of the entry
public var group: gid_t? {
get {
gid_t(exactly: archive_entry_gid(underlying))
}
set {
archive_entry_set_gid(underlying, Int64(newValue ?? 0))
}
}
/// The path of file this entry hardlinks to
public var hardlink: String? {
get {
guard let cstr = archive_entry_hardlink(underlying) else {
return nil
}
return String(cString: cstr)
}
set {
guard let newValue else {
archive_entry_set_hardlink(underlying, nil)
return
}
newValue.withCString {
archive_entry_set_hardlink(underlying, $0)
}
}
}
/// 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 {
return nil
}
return String(cString: cstr, encoding: .utf8)
}
set {
guard let newValue else {
archive_entry_set_hardlink_utf8(underlying, nil)
return
}
newValue.withCString {
archive_entry_set_hardlink_utf8(underlying, $0)
}
}
}
/// The string representation of the permissions of the entry
public var strmode: String? {
if let cstr = archive_entry_strmode(underlying) {
return String(cString: cstr)
}
return nil
}
/// The type of file this entry represents.
public var fileType: URLFileResourceType {
get {
switch archive_entry_filetype(underlying) {
case S_IFIFO: return .namedPipe
case S_IFCHR: return .characterSpecial
case S_IFDIR: return .directory
case S_IFBLK: return .blockSpecial
case S_IFREG: return .regular
case S_IFLNK: return .symbolicLink
case S_IFSOCK: return .socket
default: return .unknown
}
}
set {
switch newValue {
case .namedPipe: archive_entry_set_filetype(underlying, UInt32(S_IFIFO as mode_t))
case .characterSpecial: archive_entry_set_filetype(underlying, UInt32(S_IFCHR as mode_t))
case .directory: archive_entry_set_filetype(underlying, UInt32(S_IFDIR as mode_t))
case .blockSpecial: archive_entry_set_filetype(underlying, UInt32(S_IFBLK as mode_t))
case .regular: archive_entry_set_filetype(underlying, UInt32(S_IFREG as mode_t))
case .symbolicLink: archive_entry_set_filetype(underlying, UInt32(S_IFLNK as mode_t))
case .socket: archive_entry_set_filetype(underlying, UInt32(S_IFSOCK as mode_t))
default: archive_entry_set_filetype(underlying, 0)
}
}
}
/// The date that the entry was last accessed
public var contentAccessDate: Date? {
get {
Date(
underlying,
archive_entry_atime_is_set,
archive_entry_atime,
archive_entry_atime_nsec)
}
set {
setDate(
newValue,
underlying, archive_entry_set_atime,
archive_entry_unset_atime)
}
}
/// The date that the entry was created
public var creationDate: Date? {
get {
Date(
underlying,
archive_entry_ctime_is_set,
archive_entry_ctime,
archive_entry_ctime_nsec)
}
set {
setDate(
newValue,
underlying, archive_entry_set_ctime,
archive_entry_unset_ctime)
}
}
/// The date that the entry was modified
public var modificationDate: Date? {
get {
Date(
underlying,
archive_entry_mtime_is_set,
archive_entry_mtime,
archive_entry_mtime_nsec)
}
set {
setDate(
newValue,
underlying, archive_entry_set_mtime,
archive_entry_unset_mtime)
}
}
/// The file path of the entry
public var path: String? {
get {
guard let pathname = archive_entry_pathname(underlying) else {
return nil
}
return String(cString: pathname)
}
set {
guard let newValue else {
archive_entry_set_pathname(underlying, nil)
return
}
newValue.withCString {
archive_entry_set_pathname(underlying, $0)
}
}
}
/// The UTF-8 encoded file path of the entry
public var pathUtf8: String? {
get {
guard let pathname = archive_entry_pathname_utf8(underlying) else {
return nil
}
return String(cString: pathname)
}
set {
guard let newValue else {
archive_entry_set_pathname_utf8(underlying, nil)
return
}
newValue.withCString {
archive_entry_set_pathname_utf8(underlying, $0)
}
}
}
/// The symlink target that the entry points to
public var symlinkTarget: String? {
get {
guard let target = archive_entry_symlink(underlying) else {
return nil
}
return String(cString: target)
}
set {
guard let newValue else {
archive_entry_set_symlink(underlying, nil)
return
}
newValue.withCString {
archive_entry_set_symlink(underlying, $0)
}
}
}
/// The extended attributes of the entry
public var xattrs: [String: Data] {
get {
archive_entry_xattr_reset(self.underlying)
var attrs: [String: Data] = [:]
var namePtr: UnsafePointer<CChar>?
var valuePtr: UnsafeRawPointer?
var size: Int = 0
while archive_entry_xattr_next(self.underlying, &namePtr, &valuePtr, &size) == 0 {
let _name = namePtr.map { String(cString: $0) }
let _value = valuePtr.map { Data(bytes: $0, count: size) }
guard let name = _name, let value = _value else {
continue
}
attrs[name] = value
}
return attrs
}
set {
archive_entry_xattr_clear(self.underlying)
for (key, value) in newValue {
value.withUnsafeBytes { ptr in
archive_entry_xattr_add_entry(self.underlying, key, ptr.baseAddress, [UInt8](value).count)
}
}
}
}
fileprivate func setDate(
_ date: Date?, _ underlying: OpaquePointer, _ setter: (OpaquePointer, time_t, CLong) -> Void,
_ unset: (OpaquePointer) -> Void
) {
if let d = date {
let ti = d.timeIntervalSince1970
let seconds = floor(ti)
let nsec = max(0, min(1_000_000_000, ti - seconds * 1_000_000_000))
setter(underlying, time_t(seconds), CLong(nsec))
} else {
unset(underlying)
}
}
}
extension Date {
init?(
_ underlying: OpaquePointer, _ isSet: (OpaquePointer) -> CInt, _ seconds: (OpaquePointer) -> time_t,
_ nsec: (OpaquePointer) -> CLong
) {
guard isSet(underlying) != 0 else { return nil }
let ti = TimeInterval(seconds(underlying)) + TimeInterval(nsec(underlying)) * 0.000_000_001
self.init(timeIntervalSince1970: ti)
}
}