diff --git a/Package.swift b/Package.swift index 63f4947..1e5ab52 100644 --- a/Package.swift +++ b/Package.swift @@ -8,12 +8,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), .package(url: "https://github.com/tsolomko/SWCompression", from: "4.8.6"), ], targets: [ .target( name: "darwin-apk", dependencies: [ + .product(name: "Algorithms", package: "swift-algorithms"), .product(name: "SWCompression", package: "SWCompression"), ], path: "Sources/apk"), diff --git a/Sources/apk/Index/ApkIndex.swift b/Sources/apk/Index/ApkIndex.swift index 4ec800a..e0d225b 100644 --- a/Sources/apk/Index/ApkIndex.swift +++ b/Sources/apk/Index/ApkIndex.swift @@ -29,3 +29,9 @@ extension ApkIndex { } } } + +extension ApkIndex: CustomStringConvertible { + var description: String { + self.packages.map(String.init).joined(separator: "\n") + } +} diff --git a/Sources/apk/Index/ApkIndexDigest.swift b/Sources/apk/Index/ApkIndexDigest.swift new file mode 100644 index 0000000..52be76d --- /dev/null +++ b/Sources/apk/Index/ApkIndexDigest.swift @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Algorithms +import CryptoKit + +struct ApkIndexDigest { + let type: DigestType + let data: Data + + init?(type: DigestType, data: Data) { + let len = switch type { + case .md5: 16 + case .sha1: 20 + case .sha256: 32 + } + guard len == data.count else { + return nil + } + self.type = type + self.data = data + } + + init?(decode: String) { + enum Encoding { case hex, base64 } + let getEncoding = { (c: Character) -> Encoding? in + switch c { + case "Q": .base64 + case "X": .hex + default: nil + } + } + let getDigestType = { (c: Character) -> DigestType? in + switch c { + case "1": .sha1 + case "2": .sha256 + default: nil + } + } + + let hexData = { (from: Substring) -> Data? in + // Ensure even number of characters + guard from.count & ~0x1 == from.count else { return nil } + //FIXME: will explode on encountering non-hexadecimal characters, works for now tho + return Data(from.map(\.hexDigitValue).lazy + .chunks(ofCount: 2) + .map { UInt8($0.last!! + $0.first!! << 4) }) + } + + if decode.count < 2 { + return nil + } else if _slowPath(decode.first!.isHexDigit) { + // Legacy MD5 hex digest mode + guard decode.count != 32, let decoded = hexData(decode[...]) else { + return nil + } + self.init(type: .md5, data: decoded) + } else { + // First two characters are a letter for the encoding type: + // - 'X': hex digest + // - 'Q': base64 string + // ...and a number for the hash digest type: + // - '1': SHA-1 + // - '2': SHA-256 (SHA-2) + guard + let encoding = getEncoding(decode.first!), + let digest = getDigestType(decode[decode.index(after: decode.startIndex)]) + else { return nil } + let dataString = decode[decode.index(decode.startIndex, offsetBy: 2)...] + + // The remaining characters are the encoded digest + var decoded: Data? = nil + if _fastPath(encoding == .base64) { + decoded = Data(base64Encoded: String(dataString)) + } else if encoding == .hex { + decoded = hexData(dataString) + } + + guard let decoded = decoded else { + return nil + } + self.init(type: digest, data: decoded) + } + } +} + +extension ApkIndexDigest: Equatable, Hashable { + @inlinable static func == (lhs: Self, rhs: Self) -> Bool { + lhs.type == rhs.type && lhs.data == rhs.data + } + + func hash(into hasher: inout Hasher) { + //self.type.hash(into: &hasher) + self.data.hash(into: &hasher) + } +} + +extension ApkIndexDigest { + enum DigestType { + case md5, sha1, sha256 + } +} + +extension ApkIndexDigest.DigestType: CustomStringConvertible { + var description: String { + switch self { + case .md5: "MD5" + case .sha1: "SHA-1" + case .sha256: "SHA-256" + } + } +} + +extension ApkIndexDigest: CustomStringConvertible { + var description: String { "[\(self.type)] \(self.data.map { String(format: "%02X", $0) }.joined())" } +} diff --git a/Sources/apk/Index/ApkIndexPackage.swift b/Sources/apk/Index/ApkIndexPackage.swift index 5ac68de..818b590 100644 --- a/Sources/apk/Index/ApkIndexPackage.swift +++ b/Sources/apk/Index/ApkIndexPackage.swift @@ -3,7 +3,7 @@ import Foundation struct ApkIndexPackage: Hashable { - let indexChecksum: String //TODO: Decode cus why not + let indexChecksum: ApkIndexDigest let name: String let version: String let architecture: String? @@ -30,7 +30,7 @@ struct ApkIndexPackage: Hashable { extension ApkIndexPackage { init(raw rawEntry: ApkRawIndexEntry) throws(Self.ParseError) { // Required fields - var indexChecksum: String? = nil + var indexChecksum: ApkIndexDigest? = nil var name: String? = nil var version: String? = nil var description: String? = nil @@ -70,7 +70,10 @@ extension ApkIndexPackage { do { dependencies = try ApkIndexDependency.extract(record.value) } catch { throw .badValue(key: record.key) } case "C": - indexChecksum = record.value // base64-encoded SHA1 hash prefixed with "Q1" + guard let digest = ApkIndexDigest(decode: record.value) else { + throw .badValue(key: record.key) + } + indexChecksum = digest case "S": guard let value = UInt64(record.value, radix: 10) else { throw .badValue(key: record.key) @@ -152,7 +155,45 @@ extension ApkIndexPackage { } extension ApkIndexPackage: CustomStringConvertible { - var description: String { "pkg(\(self.name))" } + var description: String { + var s = String() + s += "index checksum: \(self.indexChecksum)\n" + s += "name: --------- \(self.name)\n" + s += "version: ------ \(self.version)\n" + if let architecture = self.architecture { + s += "architecture: - \(architecture)\n" + } + s += "package size: - \(self.packageSize) byte(s) (\(self.packageSize.formatted(.byteCount(style: .file))))\n" + s += "installed size: \(self.installedSize) byte(s) (\(self.installedSize.formatted(.byteCount(style: .file))))\n" + s += "description: -- \(self.packageDescription)\n" + s += "url: ---------- \(self.url)\n" + s += "license: ------ \(self.license)\n" + if let origin = self.origin { + s += "origin: ------- \(origin)\n" + } + if let maintainer = self.maintainer { + s += "maintainer: --- \(maintainer)\n" + } + if let buildTime = self.buildTime { + s += "build time: --- \(buildTime)\n" + } + if let commit = self.commit { + s += "commit: ------- \(commit)\n" + } + if let providerPrio = self.providerPriority { + s += "provider prio: \(providerPrio)\n" + } + if !self.dependencies.isEmpty { + s += "dependencies: - \(self.dependencies.map(String.init).joined(separator: " "))\n" + } + if !self.provides.isEmpty { + s += "provides: ----- \(self.provides.map { $0.name }.joined(separator: " "))\n" + } + if !self.installIf.isEmpty { + s += "install if: --- \(self.installIf.map { $0.name }.joined(separator: " "))\n" + } + return s + } } fileprivate extension Optional { diff --git a/Sources/apk/Index/ApkIndexUpdate.swift b/Sources/apk/Index/ApkIndexUpdate.swift index caadb65..6358329 100644 --- a/Sources/apk/Index/ApkIndexUpdate.swift +++ b/Sources/apk/Index/ApkIndexUpdate.swift @@ -42,13 +42,10 @@ public struct ApkIndexUpdater { do { let tables = try repositories.map { try readIndex(URL(filePath: $0.localName)) } index = ApkIndex.merge(tables) + try index.description.write(to: URL(fileURLWithPath: "packages.txt"), atomically: false, encoding: .utf8) } catch { fatalError(error.localizedDescription) } - - for package in index.packages { - print("\(package.name):", package.dependencies) - } } private func readIndex(_ indexURL: URL) throws -> ApkIndex {