Files
wristivo/Packages/WristivoKit/Sources/SamsungProtocol/SamsungEventParser.swift
T

83 lines
2.6 KiB
Swift

import Foundation
/// Events accepted from the candidate Samsung remote-control channel.
///
/// This parser intentionally exposes no arbitrary method or key supplied by the TV.
/// Unknown events are inert and can be counted without logging their payload.
public enum SamsungRemoteEvent: Sendable, Equatable {
case connected(token: String?)
case unauthorized
case timedOut
case error
case unknown
}
public enum SamsungEventParserError: Error, Sendable, Equatable {
case empty
case oversized
case malformed
case invalidEventName
case invalidToken
}
/// A size-bounded, allowlist-based parser for WebSocket event messages.
public struct SamsungEventParser: Sendable {
public static let defaultMaximumMessageBytes = 16 * 1024
public static let maximumEventNameBytes = 128
public static let maximumTokenBytes = 4 * 1024
public let maximumMessageBytes: Int
public init(maximumMessageBytes: Int = Self.defaultMaximumMessageBytes) {
self.maximumMessageBytes = max(1, maximumMessageBytes)
}
public func parse(_ data: Data) throws -> SamsungRemoteEvent {
guard !data.isEmpty else { throw SamsungEventParserError.empty }
guard data.count <= maximumMessageBytes else {
throw SamsungEventParserError.oversized
}
let message: Message
do {
message = try JSONDecoder().decode(Message.self, from: data)
} catch {
throw SamsungEventParserError.malformed
}
guard let eventData = message.event.data(using: .utf8),
!eventData.isEmpty,
eventData.count <= Self.maximumEventNameBytes else {
throw SamsungEventParserError.invalidEventName
}
switch message.event {
case "ms.channel.connect":
guard let token = message.data?.token else { return .connected(token: nil) }
guard let tokenData = token.data(using: .utf8),
!tokenData.isEmpty,
tokenData.count <= Self.maximumTokenBytes else {
throw SamsungEventParserError.invalidToken
}
return .connected(token: token)
case "ms.channel.unauthorized":
return .unauthorized
case "ms.channel.timeOut":
return .timedOut
case "ms.error":
return .error
default:
return .unknown
}
}
private struct Message: Decodable {
let event: String
let data: EventData?
}
private struct EventData: Decodable {
let token: String?
}
}