Integrate live phone and watch remote prototype with guarded motion control
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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?
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user