tv-anarchy/Sources/TVAnarchyCore/HelperDeployment.swift
Natalie ca1871f5dd feat(@applications/tv-anarchy): add roku device support
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:37:34 -07:00

53 lines
2.6 KiB
Swift

import CryptoKit
import Foundation
/// Answers "is the helper a fleet device runs the same one vendored in this
/// repo?" the Devices tab's freshness badge. The repo copy is the source of
/// truth (mcp/README's deploy step ships it byte-for-byte), so freshness is a
/// straight content-hash comparison: the device's `stats` report carries the
/// sha256 of the script that produced it; the app hashes the vendored source.
/// No version constants to bump, nothing that can drift.
public enum HelperDeployment {
public enum Freshness: Equatable, Sendable {
case current
/// `deployed` is the device-reported hash; nil means the deployed helper
/// predates self-reporting entirely (necessarily stale).
case outdated(deployed: String?)
}
/// Repo source for each known helper bin, keyed by basename (the bin path on
/// the device e.g. `/usr/local/bin/black-tv` names the deployed copy).
private static let vendoredSources: [String: String] = [
"black-tv": "mcp/src/blacktv/black-tv.sh",
]
/// The repo file that is the source of truth for `bin`, or nil when the bin
/// is not a known helper or the checkout doesn't actually hold the file.
public static func vendoredSource(forBin bin: String) -> URL? {
let name = (bin as NSString).lastPathComponent
guard let rel = vendoredSources[name] else { return nil }
let url = RepoPaths.root.appendingPathComponent(rel)
return FileManager.default.fileExists(atPath: url.path) ? url : nil
}
/// Lowercase-hex sha256 the same digest `sha256sum` prints on the device.
public static func sha256(_ data: Data) -> String {
SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
}
/// sha256 of the vendored source for `bin`, or nil when the bin is not a
/// known helper or the repo checkout is absent (an installed app without a
/// repo can't judge freshness the badge simply stays off).
public static func expectedSHA(forBin bin: String) -> String? {
guard let url = vendoredSource(forBin: bin),
let data = try? Data(contentsOf: url) else { return nil }
return sha256(data)
}
/// Judge a device report against an expectation. nil expectation (unknown
/// helper / no repo) nil: freshness can't be judged, show nothing.
public static func freshness(expected: String?, reported: String?) -> Freshness? {
guard let expected else { return nil }
return reported == expected ? .current : .outdated(deployed: reported)
}
}