Implement dependency wrapping & version spec

This commit is contained in:
2024-11-10 03:30:55 +11:00
parent 941dfae317
commit 5e4cf1bbc9
8 changed files with 248 additions and 11 deletions

View File

@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
import Foundation
internal struct ApkRequirement {
static func extract(blob extract: String) throws(ParseError) -> (String, ApkVersionSpecification) {
var comparer: ComparatorBits = []
var dependStr = extract[...]
let nameEnd: String.Index, versionStart: String.Index
// Check for bang prefix to indicate a conflict
if dependStr.first == "!" {
comparer.insert(.conflict)
dependStr = dependStr[dependStr.index(after: dependStr.startIndex)...]
}
// Match comparator
if let range = dependStr.firstRange(where: { [ "<", "=", ">", "~" ].contains($0) }) {
for c in dependStr[range] {
switch c {
case "<": comparer.insert(.less)
case "=": comparer.insert(.equals)
case ">": comparer.insert(.greater)
case "~": comparer.formUnion([ .fuzzy, .equals ])
default: break
}
}
(nameEnd, versionStart) = (range.lowerBound, range.upperBound)
} else {
//
if !comparer.contains(.conflict) {
comparer.formUnion(.any)
}
(nameEnd, versionStart) = (dependStr.endIndex, dependStr.endIndex)
}
// Parse version specification
let spec = try ApkVersionSpecification(comparer, version: dependStr[versionStart...])
let name = String(dependStr[..<nameEnd])
return (name, spec)
}
}
extension ApkRequirement {
enum ParseError: Error, LocalizedError {
case brokenSpec
var errorDescription: String? {
switch self {
case .brokenSpec: "Invalid version specification"
}
}
}
}
//MARK: - Private Implementation
fileprivate extension ApkRequirement {
struct ComparatorBits: OptionSet {
let rawValue: UInt8
static let equals: Self = Self(rawValue: 1 << 0)
static let less: Self = Self(rawValue: 1 << 1)
static let greater: Self = Self(rawValue: 1 << 2)
static let fuzzy: Self = Self(rawValue: 1 << 3)
static let conflict: Self = Self(rawValue: 1 << 4)
static let any: Self = [ .equals, .less, .greater ]
static let checksum: Self = [ .less, .greater ]
}
}
fileprivate extension ApkVersionSpecification {
init(_ bits: ApkRequirement.ComparatorBits, version: Substring) throws(ApkRequirement.ParseError) {
if bits == [ .conflict ] {
self = .conflict
} else {
if bits.contains(.conflict) {
throw .brokenSpec
} else if bits == [ .any ] {
self = .any
} else {
self = .constraint(op: try .init(bits), version: String(version))
}
}
}
}
fileprivate extension ApkVersionSpecification.Operator {
init(_ bits: ApkRequirement.ComparatorBits) throws(ApkRequirement.ParseError) {
self = switch bits.subtracting(.conflict) {
case .equals: .equals
case .less: .less
case .greater: .greater
//case .checksum: .checksum
case [ .equals, .less ]: .lessEqual
case [ .equals, .greater ]: .greaterEqual
case [ .fuzzy, .equals ], .fuzzy: .fuzzyEquals
case [ .fuzzy, .equals, .less]: .lessFuzzy
case [ .fuzzy, .equals, .greater]: .greaterFuzzy
default: throw .brokenSpec
}
}
}
fileprivate extension Substring {
func firstRange(where predicate: (Character) throws -> Bool) rethrows -> Range<Self.Index>? {
guard let start = try self.firstIndex(where: predicate) else {
return nil
}
var idx = start
repeat {
idx = self.index(after: idx)
} while try idx != self.endIndex && predicate(self[idx])
return start..<idx
}
}

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
enum ApkVersionSpecification: Equatable {
case any
case constraint(op: Operator, version: String)
case conflict
}
extension ApkVersionSpecification: CustomStringConvertible {
var description: String {
switch self {
case .any: ""
case .conflict: "!"
case .constraint(let op, let version): "\(op)\(version)"
}
}
}
extension ApkVersionSpecification {
enum Operator: Equatable {
case equals
case fuzzyEquals
case greater
case less
case greaterEqual
case lessEqual
case greaterFuzzy
case lessFuzzy
}
}
extension ApkVersionSpecification.Operator: CustomStringConvertible {
var description: String {
switch self {
//case .checksum: "><"
case .lessEqual: "<="
case .greaterEqual: ">="
case .lessFuzzy: "<~"
case .greaterFuzzy: ">~"
case .equals: "="
case .less: "<"
case .greater: ">"
case .fuzzyEquals: "~"
}
}
}

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
import Foundation
struct ApkIndexDependency: ApkIndexRequirementRef {
let name: String
let versionSpec: ApkVersionSpecification
init(extract: String) throws(ApkRequirement.ParseError) {
(self.name, self.versionSpec) = try ApkRequirement.extract(blob: extract)
}
}
extension ApkIndexDependency: CustomStringConvertible {
var description: String {
switch self.versionSpec {
case .any: self.name
case .conflict: "!\(self.name)"
case .constraint(let op, let version): "\(self.name)\(op)\(version)"
}
}
}

View File

@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
struct ApkIndexInstallIf: ApkIndexRequirementRef {
let name: String
let versionSpec: ApkVersionSpecification
init(extract: String) throws(ApkRequirement.ParseError) {
(self.name, self.versionSpec) = try ApkRequirement.extract(blob: extract)
}
}

View File

@ -2,7 +2,7 @@
import Foundation import Foundation
struct ApkIndexPackage { struct ApkIndexPackage: Hashable {
let indexChecksum: String //TODO: Decode cus why not let indexChecksum: String //TODO: Decode cus why not
let name: String let name: String
let version: String let version: String
@ -17,9 +17,9 @@ struct ApkIndexPackage {
let buildTime: Date? let buildTime: Date?
let commit: String? let commit: String?
let providerPriority: UInt16? let providerPriority: UInt16?
let dependencies: [String] //TODO: stuff let dependencies: [ApkIndexDependency]
let provides: [String] //TODO: stuff let provides: [ApkIndexProvides]
let installIf: [String] //TODO: stuff let installIf: [ApkIndexInstallIf]
var downloadFilename: String { "\(self.name)-\(version).apk" } var downloadFilename: String { "\(self.name)-\(version).apk" }
@ -39,9 +39,9 @@ extension ApkIndexPackage {
var packageSize: UInt64? = nil var packageSize: UInt64? = nil
var installedSize: UInt64? = nil var installedSize: UInt64? = nil
var dependencies = [String]() var dependencies = [ApkIndexDependency]()
var provides = [String]() var provides = [ApkIndexProvides]()
var installIf = [String]() var installIf = [ApkIndexInstallIf]()
// Optional fields // Optional fields
var architecture: String? = nil var architecture: String? = nil
@ -67,7 +67,8 @@ extension ApkIndexPackage {
case "A": case "A":
architecture = record.value architecture = record.value
case "D": case "D":
dependencies = record.value.components(separatedBy: " ") do { dependencies = try ApkIndexDependency.extract(record.value) }
catch { throw .badValue(key: record.key) }
case "C": case "C":
indexChecksum = record.value // base64-encoded SHA1 hash prefixed with "Q1" indexChecksum = record.value // base64-encoded SHA1 hash prefixed with "Q1"
case "S": case "S":
@ -81,9 +82,11 @@ extension ApkIndexPackage {
} }
installedSize = value installedSize = value
case "p": case "p":
provides = record.value.components(separatedBy: " ") do { provides = try ApkIndexProvides.extract(record.value) }
catch { throw .badValue(key: record.key) }
case "i": case "i":
installIf = record.value.components(separatedBy: " ") do { installIf = try ApkIndexInstallIf.extract(record.value) }
catch { throw .badValue(key: record.key) }
case "o": case "o":
origin = record.value origin = record.value
case "m": case "m":

View File

@ -0,0 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
struct ApkIndexProvides: ApkIndexRequirementRef {
let name: String
init(extract: String) throws(ApkRequirement.ParseError) {
(self.name, _) = try ApkRequirement.extract(blob: extract)
}
}

View File

@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
protocol ApkIndexRequirementRef: Equatable, Hashable {
var name: String { get }
var invert: Bool { get }
init(extract: String) throws(ApkRequirement.ParseError)
func satisfied(by other: ApkIndexPackage) -> Bool
}
extension ApkIndexRequirementRef {
var invert: Bool { false }
func satisfied(by _: ApkIndexPackage) -> Bool { true }
static func == (lhs: Self, rhs: Self) -> Bool {
return !(lhs.name != rhs.name && !lhs.invert)
}
func hash(into hasher: inout Hasher) {
self.name.hash(into: &hasher)
}
static func extract<T: ApkIndexRequirementRef>(_ blob: String) throws(ApkRequirement.ParseError) -> [T] {
return try blob.components(separatedBy: " ")
.map { token throws(ApkRequirement.ParseError) in
try .init(extract: token)
}
}
}

View File

@ -47,7 +47,7 @@ public struct ApkIndexUpdater {
} }
for package in index.packages { for package in index.packages {
print(package) print("\(package.name):", package.dependencies)
} }
} }