containerization-framework 0.4.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// ContainerizationEXT4: `EXT4.EXT4Reader`, `EXT4.Formatter` and the values
// they take and return, and `EXT4Unpacker`, from Containerization.
//
// A `SuperBlock` or an `Inode` crosses as its bytes, and an archive's format
// and filter as their `rawValue`s.
//===----------------------------------------------------------------------===//

import Containerization
import ContainerizationArchive
import ContainerizationEXT4
import ContainerizationOCI
import Foundation
import SystemPackage

func openExt4Reader(blockDevice: RustStr) -> CzOutcome {
  let blockDevice = FilePath(blockDevice.toString())

  return CzOutcome { CzExt4Reader(try EXT4.EXT4Reader(blockDevice: blockDevice)) }
}

/// Unchecked: Rust's `Ext4Reader` isn't `Sync`, so one call runs at a time.
final class CzExt4Reader: @unchecked Sendable {
  let reader: EXT4.EXT4Reader

  init(_ reader: EXT4.EXT4Reader) {
    self.reader = reader
  }

  func superBlock() -> RustVec<UInt8> {
    rustBytes(memoryBytes(of: reader.superBlock))
  }

  func exists(path: RustStr, followSymlinks: Bool) -> Bool {
    reader.exists(FilePath(path.toString()), followSymlinks: followSymlinks)
  }

  /// The outcome holds the inode's number and the inode.
  func stat(path: RustStr, followSymlinks: Bool) -> CzOutcome {
    let path = FilePath(path.toString())

    return CzOutcome {
      let (number, inode) = try reader.stat(path, followSymlinks: followSymlinks)
      return (number, inode)
    }
  }

  func listDirectory(path: RustStr) -> CzOutcome {
    let path = FilePath(path.toString())

    return CzOutcome { try reader.listDirectory(path) }
  }

  func readFile(at: RustStr, offset: UInt64, count: UInt?, followSymlinks: Bool) -> CzOutcome {
    let at = FilePath(at.toString())
    let count = count.map { Int($0) }

    return CzOutcome { try reader.readFile(at: at, offset: offset, count: count, followSymlinks: followSymlinks) }
  }

  func export(archive: RustStr) -> CzOutcome {
    let archive = FilePath(archive.toString())

    return CzOutcome { try reader.export(archive: archive) }
  }
}

func readInlineExtendedAttributes(buffer: RustVec<UInt8>) -> CzOutcome {
  let buffer = [UInt8](buffer)

  return CzOutcome { try EXT4.EXT4Reader.readInlineExtendedAttributes(from: buffer) }
}

func readBlockExtendedAttributes(buffer: RustVec<UInt8>) -> CzOutcome {
  let buffer = [UInt8](buffer)

  return CzOutcome { try EXT4.EXT4Reader.readBlockExtendedAttributes(from: buffer) }
}

func rootInode() -> CzOutcome {
  CzOutcome { EXT4.Inode.Root() }
}

func compressExtendedAttributeName(name: RustStr) -> CzOutcome {
  let name = name.toString()

  return CzOutcome {
    let compressed = EXT4.ExtendedAttribute.compressName(name)
    return (compressed.id, compressed.str)
  }
}

func decompressExtendedAttributeName(id: Int, suffix: RustStr) -> CzOutcome {
  let suffix = suffix.toString()

  return CzOutcome { EXT4.ExtendedAttribute.decompressName(id: id, suffix: suffix) }
}

func newExt4Formatter(devicePath: RustStr, options: RustFormatterOptions) -> CzOutcome {
  let devicePath = FilePath(devicePath.toString())
  var journal: EXT4.JournalConfig?
  if options.hasJournal() {
    let mode = options.hasJournalMode() ? EXT4.JournalConfig.JournalMode(options.journalMode()) : nil
    journal = EXT4.JournalConfig(size: options.journalSize(), defaultMode: mode)
  }
  let blockSize = options.blockSize()
  let minDiskSize = options.minDiskSize()

  return CzOutcome {
    CzExt4Formatter(
      try EXT4.Formatter(devicePath, blockSize: blockSize, minDiskSize: minDiskSize, journal: journal)
    )
  }
}

/// The outcome holds the size and the number of items.
func scanArchiveHeaders(format: RustStr, filter: RustStr, file: RustStr) -> CzOutcome {
  let format = format.toString()
  let filter = filter.toString()
  let file = URL(filePath: file.toString())

  return CzOutcome {
    let totals = try EXT4.Formatter.scanArchiveHeaders(
      format: try archiveFormat(format),
      filter: try archiveFilter(filter),
      file: file
    )
    return (totals.size, totals.items)
  }
}

/// Unchecked: Rust's `Formatter` isn't `Sync`, so one call runs at a time.
final class CzExt4Formatter: @unchecked Sendable {
  let formatter: EXT4.Formatter

  init(_ formatter: EXT4.Formatter) {
    self.formatter = formatter
  }

  func link(link: RustStr, target: RustStr) -> CzOutcome {
    let link = FilePath(link.toString())
    let target = FilePath(target.toString())

    return CzOutcome { try formatter.link(link: link, target: target) }
  }

  func unlink(path: RustStr, directoryWhiteout: Bool) -> CzOutcome {
    let path = FilePath(path.toString())

    return CzOutcome { try formatter.unlink(path: path, directoryWhiteout: directoryWhiteout) }
  }

  /// The dates are seconds since 1970. `buf` stands for `nil` when `hasBuf` is
  /// false, and the xattrs when `hasXattrs` is.
  func create(
    path: RustStr,
    link: RustString?,
    mode: UInt16,
    access: Double,
    modification: Double,
    creation: Double,
    now: Double,
    hasBuf: Bool,
    buf: RustVec<UInt8>,
    uid: UInt32?,
    gid: UInt32?,
    hasXattrs: Bool,
    xattrNames: RustVec<RustString>,
    xattrLengths: RustVec<UInt64>,
    xattrValues: RustVec<UInt8>,
    recursion: Bool
  ) -> CzOutcome {
    let path = FilePath(path.toString())
    let link = link.map { FilePath($0.toString()) }
    var ts = FileTimestamps(
      access: Date(timeIntervalSince1970: access),
      modification: Date(timeIntervalSince1970: modification),
      creation: Date(timeIntervalSince1970: creation)
    )
    ts.now = Date(timeIntervalSince1970: now)
    let stream = hasBuf ? InputStream(data: Data(buf)) : nil
    let xattrs = hasXattrs ? dataMap(names: xattrNames, lengths: xattrLengths, values: xattrValues) : nil

    return CzOutcome {
      stream?.open()
      defer { stream?.close() }

      return try formatter.create(
        path: path,
        link: link,
        mode: mode,
        ts: ts,
        buf: stream,
        uid: uid,
        gid: gid,
        xattrs: xattrs,
        recursion: recursion
      )
    }
  }

  func close() -> CzOutcome {
    CzOutcome { try formatter.close() }
  }

  func unpack(source: RustStr, format: RustStr, compression: RustStr, progress: RustProgressHandler) -> CzOutcome {
    // Not `Sendable`, but a class only the task below uses.
    nonisolated(unsafe) let formatter = formatter
    let source = URL(filePath: source.toString())
    let format = format.toString()
    let compression = compression.toString()
    let progress = progressHandler(progress)

    return CzOutcome {
      let format = try archiveFormat(format)
      let filter = try archiveFilter(compression)
      return try blocking {
        try await formatter.unpack(source: source, format: format, compression: filter, progress: progress)
      }
    }
  }

  func unpackReader(reader: CzArchiveReader, progress: RustProgressHandler) -> CzOutcome {
    // Not `Sendable`, but classes only the task below uses.
    nonisolated(unsafe) let formatter = formatter
    nonisolated(unsafe) let reader = reader.reader
    let progress = progressHandler(progress)

    return CzOutcome { try blocking { try await formatter.unpack(reader: reader, progress: progress) } }
  }
}

extension CzOutcome {
  func ext4Formatter() -> CzExt4Formatter { taken() }

  func inodeNumber() -> UInt32 {
    (taken() as (EXT4.InodeNumber, EXT4.Inode)).0
  }

  /// An inode, alone or with its number.
  func inodeBytes() -> RustVec<UInt8> {
    let inode = held((EXT4.InodeNumber, EXT4.Inode).self)?.1 ?? taken()
    return rustBytes(memoryBytes(of: inode))
  }

  func compressedNameId() -> UInt8 {
    (taken() as (UInt8, String)).0
  }

  func compressedNameStr() -> String {
    (taken() as (UInt8, String)).1
  }

  func scannedSize() -> Int64 {
    (taken() as (Int64, Int)).0
  }

  func scannedItems() -> Int {
    (taken() as (Int64, Int)).1
  }
}

/// A value's bytes, as it is laid out in memory.
private func memoryBytes<T>(of value: T) -> Data {
  withUnsafeBytes(of: value) { Data($0) }
}

func unpackExt4(
  unpacker: RustExt4Unpacker,
  image: CzImage,
  platform: RustPlatform,
  at: RustStr,
  progress: RustProgressHandler
) -> CzOutcome {
  // Not `Sendable`, but a value only the task below uses.
  nonisolated(unsafe) let unpacker = EXT4Unpacker(unpacker)
  let image = image.image
  let platform = Platform(platform)
  let at = URL(filePath: at.toString())
  let progress = progressHandler(progress)

  return CzOutcome {
    try blocking { try await unpacker.unpack(image, for: platform, at: at, progress: progress) }
  }
}

// For unit tests that compare Rust's copies with Swift's values.

func ext4FileModeFlags() -> RustVec<UInt16> {
  let flags: [EXT4.FileModeFlag] = [
    .S_IXOTH, .S_IWOTH, .S_IROTH, .S_IXGRP, .S_IWGRP, .S_IRGRP, .S_IXUSR, .S_IWUSR, .S_IRUSR, .S_ISVTX,
    .S_ISGID, .S_ISUID, .S_IFIFO, .S_IFCHR, .S_IFDIR, .S_IFBLK, .S_IFREG, .S_IFLNK, .S_IFSOCK, .TypeMask,
  ]
  let vec = RustVec<UInt16>()
  for flag in flags {
    vec.push(value: EXT4.Inode.Mode(flag, 0))
  }
  return vec
}

func ext4PrefixMapKeys() -> RustVec<Int> {
  let vec = RustVec<Int>()
  for key in EXT4.ExtendedAttribute.prefixMap.keys.sorted() {
    vec.push(value: key)
  }
  return vec
}

func ext4PrefixMapValues() -> RustVec<RustString> {
  rustStrings(EXT4.ExtendedAttribute.prefixMap.sorted { $0.key < $1.key }.map(\.value))
}

func ext4SuperBlockMagic() -> UInt16 {
  EXT4.SuperBlockMagic
}

/// The sizes of `SuperBlock` and `Inode`, each followed by the offsets of the
/// fields Rust's test names.
func ext4Layout() -> RustVec<UInt64> {
  let superBlock: [PartialKeyPath<EXT4.SuperBlock>] = [
    \.magic, \.mmpBlock, \.lastErrorBlock, \.reserved, \.checksum,
  ]
  let inode: [PartialKeyPath<EXT4.Inode>] = [\.block, \.projid, \.inlineXattrs]
  let layout =
    [MemoryLayout<EXT4.SuperBlock>.size]
    + superBlock.map { MemoryLayout<EXT4.SuperBlock>.offset(of: $0) ?? -1 }
    + [MemoryLayout<EXT4.Inode>.size]
    + inode.map { MemoryLayout<EXT4.Inode>.offset(of: $0) ?? -1 }

  let vec = RustVec<UInt64>()
  for value in layout {
    vec.push(value: UInt64(bitPattern: Int64(value)))
  }
  return vec
}

/// A `FileTimestamps`' `accessLo` and `accessHi`, for a date as seconds since
/// 1970.
func fileTimestampsAccess(seconds: Double) -> RustVec<UInt32> {
  let timestamps = FileTimestamps(access: Date(timeIntervalSince1970: seconds), modification: nil, creation: nil)
  let vec = RustVec<UInt32>()
  vec.push(value: timestamps.accessLo)
  vec.push(value: timestamps.accessHi)
  return vec
}