diff --git a/Sources/apk/Index/ApkIndex.swift b/Sources/apk/Index/ApkIndex.swift index 76ff75f..e5c79ef 100644 --- a/Sources/apk/Index/ApkIndex.swift +++ b/Sources/apk/Index/ApkIndex.swift @@ -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(_ 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") } } diff --git a/Sources/apk/Index/ApkIndexDependency.swift b/Sources/apk/Index/ApkIndexDependency.swift index 740cc0f..6eabeb0 100644 --- a/Sources/apk/Index/ApkIndexDependency.swift +++ b/Sources/apk/Index/ApkIndexDependency.swift @@ -5,7 +5,7 @@ import Foundation -struct ApkIndexDependency: Hashable { +public struct ApkIndexDependency: Hashable { let requirement: ApkRequirement init(requirement: ApkRequirement) { diff --git a/Sources/apk/Index/ApkIndexDigest.swift b/Sources/apk/Index/ApkIndexDigest.swift index 2de5798..032a43c 100644 --- a/Sources/apk/Index/ApkIndexDigest.swift +++ b/Sources/apk/Index/ApkIndexDigest.swift @@ -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)]] diff --git a/Sources/apk/Index/ApkIndexInstallIf.swift b/Sources/apk/Index/ApkIndexInstallIf.swift index 7cd382b..9b0e353 100644 --- a/Sources/apk/Index/ApkIndexInstallIf.swift +++ b/Sources/apk/Index/ApkIndexInstallIf.swift @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -struct ApkIndexInstallIf: Hashable { +public struct ApkIndexInstallIf: Hashable { let requirement: ApkRequirement init(requirement: ApkRequirement) { diff --git a/Sources/apk/Index/ApkIndexPackage.swift b/Sources/apk/Index/ApkIndexPackage.swift index 3c5952c..462aff1 100644 --- a/Sources/apk/Index/ApkIndexPackage.swift +++ b/Sources/apk/Index/ApkIndexPackage.swift @@ -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" diff --git a/Sources/apk/Index/ApkIndexProvides.swift b/Sources/apk/Index/ApkIndexProvides.swift index 759ee11..0900887 100644 --- a/Sources/apk/Index/ApkIndexProvides.swift +++ b/Sources/apk/Index/ApkIndexProvides.swift @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -struct ApkIndexProvides: Hashable { +public struct ApkIndexProvides: Hashable { let name: String init(requirement: ApkRequirement) { diff --git a/Sources/apk/Index/ApkIndexReading.swift b/Sources/apk/Index/ApkIndexReading.swift new file mode 100644 index 0000000..f0231c1 --- /dev/null +++ b/Sources/apk/Index/ApkIndexReading.swift @@ -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" + } + } +} diff --git a/Sources/apk/Index/ApkIndexRepository.swift b/Sources/apk/Index/ApkIndexRepository.swift new file mode 100644 index 0000000..3a7dc4c --- /dev/null +++ b/Sources/apk/Index/ApkIndexRepository.swift @@ -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() + } +} diff --git a/Sources/apk/Index/ApkIndexUpdate.swift b/Sources/apk/Index/ApkIndexUpdate.swift index 924fafe..2fe0688 100644 --- a/Sources/apk/Index/ApkIndexUpdate.swift +++ b/Sources/apk/Index/ApkIndexUpdate.swift @@ -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() - } - } -} diff --git a/Sources/dpk-cli/CommandLine.swift b/Sources/dpk-cli/CommandLine.swift index daa1300..d8dcba7 100644 --- a/Sources/dpk-cli/CommandLine.swift +++ b/Sources/dpk-cli/CommandLine.swift @@ -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 ]) } diff --git a/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift b/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift new file mode 100644 index 0000000..cb37b31 --- /dev/null +++ b/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift @@ -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) } + } +}