chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user