1use std::collections::BTreeMap;
2
3use serde_json::Value as JsonValue;
4use sim_codec::{DecodeBudget, DecodeLimits};
5use sim_kernel::{CodecId, Error, Expr, Result, Symbol};
6
7use crate::identity::content_id_string;
8use crate::warrant::parse_content_id_string;
9use crate::{BridgeBook, BridgeHeader, BridgePacket, BridgePart, BridgeProvenance, BridgeWarrant};
10
11pub fn encode_bridge_text(packet: &BridgePacket, book: &BridgeBook) -> Result<String> {
13 book.validate_packet(packet)?;
14 let mut lines = vec![
15 "BRIDGE/1".to_owned(),
16 format!("CID {}", packet.header.cid.as_deref().unwrap_or("nil")),
17 format!("MOVE {}", symbol_text(&packet.header.move_kind)),
18 format!("FROM {}", checked_token("FROM", &packet.header.from)?),
19 format!("TO {}", string_list(&packet.header.to)?),
20 format!("ROLE {}", symbol_text(&packet.header.role)),
21 format!("PARENTS {}", string_list(&packet.header.parents)?),
22 format!("TASK {}", symbol_text(&packet.header.task)),
23 format!("OUTPUT {}", symbol_text(&packet.header.output)),
24 format!("CEIL {}", symbol_list(&packet.header.ceiling)),
25 format!("CONTEXT {}", symbol_list(&packet.header.context)),
26 format!(
27 "PROV author={} card={}",
28 symbol_text(&packet.header.provenance.author),
29 packet.header.provenance.card.as_deref().unwrap_or("nil")
30 ),
31 ];
32 if let Some(warrant) = &packet.warrant {
33 lines.push(format!("WARRANT {}", warrant_text(warrant)));
34 }
35 lines.push("BODY".to_owned());
36 for part in &packet.body {
37 lines.push(format!(
38 "{} {} payload={}",
39 part_keyword(&part.kind),
40 symbol_text(&part.id),
41 payload_text(&part.payload)?
42 ));
43 }
44 lines.push("END".to_owned());
45 Ok(format!("{}\n", lines.join("\n")))
46}
47
48pub fn decode_bridge_text(text: &str, book: &BridgeBook) -> Result<BridgePacket> {
50 decode_bridge_text_with_limits(text, book, CodecId(0), DecodeLimits::default())
51}
52
53pub fn decode_bridge_text_with_limits(
55 text: &str,
56 book: &BridgeBook,
57 codec: CodecId,
58 limits: DecodeLimits,
59) -> Result<BridgePacket> {
60 let mut budget = DecodeBudget::new(limits);
61 budget.check_input_bytes(codec, text.len())?;
62 let mut lines = text.lines();
63 match lines.next() {
64 Some("BRIDGE/1") => {}
65 _ => {
66 return Err(Error::Eval(
67 "BRIDGE packet must start with BRIDGE/1".to_owned(),
68 ));
69 }
70 }
71
72 let mut headers = BTreeMap::new();
73 let mut body_lines = Vec::new();
74 let mut in_body = false;
75 let mut ended = false;
76 for line in lines {
77 if ended {
78 if !line.trim().is_empty() {
79 return Err(Error::Eval("BRIDGE packet has text after END".to_owned()));
80 }
81 continue;
82 }
83 if line == "BODY" {
84 if in_body {
85 return Err(Error::Eval("duplicate BRIDGE BODY marker".to_owned()));
86 }
87 in_body = true;
88 continue;
89 }
90 if line == "END" {
91 if !in_body {
92 return Err(Error::Eval("BRIDGE END before BODY".to_owned()));
93 }
94 ended = true;
95 continue;
96 }
97 if in_body {
98 body_lines.push(line.to_owned());
99 } else {
100 let (key, value) = split_header(line)?;
101 if !is_known_header(key) {
102 return Err(Error::Eval(format!("unknown BRIDGE header {key}")));
103 }
104 if headers.insert(key.to_owned(), value.to_owned()).is_some() {
105 return Err(Error::Eval(format!("duplicate BRIDGE header {key}")));
106 }
107 }
108 }
109 if !ended {
110 return Err(Error::Eval("BRIDGE packet is missing END".to_owned()));
111 }
112 let packet = BridgePacket {
113 header: BridgeHeader {
114 cid: header(&headers, "CID").and_then(parse_cid)?,
115 move_kind: parse_symbol(header(&headers, "MOVE")?),
116 from: header(&headers, "FROM")?.to_owned(),
117 to: parse_string_list(header(&headers, "TO")?)?,
118 role: parse_symbol(header(&headers, "ROLE")?),
119 parents: parse_string_list(header(&headers, "PARENTS")?)?,
120 task: parse_symbol(header(&headers, "TASK")?),
121 output: parse_symbol(header(&headers, "OUTPUT")?),
122 ceiling: parse_symbol_list(header(&headers, "CEIL")?)?,
123 context: parse_symbol_list(header(&headers, "CONTEXT")?)?,
124 provenance: parse_provenance(header(&headers, "PROV")?)?,
125 },
126 body: body_lines
127 .iter()
128 .map(|line| parse_part(line, book, codec, &mut budget))
129 .collect::<Result<Vec<_>>>()?,
130 warrant: match headers.get("WARRANT") {
131 Some(value) => Some(parse_warrant(value)?),
132 None => None,
133 },
134 };
135 book.validate_packet(&packet)?;
136 Ok(packet)
137}
138
139fn split_header(line: &str) -> Result<(&str, &str)> {
140 line.split_once(' ')
141 .ok_or_else(|| Error::Eval(format!("malformed BRIDGE header line {line:?}")))
142}
143
144fn is_known_header(header: &str) -> bool {
145 matches!(
146 header,
147 "CID"
148 | "MOVE"
149 | "FROM"
150 | "TO"
151 | "ROLE"
152 | "PARENTS"
153 | "TASK"
154 | "OUTPUT"
155 | "CEIL"
156 | "CONTEXT"
157 | "PROV"
158 | "WARRANT"
159 )
160}
161
162fn header<'a>(headers: &'a BTreeMap<String, String>, name: &str) -> Result<&'a str> {
163 headers
164 .get(name)
165 .map(String::as_str)
166 .ok_or_else(|| Error::Eval(format!("BRIDGE packet is missing {name} header")))
167}
168
169fn parse_cid(value: &str) -> Result<Option<String>> {
170 if value == "nil" {
171 Ok(None)
172 } else {
173 Ok(Some(value.to_owned()))
174 }
175}
176
177fn parse_provenance(value: &str) -> Result<BridgeProvenance> {
178 let mut author = None;
179 let mut card = None;
180 for item in value.split(' ') {
181 let Some((key, value)) = item.split_once('=') else {
182 return Err(Error::Eval(format!("malformed BRIDGE provenance {item}")));
183 };
184 match key {
185 "author" => author = Some(parse_symbol(value)),
186 "card" => {
187 card = Some(if value == "nil" {
188 None
189 } else {
190 Some(value.to_owned())
191 })
192 }
193 _ => {
194 return Err(Error::Eval(format!(
195 "unknown BRIDGE provenance field {key}"
196 )));
197 }
198 }
199 }
200 Ok(BridgeProvenance {
201 author: author.ok_or_else(|| Error::Eval("BRIDGE provenance missing author".to_owned()))?,
202 card: card.ok_or_else(|| Error::Eval("BRIDGE provenance missing card".to_owned()))?,
203 })
204}
205
206fn warrant_text(warrant: &BridgeWarrant) -> String {
207 format!(
208 "moves={} frames={} parts={}",
209 content_id_string(&warrant.moves),
210 content_id_string(&warrant.frames),
211 warrant_parts_text(&warrant.parts)
212 )
213}
214
215fn warrant_parts_text(parts: &[(Symbol, sim_kernel::ContentId)]) -> String {
216 format!(
217 "[{}]",
218 parts
219 .iter()
220 .map(|(kind, id)| format!("{}={}", symbol_text(kind), content_id_string(id)))
221 .collect::<Vec<_>>()
222 .join(",")
223 )
224}
225
226fn parse_warrant(value: &str) -> Result<BridgeWarrant> {
227 let mut moves = None;
228 let mut frames = None;
229 let mut parts = None;
230 for item in value.split(' ') {
231 let Some((key, value)) = item.split_once('=') else {
232 return Err(Error::Eval(format!("malformed BRIDGE warrant {item}")));
233 };
234 match key {
235 "moves" => moves = Some(parse_content_id_string(value)?),
236 "frames" => frames = Some(parse_content_id_string(value)?),
237 "parts" => parts = Some(parse_warrant_parts(value)?),
238 _ => return Err(Error::Eval(format!("unknown BRIDGE warrant field {key}"))),
239 }
240 }
241 Ok(BridgeWarrant {
242 moves: moves.ok_or_else(|| Error::Eval("BRIDGE warrant missing moves".to_owned()))?,
243 frames: frames.ok_or_else(|| Error::Eval("BRIDGE warrant missing frames".to_owned()))?,
244 parts: parts.ok_or_else(|| Error::Eval("BRIDGE warrant missing parts".to_owned()))?,
245 })
246}
247
248fn parse_warrant_parts(text: &str) -> Result<Vec<(Symbol, sim_kernel::ContentId)>> {
249 parse_list(text)?
250 .into_iter()
251 .map(|item| {
252 let (kind, cid) = item
253 .split_once('=')
254 .ok_or_else(|| Error::Eval(format!("malformed BRIDGE warrant part {item}")))?;
255 Ok((parse_symbol(kind), parse_content_id_string(cid)?))
256 })
257 .collect()
258}
259
260fn parse_part(
261 line: &str,
262 book: &BridgeBook,
263 codec: CodecId,
264 budget: &mut DecodeBudget,
265) -> Result<BridgePart> {
266 let mut fields = line.splitn(3, ' ');
267 let keyword = fields
268 .next()
269 .ok_or_else(|| Error::Eval("empty BRIDGE part line".to_owned()))?;
270 let id = fields
271 .next()
272 .ok_or_else(|| Error::Eval(format!("BRIDGE part {keyword} missing id")))?;
273 let rest = fields
274 .next()
275 .ok_or_else(|| Error::Eval(format!("BRIDGE part {keyword} missing payload")))?;
276 let payload = rest
277 .strip_prefix("payload=")
278 .ok_or_else(|| Error::Eval(format!("BRIDGE part {keyword} has unknown field")))?;
279 let kind = kind_from_keyword(keyword);
280 book.parts.require_registered(&kind)?;
281 let payload = parse_payload(payload, codec, budget)?;
282 match &kind {
283 kind if *kind == Symbol::qualified("bridge", "Frame") => {
284 book.frames.validate_payload(&payload)?;
285 }
286 kind if *kind == Symbol::qualified("bridge", "Call") => {
287 crate::validate_call_payload(&payload)?;
288 }
289 kind if *kind == Symbol::qualified("bridge", "Weave") => {
290 crate::validate_weave_payload(&payload)?;
291 }
292 kind if collab_part(kind) => {
293 crate::validate_collab_payload(kind, &payload)?;
294 }
295 _ => {}
296 }
297 Ok(BridgePart {
298 id: parse_symbol(id),
299 kind,
300 payload,
301 })
302}
303
304fn payload_text(expr: &Expr) -> Result<String> {
305 serde_json::to_string(&sim_codec_json::expr_to_json(expr))
306 .map_err(|err| Error::Eval(format!("encode BRIDGE payload JSON: {err}")))
307}
308
309fn parse_payload(text: &str, codec: CodecId, budget: &mut DecodeBudget) -> Result<Expr> {
310 let value = serde_json::from_str::<JsonValue>(text)
311 .map_err(|err| Error::Eval(format!("parse BRIDGE payload JSON: {err}")))?;
312 sim_codec_json::json_to_expr(codec, &value, budget, 0)
313}
314
315fn symbol_text(symbol: &Symbol) -> String {
316 symbol.as_qualified_str()
317}
318
319fn parse_symbol(text: &str) -> Symbol {
320 match text.split_once('/') {
321 Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
322 Symbol::qualified(namespace.to_owned(), name.to_owned())
323 }
324 _ => Symbol::new(text.to_owned()),
325 }
326}
327
328fn string_list(items: &[String]) -> Result<String> {
329 let tokens = items
330 .iter()
331 .map(|item| checked_token("list item", item))
332 .collect::<Result<Vec<_>>>()?;
333 Ok(format!("[{}]", tokens.join(",")))
334}
335
336fn symbol_list(items: &[Symbol]) -> String {
337 format!(
338 "[{}]",
339 items.iter().map(symbol_text).collect::<Vec<_>>().join(",")
340 )
341}
342
343fn parse_string_list(text: &str) -> Result<Vec<String>> {
344 parse_list(text).map(|items| items.into_iter().map(str::to_owned).collect())
345}
346
347fn parse_symbol_list(text: &str) -> Result<Vec<Symbol>> {
348 parse_list(text).map(|items| items.into_iter().map(parse_symbol).collect())
349}
350
351fn parse_list(text: &str) -> Result<Vec<&str>> {
352 let inner = text
353 .strip_prefix('[')
354 .and_then(|text| text.strip_suffix(']'))
355 .ok_or_else(|| Error::Eval(format!("BRIDGE list must use brackets: {text}")))?;
356 if inner.is_empty() {
357 Ok(Vec::new())
358 } else {
359 Ok(inner.split(',').collect())
360 }
361}
362
363fn checked_token<'a>(label: &str, value: &'a str) -> Result<&'a str> {
364 if value.is_empty()
365 || value
366 .chars()
367 .any(|ch| ch.is_whitespace() || matches!(ch, '[' | ']' | ','))
368 {
369 Err(Error::Eval(format!(
370 "BRIDGE {label} must be a non-empty token"
371 )))
372 } else {
373 Ok(value)
374 }
375}
376
377fn part_keyword(kind: &Symbol) -> String {
378 kind.name.to_ascii_uppercase()
379}
380
381fn kind_from_keyword(keyword: &str) -> Symbol {
382 let mut chars = keyword.chars();
383 let name = match chars.next() {
384 Some(first) => format!(
385 "{}{}",
386 first.to_ascii_uppercase(),
387 chars.as_str().to_ascii_lowercase()
388 ),
389 None => String::new(),
390 };
391 Symbol::qualified("bridge", name)
392}
393
394fn collab_part(kind: &Symbol) -> bool {
395 matches!(
396 kind.name.as_ref(),
397 "Review" | "Vote" | "Patch" | "Evidence" | "Receipt" | "Attest"
398 )
399}