containerization-framework 0.2.1

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// The Rust model, read into Containerization's own types.
//
// Read once, on the calling thread, from the opaque Rust values (`RustMount`,
// `RustBootSpec`, ...) into `Sendable` types the async work can carry. Fields
// Rust leaves unset keep the image's or library's value.
//===----------------------------------------------------------------------===//

import Containerization
import ContainerizationExtras
import ContainerizationOCI
import Foundation
// For `FilePermissions` (relayed socket mode).
import SystemPackage

/// A list Rust lends by index: `x_len`, `x_at`.
func list<Element>(_ count: UInt, _ element: (UInt) throws -> Element) rethrows -> [Element] {
    try (0..<count).map(element)
}

/// A list of strings Rust lends by index.
func strings(_ count: UInt, _ element: (UInt) -> RustStr) -> [String] {
    list(count) { element($0).toString() }
}

/// A map Rust lends by index: `x_len`, `x_key_at`, `x_value_at`.
func dictionary(_ count: UInt, _ key: (UInt) -> RustStr, _ value: (UInt) -> RustStr) -> [String: String] {
    Dictionary(uniqueKeysWithValues: list(count) { (key($0).toString(), value($0).toString()) })
}

extension Containerization.Mount {
    init(_ mount: RustMountRef) {
        let options = strings(mount.runtime_options_len(), mount.runtime_options_at)
        let runtimeOptions: RuntimeOptions =
            switch mount.runtime_kind() {
            case .Virtioblk: .virtioblk(options)
            case .Virtiofs: .virtiofs(options)
            case .Generic: .any(options)
            }

        self.init(
            type: mount.mount_type().toString(),
            source: mount.source().toString(),
            destination: mount.destination().toString(),
            options: strings(mount.options_len(), mount.options_at),
            runtimeOptions: runtimeOptions
        )
    }
}

extension UnixSocketConfiguration {
    init(_ socket: RustUnixSocketConfigurationRef) throws {
        // `CModeT` is 16-bit: refuse a mode that doesn't fit, not truncate it.
        let permissions = try socket.permissions().map { mode in
            guard let mode = CModeT(exactly: mode) else {
                throw BridgeError.malformed("socket permissions", String(mode, radix: 8))
            }
            return FilePermissions(rawValue: mode)
        }

        let direction: Direction =
            switch socket.direction() {
            case .Into: .into
            case .OutOf: .outOf
            }

        self.init(
            source: URL(filePath: socket.source().toString()),
            destination: URL(filePath: socket.destination().toString()),
            permissions: permissions,
            direction: direction
        )
    }
}

extension NATInterface {
    init(_ interface: RustNatInterfaceRef) throws {
        self.init(
            ipv4Address: try CIDRv4(interface.ipv4_address().toString()),
            ipv4Gateway: try interface.ipv4_gateway().map { try IPv4Address($0.toString()) },
            ipv6Address: try interface.ipv6_address().map { try CIDRv6($0.toString()) },
            ipv6Gateway: try interface.ipv6_gateway().map { try IPv6Address($0.toString()) },
            macAddress: try interface.mac_address().map { try MACAddress($0.toString()) },
            mtu: interface.mtu()
        )
    }
}

extension DNS {
    init(_ dns: RustDnsRef) throws {
        self.init(
            nameservers: strings(dns.nameservers_len(), dns.nameservers_at),
            domain: dns.domain()?.toString(),
            searchDomains: strings(dns.search_domains_len(), dns.search_domains_at),
            options: strings(dns.options_len(), dns.options_at)
        )
        // The library doesn't check; a hostname would silently break DNS.
        try validate()
    }
}

extension Hosts {
    init(_ hosts: RustHostsRef) {
        self.init(
            entries: list(hosts.entries_len(), hosts.entries_at).map { entry in
                Hosts.Entry(
                    ipAddress: entry.ip_address().toString(),
                    hostnames: strings(entry.hostnames_len(), entry.hostnames_at),
                    comment: entry.comment()?.toString()
                )
            },
            comment: hosts.comment()?.toString()
        )
    }
}

extension User {
    init(_ user: RustUserRef) {
        self.init(
            uid: user.uid(),
            gid: user.gid(),
            umask: user.umask(),
            additionalGids: list(user.additional_gids_len(), user.additional_gids_at),
            username: user.username().toString()
        )
    }
}

/// `LinuxProcessConfiguration`, over one already seeded from the image.
struct ProcessSettings: Sendable {
    var arguments: [String]?
    var environmentVariables: [String]
    var workingDirectory: String?
    var user: User?

    init(_ process: RustLinuxProcessConfigurationRef) {
        arguments = process.has_arguments() ? strings(process.arguments_len(), process.arguments_at) : nil
        environmentVariables = strings(process.environment_variables_len(), process.environment_variables_at)
        workingDirectory = process.working_directory()?.toString()
        user = process.has_user() ? User(process.user()) : nil
    }

    func apply(to process: inout LinuxProcessConfiguration) {
        if let arguments {
            process.arguments = arguments
        }

        // Last, so a variable the caller sets beats the image's.
        process.environmentVariables += environmentVariables

        if let workingDirectory {
            process.workingDirectory = workingDirectory
        }

        if let user {
            process.user = user
        }
    }
}

/// `LinuxContainer.Configuration`, over one `ContainerManager` seeded.
struct ContainerSettings: Sendable {
    var process: ProcessSettings
    var cpus: Int
    var memoryInBytes: UInt64
    var hostname: String?
    var sysctl: [String: String]
    var interfaces: [NATInterface]
    var sockets: [UnixSocketConfiguration]
    var mounts: [Containerization.Mount]
    var maskedPaths: [String]
    var readonlyPaths: [String]
    var dns: DNS?
    var hosts: Hosts?
    var virtualization: Bool
    var bootLog: BootLog?
    var ociRuntimePath: String?
    var seccompProfile: LinuxContainer.Configuration.SeccompProfile
    var useInit: Bool

    init(_ configuration: RustLinuxContainerConfigurationRef) throws {
        process = ProcessSettings(configuration.process())
        cpus = Int(configuration.cpus())
        memoryInBytes = configuration.memory_in_bytes()
        hostname = configuration.hostname()?.toString()
        sysctl = dictionary(configuration.sysctl_len(), configuration.sysctl_key_at, configuration.sysctl_value_at)
        interfaces = try list(configuration.interfaces_len()) { try NATInterface(configuration.interfaces_at($0)) }
        sockets = try list(configuration.sockets_len()) {
            try UnixSocketConfiguration(configuration.sockets_at($0))
        }
        mounts = list(configuration.mounts_len()) { Containerization.Mount(configuration.mounts_at($0)) }
        maskedPaths = strings(configuration.masked_paths_len(), configuration.masked_paths_at)
        readonlyPaths = strings(configuration.readonly_paths_len(), configuration.readonly_paths_at)
        dns = configuration.has_dns() ? try DNS(configuration.dns()) : nil
        hosts = configuration.has_hosts() ? Hosts(configuration.hosts()) : nil
        virtualization = configuration.virtualization()

        if configuration.has_boot_log() {
            let bootLog = configuration.boot_log()
            self.bootLog = .file(path: URL(filePath: bootLog.path().toString()), append: bootLog.append())
        } else {
            bootLog = nil
        }

        ociRuntimePath = configuration.oci_runtime_path()?.toString()
        seccompProfile =
            switch configuration.seccomp_mode() {
            case .Unconfined: .unconfined
            case .Default: .default
            case .Profile:
                .profile(try LinuxSeccomp.decode(from: Data((configuration.seccomp_profile()?.toString() ?? "").utf8)))
            }
        useInit = configuration.use_init()
    }

    func apply(to config: inout LinuxContainer.Configuration) {
        process.apply(to: &config.process)
        config.cpus = cpus
        config.memoryInBytes = memoryInBytes
        config.hostname = hostname
        config.sysctl = sysctl
        config.interfaces = interfaces
        config.sockets = sockets
        config.mounts = mounts
        config.maskedPaths = maskedPaths
        config.readonlyPaths = readonlyPaths
        config.dns = dns
        config.hosts = hosts
        config.virtualization = virtualization

        // Absent keeps the manager's, in the container's directory.
        if let bootLog {
            config.bootLog = bootLog
        }

        config.ociRuntimePath = ociRuntimePath
        config.seccompProfile = seccompProfile
        config.useInit = useInit
    }
}