Files
punktfunk/clients/apple/Sources/PunktfunkShared/OsChain.swift
T
enricobuehlerandClaude Fable 5 88c8688b47 feat(client/apple): host cards wear the OS mark, and the store learns it
`DiscoveredHost` reads the new `os=` TXT (sanitized in PunktfunkShared's
OsChain.swift — the pure grammar + walk, mirrored from pf-client-core so every
platform resolves identically, and dependency-free so the widget could use it);
`StoredHost` appends optional `osChain` per the frozen app↔widget contract
(optional + appended last; legacy JSON still decodes, pinned by the round-trip
tests), and `HostStore.updateOsChain` learns it beside the MACs.

The art rides PunktfunkKit's proven resource path (the fonts precedent): ten
template vector imagesets in Resources/OsIcons.xcassets, compiled by the Xcode
build into the Kit bundle's Assets.car (verified: all ten resolve in the built
app) and tinting via foregroundStyle like an SF Symbol. Saved and discovered
cards lead their status line with the mark; a host that advertises nothing
renders exactly as before. GamepadHome tile + widget marks are follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 17:58:33 +02:00

44 lines
2.0 KiB
Swift

// The client half of the host's OS-identity advertisement (mDNS `os` TXT — producer:
// punktfunk-host's osinfo.rs): sanitize the untrusted chain once, and turn it into the
// icon-lookup order every surface walks. Mirrors pf-client-core's `os.rs` (and the Kotlin
// port) so all platforms resolve identically; in the dependency-free shared module so the
// widget could use it too. The Image lookup itself lives in PunktfunkKit (OsIcon.swift).
import Foundation
/// Reduce a raw `os` TXT value (e.g. `linux/fedora/bazzite`) to the trusted grammar:
/// lowercase slash-separated tokens of `[a-z0-9._-]`, each ≤ 32 chars, at most 5. mDNS is
/// unauthenticated input — anything outside the grammar is dropped, and a value that
/// sanitizes to nothing becomes `""` (same rendering as a host that advertises no `os`).
public func sanitizeOsChain(_ raw: String) -> String {
raw.lowercased()
.split(separator: "/", omittingEmptySubsequences: true)
.map { token -> String in
let kept = token.unicodeScalars.filter { s in
(0x61...0x7A).contains(s.value) // a-z
|| (0x30...0x39).contains(s.value) // 0-9
|| s == "." || s == "_" || s == "-"
}
return String(String.UnicodeScalarView(kept.prefix(32)))
}
.filter { !$0.isEmpty }
.prefix(5)
.joined(separator: "/")
}
/// The icon-lookup order for a chain: sanitized tokens most-specific-first, with brand
/// aliases applied (`macos` → `apple` art, `steamos` → `steam` art). A surface takes the
/// first token it has art for; empty means "no OS icon" (older host / garbage advert).
public func osIconTokens(_ chain: String?) -> [String] {
sanitizeOsChain(chain ?? "")
.split(separator: "/")
.reversed()
.map {
switch $0 {
case "macos": return "apple"
case "steamos": return "steam"
default: return String($0)
}
}
}