import Foundation /// Test-side mini-port of the server's `parseClientMessage` (src/protocol.ts:45-176) /// plus `parseApproveMode` (src/server.ts:94-102). Used by the roundtrip property /// test and the cross-implementation vector tests: `MessageCodec.encode` output is /// fed through THIS parser, which itself is pinned against the vectors transcribed /// from test/protocol.test.ts — so the Swift encoder and the TS server parser /// cannot drift silently (the live tripwire is T-iOS-16's real-server CI). enum ServerViewMessage: Equatable { case attach(sessionId: String?, cwd: String?) case input(data: String) case resize(cols: Int, rows: Int) case approve case reject } enum ServerViewParser { /// Mirror of src/protocol.ts SESSION_ID_RE (UUID v4, /i). private static let sessionIdPattern = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" /// Mirror of src/server.ts:76 PERMISSION_MODES. static let permissionModes: Set = ["default", "acceptEdits", "plan", "auto"] /// Mirror of src/protocol.ts:27 ALLOWED_TYPES. private static let allowedTypes: Set = ["attach", "input", "resize", "approve", "reject"] // MARK: - parseClientMessage mirror (never throws; invalid → nil) static func parse(_ raw: String) -> ServerViewMessage? { guard let data = raw.data(using: .utf8), let parsed = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]), let obj = parsed as? [String: Any], let type = obj["type"] as? String, allowedTypes.contains(type) else { return nil } switch type { case "resize": return parseResize(obj) case "input": return parseInput(obj) case "approve": return .approve case "reject": return .reject default: return parseAttach(obj) } } /// Mirror of src/server.ts:94-102 — reads `mode` from the RAW frame's top level. static func topLevelMode(_ raw: String) -> String? { guard let data = raw.data(using: .utf8), let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any], let mode = obj["mode"] as? String, permissionModes.contains(mode) else { return nil } return mode } // MARK: - Per-type validators (src/protocol.ts:90-176) private static func parseResize(_ obj: [String: Any]) -> ServerViewMessage? { guard let cols = integerDimension(obj["cols"]), let rows = integerDimension(obj["rows"]) else { return nil } return .resize(cols: cols, rows: rows) } /// Mirror of isValidDimension: number, integer, 1...1000 (src/protocol.ts:113-115). private static func integerDimension(_ value: Any?) -> Int? { guard let number = value as? NSNumber, !isJSONBoolean(number) else { return nil } let doubleValue = number.doubleValue guard doubleValue == doubleValue.rounded(.towardZero), doubleValue >= 1, doubleValue <= 1000 else { return nil } return number.intValue } private static func isJSONBoolean(_ number: NSNumber) -> Bool { CFGetTypeID(number) == CFBooleanGetTypeID() } private static func parseInput(_ obj: [String: Any]) -> ServerViewMessage? { guard let data = obj["data"] as? String else { return nil } return .input(data: data) } private static func parseAttach(_ obj: [String: Any]) -> ServerViewMessage? { // sessionId key must be explicitly present (src/protocol.ts:132-134). guard obj.index(forKey: "sessionId") != nil else { return nil } var cwd: String? if let rawCwd = obj["cwd"] { guard let cwdString = rawCwd as? String, cwdString.hasPrefix("/") else { return nil } cwd = cwdString } if obj["sessionId"] is NSNull { return .attach(sessionId: nil, cwd: cwd) } guard let sessionId = obj["sessionId"] as? String, isServerValidSessionId(sessionId) else { return nil } return .attach(sessionId: sessionId, cwd: cwd) } static func isServerValidSessionId(_ candidate: String) -> Bool { candidate.range( of: sessionIdPattern, options: [.regularExpression, .caseInsensitive] ) != nil } }