mirror of
				https://github.com/GayPizzaSpecifications/darwin-apk.git
				synced 2025-11-04 07:59:38 +00:00 
			
		
		
		
	Initial implementation of APKINDEX, fetching, reading, parsing, & merging
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -3,7 +3,6 @@
 | 
			
		||||
/Packages
 | 
			
		||||
xcuserdata/
 | 
			
		||||
DerivedData/
 | 
			
		||||
.swiftpm/configuration/registries.json
 | 
			
		||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
 | 
			
		||||
.swiftpm/
 | 
			
		||||
.netrc
 | 
			
		||||
Package.resolved
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,24 @@ import PackageDescription
 | 
			
		||||
 | 
			
		||||
let package = Package(
 | 
			
		||||
  name: "darwin-apk",
 | 
			
		||||
  platforms: [
 | 
			
		||||
    .macOS(.v13),
 | 
			
		||||
  ],
 | 
			
		||||
  dependencies: [
 | 
			
		||||
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
 | 
			
		||||
    .package(url: "https://github.com/tsolomko/SWCompression", from: "4.8.6"),
 | 
			
		||||
  ],
 | 
			
		||||
  targets: [
 | 
			
		||||
    .target(
 | 
			
		||||
      name: "darwin-apk",
 | 
			
		||||
      dependencies: [
 | 
			
		||||
        .product(name: "SWCompression", package: "SWCompression"),
 | 
			
		||||
      ],
 | 
			
		||||
      path: "Sources/apk"),
 | 
			
		||||
    .executableTarget(
 | 
			
		||||
      name: "dpk",
 | 
			
		||||
      dependencies: [
 | 
			
		||||
        "darwin-apk",
 | 
			
		||||
        .product(name: "ArgumentParser", package: "swift-argument-parser"),
 | 
			
		||||
      ],
 | 
			
		||||
      path: "Sources/dpk-cli"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								Sources/apk/Index/ApkIndex.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								Sources/apk/Index/ApkIndex.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
struct ApkIndex {
 | 
			
		||||
  let packages: [ApkIndexPackage]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndex {
 | 
			
		||||
  func first(name: String) -> ApkIndexPackage? {
 | 
			
		||||
    self.packages.first {
 | 
			
		||||
      $0.name == name
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndex {
 | 
			
		||||
  static func merge<S: Sequence>(_ tables: S) -> Self where S.Element == Self {
 | 
			
		||||
    Self.init(packages: tables.flatMap(\.packages))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static func merge(_ tables: Self...) -> ApkIndex {
 | 
			
		||||
    Self.init(packages: tables.flatMap(\.packages))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndex {
 | 
			
		||||
  init(raw: ApkRawIndex) throws {
 | 
			
		||||
    self.packages = try raw.packages.map {
 | 
			
		||||
      try ApkIndexPackage(raw: $0)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								Sources/apk/Index/ApkIndexDownloader.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Sources/apk/Index/ApkIndexDownloader.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
struct ApkIndexDownloader {
 | 
			
		||||
  func downloadFile(remote remoteURL: URL, destination destLocalURL: URL) {
 | 
			
		||||
    let sem = DispatchSemaphore.init(value: 0)
 | 
			
		||||
    let downloadTask = URLSession.shared.downloadTask(with: remoteURL) { url, response, error in
 | 
			
		||||
      if let localURL = url {
 | 
			
		||||
        do {
 | 
			
		||||
          // Replace existing APKINDEX.tar.gz files
 | 
			
		||||
          if FileManager.default.fileExists(atPath: destLocalURL.path()) {
 | 
			
		||||
            try FileManager.default.removeItem(at: destLocalURL)
 | 
			
		||||
          }
 | 
			
		||||
          // Move temporary to the new location
 | 
			
		||||
          try FileManager.default.moveItem(at: localURL, to: destLocalURL)
 | 
			
		||||
        } catch {
 | 
			
		||||
          print("Download error: \(error.localizedDescription)")
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      sem.signal()
 | 
			
		||||
    }
 | 
			
		||||
    downloadTask.resume()
 | 
			
		||||
    sem.wait()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										164
									
								
								Sources/apk/Index/ApkIndexPackage.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								Sources/apk/Index/ApkIndexPackage.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,164 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
struct ApkIndexPackage {
 | 
			
		||||
  let indexChecksum: String  //TODO: Decode cus why not
 | 
			
		||||
  let name: String
 | 
			
		||||
  let version: String
 | 
			
		||||
  let architecture: String?
 | 
			
		||||
  let packageSize: UInt64
 | 
			
		||||
  let installedSize: UInt64
 | 
			
		||||
  let packageDescription: String
 | 
			
		||||
  let url: String
 | 
			
		||||
  let license: String
 | 
			
		||||
  let origin: String?
 | 
			
		||||
  let maintainer: String?
 | 
			
		||||
  let buildTime: Date?
 | 
			
		||||
  let commit: String?
 | 
			
		||||
  let providerPriority: UInt16?
 | 
			
		||||
  let dependencies: [String]  //TODO: stuff
 | 
			
		||||
  let provides: [String]  //TODO: stuff
 | 
			
		||||
  let installIf: [String]  //TODO: stuff
 | 
			
		||||
 | 
			
		||||
  var downloadFilename: String { "\(self.name)-\(version).apk" }
 | 
			
		||||
 | 
			
		||||
  //TODO: Implementation
 | 
			
		||||
  //lazy var semanticVersion: (Int, Int, Int) = (0, 0, 0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexPackage {
 | 
			
		||||
  init(raw rawEntry: ApkRawIndexEntry) throws(Self.ParseError) {
 | 
			
		||||
    // Required fields
 | 
			
		||||
    var indexChecksum: String? = nil
 | 
			
		||||
    var name: String? = nil
 | 
			
		||||
    var version: String? = nil
 | 
			
		||||
    var description: String? = nil
 | 
			
		||||
    var url: String? = nil
 | 
			
		||||
    var license: String? = nil
 | 
			
		||||
    var packageSize: UInt64? = nil
 | 
			
		||||
    var installedSize: UInt64? = nil
 | 
			
		||||
 | 
			
		||||
    var dependencies = [String]()
 | 
			
		||||
    var provides = [String]()
 | 
			
		||||
    var installIf = [String]()
 | 
			
		||||
 | 
			
		||||
    // Optional fields
 | 
			
		||||
    var architecture: String? = nil
 | 
			
		||||
    var origin: String? = nil
 | 
			
		||||
    var maintainer: String? = nil
 | 
			
		||||
    var buildTime: Date? = nil
 | 
			
		||||
    var commit: String? = nil
 | 
			
		||||
    var providerPriority: UInt16? = nil
 | 
			
		||||
 | 
			
		||||
    // Read all the raw records for this entry
 | 
			
		||||
    for record in rawEntry.fields {
 | 
			
		||||
      switch record.key {
 | 
			
		||||
      case "P":
 | 
			
		||||
        name = record.value
 | 
			
		||||
      case "V":
 | 
			
		||||
        version = record.value
 | 
			
		||||
      case "T":
 | 
			
		||||
        description = record.value
 | 
			
		||||
      case "U":
 | 
			
		||||
        url = record.value
 | 
			
		||||
      case "L":
 | 
			
		||||
        license = record.value
 | 
			
		||||
      case "A":
 | 
			
		||||
        architecture = record.value
 | 
			
		||||
      case "D":
 | 
			
		||||
        dependencies = record.value.components(separatedBy: " ")
 | 
			
		||||
      case "C":
 | 
			
		||||
        indexChecksum = record.value  // base64-encoded SHA1 hash prefixed with "Q1"
 | 
			
		||||
      case "S":
 | 
			
		||||
        guard let value = UInt64(record.value, radix: 10) else {
 | 
			
		||||
          throw .badValue(key: record.key)
 | 
			
		||||
        }
 | 
			
		||||
        packageSize = value
 | 
			
		||||
      case "I":
 | 
			
		||||
        guard let value = UInt64(record.value, radix: 10) else {
 | 
			
		||||
          throw .badValue(key: record.key)
 | 
			
		||||
        }
 | 
			
		||||
        installedSize = value
 | 
			
		||||
      case "p":
 | 
			
		||||
        provides = record.value.components(separatedBy: " ")
 | 
			
		||||
      case "i":
 | 
			
		||||
        installIf = record.value.components(separatedBy: " ")
 | 
			
		||||
      case "o":
 | 
			
		||||
        origin = record.value
 | 
			
		||||
      case "m":
 | 
			
		||||
        maintainer = record.value
 | 
			
		||||
      case "t":
 | 
			
		||||
        guard let timet = UInt64(record.value, radix: 10),
 | 
			
		||||
            let timetInterval = TimeInterval(exactly: timet) else {
 | 
			
		||||
          throw .badValue(key: record.key)
 | 
			
		||||
        }
 | 
			
		||||
        buildTime = Date(timeIntervalSince1970: timetInterval)
 | 
			
		||||
      case "c":
 | 
			
		||||
        commit = record.value
 | 
			
		||||
      case "k":
 | 
			
		||||
        guard let value = UInt64(record.value, radix: 10),
 | 
			
		||||
            (0..<UInt64(UInt16.max)).contains(value) else {
 | 
			
		||||
          throw .badValue(key: record.key)
 | 
			
		||||
        }
 | 
			
		||||
        providerPriority = UInt16(truncatingIfNeeded: value)
 | 
			
		||||
      case "F", "M", "R", "Z", "r", "q", "a", "s", "f":
 | 
			
		||||
        break // installed db entries
 | 
			
		||||
      default:
 | 
			
		||||
        // Safe to ignore
 | 
			
		||||
        guard record.key.isLowercase else {
 | 
			
		||||
          throw .badValue(key: record.key)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.indexChecksum = try indexChecksum.unwrap(or: Self.ParseError.required(key: "C"))
 | 
			
		||||
    self.name = try name.unwrap(or: Self.ParseError.required(key: "P"))
 | 
			
		||||
    self.version = try version.unwrap(or: Self.ParseError.required(key: "V"))
 | 
			
		||||
    self.packageDescription = try description.unwrap(or: Self.ParseError.required(key: "T"))
 | 
			
		||||
    self.url = try url.unwrap(or: Self.ParseError.required(key: "U"))
 | 
			
		||||
    self.license = try license.unwrap(or: Self.ParseError.required(key: "L"))
 | 
			
		||||
    self.packageSize = try packageSize.unwrap(or: Self.ParseError.required(key: "S"))
 | 
			
		||||
    self.installedSize = try installedSize.unwrap(or: Self.ParseError.required(key: "I"))
 | 
			
		||||
 | 
			
		||||
    self.architecture = architecture
 | 
			
		||||
    self.origin = origin
 | 
			
		||||
    self.maintainer = maintainer
 | 
			
		||||
    self.buildTime = buildTime
 | 
			
		||||
    self.commit = commit
 | 
			
		||||
    self.providerPriority = providerPriority
 | 
			
		||||
 | 
			
		||||
    self.dependencies = dependencies
 | 
			
		||||
    self.provides = provides
 | 
			
		||||
    self.installIf = installIf
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public enum ParseError: Error, LocalizedError {
 | 
			
		||||
    case badValue(key: Character)
 | 
			
		||||
    case unexpectedKey(key: Character)
 | 
			
		||||
    case required(key: Character)
 | 
			
		||||
 | 
			
		||||
    public var errorDescription: String? {
 | 
			
		||||
      switch self {
 | 
			
		||||
      case .badValue(let key):      "Bad value for key \"\(key)\""
 | 
			
		||||
      case .unexpectedKey(let key): "Unexpected key \"\(key)\""
 | 
			
		||||
      case .required(let key):      "Missing required key \"\(key)\""
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexPackage: CustomStringConvertible {
 | 
			
		||||
  var description: String { "pkg(\(self.name))" }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fileprivate extension Optional {
 | 
			
		||||
  func unwrap<E: Error>(or error: @autoclosure () -> E) throws(E) -> Wrapped {
 | 
			
		||||
    switch self {
 | 
			
		||||
    case .some(let v):
 | 
			
		||||
      return v
 | 
			
		||||
    case .none:
 | 
			
		||||
      throw error()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								Sources/apk/Index/ApkIndexUpdate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								Sources/apk/Index/ApkIndexUpdate.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import SWCompression
 | 
			
		||||
import CryptoKit
 | 
			
		||||
 | 
			
		||||
public struct ApkIndexUpdater {
 | 
			
		||||
  var repositories: [String]
 | 
			
		||||
  var architectures: [String]
 | 
			
		||||
 | 
			
		||||
  public init() {
 | 
			
		||||
    self.repositories = [
 | 
			
		||||
      "https://dl-cdn.alpinelinux.org/alpine/v3.21/main",
 | 
			
		||||
      "https://dl-cdn.alpinelinux.org/alpine/edge/community"
 | 
			
		||||
    ]
 | 
			
		||||
    // other archs: "armhf", "armv7", "loongarch64", "ppc64le", "riscv64", "s390x", "x86"
 | 
			
		||||
    self.architectures = [ "aarch64", "x86_64" ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public func update() {
 | 
			
		||||
    let repositories = self.repositories.flatMap { repo in
 | 
			
		||||
      self.architectures.map { arch in
 | 
			
		||||
        Repository(name: repo, arch: arch)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let downloader = ApkIndexDownloader()
 | 
			
		||||
    for repo in repositories {
 | 
			
		||||
      let localIndex = URL(filePath: repo.localName)
 | 
			
		||||
#if false
 | 
			
		||||
      let shouldDownload = true
 | 
			
		||||
#else
 | 
			
		||||
      let shouldDownload = !FileManager.default.fileExists(atPath: localIndex.path())
 | 
			
		||||
#endif
 | 
			
		||||
      if shouldDownload {
 | 
			
		||||
        print("Fetching index for \"\(repo.name)\"")
 | 
			
		||||
        downloader.downloadFile(remote: repo.url, destination: localIndex)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let index: ApkIndex
 | 
			
		||||
    do {
 | 
			
		||||
      let tables = try repositories.map { try readIndex(URL(filePath: $0.localName)) }
 | 
			
		||||
      index = ApkIndex.merge(tables)
 | 
			
		||||
    } catch {
 | 
			
		||||
      fatalError(error.localizedDescription)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for package in index.packages {
 | 
			
		||||
      print(package)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private func readIndex(_ indexURL: URL) throws -> ApkIndex {
 | 
			
		||||
    let tarSignature: [TarReader.Entry]
 | 
			
		||||
    let tarRecords: [TarReader.Entry]
 | 
			
		||||
 | 
			
		||||
    let tars = try GzipArchive.multiUnarchive(  // Slow...
 | 
			
		||||
      archive: Data(contentsOf: indexURL))
 | 
			
		||||
    assert(tars.count >= 2)
 | 
			
		||||
 | 
			
		||||
    var signatureStream = MemoryInputStream(buffer: tars[0].data)
 | 
			
		||||
    tarSignature = try TarReader.read(&signatureStream)
 | 
			
		||||
    var recordsStream = MemoryInputStream(buffer: tars[1].data)
 | 
			
		||||
    tarRecords = try TarReader.read(&recordsStream)
 | 
			
		||||
 | 
			
		||||
    guard case .file(let signatureName, _) = tarSignature.first
 | 
			
		||||
    else { fatalError("Missing signature") }
 | 
			
		||||
    print(signatureName)
 | 
			
		||||
    guard let apkIndexFile = tarRecords.firstFile(name: "APKINDEX")
 | 
			
		||||
    else { fatalError("APKINDEX missing") }
 | 
			
		||||
    guard let description = tarRecords.firstFile(name: "DESCRIPTION")
 | 
			
		||||
    else { fatalError("DESCRIPTION missing") }
 | 
			
		||||
 | 
			
		||||
    let reader = TextInputStream(binaryStream: MemoryInputStream(buffer: apkIndexFile))
 | 
			
		||||
    return try ApkIndex(raw:
 | 
			
		||||
      try ApkRawIndex(lines: reader.lines))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexUpdater {
 | 
			
		||||
  struct Repository {
 | 
			
		||||
    let name: String
 | 
			
		||||
    let arch: String
 | 
			
		||||
    let discriminator: String
 | 
			
		||||
 | 
			
		||||
    private static func resolveApkIndex(_ repo: String, _ arch: String)
 | 
			
		||||
      -> String { "\(repo)/\(arch)/APKINDEX.tar.gz" }
 | 
			
		||||
    var url: URL { URL(string: Self.resolveApkIndex(self.name, self.arch))! }
 | 
			
		||||
    var localName: String { "APKINDEX.\(discriminator).tar.gz" }
 | 
			
		||||
 | 
			
		||||
    init(name repo: String, arch: String) {
 | 
			
		||||
      self.name = repo
 | 
			
		||||
      self.arch = arch
 | 
			
		||||
 | 
			
		||||
      let urlSHA1Digest = Data(Insecure.SHA1.hash(data: Data(Self.resolveApkIndex(repo, arch).utf8)))
 | 
			
		||||
      self.discriminator = urlSHA1Digest.subdata(in: 0..<3).map { String(format: "%02x", $0) }.joined()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										77
									
								
								Sources/apk/Index/ApkRawIndex.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								Sources/apk/Index/ApkRawIndex.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
struct ApkRawIndex {
 | 
			
		||||
  let packages: [ApkRawIndexEntry]
 | 
			
		||||
 | 
			
		||||
  init(lines: any Sequence<String>) throws {
 | 
			
		||||
    var packages = [ApkRawIndexEntry]()
 | 
			
		||||
 | 
			
		||||
    var recordLines = [String]()
 | 
			
		||||
    recordLines.reserveCapacity(15)
 | 
			
		||||
 | 
			
		||||
    for line in lines {
 | 
			
		||||
      if line.trimmingCharacters(in: .whitespaces).isEmpty {
 | 
			
		||||
        if !recordLines.isEmpty {
 | 
			
		||||
          packages.append(try .init(parsingEntryLines: recordLines))
 | 
			
		||||
          recordLines.removeAll(keepingCapacity: true)
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        recordLines.append(line)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if !recordLines.isEmpty {
 | 
			
		||||
      packages.append(try .init(parsingEntryLines: recordLines))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.packages = packages
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ApkRawIndexEntry {
 | 
			
		||||
  let fields: [Record]
 | 
			
		||||
 | 
			
		||||
  struct Record {
 | 
			
		||||
    let key: Character
 | 
			
		||||
    let value: String
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkRawIndexEntry {
 | 
			
		||||
  init(parsingEntryLines lines: any Sequence<String>) throws {
 | 
			
		||||
    self.fields = try lines.map { line in
 | 
			
		||||
      guard let splitIdx = line.firstIndex(of: ":"),
 | 
			
		||||
          line.distance(from: line.startIndex, to: splitIdx) == 1 else {
 | 
			
		||||
        throw ApkRawIndexError.badPair
 | 
			
		||||
      }
 | 
			
		||||
      return Record(
 | 
			
		||||
        key:   line.first!,
 | 
			
		||||
        value: String(line[line.index(after: splitIdx)...]))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func toMap() -> [Character: String] {
 | 
			
		||||
    Dictionary(uniqueKeysWithValues: self.fields.map { $0.pair })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func lookup(_ key: Character) -> String? {
 | 
			
		||||
    fields.first(where: { $0.key == key })?.value
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkRawIndexEntry.Record {
 | 
			
		||||
  var pair: (Character, String) {
 | 
			
		||||
    (self.key, self.value)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ApkRawIndexError: Error, LocalizedError {
 | 
			
		||||
  case badPair
 | 
			
		||||
 | 
			
		||||
  var errorDescription: String? {
 | 
			
		||||
    switch self {
 | 
			
		||||
    case .badPair: "Malformed raw key-value pair"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								Sources/apk/Utility/FileInputStream.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Sources/apk/Utility/FileInputStream.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public struct FileInputStream: InputStream {
 | 
			
		||||
  private var _hnd: FileHandle
 | 
			
		||||
 | 
			
		||||
  public init(_ fileURL: URL) throws {
 | 
			
		||||
    self._hnd = try FileHandle(forReadingFrom: fileURL)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public mutating func seek(_ whence: StreamWhence) throws(StreamError) {
 | 
			
		||||
    let applyOffset = { (position: UInt64, offset: Int) throws(StreamError) -> UInt64 in
 | 
			
		||||
      if offset < 0 {
 | 
			
		||||
        let (newPosition, overflow) = position.subtractingReportingOverflow(UInt64(-offset))
 | 
			
		||||
        if overflow { throw .seekRange }
 | 
			
		||||
        return newPosition
 | 
			
		||||
      } else {
 | 
			
		||||
        let (newPosition, overflow) = position.addingReportingOverflow(UInt64(offset))
 | 
			
		||||
        if overflow { throw .overflow }
 | 
			
		||||
        return newPosition
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch whence {
 | 
			
		||||
    case .set(let position):
 | 
			
		||||
      if position < 0 { throw .seekRange }
 | 
			
		||||
      do { try self._hnd.seek(toOffset: UInt64(truncatingIfNeeded: position)) }
 | 
			
		||||
      catch {
 | 
			
		||||
        throw .fileHandleError(error)
 | 
			
		||||
      }
 | 
			
		||||
    case .current(let offset):
 | 
			
		||||
      do { try self._hnd.seek(toOffset: try applyOffset(try self._hnd.offset(), offset)) }
 | 
			
		||||
      catch {
 | 
			
		||||
        if error is StreamError {
 | 
			
		||||
          throw error as! StreamError
 | 
			
		||||
        } else {
 | 
			
		||||
          throw .fileHandleError(error)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    case .end(let offset):
 | 
			
		||||
      do { try self._hnd.seek(toOffset: applyOffset(try self._hnd.seekToEnd(), offset)) }
 | 
			
		||||
      catch {
 | 
			
		||||
        if error is StreamError {
 | 
			
		||||
          throw error as! StreamError
 | 
			
		||||
        } else {
 | 
			
		||||
          throw .fileHandleError(error)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public var tell: Int {
 | 
			
		||||
    get throws(StreamError) {
 | 
			
		||||
      let offset: UInt64
 | 
			
		||||
      do { offset = try self._hnd.offset() }
 | 
			
		||||
      catch {
 | 
			
		||||
        throw .fileHandleError(error)
 | 
			
		||||
      }
 | 
			
		||||
      if offset > Int.max { throw .overflow }
 | 
			
		||||
      return Int(truncatingIfNeeded: offset)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public mutating func read(_ count: Int) throws(StreamError) -> Data {
 | 
			
		||||
    do {
 | 
			
		||||
      return try self._hnd.read(upToCount: count) ?? Data()
 | 
			
		||||
    } catch {
 | 
			
		||||
      throw .fileHandleError(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								Sources/apk/Utility/InputStream.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Sources/apk/Utility/InputStream.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public protocol InputStream: Stream, IteratorProtocol {
 | 
			
		||||
  associatedtype Element = UInt8
 | 
			
		||||
 | 
			
		||||
  mutating func read(_ count: Int) throws(StreamError) -> Data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension InputStream {
 | 
			
		||||
  mutating func read(_ size: Int, items: Int) throws(StreamError) -> Data {
 | 
			
		||||
    try self.read(size * items)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension InputStream {
 | 
			
		||||
  mutating func next() -> UInt8? {
 | 
			
		||||
    try? self.read(1).first
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								Sources/apk/Utility/MemoryInputStream.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								Sources/apk/Utility/MemoryInputStream.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public struct MemoryInputStream: InputStream {
 | 
			
		||||
  private var _buf: [UInt8]! = nil
 | 
			
		||||
  private let _sli: ArraySlice<UInt8>
 | 
			
		||||
  private let _len: Int
 | 
			
		||||
  private var _idx = 0
 | 
			
		||||
 | 
			
		||||
  public init(buffer: Data) {
 | 
			
		||||
    self._len = buffer.count
 | 
			
		||||
    self._buf = [UInt8](repeating: 0, count: self._len)
 | 
			
		||||
    self._buf.withUnsafeMutableBytes { _ = buffer.copyBytes(to: $0) }
 | 
			
		||||
    self._sli = self._buf[...]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public init(view: ArraySlice<UInt8>) {
 | 
			
		||||
    self._sli = view
 | 
			
		||||
    self._len = view.count
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public mutating func seek(_ whence: StreamWhence) throws(StreamError) {
 | 
			
		||||
    let (position, overflow) = switch whence {
 | 
			
		||||
    case .set(let position):   (position, false)
 | 
			
		||||
    case .current(let offset): self._idx.addingReportingOverflow(offset)
 | 
			
		||||
    case .end(let offset):     self._len.addingReportingOverflow(offset)
 | 
			
		||||
    }
 | 
			
		||||
    if overflow {
 | 
			
		||||
      throw .overflow
 | 
			
		||||
    } else if position < 0 {
 | 
			
		||||
      throw .seekRange
 | 
			
		||||
    } else {
 | 
			
		||||
      self._idx = position
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public var tell: Int {
 | 
			
		||||
    get throws(StreamError) {
 | 
			
		||||
      self._idx
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public mutating func read(_ count: Int) throws(StreamError) -> Data {
 | 
			
		||||
    let beg = min(self._idx, self._len)
 | 
			
		||||
    let end = min(self._idx + count, self._len)
 | 
			
		||||
    let bytes = Data(self._sli[beg..<end])
 | 
			
		||||
    self._idx += beg.distance(to: end)
 | 
			
		||||
    return bytes
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public mutating func next() -> UInt8? {
 | 
			
		||||
    if self._idx < self._len {
 | 
			
		||||
      let byte = self._sli[self._idx]
 | 
			
		||||
      self._idx += 1
 | 
			
		||||
      return byte
 | 
			
		||||
    } else {
 | 
			
		||||
      return nil
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								Sources/apk/Utility/Stream.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Sources/apk/Utility/Stream.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public protocol Stream {
 | 
			
		||||
  mutating func seek(_ whence: StreamWhence) throws(StreamError)
 | 
			
		||||
  var tell: Int { get throws(StreamError) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum StreamWhence {
 | 
			
		||||
  case set(_ position: Int)
 | 
			
		||||
  case current(_ offset: Int)
 | 
			
		||||
  case end(_ offset: Int)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum StreamError: Error, LocalizedError {
 | 
			
		||||
  case unsupported
 | 
			
		||||
  case seekRange
 | 
			
		||||
  case overflow
 | 
			
		||||
  case fileHandleError(_ error: any Error)
 | 
			
		||||
 | 
			
		||||
  public var errorDescription: String? {
 | 
			
		||||
    switch self {
 | 
			
		||||
    case .unsupported: "Unsupported operation"
 | 
			
		||||
    case .seekRange: "Seek out of range"
 | 
			
		||||
    case .overflow: "Stream position overflowed"
 | 
			
		||||
    case .fileHandleError(let error): "Error from file handle: \(error.localizedDescription)"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								Sources/apk/Utility/TarMemoryReader.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								Sources/apk/Utility/TarMemoryReader.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,121 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public struct TarReader {
 | 
			
		||||
  private static let tarBlockSize  = 512
 | 
			
		||||
  private static let tarTypeOffset = 156
 | 
			
		||||
  private static let tarNameOffset =   0
 | 
			
		||||
  private static let tarNameSize   = 100
 | 
			
		||||
  private static let tarSizeOffset = 124
 | 
			
		||||
  private static let tarSizeSize   =  12
 | 
			
		||||
 | 
			
		||||
  public enum Entry {
 | 
			
		||||
    case file(name: String, data: Data)
 | 
			
		||||
    case directory(name: String)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static func read<S: InputStream>(_ stream: inout S) throws -> [Entry] {
 | 
			
		||||
    var entries = [Entry]()
 | 
			
		||||
 | 
			
		||||
    while true {
 | 
			
		||||
      let tarBlock = try stream.read(Self.tarBlockSize)
 | 
			
		||||
      if tarBlock.isEmpty { break }
 | 
			
		||||
      if tarBlock.count < Self.tarBlockSize { throw TarError.unexpectedEndOfStream }
 | 
			
		||||
 | 
			
		||||
      let type = UnicodeScalar(tarBlock[Self.tarTypeOffset])
 | 
			
		||||
      switch type {
 | 
			
		||||
      case "0":  // Regular file
 | 
			
		||||
        // Read metadata
 | 
			
		||||
        let name = try Self.readName(tarBlock)
 | 
			
		||||
        let size = try Self.readSize(tarBlock)
 | 
			
		||||
 | 
			
		||||
        // Read file data
 | 
			
		||||
        var data = Data()
 | 
			
		||||
        var bytesRemaining = size, readAmount = 0
 | 
			
		||||
        while bytesRemaining > 0 {
 | 
			
		||||
          //FIXME: just read the whole thing at once tbh
 | 
			
		||||
          readAmount = min(bytesRemaining, Self.tarBlockSize)
 | 
			
		||||
          let block = try stream.read(readAmount)
 | 
			
		||||
          if block.count < readAmount { throw TarError.unexpectedEndOfStream }
 | 
			
		||||
          data += block
 | 
			
		||||
          bytesRemaining -= readAmount
 | 
			
		||||
        }
 | 
			
		||||
        entries.append(.file(name: name, data: data))
 | 
			
		||||
 | 
			
		||||
        // Seek to next block
 | 
			
		||||
        let seekAmount = Self.tarBlockSize - readAmount
 | 
			
		||||
        if seekAmount > 0 {
 | 
			
		||||
          try stream.seek(.current(seekAmount))
 | 
			
		||||
        }
 | 
			
		||||
      case "5":
 | 
			
		||||
        // Directory
 | 
			
		||||
        let name = try Self.readName(tarBlock)
 | 
			
		||||
        entries.append(.directory(name: name))
 | 
			
		||||
      case "\0":
 | 
			
		||||
        // Null block, might also be a legacy regular file
 | 
			
		||||
        break
 | 
			
		||||
      case "x":
 | 
			
		||||
        // Extended header block
 | 
			
		||||
        try stream.seek(.current(Self.tarBlockSize))
 | 
			
		||||
      // Symlink, Reserved, Character, Block, FIFO, Reserved, Global, ignore all these
 | 
			
		||||
      case "1", "2", "3", "4", "6", "7", "g":
 | 
			
		||||
        let size = try self.readSize(tarBlock)
 | 
			
		||||
        let blockCount = (size - 1) / Self.tarBlockSize + 1  // Compute blocks to skip
 | 
			
		||||
        try stream.seek(.current(Self.tarBlockSize * blockCount))
 | 
			
		||||
      default: throw TarError.invalidType(type: type)  // Not a TAR type
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return entries
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static func readName(_ tar: Data, offset: Int = Self.tarNameOffset) throws (TarError) -> String {
 | 
			
		||||
    var nameSize = Self.tarNameSize
 | 
			
		||||
    for i in 0...Self.tarNameSize {
 | 
			
		||||
      if tar[offset + i] == 0 {
 | 
			
		||||
        nameSize = i
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    let data = tar.subdata(in: offset..<offset + nameSize)
 | 
			
		||||
    guard let name = String(data: data, encoding: .utf8) else { throw TarError.badNameField }
 | 
			
		||||
    return name
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static func readSize(_ tar: Data, offset: Int = Self.tarSizeOffset) throws (TarError) -> Int {
 | 
			
		||||
    let sizeData = tar.subdata(in: offset..<offset + Self.tarSizeSize)
 | 
			
		||||
    let sizeEnd = sizeData.firstIndex(of: 0) ?? sizeData.endIndex  // Find the null terminator
 | 
			
		||||
    guard
 | 
			
		||||
      let sizeString = String(data: sizeData[..<sizeEnd], encoding: .ascii),
 | 
			
		||||
      let result = Int(sizeString, radix: 0o10) else { throw TarError.badSizeField }
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum TarError: Error, LocalizedError {
 | 
			
		||||
  case unexpectedEndOfStream
 | 
			
		||||
  case invalidType(type: UnicodeScalar)
 | 
			
		||||
  case badNameField, badSizeField
 | 
			
		||||
 | 
			
		||||
  public var errorDescription: String? {
 | 
			
		||||
    switch self {
 | 
			
		||||
    case .unexpectedEndOfStream: "Stream unexpectedly ended early"
 | 
			
		||||
    case .invalidType(let type): "Invalid block type \(type) found"
 | 
			
		||||
    case .badNameField: "Bad name field"
 | 
			
		||||
    case .badSizeField: "Bad size field"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension Array<TarReader.Entry> {
 | 
			
		||||
  func firstFile(name firstNameMatch: String) -> Data? {
 | 
			
		||||
    for entry in self {
 | 
			
		||||
      if case .file(let name, let data) = entry {
 | 
			
		||||
        if name == firstNameMatch {
 | 
			
		||||
          return data
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return nil
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								Sources/apk/Utility/TextInputStream.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								Sources/apk/Utility/TextInputStream.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
//FIXME: I don't like this, also SLOWWW
 | 
			
		||||
struct TextInputStream<InStream: InputStream> where InStream.Element == UInt8 {
 | 
			
		||||
  private var _stream: InStream
 | 
			
		||||
 | 
			
		||||
  public init(binaryStream: InStream) {
 | 
			
		||||
    _stream = binaryStream
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public var lines: LineSequence {
 | 
			
		||||
    LineSequence(_stream: self._stream)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public struct LineSequence: Sequence {
 | 
			
		||||
    public typealias Element = String
 | 
			
		||||
 | 
			
		||||
    fileprivate var _stream: InStream
 | 
			
		||||
 | 
			
		||||
    public struct Iterator: IteratorProtocol where InStream.Element == UInt8 {
 | 
			
		||||
      public typealias Element = String
 | 
			
		||||
 | 
			
		||||
      fileprivate init(stream: InStream) {
 | 
			
		||||
        self._stream = stream
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      private var _stream: InStream
 | 
			
		||||
      private var _utf8Decoder = UTF8()
 | 
			
		||||
      private var _scalars = [Unicode.Scalar]()
 | 
			
		||||
      private var _lastChar: UnicodeScalar = "\0"
 | 
			
		||||
      private var _eof = false
 | 
			
		||||
 | 
			
		||||
      private mutating func decodeScalarsLine() {
 | 
			
		||||
        Decode: while true {
 | 
			
		||||
          switch self._utf8Decoder.decode(&self._stream) {
 | 
			
		||||
          case .scalarValue(let value):
 | 
			
		||||
            if value == "\n" {
 | 
			
		||||
              if self._lastChar == "\n" { break }
 | 
			
		||||
              else { break Decode }
 | 
			
		||||
            } else if value == "\r" {
 | 
			
		||||
              break Decode
 | 
			
		||||
            }
 | 
			
		||||
            self._scalars.append(value)
 | 
			
		||||
            self._lastChar = value
 | 
			
		||||
          case .emptyInput:
 | 
			
		||||
            self._eof = true
 | 
			
		||||
            break Decode
 | 
			
		||||
          case .error:
 | 
			
		||||
            break Decode
 | 
			
		||||
            //FIXME: repair like the stdlib does
 | 
			
		||||
            //scalars.append(UTF8.encodedReplacementCharacter)
 | 
			
		||||
            //lastChar = UTF8.encodedReplacementCharacter
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      public mutating func next() -> String? {
 | 
			
		||||
        // Return early if we already hit the end of the stream
 | 
			
		||||
        guard !self._eof else {
 | 
			
		||||
          return nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Decode a line of scalars
 | 
			
		||||
        self.decodeScalarsLine()
 | 
			
		||||
        defer {
 | 
			
		||||
          self._scalars.removeAll(keepingCapacity: true)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Ignore the final empty newline
 | 
			
		||||
        guard !self._eof || !self._scalars.isEmpty else {
 | 
			
		||||
          return nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Convert to string and return
 | 
			
		||||
        var string = String()
 | 
			
		||||
        string.unicodeScalars.append(contentsOf: self._scalars)
 | 
			
		||||
        return string
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public func makeIterator() -> Iterator {
 | 
			
		||||
      Iterator(stream: self._stream)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -8,68 +8,9 @@ struct DarwinApkCLI: ParsableCommand {
 | 
			
		||||
    commandName: "dpk",
 | 
			
		||||
    abstract: "Command-line interface for managing packages installed via darwin-apk.",
 | 
			
		||||
    subcommands: [
 | 
			
		||||
      Install.self,
 | 
			
		||||
      Remove.self,
 | 
			
		||||
      Update.self,
 | 
			
		||||
      Upgrade.self
 | 
			
		||||
      DpkInstallCommand.self,
 | 
			
		||||
      DpkRemoveCommand.self,
 | 
			
		||||
      DpkUpdateCommand.self,
 | 
			
		||||
      DpkUpgradeCommand.self
 | 
			
		||||
    ])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension DarwinApkCLI {
 | 
			
		||||
  struct Install: ParsableCommand {
 | 
			
		||||
    static let configuration = CommandConfiguration(
 | 
			
		||||
      commandName: "add",
 | 
			
		||||
      abstract: "Install package(s) to the system.",
 | 
			
		||||
      aliases: [ "install", "i", "a" ])
 | 
			
		||||
 | 
			
		||||
    @Argument(help: "One or more package names to install to the system.")
 | 
			
		||||
    var packages: [String]
 | 
			
		||||
 | 
			
		||||
    func run() throws {
 | 
			
		||||
      print("installing \"\(packages.joined(separator: "\", \""))\"")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  struct Remove: ParsableCommand {
 | 
			
		||||
    static let configuration = CommandConfiguration(
 | 
			
		||||
      commandName: "remove",
 | 
			
		||||
      abstract: "Remove specified package(s) from the system.",
 | 
			
		||||
      aliases: [ "uninstall", "del", "rem", "r" ])
 | 
			
		||||
 | 
			
		||||
    @Argument(help: "One or more package names to uninstall from the system.")
 | 
			
		||||
    var packages: [String]
 | 
			
		||||
 | 
			
		||||
    func run() throws {
 | 
			
		||||
      print("uninstalling \"\(packages.joined(separator: "\", \""))\"")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  struct Update: ParsableCommand {
 | 
			
		||||
    static let configuration = CommandConfiguration(
 | 
			
		||||
      commandName: "update",
 | 
			
		||||
      abstract: "Update the system package repositories.",
 | 
			
		||||
      aliases: [ "u" ])
 | 
			
		||||
 | 
			
		||||
    func run() throws {
 | 
			
		||||
      print("updating package repositories")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  struct Upgrade: ParsableCommand {
 | 
			
		||||
    static let configuration = CommandConfiguration(
 | 
			
		||||
      commandName: "upgrade",
 | 
			
		||||
      abstract: "Upgrade installed packages.",
 | 
			
		||||
      aliases: [ "U" ])
 | 
			
		||||
 | 
			
		||||
    @Argument(help: "Optionally specify packages to upgrade. Otherwise upgrade all packages installed on the system.")
 | 
			
		||||
    var packages: [String] = []
 | 
			
		||||
 | 
			
		||||
    func run() throws {
 | 
			
		||||
      if packages.isEmpty {
 | 
			
		||||
        print("upgrading system")
 | 
			
		||||
      } else {
 | 
			
		||||
        print("upgrading invidual packages: \"\(packages.joined(separator: "\", \""))\"")
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								Sources/dpk-cli/Subcommands/DpkInstallCommand.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Sources/dpk-cli/Subcommands/DpkInstallCommand.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import ArgumentParser
 | 
			
		||||
 | 
			
		||||
struct DpkInstallCommand: ParsableCommand {
 | 
			
		||||
  static let configuration = CommandConfiguration(
 | 
			
		||||
    commandName: "add",
 | 
			
		||||
    abstract: "Install package(s) to the system.",
 | 
			
		||||
    aliases: [ "a", "install" ])
 | 
			
		||||
 | 
			
		||||
  @Argument(help: "One or more package names to install to the system.")
 | 
			
		||||
  var packages: [String]
 | 
			
		||||
 | 
			
		||||
  func run() throws {
 | 
			
		||||
    print("installing \"\(packages.joined(separator: "\", \""))\"")
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								Sources/dpk-cli/Subcommands/DpkRemoveCommand.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Sources/dpk-cli/Subcommands/DpkRemoveCommand.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import ArgumentParser
 | 
			
		||||
 | 
			
		||||
struct DpkRemoveCommand: ParsableCommand {
 | 
			
		||||
  static let configuration = CommandConfiguration(
 | 
			
		||||
    commandName: "remove",
 | 
			
		||||
    abstract: "Remove specified package(s) from the system.",
 | 
			
		||||
    aliases: [ "r", "rem", "del", "uninstall" ])
 | 
			
		||||
 | 
			
		||||
  @Argument(help: "One or more package(s) to uninstall from the system.")
 | 
			
		||||
  var packages: [String]
 | 
			
		||||
 | 
			
		||||
  func run() throws {
 | 
			
		||||
    print("uninstalling \"\(packages.joined(separator: "\", \""))\"")
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								Sources/dpk-cli/Subcommands/DpkUpdateCommand.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Sources/dpk-cli/Subcommands/DpkUpdateCommand.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import ArgumentParser
 | 
			
		||||
import darwin_apk
 | 
			
		||||
 | 
			
		||||
struct DpkUpdateCommand: ParsableCommand {
 | 
			
		||||
  static let configuration = CommandConfiguration(
 | 
			
		||||
    commandName: "update",
 | 
			
		||||
    abstract: "Update the system package repositories.",
 | 
			
		||||
    aliases: [ "u" ])
 | 
			
		||||
 | 
			
		||||
  func run() throws {
 | 
			
		||||
    print("Updating package repositories")
 | 
			
		||||
    var updater = ApkIndexUpdater()
 | 
			
		||||
    updater.update()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								Sources/dpk-cli/Subcommands/DpkUpgradeCommand.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Sources/dpk-cli/Subcommands/DpkUpgradeCommand.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
import ArgumentParser
 | 
			
		||||
 | 
			
		||||
struct DpkUpgradeCommand: ParsableCommand {
 | 
			
		||||
  static let configuration = CommandConfiguration(
 | 
			
		||||
    commandName: "upgrade",
 | 
			
		||||
    abstract: "Upgrade installed packages.",
 | 
			
		||||
    aliases: [ "U" ])
 | 
			
		||||
 | 
			
		||||
  @Argument(help: "Optionally specify packages to upgrade. Otherwise upgrade all packages installed on the system.")
 | 
			
		||||
  var packages: [String] = []
 | 
			
		||||
 | 
			
		||||
  func run() throws {
 | 
			
		||||
    if packages.isEmpty {
 | 
			
		||||
      print("upgrading system")
 | 
			
		||||
    } else {
 | 
			
		||||
      print("upgrading invidual packages: \"\(packages.joined(separator: "\", \""))\"")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user