import Foundation
enum RemoteFramingError: Error { case frameTooLarge(UInt32) }
/// Length-prefixed JSON framing over a TCP byte stream: a big-endian `UInt32` length followed by
/// the JSON body. Pure (no Network.framework) so it is host-testable; `RemoteLink` feeds each
/// `NWConnection` read into `Decoder.consume` and sends `encode`d frames.
enum RemoteFraming {
/// Hard ceiling on a single frame. A RemoteState (selection + params) is a few KB; 1 MB is ample
/// headroom while still rejecting a corrupt/oversized header fast (we can't resync past it, so the
/// decoder throws and the link tears down rather than buffering megabytes of garbage).
static let maxFrameBytes: UInt32 = 1024 * 1024
static func encode(_ message: RemoteMessage) throws -> Data {
let body = try JSONEncoder().encode(message)
var len = UInt32(body.count).bigEndian
var out = Data(bytes: &len, count: 4)
out.append(body)
return out
}
/// Accumulates bytes across reads and emits whole messages as soon as each frame completes.
struct Decoder {
private var buffer = Data()
mutating func consume(_ chunk: Data) throws -> [RemoteMessage] {
buffer.append(chunk)
var messages: [RemoteMessage] = []
while buffer.count >= 4 {
let len = buffer.prefix(4).reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
guard len <= RemoteFraming.maxFrameBytes else { throw RemoteFramingError.frameTooLarge(len) }
guard buffer.count >= 4 + Int(len) else { break } // wait for the rest of the frame
let body = buffer.subdata(in: 4 ..< 4 + Int(len))
buffer.removeSubrange(0 ..< 4 + Int(len)) // advance PAST this frame before decoding
// Skip a zero-length or undecodable frame (corrupt packet, version skew) rather than
// leaving it to be misread as the next length header — one bad frame must not poison
// the stream. The frame boundary is known from the length, so recovery is exact.
if len > 0, let message = try? JSONDecoder().decode(RemoteMessage.self, from: body) {
messages.append(message)
}
}
return messages
}
}
}