Break up Brief + Document (#311)

This commit is contained in:
Max Goedjen 2022-01-01 21:46:01 -08:00 committed by GitHub
parent eb282b4116
commit da2c460c60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 186 additions and 107 deletions

View File

@ -0,0 +1,15 @@
# ``Brief``
Brief is a collection of protocols and concrete implmentation describing updates.
## Topics
### Versioning
- ``SemVer``
- ``Release``
### Updater
- ``UpdaterProtocol``
- ``Updater``

View File

@ -0,0 +1,80 @@
import Foundation
/// A release is a representation of a downloadable update.
public struct Release: Codable {
/// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String
/// A boolean describing whether or not the release is a prerelase build.
public let prerelease: Bool
/// A URL pointing to the HTML page for the release.
public let html_url: URL
/// A user-facing description of the contents of the update.
public let body: String
/// Initializes a Release.
/// - Parameters:
/// - name: The user-facing name of the release.
/// - prerelease: A boolean describing whether or not the release is a prerelase build.
/// - html_url: A URL pointing to the HTML page for the release.
/// - body: A user-facing description of the contents of the update.
public init(name: String, prerelease: Bool, html_url: URL, body: String) {
self.name = name
self.prerelease = prerelease
self.html_url = html_url
self.body = body
}
}
extension Release: Identifiable {
public var id: String {
html_url.absoluteString
}
}
extension Release: Comparable {
public static func < (lhs: Release, rhs: Release) -> Bool {
lhs.version < rhs.version
}
}
extension Release {
/// A boolean describing whether or not the release contains critical security content.
/// - Note: this is determined by the presence of the phrase "Critical Security Update" in the ``body``.
/// - Warning: If this property is true, the user will not be able to dismiss UI or reminders associated with the update.
public var critical: Bool {
body.contains(Constants.securityContent)
}
/// A ``SemVer`` representation of the version number of the release.
public var version: SemVer {
SemVer(name)
}
/// The minimum macOS version required to run the update.
public var minimumOSVersion: SemVer {
guard let range = body.range(of: "Minimum macOS Version"),
let numberStart = body.rangeOfCharacter(from: CharacterSet.decimalDigits, options: [], range: range.upperBound..<body.endIndex) else { return SemVer("11.0.0") }
let numbersEnd = body.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines, options: [], range: numberStart.upperBound..<body.endIndex)?.lowerBound ?? body.endIndex
let version = numberStart.lowerBound..<numbersEnd
return SemVer(String(body[version]))
}
}
extension Release {
enum Constants {
static let securityContent = "Critical Security Update"
}
}

View File

@ -0,0 +1,43 @@
import Foundation
/// A representation of a Semantic Version.
public struct SemVer {
/// The SemVer broken into an array of integers.
let versionNumbers: [Int]
/// Initializes a SemVer from a string representation.
/// - Parameter version: A string representation of the SemVer, formatted as "major.minor.patch".
public init(_ version: String) {
// Betas have the format 1.2.3_beta1
let strippedBeta = version.split(separator: "_").first!
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
while split.count < 3 {
split.append(0)
}
versionNumbers = split
}
/// Initializes a SemVer from an ``OperatingSystemVersion`` representation.
/// - Parameter version: An ``OperatingSystemVersion`` representation of the SemVer.
public init(_ version: OperatingSystemVersion) {
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
}
}
extension SemVer: Comparable {
public static func < (lhs: SemVer, rhs: SemVer) -> Bool {
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
if latest < current {
return true
} else if latest > current {
return false
}
}
return false
}
}

View File

@ -1,22 +1,24 @@
import Foundation
import Combine
public protocol UpdaterProtocol: ObservableObject {
var update: Release? { get }
var testBuild: Bool { get }
}
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
public class Updater: ObservableObject, UpdaterProtocol {
@Published public var update: Release?
public let testBuild: Bool
/// The current OS version.
private let osVersion: SemVer
/// The current version of the app that is running.
private let currentVersion: SemVer
public init(checkOnLaunch: Bool, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
/// Initializes an Updater.
/// - Parameters:
/// - checkOnLaunch: A boolean describing whether the Updater should check for available updates on launch.
/// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
/// - osVersion: The current OS version.
/// - currentVersion: The current version of the app that is running.
public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
self.osVersion = osVersion
self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0")
@ -24,12 +26,13 @@ public class Updater: ObservableObject, UpdaterProtocol {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
checkForUpdates()
}
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
self.checkForUpdates()
}
timer.tolerance = 60*60
}
/// Manually trigger an update check.
public func checkForUpdates() {
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
guard let data = data else { return }
@ -38,6 +41,8 @@ public class Updater: ObservableObject, UpdaterProtocol {
}.resume()
}
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
/// - Parameter release: The release to ignore.
public func ignore(release: Release) {
guard !release.critical else { return }
defaults.set(true, forKey: release.name)
@ -50,6 +55,8 @@ public class Updater: ObservableObject, UpdaterProtocol {
extension Updater {
/// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run.
/// - Parameter releases: An array of ``Release`` objects.
func evaluate(releases: [Release]) {
guard let release = releases
.sorted()
@ -66,49 +73,18 @@ extension Updater {
}
}
/// Checks whether the user has ignored a release.
/// - Parameter release: The release to check.
/// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical.
func userIgnored(release: Release) -> Bool {
guard !release.critical else { return false }
return defaults.bool(forKey: release.name)
}
/// The user defaults used to store user ignore state.
var defaults: UserDefaults {
UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")!
}
}
public struct SemVer {
let versionNumbers: [Int]
public init(_ version: String) {
// Betas have the format 1.2.3_beta1
let strippedBeta = version.split(separator: "_").first!
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
while split.count < 3 {
split.append(0)
}
versionNumbers = split
}
public init(_ version: OperatingSystemVersion) {
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
}
}
extension SemVer: Comparable {
public static func < (lhs: SemVer, rhs: SemVer) -> Bool {
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
if latest < current {
return true
} else if latest > current {
return false
}
}
return false
}
}
@ -119,63 +95,3 @@ extension Updater {
}
}
public struct Release: Codable {
public let name: String
public let prerelease: Bool
public let html_url: URL
public let body: String
public init(name: String, prerelease: Bool, html_url: URL, body: String) {
self.name = name
self.prerelease = prerelease
self.html_url = html_url
self.body = body
}
}
extension Release: Identifiable {
public var id: String {
html_url.absoluteString
}
}
extension Release: Comparable {
public static func < (lhs: Release, rhs: Release) -> Bool {
lhs.version < rhs.version
}
}
extension Release {
public var critical: Bool {
body.contains(Constants.securityContent)
}
public var version: SemVer {
SemVer(name)
}
public var minimumOSVersion: SemVer {
guard let range = body.range(of: "Minimum macOS Version"),
let numberStart = body.rangeOfCharacter(from: CharacterSet.decimalDigits, options: [], range: range.upperBound..<body.endIndex) else { return SemVer("11.0.0") }
let numbersEnd = body.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines, options: [], range: numberStart.upperBound..<body.endIndex)?.lowerBound ?? body.endIndex
let version = numberStart.lowerBound..<numbersEnd
return SemVer(String(body[version]))
}
}
extension Release {
enum Constants {
static let securityContent = "Critical Security Update"
}
}

View File

@ -0,0 +1,12 @@
import Foundation
/// A protocol for retreiving the latest available version of an app.
public protocol UpdaterProtocol: ObservableObject {
/// The latest update
var update: Release? { get }
/// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build)
var testBuild: Bool { get }
}

View File

@ -0,0 +1,13 @@
# ``SecretAgentKit``
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
## Overview
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
## Topics
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->

View File

@ -1,4 +1,4 @@
# ````SecretKit````
# ``SecretKit``
SecretKit is a collection of protocols describing secrets and stores.

View File

@ -1,3 +1,3 @@
# ````SecureEnclaveSecretKit````
# ``SecureEnclaveSecretKit``
SecureEnclaveSecretKit contains implementations of SecretKit protocols backed by the Secure Enclave.

View File

@ -1,3 +1,3 @@
# ````SmartCardSecretKit````
# ``SmartCardSecretKit``
SmartCardSecretKit contains implementations of SecretKit protocols backed by a Smart Card.