Skip to main content

sim_codec_bridge/
line.rs

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
11/// Encodes a BRIDGE packet to the strict `BRIDGE/1` line face.
12pub 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
48/// Decodes the strict `BRIDGE/1` line face to a BRIDGE packet.
49pub fn decode_bridge_text(text: &str, book: &BridgeBook) -> Result<BridgePacket> {
50    decode_bridge_text_with_limits(text, book, CodecId(0), DecodeLimits::default())
51}
52
53/// Decodes the strict `BRIDGE/1` line face under caller-supplied decode limits.
54pub 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}