mirror of
https://github.com/GayPizzaSpecifications/darwin-apk.git
synced 2025-08-03 21:41:31 +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
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
struct ApkIndex {
|
public struct ApkIndex {
|
||||||
let packages: [ApkIndexPackage]
|
public let packages: [ApkIndexPackage]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ApkIndex {
|
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 {
|
static func merge<S: Sequence>(_ tables: S) -> Self where S.Element == Self {
|
||||||
Self.init(packages: tables.flatMap(\.packages))
|
Self.init(packages: tables.flatMap(\.packages))
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ extension ApkIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ApkIndex: CustomStringConvertible {
|
extension ApkIndex: CustomStringConvertible {
|
||||||
var description: String {
|
public var description: String {
|
||||||
self.packages.map(String.init).joined(separator: "\n")
|
self.packages.map(String.init).joined(separator: "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct ApkIndexDependency: Hashable {
|
public struct ApkIndexDependency: Hashable {
|
||||||
let requirement: ApkRequirement
|
let requirement: ApkRequirement
|
||||||
|
|
||||||
init(requirement: ApkRequirement) {
|
init(requirement: ApkRequirement) {
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
struct ApkIndexDigest {
|
public struct ApkIndexDigest {
|
||||||
let type: DigestType
|
public let type: DigestType
|
||||||
let data: Data
|
public let data: Data
|
||||||
|
|
||||||
init?(type: DigestType, data: Data) {
|
init?(type: DigestType, data: Data) {
|
||||||
let len = switch type {
|
let len = switch type {
|
||||||
@ -78,24 +78,24 @@ struct ApkIndexDigest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ApkIndexDigest: Equatable, Hashable {
|
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
|
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.type.hash(into: &hasher)
|
||||||
self.data.hash(into: &hasher)
|
self.data.hash(into: &hasher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ApkIndexDigest {
|
public extension ApkIndexDigest {
|
||||||
enum DigestType {
|
enum DigestType {
|
||||||
case md5, sha1, sha256
|
case md5, sha1, sha256
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ApkIndexDigest.DigestType: CustomStringConvertible {
|
extension ApkIndexDigest.DigestType: CustomStringConvertible {
|
||||||
var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .md5: "MD5"
|
case .md5: "MD5"
|
||||||
case .sha1: "SHA-1"
|
case .sha1: "SHA-1"
|
||||||
@ -107,7 +107,7 @@ extension ApkIndexDigest.DigestType: CustomStringConvertible {
|
|||||||
extension ApkIndexDigest: CustomStringConvertible {
|
extension ApkIndexDigest: CustomStringConvertible {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private static let hex = Array("0123456789ABCDEF".unicodeScalars)
|
private static let hex = Array("0123456789ABCDEF".unicodeScalars)
|
||||||
var description: String {
|
public var description: String {
|
||||||
var s = "[\(self.type)] "
|
var s = "[\(self.type)] "
|
||||||
s.reserveCapacity(10 + self.data.count * 2)
|
s.reserveCapacity(10 + self.data.count * 2)
|
||||||
Self.hex.withUnsafeBufferPointer { hp in
|
Self.hex.withUnsafeBufferPointer { hp in
|
||||||
@ -120,7 +120,7 @@ extension ApkIndexDigest: CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
private static let hex = "0123456789ABCDEF".map(\.asciiValue!)
|
private static let hex = "0123456789ABCDEF".map(\.asciiValue!)
|
||||||
var description: String {
|
public var description: String {
|
||||||
Self.hex.withUnsafeBufferPointer { hp in
|
Self.hex.withUnsafeBufferPointer { hp in
|
||||||
let hexChars = self.data.flatMap { b in
|
let hexChars = self.data.flatMap { b in
|
||||||
[hp[Int(b >> 4)], hp[Int(b & 15)]]
|
[hp[Int(b >> 4)], hp[Int(b & 15)]]
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
struct ApkIndexInstallIf: Hashable {
|
public struct ApkIndexInstallIf: Hashable {
|
||||||
let requirement: ApkRequirement
|
let requirement: ApkRequirement
|
||||||
|
|
||||||
init(requirement: ApkRequirement) {
|
init(requirement: ApkRequirement) {
|
||||||
|
@ -5,26 +5,26 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct ApkIndexPackage: Hashable {
|
public struct ApkIndexPackage: Hashable {
|
||||||
let indexChecksum: ApkIndexDigest
|
public let indexChecksum: ApkIndexDigest
|
||||||
let name: String
|
public let name: String
|
||||||
let version: String
|
public let version: String
|
||||||
let architecture: String?
|
public let architecture: String?
|
||||||
let packageSize: UInt64
|
public let packageSize: UInt64
|
||||||
let installedSize: UInt64
|
public let installedSize: UInt64
|
||||||
let packageDescription: String
|
public let packageDescription: String
|
||||||
let url: String
|
public let url: String
|
||||||
let license: String
|
public let license: String
|
||||||
let origin: String?
|
public let origin: String?
|
||||||
let maintainer: String?
|
public let maintainer: String?
|
||||||
let buildTime: Date?
|
public let buildTime: Date?
|
||||||
let commit: String?
|
public let commit: String?
|
||||||
let providerPriority: UInt16?
|
public let providerPriority: UInt16?
|
||||||
let dependencies: [ApkIndexDependency]
|
public let dependencies: [ApkIndexDependency]
|
||||||
let provides: [ApkIndexProvides]
|
public let provides: [ApkIndexProvides]
|
||||||
let installIf: [ApkIndexInstallIf]
|
public let installIf: [ApkIndexInstallIf]
|
||||||
|
|
||||||
var downloadFilename: String { "\(self.name)-\(version).apk" }
|
public var downloadFilename: String { "\(self.name)-\(version).apk" }
|
||||||
|
|
||||||
//TODO: Implementation
|
//TODO: Implementation
|
||||||
//lazy var semanticVersion: (Int, Int, Int) = (0, 0, 0)
|
//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 {
|
extension ApkIndexPackage: CustomStringConvertible {
|
||||||
var description: String {
|
public var description: String {
|
||||||
var s = String()
|
var s = String()
|
||||||
s += "index checksum: \(self.indexChecksum)\n"
|
s += "index checksum: \(self.indexChecksum)\n"
|
||||||
s += "name: --------- \(self.name)\n"
|
s += "name: --------- \(self.name)\n"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
struct ApkIndexProvides: Hashable {
|
public struct ApkIndexProvides: Hashable {
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
init(requirement: ApkRequirement) {
|
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 Foundation
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
public struct ApkIndexUpdater {
|
public struct ApkIndexUpdater {
|
||||||
var repositories: [String]
|
var repositories: [String]
|
||||||
@ -22,7 +21,7 @@ public struct ApkIndexUpdater {
|
|||||||
public func update() {
|
public func update() {
|
||||||
let repositories = self.repositories.flatMap { repo in
|
let repositories = self.repositories.flatMap { repo in
|
||||||
self.architectures.map { arch 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))
|
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
|
import ArgumentParser
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct DarwinApkCLI: ParsableCommand {
|
struct DarwinApkCLI: AsyncParsableCommand {
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
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.",
|
||||||
@ -14,6 +14,7 @@ struct DarwinApkCLI: ParsableCommand {
|
|||||||
DpkInstallCommand.self,
|
DpkInstallCommand.self,
|
||||||
DpkRemoveCommand.self,
|
DpkRemoveCommand.self,
|
||||||
DpkUpdateCommand.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