mirror of
				https://github.com/GayPizzaSpecifications/darwin-apk.git
				synced 2025-11-03 15:49:37 +00:00 
			
		
		
		
	try implementing a command that can search the index for packages
This commit is contained in:
		@ -3,8 +3,8 @@
 | 
			
		||||
 * SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
struct ApkIndex {
 | 
			
		||||
  let packages: [ApkIndexPackage]
 | 
			
		||||
public struct ApkIndex {
 | 
			
		||||
  public let packages: [ApkIndexPackage]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndex {
 | 
			
		||||
@ -15,7 +15,7 @@ extension ApkIndex {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndex {
 | 
			
		||||
public extension ApkIndex {
 | 
			
		||||
  static func merge<S: Sequence>(_ tables: S) -> Self where S.Element == Self {
 | 
			
		||||
    Self.init(packages: tables.flatMap(\.packages))
 | 
			
		||||
  }
 | 
			
		||||
@ -34,7 +34,7 @@ extension ApkIndex {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndex: CustomStringConvertible {
 | 
			
		||||
  var description: String {
 | 
			
		||||
  public var description: String {
 | 
			
		||||
    self.packages.map(String.init).joined(separator: "\n")
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
struct ApkIndexDependency: Hashable {
 | 
			
		||||
public struct ApkIndexDependency: Hashable {
 | 
			
		||||
  let requirement: ApkRequirement
 | 
			
		||||
 | 
			
		||||
  init(requirement: ApkRequirement) {
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,9 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import CryptoKit
 | 
			
		||||
 | 
			
		||||
struct ApkIndexDigest {
 | 
			
		||||
  let type: DigestType
 | 
			
		||||
  let data: Data
 | 
			
		||||
public struct ApkIndexDigest {
 | 
			
		||||
  public let type: DigestType
 | 
			
		||||
  public let data: Data
 | 
			
		||||
 | 
			
		||||
  init?(type: DigestType, data: Data) {
 | 
			
		||||
    let len = switch type {
 | 
			
		||||
@ -78,24 +78,24 @@ struct ApkIndexDigest {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexDigest: Equatable, Hashable {
 | 
			
		||||
  @inlinable static func == (lhs: Self, rhs: Self) -> Bool {
 | 
			
		||||
  @inlinable public static func == (lhs: Self, rhs: Self) -> Bool {
 | 
			
		||||
    lhs.type == rhs.type && lhs.data == rhs.data
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func hash(into hasher: inout Hasher) {
 | 
			
		||||
  public func hash(into hasher: inout Hasher) {
 | 
			
		||||
    //self.type.hash(into: &hasher)
 | 
			
		||||
    self.data.hash(into: &hasher)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexDigest {
 | 
			
		||||
public extension ApkIndexDigest {
 | 
			
		||||
  enum DigestType {
 | 
			
		||||
    case md5, sha1, sha256
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexDigest.DigestType: CustomStringConvertible {
 | 
			
		||||
  var description: String {
 | 
			
		||||
  public var description: String {
 | 
			
		||||
    switch self {
 | 
			
		||||
    case .md5: "MD5"
 | 
			
		||||
    case .sha1: "SHA-1"
 | 
			
		||||
@ -107,7 +107,7 @@ extension ApkIndexDigest.DigestType: CustomStringConvertible {
 | 
			
		||||
extension ApkIndexDigest: CustomStringConvertible {
 | 
			
		||||
#if DEBUG
 | 
			
		||||
  private static let hex = Array("0123456789ABCDEF".unicodeScalars)
 | 
			
		||||
  var description: String {
 | 
			
		||||
  public var description: String {
 | 
			
		||||
    var s = "[\(self.type)] "
 | 
			
		||||
    s.reserveCapacity(10 + self.data.count * 2)
 | 
			
		||||
    Self.hex.withUnsafeBufferPointer { hp in
 | 
			
		||||
@ -120,7 +120,7 @@ extension ApkIndexDigest: CustomStringConvertible {
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  private static let hex = "0123456789ABCDEF".map(\.asciiValue!)
 | 
			
		||||
  var description: String {
 | 
			
		||||
  public var description: String {
 | 
			
		||||
    Self.hex.withUnsafeBufferPointer { hp in
 | 
			
		||||
      let hexChars = self.data.flatMap { b in
 | 
			
		||||
        [hp[Int(b >> 4)], hp[Int(b & 15)]]
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
 * SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
struct ApkIndexInstallIf: Hashable {
 | 
			
		||||
public struct ApkIndexInstallIf: Hashable {
 | 
			
		||||
  let requirement: ApkRequirement
 | 
			
		||||
 | 
			
		||||
  init(requirement: ApkRequirement) {
 | 
			
		||||
 | 
			
		||||
@ -5,26 +5,26 @@
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
struct ApkIndexPackage: Hashable {
 | 
			
		||||
  let indexChecksum: ApkIndexDigest
 | 
			
		||||
  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: [ApkIndexDependency]
 | 
			
		||||
  let provides: [ApkIndexProvides]
 | 
			
		||||
  let installIf: [ApkIndexInstallIf]
 | 
			
		||||
public struct ApkIndexPackage: Hashable {
 | 
			
		||||
  public let indexChecksum: ApkIndexDigest
 | 
			
		||||
  public let name: String
 | 
			
		||||
  public let version: String
 | 
			
		||||
  public let architecture: String?
 | 
			
		||||
  public let packageSize: UInt64
 | 
			
		||||
  public let installedSize: UInt64
 | 
			
		||||
  public let packageDescription: String
 | 
			
		||||
  public let url: String
 | 
			
		||||
  public let license: String
 | 
			
		||||
  public let origin: String?
 | 
			
		||||
  public let maintainer: String?
 | 
			
		||||
  public let buildTime: Date?
 | 
			
		||||
  public let commit: String?
 | 
			
		||||
  public let providerPriority: UInt16?
 | 
			
		||||
  public let dependencies: [ApkIndexDependency]
 | 
			
		||||
  public let provides: [ApkIndexProvides]
 | 
			
		||||
  public let installIf: [ApkIndexInstallIf]
 | 
			
		||||
 | 
			
		||||
  var downloadFilename: String { "\(self.name)-\(version).apk" }
 | 
			
		||||
  public var downloadFilename: String { "\(self.name)-\(version).apk" }
 | 
			
		||||
 | 
			
		||||
  //TODO: Implementation
 | 
			
		||||
  //lazy var semanticVersion: (Int, Int, Int) = (0, 0, 0)
 | 
			
		||||
@ -167,8 +167,16 @@ extension ApkIndexPackage {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension ApkIndexPackage {
 | 
			
		||||
  var shortDescription: String {
 | 
			
		||||
    "\(self.name)-\(self.version) \(self.architecture ?? "")\n \\_ \(self.packageDescription)"
 | 
			
		||||
    // ugrep/stable 3.11.2+dfsg-1 amd64
 | 
			
		||||
    //   faster grep with an interactive query UI
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ApkIndexPackage: CustomStringConvertible {
 | 
			
		||||
  var description: String {
 | 
			
		||||
  public var description: String {
 | 
			
		||||
    var s = String()
 | 
			
		||||
    s += "index checksum: \(self.indexChecksum)\n"
 | 
			
		||||
    s += "name: --------- \(self.name)\n"
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
 * SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
struct ApkIndexProvides: Hashable {
 | 
			
		||||
public struct ApkIndexProvides: Hashable {
 | 
			
		||||
  let name: String
 | 
			
		||||
 | 
			
		||||
  init(requirement: ApkRequirement) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								Sources/apk/Index/ApkIndexReading.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Sources/apk/Index/ApkIndexReading.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
/*
 | 
			
		||||
 * darwin-apk © 2024 Gay Pizza Specifications
 | 
			
		||||
 * SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public extension ApkIndex {
 | 
			
		||||
  init(readFrom indexURL: URL) throws {
 | 
			
		||||
    let file = try FileInputStream(indexURL)
 | 
			
		||||
    var gzip = GZipReader()
 | 
			
		||||
    var tarRecords = [TarReader.Entry]()
 | 
			
		||||
    for tarData in try (0..<2).map({ _ in try gzip.read(inStream: file) }) {
 | 
			
		||||
      let tarStream = MemoryInputStream(buffer: tarData)
 | 
			
		||||
      tarRecords += try TarReader.read(tarStream)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    guard case .file(let signatureName, _) = tarRecords.first else {
 | 
			
		||||
      throw ApkIndexReadingError.missingSignature
 | 
			
		||||
    }
 | 
			
		||||
    guard let apkIndexFile = tarRecords.firstFile(name: "APKINDEX") else {
 | 
			
		||||
      throw ApkIndexReadingError.missingIndex
 | 
			
		||||
    }
 | 
			
		||||
    guard let description = tarRecords.firstFile(name: "DESCRIPTION") else {
 | 
			
		||||
      throw ApkIndexReadingError.missingDescription
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try self.init(raw:
 | 
			
		||||
      try ApkRawIndex(lines: MemoryInputStream(buffer: apkIndexFile).lines))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum ApkIndexReadingError: Error, LocalizedError {
 | 
			
		||||
  case missingSignature
 | 
			
		||||
  case missingIndex
 | 
			
		||||
  case missingDescription
 | 
			
		||||
 | 
			
		||||
  public var errorDescription: String? {
 | 
			
		||||
    switch self {
 | 
			
		||||
    case .missingSignature:   "Missing signature"
 | 
			
		||||
    case .missingIndex:       "APKINDEX missing"
 | 
			
		||||
    case .missingDescription: "DESCRIPTION missing"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								Sources/apk/Index/ApkIndexRepository.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Sources/apk/Index/ApkIndexRepository.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
/*
 | 
			
		||||
 * darwin-apk © 2024 Gay Pizza Specifications
 | 
			
		||||
 * SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import CryptoKit
 | 
			
		||||
 | 
			
		||||
public struct ApkIndexRepository {
 | 
			
		||||
  public let name: String
 | 
			
		||||
  public let arch: String
 | 
			
		||||
  public let discriminator: String
 | 
			
		||||
 | 
			
		||||
  private static func resolveApkIndex(_ repo: String, _ arch: String)
 | 
			
		||||
    -> String { "\(repo)/\(arch)/APKINDEX.tar.gz" }
 | 
			
		||||
 | 
			
		||||
  public var url: URL {
 | 
			
		||||
    URL(string: Self.resolveApkIndex(self.name, self.arch))!
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public var localName: String { "APKINDEX.\(discriminator).tar.gz" }
 | 
			
		||||
 | 
			
		||||
  public 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()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -4,7 +4,6 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import CryptoKit
 | 
			
		||||
 | 
			
		||||
public struct ApkIndexUpdater {
 | 
			
		||||
  var repositories: [String]
 | 
			
		||||
@ -22,7 +21,7 @@ public struct ApkIndexUpdater {
 | 
			
		||||
  public func update() {
 | 
			
		||||
    let repositories = self.repositories.flatMap { repo in
 | 
			
		||||
      self.architectures.map { arch in
 | 
			
		||||
        Repository(name: repo, arch: arch)
 | 
			
		||||
        ApkIndexRepository(name: repo, arch: arch)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -107,24 +106,3 @@ public struct ApkIndexUpdater {
 | 
			
		||||
      try ApkRawIndex(lines: MemoryInputStream(buffer: apkIndexFile).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()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@
 | 
			
		||||
import ArgumentParser
 | 
			
		||||
 | 
			
		||||
@main
 | 
			
		||||
struct DarwinApkCLI: ParsableCommand {
 | 
			
		||||
struct DarwinApkCLI: AsyncParsableCommand {
 | 
			
		||||
  static let configuration = CommandConfiguration(
 | 
			
		||||
    commandName: "dpk",
 | 
			
		||||
    abstract: "Command-line interface for managing packages installed via darwin-apk.",
 | 
			
		||||
@ -14,6 +14,7 @@ struct DarwinApkCLI: ParsableCommand {
 | 
			
		||||
      DpkInstallCommand.self,
 | 
			
		||||
      DpkRemoveCommand.self,
 | 
			
		||||
      DpkUpdateCommand.self,
 | 
			
		||||
      DpkUpgradeCommand.self
 | 
			
		||||
      DpkUpgradeCommand.self,
 | 
			
		||||
      DpkSearchCommand.self
 | 
			
		||||
    ])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										83
									
								
								Sources/dpk-cli/Subcommands/DpkSearchCommand.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								Sources/dpk-cli/Subcommands/DpkSearchCommand.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
/*
 | 
			
		||||
 * darwin-apk © 2024 Gay Pizza Specifications
 | 
			
		||||
 * SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import ArgumentParser
 | 
			
		||||
import darwin_apk
 | 
			
		||||
 | 
			
		||||
struct DpkSearchCommand: AsyncParsableCommand {
 | 
			
		||||
  static let configuration = CommandConfiguration(
 | 
			
		||||
    commandName: "search",
 | 
			
		||||
    abstract: "Search for packages with a pattern matching name and description",
 | 
			
		||||
    aliases: [ "s" ])
 | 
			
		||||
 | 
			
		||||
  @Flag
 | 
			
		||||
  var nameOnly: Bool = false
 | 
			
		||||
 | 
			
		||||
  @Argument
 | 
			
		||||
  var patterns: [String]
 | 
			
		||||
 | 
			
		||||
  func run() async throws(ExitCode) {
 | 
			
		||||
    let re: [Regex<_StringProcessing.AnyRegexOutput>]
 | 
			
		||||
    do {
 | 
			
		||||
      re = try patterns.map(Regex.init)
 | 
			
		||||
    } catch {
 | 
			
		||||
      print("Bad pattern \(error.localizedDescription)")
 | 
			
		||||
      throw .validationFailure
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let repositories: [String], architectures: [String]
 | 
			
		||||
    do {
 | 
			
		||||
      repositories = try await PropertyFile.read(name: "repositories")
 | 
			
		||||
    } catch {
 | 
			
		||||
      print("Failed to read repositories: \(error.localizedDescription)")
 | 
			
		||||
      throw .failure
 | 
			
		||||
    }
 | 
			
		||||
    do {
 | 
			
		||||
      architectures = try await PropertyFile.read(name: "arch")
 | 
			
		||||
    } catch {
 | 
			
		||||
      print("Failed to read arch: \(error.localizedDescription)")
 | 
			
		||||
      throw .failure
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let localRepositories = repositories.flatMap { repo in
 | 
			
		||||
      architectures.map { arch in
 | 
			
		||||
        URL(filePath: ApkIndexRepository(name: repo, arch: arch).localName, directoryHint: .notDirectory)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    let index: ApkIndex
 | 
			
		||||
    do {
 | 
			
		||||
      index = ApkIndex.merge(try localRepositories.map(ApkIndex.init))
 | 
			
		||||
    } catch {
 | 
			
		||||
      print("Failed to build package index: \(error.localizedDescription)")
 | 
			
		||||
      throw .failure
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    do {
 | 
			
		||||
      for package in index.packages {
 | 
			
		||||
        for pattern in re {
 | 
			
		||||
          if try
 | 
			
		||||
              pattern.firstMatch(in: package.name) != nil ||
 | 
			
		||||
              (!self.nameOnly && pattern.firstMatch(in: package.packageDescription) != nil) {
 | 
			
		||||
            print(package.shortDescription)
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch {
 | 
			
		||||
      print("Something went wrong: \(error.localizedDescription)")
 | 
			
		||||
      throw .failure
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PropertyFile {
 | 
			
		||||
  static func read(name: String) async throws -> [String] {
 | 
			
		||||
    try await URL(filePath: name, directoryHint: .notDirectory).lines
 | 
			
		||||
      .map { $0.trimmingCharacters(in: .whitespaces) }
 | 
			
		||||
      .filter { !$0.isEmpty && $0.first != "#" }  // Ignore empty & commented lines
 | 
			
		||||
      .reduce(into: [String]()) { $0.append($1) }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user