155 lines
6.8 KiB
Swift
155 lines
6.8 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
|
|
/// A deliberately narrow description of the remote operating environment.
|
|
///
|
|
/// Values originate on an untrusted SSH host and can be included in model
|
|
/// requests, so every field is accepted only when it fits a small ASCII token
|
|
/// grammar. Human-readable os-release fields are intentionally excluded.
|
|
public nonisolated struct RemoteSystemInfo: Hashable, Codable, Sendable {
|
|
public var distributionID: String?
|
|
public var distributionLikeIDs: [String]
|
|
public var distributionVersion: String?
|
|
public var kernelName: String?
|
|
public var kernelRelease: String?
|
|
public var architecture: String?
|
|
|
|
public init(
|
|
distributionID: String? = nil,
|
|
distributionLikeIDs: [String] = [],
|
|
distributionVersion: String? = nil,
|
|
kernelName: String? = nil,
|
|
kernelRelease: String? = nil,
|
|
architecture: String? = nil
|
|
) {
|
|
self.distributionID = RemoteSystemIdentifier.sanitized(distributionID)
|
|
self.distributionLikeIDs = RemoteSystemIdentifier.normalizedList(distributionLikeIDs)
|
|
self.distributionVersion = Self.sanitized(distributionVersion)
|
|
self.kernelName = Self.sanitized(kernelName)
|
|
self.kernelRelease = Self.sanitized(kernelRelease)
|
|
self.architecture = Self.sanitized(architecture)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case distributionID, distributionLikeIDs, distributionVersion
|
|
case kernelName, kernelRelease, architecture
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.init(
|
|
distributionID: try container.decodeIfPresent(String.self, forKey: .distributionID),
|
|
distributionLikeIDs: try container.decodeIfPresent(
|
|
[String].self, forKey: .distributionLikeIDs
|
|
) ?? [],
|
|
distributionVersion: try container.decodeIfPresent(
|
|
String.self, forKey: .distributionVersion
|
|
),
|
|
kernelName: try container.decodeIfPresent(String.self, forKey: .kernelName),
|
|
kernelRelease: try container.decodeIfPresent(String.self, forKey: .kernelRelease),
|
|
architecture: try container.decodeIfPresent(String.self, forKey: .architecture)
|
|
)
|
|
}
|
|
|
|
public func encode(to encoder: any Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encodeIfPresent(distributionID, forKey: .distributionID)
|
|
if !distributionLikeIDs.isEmpty {
|
|
try container.encode(distributionLikeIDs, forKey: .distributionLikeIDs)
|
|
}
|
|
try container.encodeIfPresent(distributionVersion, forKey: .distributionVersion)
|
|
try container.encodeIfPresent(kernelName, forKey: .kernelName)
|
|
try container.encodeIfPresent(kernelRelease, forKey: .kernelRelease)
|
|
try container.encodeIfPresent(architecture, forKey: .architecture)
|
|
}
|
|
|
|
/// Reads either the legacy `id|version|kernel_name|kernel_release|arch`
|
|
/// line or the current `id|id_like|version|kernel_name|kernel_release|arch`
|
|
/// line printed by the PTY bootstrap.
|
|
///
|
|
/// Empty fields are meaningful (a BSD has no os-release), so the split keeps
|
|
/// them rather than collapsing the run of separators.
|
|
public init?(probeFields line: some StringProtocol) {
|
|
let fields = line.split(separator: "|", omittingEmptySubsequences: false)
|
|
guard fields.count == 5 || fields.count == 6 else { return nil }
|
|
let values = fields.map { Self.unquoted(String($0)) }
|
|
if values.count == 6 {
|
|
self.init(
|
|
distributionID: values[0],
|
|
distributionLikeIDs: values[1].split(whereSeparator: \.isWhitespace).map(String.init),
|
|
distributionVersion: values[2], kernelName: values[3],
|
|
kernelRelease: values[4], architecture: values[5]
|
|
)
|
|
} else {
|
|
self.init(
|
|
distributionID: values[0], distributionVersion: values[1],
|
|
kernelName: values[2], kernelRelease: values[3], architecture: values[4]
|
|
)
|
|
}
|
|
if isEmpty { return nil }
|
|
}
|
|
|
|
public var isEmpty: Bool {
|
|
distributionID == nil && distributionLikeIDs.isEmpty && distributionVersion == nil
|
|
&& kernelName == nil && kernelRelease == nil && architecture == nil
|
|
}
|
|
|
|
/// Reaches a model only after the validation above has made each value
|
|
/// incapable of carrying prose or prompt instructions.
|
|
///
|
|
/// Lines without a header, deliberately: `SessionContext.modelContext` owns
|
|
/// the one fenced "data only" block these join, and a second preamble
|
|
/// stacked on top of the first reads as boilerplate.
|
|
public var factLines: [String] {
|
|
var lines: [String] = []
|
|
if let distributionID { lines.append("distribution_id: \(distributionID)") }
|
|
if !distributionLikeIDs.isEmpty {
|
|
lines.append("distribution_like_ids: \(distributionLikeIDs.joined(separator: " "))")
|
|
}
|
|
if let distributionVersion {
|
|
lines.append("distribution_version: \(distributionVersion)")
|
|
}
|
|
if let kernelName { lines.append("kernel_name: \(kernelName)") }
|
|
if let kernelRelease { lines.append("kernel_release: \(kernelRelease)") }
|
|
if let architecture { lines.append("architecture: \(architecture)") }
|
|
return lines
|
|
}
|
|
|
|
/// os-release quotes values that could contain spaces. Stripping a matched
|
|
/// pair here rather than in `sanitized` keeps the ASCII-token grammar itself
|
|
/// free of exceptions: `"22.04"` becomes `22.04`, while a lone quote stays
|
|
/// and is rejected.
|
|
private static func unquoted(_ raw: String) -> String {
|
|
guard raw.count >= 2, let first = raw.first, let last = raw.last,
|
|
first == last, first == "\"" || first == "'"
|
|
else { return raw }
|
|
return String(raw.dropFirst().dropLast())
|
|
}
|
|
|
|
private static func sanitized(_ proposed: String?) -> String? {
|
|
RemoteSystemIdentifier.sanitized(proposed)
|
|
}
|
|
}
|
|
|
|
public nonisolated enum RemoteDistributionDetection: Hashable, Sendable {
|
|
case unavailable
|
|
case resolved(DetectedDistribution?)
|
|
}
|
|
|
|
extension RemoteSystemInfo {
|
|
public var distributionDetection: RemoteDistributionDetection {
|
|
guard let kernelName else { return .unavailable }
|
|
let kernel = kernelName.lowercased()
|
|
if kernel == "darwin" {
|
|
return .resolved(DetectedDistribution(id: "macos"))
|
|
}
|
|
if ["freebsd", "openbsd", "netbsd", "dragonfly"].contains(kernel) {
|
|
return .resolved(DetectedDistribution(id: kernel))
|
|
}
|
|
guard kernel == "linux" else { return .resolved(nil) }
|
|
return .resolved(DetectedDistribution(id: distributionID ?? "linux", likeIDs: distributionLikeIDs))
|
|
}
|
|
}
|