containerization-framework 0.4.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// How a throwing Swift call reaches Rust.
//
// swift-bridge 0.1.59's `Result<_, String>` from Swift miscompiles with most
// argument and value types, so a throwing method returns a `CzOutcome`
// instead: the value it made, or the message of what it threw. Rust asks
// `error`, then takes the value it expects, once.
//
// Function names and labels are the `swift_name`s and `label`s in
// `src/bridge/mod.rs`, or else Rust's own; swift-bridge's generated shims call
// with them, so they are part of the contract.
//===----------------------------------------------------------------------===//

import Containerization
import ContainerizationArchive
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import Foundation

/// Unchecked: Rust reads an outcome once, on the thread it was returned to.
/// Public because swift-bridge's glue for a Rust method that takes one is.
public final class CzOutcome: @unchecked Sendable {
  private let failure: String?
  private let failureCode: String?
  private let value: Any?

  /// Runs `body`, keeping what it returns or the message of what it throws,
  /// and the code of a thrown `ContainerizationError`.
  init(_ body: () throws -> Any) {
    do {
      value = try body()
      failure = nil
      failureCode = nil
    } catch {
      value = nil
      failure = "\(error)"
      failureCode = (error as? ContainerizationError)?.code.description
    }
  }

  /// An outcome holding part of another's value, or `Absent` for `nil`, for
  /// Rust to read with the same getters. (Not an `init`: a trailing closure
  /// could bind to its `Any?`.)
  static func holding(_ part: Any?) -> CzOutcome {
    CzOutcome { absent(part) }
  }

  func error() -> String? {
    failure
  }

  /// A thrown `ContainerizationError`'s `code.description`.
  func errorCode() -> String? {
    failureCode
  }

  func taken<T>() -> T {
    cast(value)
  }

  /// The value, if it is a `T`, for a getter that reads more than one shape.
  func held<T>(_: T.Type) -> T? {
    value as? T
  }

  func cast<T>(_ held: Any?) -> T {
    guard let held = held as? T else {
      preconditionFailure("Rust took a \(T.self) from an outcome holding \(String(describing: held))")
    }
    return held
  }

  func localContentStore() -> CzLocalContentStore { taken() }
  func content() -> CzContent { taken() }
  func imageStore() -> CzImageStore { taken() }
  func image() -> CzImage { taken() }
  func images() -> CzImages { taken() }
  func initImage() -> CzInitImage { taken() }
  func containerManager() -> CzContainerManager { taken() }
  func linuxContainer() -> CzLinuxContainer { taken() }
  func vmnetNetwork() -> CzVmnetNetwork { taken() }
  func vmnetInterface() -> CzVmnetInterface { taken() }
  func linuxProcess() -> CzLinuxProcess { taken() }
  func virtualMachineManager() -> CzVirtualMachineManager { taken() }
  func virtualMachineInstance() -> CzVirtualMachineInstance { taken() }
  func vsockListener() -> CzVsockListener { taken() }
  func linuxPod() -> CzLinuxPod { taken() }
  func contentWriter() -> CzContentWriter { taken() }
  func ext4Reader() -> CzExt4Reader { taken() }
  func archiveWriter() -> CzArchiveWriter { taken() }
  func archiveReader() -> CzArchiveReader { taken() }

  // An archive entry, alone or with the data or reader it was read with.

  func writeEntry() -> CzWriteEntry {
    switch value {
    case let (entry, _) as (WriteEntry, Data): CzWriteEntry(entry)
    case let (entry, _) as (WriteEntry, ArchiveEntryReader): CzWriteEntry(entry)
    default: taken()
    }
  }

  func entryData() -> RustVec<UInt8> {
    rustBytes((taken() as (WriteEntry, Data)).1)
  }

  func archiveEntryReader() -> CzArchiveEntryReader {
    CzArchiveEntryReader((taken() as (WriteEntry, ArchiveEntryReader)).1)
  }

  /// A held `[String: Data]`'s keys, and the value of one.
  func dataMapKeys() -> RustVec<RustString> {
    rustStrings((taken() as [String: Data]).keys)
  }

  func dataMapValue(key: RustStr) -> RustVec<UInt8> {
    rustBytes((taken() as [String: Data])[key.toString()] ?? Data())
  }

  func writtenSize() -> Int64 {
    (taken() as Written).size
  }

  func writtenDigest() -> String {
    (taken() as Written).digest
  }

  // A `Mount`, field by field: Rust builds its own.

  func mountType() -> String {
    (taken() as Containerization.Mount).type
  }

  func mountSource() -> String {
    (taken() as Containerization.Mount).source
  }

  func mountDestination() -> String {
    (taken() as Containerization.Mount).destination
  }

  func mountOptions() -> RustVec<RustString> {
    rustStrings((taken() as Containerization.Mount).options)
  }

  func mountRuntimeKind() -> RuntimeKind {
    runtimeKind((taken() as Containerization.Mount).runtimeOptions).0
  }

  func mountRuntimeOptions() -> RustVec<RustString> {
    rustStrings(runtimeKind((taken() as Containerization.Mount).runtimeOptions).1)
  }

  // An `AttachedFilesystem`, field by field: Rust builds its own.

  func attachedFilesystemType() -> String {
    (taken() as AttachedFilesystem).type
  }

  func attachedFilesystemSource() -> String {
    (taken() as AttachedFilesystem).source
  }

  func attachedFilesystemDestination() -> String {
    (taken() as AttachedFilesystem).destination
  }

  func attachedFilesystemOptions() -> RustVec<RustString> {
    rustStrings((taken() as AttachedFilesystem).options)
  }

  // A `Platform`, field by field: Rust builds its own.

  func platformArchitecture() -> String {
    (taken() as Platform).architecture
  }

  func platformOs() -> String {
    (taken() as Platform).os
  }

  func platformOsVersion() -> String? {
    (taken() as Platform).osVersion
  }

  func platformHasOsFeatures() -> Bool {
    (taken() as Platform).osFeatures != nil
  }

  func platformOsFeatures() -> RustVec<RustString> {
    rustStrings((taken() as Platform).osFeatures ?? [])
  }

  func platformVariant() -> String? {
    (taken() as Platform).variant
  }

  // Addresses, field by field: Rust builds its own.

  func boolean() -> Bool { taken() }

  /// Whether an optional result is there, rather than `Absent`.
  func isSome() -> Bool {
    !(value is Absent)
  }

  /// The length of a held array.
  func len() -> UInt {
    UInt((taken() as [Any]).count)
  }

  /// The `index`th element of a held array, as an outcome of its own.
  func at(index: UInt) -> CzOutcome {
    CzOutcome.holding((taken() as [Any])[Int(index)])
  }

  /// A held `[String: String]`'s keys, and its values in the same order.
  func mapKeys() -> RustVec<RustString> {
    rustStrings((taken() as [String: String]).keys)
  }

  func mapValues() -> RustVec<RustString> {
    rustStrings((taken() as [String: String]).values)
  }

  /// Whether the address held, alone or in a CIDR block, is IPv6.
  func isIPv6() -> Bool {
    heldAddress() is IPv6Address
  }

  func ipv4Value() -> UInt32 {
    (cast(heldAddress()) as IPv4Address).value
  }

  func ipv6High() -> UInt64 {
    UInt64((cast(heldAddress()) as IPv6Address).value >> 64)
  }

  func ipv6Low() -> UInt64 {
    UInt64(truncatingIfNeeded: (cast(heldAddress()) as IPv6Address).value)
  }

  func ipv6Zone() -> String? {
    (cast(heldAddress()) as IPv6Address).zone
  }

  func prefixLength() -> UInt8 {
    switch value {
    case let cidr as CIDRv4: cidr.prefix.length
    case let cidr as CIDRv6: cidr.prefix.length
    case let cidr as CIDR: cidr.prefix.length
    default: (taken() as Prefix).length
    }
  }

  func macValue() -> UInt64 {
    (taken() as MACAddress).value
  }

  func wideHigh() -> UInt64 {
    UInt64((taken() as UInt128) >> 64)
  }

  func wideLow() -> UInt64 {
    UInt64(truncatingIfNeeded: taken() as UInt128)
  }

  /// The `IPv4Address` or `IPv6Address` held, alone or in an `IPAddress`, a
  /// CIDR block or a `CIDR`.
  private func heldAddress() -> Any? {
    switch value {
    case let cidr as CIDRv4: cidr.address
    case let cidr as CIDRv6: cidr.address
    case let cidr as CIDR: unwrapped(cidr.address)
    case let address as IPAddress: unwrapped(address)
    default: value
    }
  }

  private func unwrapped(_ address: IPAddress) -> Any {
    switch address {
    case .v4(let address): address
    case .v6(let address): address
    }
  }

  func exitCode() -> Int32 {
    (taken() as ExitStatus).exitCode
  }

  /// Seconds since 1970.
  func exitedAt() -> Double {
    (taken() as ExitStatus).exitedAt.timeIntervalSince1970
  }

  /// A `UInt64`, or the bytes a delete freed.
  func number() -> UInt64 {
    if let deleted = value as? ([String], UInt64) {
      return deleted.1
    }
    return taken()
  }

  func text() -> String { taken() }

  /// A `[String]`, or the digests a delete removed.
  func strings() -> RustVec<RustString> {
    if let deleted = value as? ([String], UInt64) {
      return rustStrings(deleted.0)
    }
    return rustStrings(taken() as [String])
  }

  /// For a `Data?`.
  func hasBytes() -> Bool {
    (taken() as Data?) != nil
  }

  /// A `Data?`, or a `[UInt8]`.
  func bytes() -> RustVec<UInt8> {
    rustBytes((value as? [UInt8]).map { Data($0) } ?? (taken() as Data?) ?? Data())
  }
}

/// Bytes as Rust's `Vec<u8>`.
func rustBytes(_ bytes: Data) -> RustVec<UInt8> {
  let vec = RustVec<UInt8>()
  for byte in bytes {
    vec.push(value: byte)
  }
  return vec
}

/// What an outcome holds for a `nil` result, which `Any` can't hold apart from
/// a missing value.
struct Absent {}

/// An optional result, with `nil` as `Absent`.
func absent(_ value: Any?) -> Any {
  value ?? Absent()
}

func rust(_ string: String) -> RustString {
  string.intoRustString()
}

func rust(_ string: String?) -> RustString? {
  string.map(rust)
}

/// Strings as Rust's `Vec<String>`.
func rustStrings(_ values: some Sequence<String>) -> RustVec<RustString> {
  let vec = RustVec<RustString>()
  for value in values {
    vec.push(value: value.intoRustString())
  }
  return vec
}

/// Rust's `Vec<String>` as strings.
func strings(_ vec: RustVec<RustString>) -> [String] {
  vec.map { $0.as_str().toString() }
}

/// A Rust map, crossing as its keys and its values in the same order.
func dictionary(_ keys: RustVec<RustString>, _ values: RustVec<RustString>) -> [String: String] {
  Dictionary(zip(strings(keys), strings(values)), uniquingKeysWith: { _, last in last })
}