package com.sptmobile.endpoint /** * `body` frame decoding — the phone-side twin of * `rust/link-proto/src/event.rs` (clean-room from the public adapter * contract; parity pinned by mirrored round-trip tests). The digest's * `Context{kind: owl_message}` row body is the composed `` verbatim, * and the `--json-payload` metadata blob rides its `json` attr — this is the * own-send echo dedup seam (KNOWN-HAZARDS 3.1). * * Escaping per the adapter contract: `\n` → `
`, HTML entities for * `<`/`>`/`"`, `&` LAST on encode — so decode order is load-bearing: * `
` first, then `<`/`>`/`"`, `&` LAST (amp-last, else * `&lt;` double-decodes to `<`). */ data class EventFrame( val attrs: List>, val body: String, ) { fun attr(name: String): String? = attrs.firstOrNull { it.first == name }?.second companion object { /** Decode an escaped body. ORDER MATTERS: `
` first, `&` LAST. */ fun decodeBody(s: String): String = s .replace("
", "\n") .replace("<", "<") .replace(">", ">") .replace(""", "\"") .replace("&", "&") /** * Parse one `` frame; null on anything malformed * (a bad digest row must never crash the timeline). Attribute values * decode with the same entity rules as the body. */ fun parse(frame: String): EventFrame? { val s = frame.trim() if (!s.startsWith("') if (headEnd < 0) return null val head = rest.substring(0, headEnd) val tail = rest.substring(headEnd + 1) if (!tail.endsWith("
")) return null val bodyEsc = tail.removeSuffix("") val attrs = mutableListOf>() var cur = head.trim() while (cur.isNotEmpty()) { val eq = cur.indexOf('=') if (eq < 0) return null val key = cur.substring(0, eq).trim() val after = cur.substring(eq + 1) if (!after.startsWith('"')) return null val value = after.substring(1) val endq = value.indexOf('"') if (endq < 0) return null attrs += key to decodeBody(value.substring(0, endq)) cur = value.substring(endq + 1).trim() } return EventFrame(attrs, decodeBody(bodyEsc)) } } }