mirror of
https://github.com/GayPizzaSpecifications/darwin-apk.git
synced 2025-08-03 21:41:31 +00:00
Consolidate version stuff
This commit is contained in:
127
Sources/apk/Version/ApkVersionCompare.swift
Normal file
127
Sources/apk/Version/ApkVersionCompare.swift
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* darwin-apk © 2024 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Darwin
|
||||
|
||||
public struct ApkVersionCompare {
|
||||
@inlinable public static func validate(_ version: String) -> Bool {
|
||||
Self.validate(ContiguousArray(version.utf8))
|
||||
}
|
||||
|
||||
public static func validate(_ version: ContiguousArray<UInt8>) -> Bool {
|
||||
var reader = ApkVersionReader(version[...])
|
||||
while true {
|
||||
switch try? reader.next() {
|
||||
case .end: return true
|
||||
case nil: return false
|
||||
default: continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable public static func compare(_ a: String, _ b: String, mode: Mode = .normal) -> Comparison? {
|
||||
Self.compare(ContiguousArray(a.utf8), ContiguousArray((b.utf8)), mode: mode)
|
||||
}
|
||||
|
||||
public static func compare(_ a: ContiguousArray<UInt8>, _ b: ContiguousArray<UInt8>, mode: Mode = .normal) -> Comparison? {
|
||||
if (a.isEmpty && b.isEmpty) || a == b {
|
||||
return .equal
|
||||
}
|
||||
|
||||
var readA = ApkVersionReader(a[...]), readB = ApkVersionReader(b[...])
|
||||
var tokenA: ApkVersionReader.TokenPart, tokenB: ApkVersionReader.TokenPart
|
||||
do {
|
||||
while true {
|
||||
(tokenA, tokenB) = (try readA.next(), try readB.next())
|
||||
guard let c = ApkVersionReader.TokenPart.compare(tokenA, tokenB) else {
|
||||
break
|
||||
}
|
||||
if c != .equal {
|
||||
return c
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Both versions are equal if they're the same length or we are fuzzy matching prefixes
|
||||
if tokenA == tokenB || (mode == .fuzzy && tokenB == .end) {
|
||||
return .equal
|
||||
}
|
||||
|
||||
// Mark non-prerelease versions as greater than the same version marked prerelease
|
||||
if case .suffix(let suffix) = tokenA, [ .alpha, .beta, .pre, .rc ].contains(suffix) {
|
||||
return .less
|
||||
} else if case .suffix(let suffix) = tokenB, [ .alpha, .beta, .pre, .rc ].contains(suffix) {
|
||||
return .greater
|
||||
} else {
|
||||
return ApkVersionReader.TokenPart.compValue(tokenB, tokenA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ApkVersionCompare {
|
||||
enum Comparison {
|
||||
case less, greater, equal
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
case normal, fuzzy
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Comparison implementation
|
||||
|
||||
fileprivate extension ApkVersionReader.TokenPart {
|
||||
static func compare(_ a: Self, _ b: Self) -> ApkVersionCompare.Comparison? {
|
||||
switch a {
|
||||
case .digit(let lhsNumber, let lhsString):
|
||||
guard case .digit(let rhsNumber, let rhsString) = b else {
|
||||
return nil
|
||||
}
|
||||
// If either are digit & zero prefixed & not initial then handle as string
|
||||
return if lhsString?.first == UInt8(ascii: "0") || rhsString?.first == UInt8(ascii: "0") {
|
||||
self.compValue(lhsString!, rhsString!)
|
||||
} else {
|
||||
Self.compValue(lhsNumber, rhsNumber)
|
||||
}
|
||||
case .letter(let lhs):
|
||||
return if case .letter(let rhs) = b { Self.compValue(lhs, rhs) } else { nil }
|
||||
case .suffixNumber(let lhs):
|
||||
return if case .suffixNumber(let rhs) = b { Self.compValue(lhs, rhs) } else { nil }
|
||||
case .revision(let lhs):
|
||||
return if case .revision(let rhs) = b { Self.compValue(lhs, rhs) } else { nil }
|
||||
case .commitHash(let lhs):
|
||||
return if case .commitHash(let rhs) = b { Self.compValue(lhs, rhs) } else { nil }
|
||||
case .suffix(let lhs):
|
||||
return if case .suffix(let rhs) = b { Self.compValue(lhs.rawValue, rhs.rawValue) } else { nil }
|
||||
case .end:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Private comparison implementation
|
||||
|
||||
static func compValue<T: Comparable>(_ a: T, _ b: T) -> ApkVersionCompare.Comparison {
|
||||
if a < b { .less }
|
||||
else if a == b { .equal }
|
||||
else { .greater }
|
||||
}
|
||||
|
||||
private static func compValue(_ a: ArraySlice<UInt8>, _ b: ArraySlice<UInt8>) -> ApkVersionCompare.Comparison {
|
||||
let minLength = min(a.count, b.count)
|
||||
let comparison = a.withUnsafeBytes { ca in
|
||||
b.withUnsafeBytes { cb in
|
||||
memcmp(ca.baseAddress!, cb.baseAddress!, minLength)
|
||||
}
|
||||
}
|
||||
if comparison != 0 {
|
||||
return comparison < 0 ? .less : .greater
|
||||
} else {
|
||||
return Self.compValue(a.count, b.count)
|
||||
}
|
||||
}
|
||||
}
|
212
Sources/apk/Version/ApkVersionReader.swift
Normal file
212
Sources/apk/Version/ApkVersionReader.swift
Normal file
@ -0,0 +1,212 @@
|
||||
/*
|
||||
* darwin-apk © 2024 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
internal struct ApkVersionReader {
|
||||
var string: ArraySlice<UInt8>
|
||||
private var seen: TokenFlag, last: TokenFlag
|
||||
|
||||
init(_ string: ArraySlice<UInt8>) {
|
||||
self.string = string
|
||||
self.seen = []
|
||||
self.last = []
|
||||
}
|
||||
|
||||
mutating func next() throws(Invalid) -> TokenPart {
|
||||
self.seen.formUnion(self.last)
|
||||
|
||||
switch string.first ?? UInt8(ascii: "0") {
|
||||
case UInt8(ascii: "a")...UInt8(ascii: "z"): // Letter suffix
|
||||
guard self.seen.contains(.initial),
|
||||
self.last.isDisjoint(with: [ .letter, .suffix, .suffixNumber, .commitHash, .revision ]) else {
|
||||
throw .invalid
|
||||
}
|
||||
self.last = .letter
|
||||
return .letter(self.advance())
|
||||
case UInt8(ascii: "."): // Version separator
|
||||
guard self.seen.contains(.initial), self.last.contains(.digit) else {
|
||||
throw .invalid
|
||||
}
|
||||
self.advance()
|
||||
fallthrough
|
||||
case UInt8(ascii: "0")...UInt8(ascii: "9"): // Numeric component
|
||||
guard self.last.isSubset(of: [ .initial, .digit, .suffix ]),
|
||||
let (number, numString) = self.readNumber() else {
|
||||
throw .invalid
|
||||
}
|
||||
if self.last == .suffix {
|
||||
self.last = .suffixNumber
|
||||
return .suffixNumber(number)
|
||||
} else {
|
||||
self.last = .digit
|
||||
if !self.seen.contains(.initial) {
|
||||
self.last.insert(.initial)
|
||||
return .digit(number, nil)
|
||||
} else {
|
||||
// Numeric digits that aren't initial might be compared as a string instead
|
||||
return .digit(number, numString)
|
||||
}
|
||||
}
|
||||
case UInt8(ascii: "_"): // Suffix
|
||||
guard self.seen.contains(.initial), self.seen.isDisjoint(with: [ .commitHash, .revision ]) else {
|
||||
throw .invalid
|
||||
}
|
||||
self.advance()
|
||||
guard let suffix = self.readVersionSuffix() else {
|
||||
throw .invalid
|
||||
}
|
||||
self.last = .suffix
|
||||
return .suffix(suffix)
|
||||
case UInt8(ascii: "~"): // Commit hash
|
||||
guard self.seen.contains(.initial), self.seen.isDisjoint(with: [ .commitHash, .revision ]) else {
|
||||
throw .invalid
|
||||
}
|
||||
self.advance()
|
||||
let end = self.string.firstIndex(where: { !$0.isHexDigit }) ?? self.string.endIndex
|
||||
let hex = self.advance(end)
|
||||
guard self.string.isEmpty else { // Commit hash should take the rest of string
|
||||
throw .invalid
|
||||
}
|
||||
self.last = .commitHash
|
||||
return .commitHash(hex)
|
||||
case UInt8(ascii: "-"): // Package revision
|
||||
guard self.seen.contains(.initial), self.seen.isDisjoint(with: .revision),
|
||||
self.advance(2) == [ UInt8(ascii: "-"), UInt8(ascii: "r") ],
|
||||
let (number, _) = self.readNumber() else {
|
||||
throw .invalid
|
||||
}
|
||||
self .last = .revision
|
||||
return .revision(number)
|
||||
case UInt8(ascii: "\0"): // End of version string
|
||||
guard self.seen.contains(.initial) else {
|
||||
throw .invalid
|
||||
}
|
||||
return .end
|
||||
default:
|
||||
throw .invalid
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Private Implementation
|
||||
|
||||
private mutating func readNumber() -> (UInt, ArraySlice<UInt8>)? {
|
||||
let maxLength = self.string.count
|
||||
let (end, result) = self.string.withUnsafeBufferPointer {
|
||||
var i = 0, accum: UInt = 0
|
||||
while i < maxLength {
|
||||
let c = $0[i]
|
||||
if !(UInt8(ascii: "0")...UInt8(ascii: "9") ~= c) {
|
||||
break
|
||||
}
|
||||
accum = accum &* 10 &+ UInt(c - UInt8(ascii: "0"))
|
||||
i += 1
|
||||
}
|
||||
return (i, accum)
|
||||
}
|
||||
if end == 0 {
|
||||
return nil
|
||||
}
|
||||
return (result, self.advance(end))
|
||||
}
|
||||
|
||||
private mutating func readVersionSuffix() -> VersionSuffix? {
|
||||
let end = self.string.firstIndex(where: { !(UInt8(ascii: "a")...UInt8(ascii: "z") ~= $0) }) ?? self.string.endIndex
|
||||
let suffix = self.advance(end - self.string.startIndex)
|
||||
return switch suffix.first { // TODO: Should this matching be stricter?
|
||||
case UInt8(ascii: "a"): .alpha
|
||||
case UInt8(ascii: "b"): .beta
|
||||
case UInt8(ascii: "c"): .cvs
|
||||
case UInt8(ascii: "g"): .git
|
||||
case UInt8(ascii: "h"): .hg
|
||||
case UInt8(ascii: "p"): suffix.count == 1 ? .p : .pre
|
||||
case UInt8(ascii: "r"): .rc
|
||||
case UInt8(ascii: "s"): .svn
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private mutating func advance(_ len: Int) -> ArraySlice<UInt8> {
|
||||
let beg = self.string.startIndex
|
||||
let end = min(string.index(beg, offsetBy: len), string.endIndex)
|
||||
defer {
|
||||
self.string = self.string[end...]
|
||||
}
|
||||
return self.string[beg..<end]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private mutating func advance() -> UInt8 {
|
||||
defer {
|
||||
self.string = string[string.index(after: string.startIndex)...]
|
||||
}
|
||||
return self.string[self.string.startIndex]
|
||||
}
|
||||
}
|
||||
|
||||
extension ApkVersionReader {
|
||||
private struct TokenFlag: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
static let initial = Self(rawValue: 1 << 0)
|
||||
static let digit = Self(rawValue: 1 << 1)
|
||||
static let letter = Self(rawValue: 1 << 2)
|
||||
static let suffix = Self(rawValue: 1 << 3)
|
||||
static let suffixNumber = Self(rawValue: 1 << 4)
|
||||
static let commitHash = Self(rawValue: 1 << 5)
|
||||
static let revision = Self(rawValue: 1 << 6)
|
||||
}
|
||||
|
||||
enum TokenPart {
|
||||
case digit(_ number: UInt, _ string: ArraySlice<UInt8>?)
|
||||
case letter(_ char: UInt8)
|
||||
case suffix(_ suffix: VersionSuffix)
|
||||
case suffixNumber(_ number: UInt)
|
||||
case commitHash(_ hash: ArraySlice<UInt8>)
|
||||
case revision(_ number: UInt)
|
||||
case end
|
||||
}
|
||||
|
||||
enum VersionSuffix: Int {
|
||||
case alpha = 0, beta = 1, cvs = 5, git = 7, hg = 8, pre = 2, p = 9, rc = 3, svn = 6
|
||||
}
|
||||
|
||||
enum Invalid: Error {
|
||||
case invalid
|
||||
}
|
||||
}
|
||||
|
||||
extension ApkVersionReader.TokenPart: Comparable {
|
||||
@inlinable var order: Int {
|
||||
switch self {
|
||||
case .digit: 1
|
||||
case .letter: 2
|
||||
case .suffix: 3
|
||||
case .suffixNumber: 4
|
||||
case .commitHash: 5
|
||||
case .revision: 6
|
||||
case .end: 7
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.order == rhs.order
|
||||
}
|
||||
|
||||
@inlinable static func < (lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.order < rhs.order
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension UInt8 {
|
||||
@inline(__always) var isHexDigit: Bool {
|
||||
switch self {
|
||||
case UInt8(ascii: "0")...UInt8(ascii: "9"),
|
||||
UInt8(ascii: "A")...UInt8(ascii: "F"),
|
||||
UInt8(ascii: "a")...UInt8(ascii: "f"):
|
||||
true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
137
Sources/apk/Version/ApkVersionRequirement.swift
Normal file
137
Sources/apk/Version/ApkVersionRequirement.swift
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* darwin-apk © 2024 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
internal struct ApkVersionRequirement: Hashable {
|
||||
let name: String
|
||||
let versionSpec: ApkVersionSpecification
|
||||
|
||||
init(name: String, spec: ApkVersionSpecification) {
|
||||
self.name = name
|
||||
self.versionSpec = spec
|
||||
}
|
||||
|
||||
init(extract: Substring) throws(ParseError) {
|
||||
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 {
|
||||
// Lack of conflict flag indicates any version
|
||||
if !comparer.contains(.conflict) {
|
||||
comparer.formUnion(.any)
|
||||
}
|
||||
(nameEnd, versionStart) = (dependStr.endIndex, dependStr.endIndex)
|
||||
}
|
||||
|
||||
// Parse version specification
|
||||
self.versionSpec = try ApkVersionSpecification(comparer, version: dependStr[versionStart...])
|
||||
self.name = String(dependStr[..<nameEnd])
|
||||
}
|
||||
}
|
||||
|
||||
extension ApkVersionRequirement: 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ApkVersionRequirement {
|
||||
enum ParseError: Error, LocalizedError {
|
||||
case brokenSpec
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .brokenSpec: "Invalid version specification"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Private Implementation
|
||||
|
||||
fileprivate extension ApkVersionRequirement {
|
||||
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: ApkVersionRequirement.ComparatorBits, version: Substring) throws(ApkVersionRequirement.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: ApkVersionRequirement.ComparatorBits) throws(ApkVersionRequirement.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
|
||||
}
|
||||
}
|
39
Sources/apk/Version/ApkVersionSpecification.swift
Normal file
39
Sources/apk/Version/ApkVersionSpecification.swift
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* darwin-apk © 2024 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
enum ApkVersionSpecification: Equatable, Hashable {
|
||||
case any
|
||||
case constraint(op: Operator, version: String)
|
||||
case conflict
|
||||
}
|
||||
|
||||
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: "~"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user