mirror of
				https://github.com/GayPizzaSpecifications/darwin-apk.git
				synced 2025-11-03 23:49: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
 | 
					/Packages
 | 
				
			||||||
xcuserdata/
 | 
					xcuserdata/
 | 
				
			||||||
DerivedData/
 | 
					DerivedData/
 | 
				
			||||||
.swiftpm/configuration/registries.json
 | 
					.swiftpm/
 | 
				
			||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
 | 
					 | 
				
			||||||
.netrc
 | 
					.netrc
 | 
				
			||||||
Package.resolved
 | 
					Package.resolved
 | 
				
			||||||
 | 
				
			|||||||
@ -3,13 +3,24 @@ import PackageDescription
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
let package = Package(
 | 
					let package = Package(
 | 
				
			||||||
  name: "darwin-apk",
 | 
					  name: "darwin-apk",
 | 
				
			||||||
 | 
					  platforms: [
 | 
				
			||||||
 | 
					    .macOS(.v13),
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
  dependencies: [
 | 
					  dependencies: [
 | 
				
			||||||
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
 | 
					    .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: [
 | 
					  targets: [
 | 
				
			||||||
 | 
					    .target(
 | 
				
			||||||
 | 
					      name: "darwin-apk",
 | 
				
			||||||
 | 
					      dependencies: [
 | 
				
			||||||
 | 
					        .product(name: "SWCompression", package: "SWCompression"),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      path: "Sources/apk"),
 | 
				
			||||||
    .executableTarget(
 | 
					    .executableTarget(
 | 
				
			||||||
      name: "dpk",
 | 
					      name: "dpk",
 | 
				
			||||||
      dependencies: [
 | 
					      dependencies: [
 | 
				
			||||||
 | 
					        "darwin-apk",
 | 
				
			||||||
        .product(name: "ArgumentParser", package: "swift-argument-parser"),
 | 
					        .product(name: "ArgumentParser", package: "swift-argument-parser"),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      path: "Sources/dpk-cli"
 | 
					      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",
 | 
					    commandName: "dpk",
 | 
				
			||||||
    abstract: "Command-line interface for managing packages installed via darwin-apk.",
 | 
					    abstract: "Command-line interface for managing packages installed via darwin-apk.",
 | 
				
			||||||
    subcommands: [
 | 
					    subcommands: [
 | 
				
			||||||
      Install.self,
 | 
					      DpkInstallCommand.self,
 | 
				
			||||||
      Remove.self,
 | 
					      DpkRemoveCommand.self,
 | 
				
			||||||
      Update.self,
 | 
					      DpkUpdateCommand.self,
 | 
				
			||||||
      Upgrade.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