680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
319 lines
9.9 KiB
Swift
319 lines
9.9 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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)
|
|
}
|
|
}
|