Surface the existing pin (keep-from-cull) and per-file delete actions as visible inline buttons on each offline cache row instead of context-menu-only: a star toggles protection from auto-cull (and restore-if-missing), a trash culls that file early. Aligns wording/icons to the star metaphor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
3.7 KiB
Swift
74 lines
3.7 KiB
Swift
import Foundation
|
|
|
|
/// A parsed (and minimally validated) wg-quick client config — the payload a
|
|
/// Device Mesh join QR carries. Parsing is the **receiving** side of the enrollment
|
|
/// `MeshJoin.clientConfig` renders; both live against the same conf shape.
|
|
public struct WGQuickConfig: Sendable, Equatable {
|
|
public let privateKey: String
|
|
/// Re-derived from the private key — never trusted from the text.
|
|
public let publicKey: String
|
|
/// This device's mesh address, without the /32 suffix.
|
|
public let address: String
|
|
public let dns: String?
|
|
public let peerPublicKey: String
|
|
public let peerEndpoint: String
|
|
/// Device name from the generator's `# WireGuard config for <name>` comment,
|
|
/// when present.
|
|
public let device: String?
|
|
/// The full original text — what gets persisted (it's already canonical).
|
|
public let text: String
|
|
}
|
|
|
|
public enum WGQuickParseError: Error, LocalizedError, Equatable {
|
|
case missingField(String)
|
|
case badPrivateKey
|
|
case notMeshAddress(String)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .missingField(let f): "That doesn't look like a WireGuard config — missing \(f)."
|
|
case .badPrivateKey: "The PrivateKey isn't a valid WireGuard key."
|
|
case .notMeshAddress(let a): "Address \(a) isn't on this install's mesh (\(MeshDefaults.meshSubnet))."
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parser for incoming wg-quick text (QR scan or paste). Strict enough to
|
|
/// reject junk before anything touches disk: the private key must be a real
|
|
/// Curve25519 scalar, the address must be on the install's mesh subnet, and the
|
|
/// `[Peer]` must carry a key + endpoint.
|
|
public enum WGQuickParser {
|
|
public static func parse(_ text: String) throws -> WGQuickConfig {
|
|
var fields: [String: String] = [:]
|
|
var device: String?
|
|
|
|
for rawLine in text.split(whereSeparator: \.isNewline) {
|
|
let line = rawLine.trimmingCharacters(in: .whitespaces)
|
|
if let name = line.wholeMatch(of: #/# WireGuard config for (\S+)/#).map({ String($0.1) }) {
|
|
device = name
|
|
continue
|
|
}
|
|
if line.isEmpty || line.hasPrefix("#") || line.hasPrefix("[") { continue }
|
|
guard let eq = line.firstIndex(of: "=") else { continue }
|
|
let key = line[..<eq].trimmingCharacters(in: .whitespaces)
|
|
let value = line[line.index(after: eq)...].trimmingCharacters(in: .whitespaces)
|
|
// First occurrence wins: Interface keys come before Peer keys except
|
|
// PublicKey/Endpoint, which only ever appear in the Peer section.
|
|
if fields[key] == nil { fields[key] = value }
|
|
}
|
|
|
|
guard let priv = fields["PrivateKey"] else { throw WGQuickParseError.missingField("PrivateKey") }
|
|
guard let keypair = WireGuardKeypair(privateKeyBase64: priv) else { throw WGQuickParseError.badPrivateKey }
|
|
guard let rawAddress = fields["Address"] else { throw WGQuickParseError.missingField("Address") }
|
|
guard let peerKey = fields["PublicKey"] else { throw WGQuickParseError.missingField("Peer PublicKey") }
|
|
guard let endpoint = fields["Endpoint"] else { throw WGQuickParseError.missingField("Peer Endpoint") }
|
|
|
|
let address = String(rawAddress.split(separator: "/").first ?? "")
|
|
guard address.hasPrefix("10.9.0.") else { throw WGQuickParseError.notMeshAddress(address) }
|
|
|
|
return WGQuickConfig(privateKey: keypair.privateKey, publicKey: keypair.publicKey,
|
|
address: address, dns: fields["DNS"],
|
|
peerPublicKey: peerKey, peerEndpoint: endpoint,
|
|
device: device, text: text)
|
|
}
|
|
}
|