bc_envelope/base/
format.rs

1//! Formats an envelope as a CBOR data structure and provides human-readable text representations.
2//!
3//! This module provides functionality for formatting envelopes in various ways:
4//!
5//! 1. **Envelope Notation**: A human-readable text representation showing the semantic structure
6//!    of an envelope (the `format` methods)
7//!
8//! 2. **CBOR Diagnostics**: Text representation of the underlying CBOR structure, following
9//!    [RFC-8949 §8](https://www.rfc-editor.org/rfc/rfc8949.html#name-diagnostic-notation)
10//!    (the `diagnostic` methods)
11//!
12//! 3. **CBOR Hex**: Hexadecimal representation of the raw CBOR bytes (the `hex` methods)
13//!
14//! All of these formats can be used for debugging, visualization, and understanding the
15//! structure of envelopes.
16//!
17//! # Examples
18//!
19//! ```
20//! use bc_envelope::prelude::*;
21//!
22//! // Create a simple envelope with assertions
23//! let envelope = Envelope::new("Alice")
24//!     .add_assertion("knows", "Bob")
25//!     .add_assertion("knows", "Carol");
26//!
27//! // Format the envelope as human-readable envelope notation
28//! let formatted = envelope.format();
29//! // Will output: "Alice" [ "knows": "Bob", "knows": "Carol" ]
30//!
31//! // Get the CBOR diagnostic notation (with annotations)
32//! let diagnostic = envelope.diagnostic_annotated();
33//!
34//! // Get the CBOR hex representation
35//! let hex = envelope.hex();
36//! ```
37
38use bc_components::XID;
39use dcbor::prelude::*;
40use crate::{Envelope, Assertion, string_utils::StringUtils, FormatContext, with_format_context};
41#[cfg(feature = "known_value")]
42use crate::{KnownValue, known_values};
43
44use super::{EnvelopeSummary, envelope::EnvelopeCase};
45
46/// Support for the various text output formats for ``Envelope``.
47impl Envelope {
48    /// Returns the envelope notation for this envelope.
49    pub fn format_opt(&self, context: Option<&FormatContext>) -> String {
50        let context = context.cloned().unwrap_or(FormatContext::default());
51        self.format_item(&context).format(context.is_flat()).trim().to_string()
52    }
53
54    /// Returns the envelope notation for this envelope.
55    ///
56    /// Uses the current format context.
57    pub fn format(&self) -> String {
58        with_format_context!(|context| {
59            self.format_opt(Some(context))
60        })
61    }
62
63    /// Returns the envelope notation for this envelope in flat format.
64    ///
65    /// In flat format, the envelope is printed on a single line.
66    pub fn format_flat(&self) -> String {
67        with_format_context!(|context: &FormatContext| {
68            let context = context.clone().set_flat(true);
69            self.format_opt(Some(&context))
70        })
71    }
72
73    /// Returns the CBOR diagnostic notation for this envelope, with annotations.
74    ///
75    /// See [RFC-8949 §8](https://www.rfc-editor.org/rfc/rfc8949.html#name-diagnostic-notation)
76    /// for information on CBOR diagnostic notation.
77    pub fn diagnostic_annotated(&self) -> String {
78        with_format_context!(|context: &FormatContext| {
79            self.tagged_cbor().diagnostic_opt(true, false, false, Some(context.tags()))
80        })
81    }
82
83    /// Returns the CBOR diagnostic notation for this envelope.
84    ///
85    /// Uses the current format context.
86    ///
87    /// See [RFC-8949 §8](https://www.rfc-editor.org/rfc/rfc8949.html#name-diagnostic-notation)
88    /// for information on CBOR diagnostic notation.
89    pub fn diagnostic(&self) -> String {
90        self.tagged_cbor().diagnostic()
91    }
92
93    /// Returns the CBOR hex dump of this envelope.
94    ///
95    /// See [RFC-8949](https://www.rfc-editor.org/rfc/rfc8949.html) for information on
96    /// the CBOR binary format.
97    pub fn hex_opt(&self, annotate: bool, context: Option<&FormatContext>) -> String {
98        let cbor: CBOR = self.clone().into();
99        cbor.hex_opt(annotate, Some(context.unwrap_or(&FormatContext::default()).tags()))
100    }
101
102    /// Returns the CBOR hex dump of this envelope.
103    ///
104    /// Uses the current format context.
105    ///
106    /// See [RFC-8949](https://www.rfc-editor.org/rfc/rfc8949.html) for information on
107    /// the CBOR binary format.
108    pub fn hex(&self) -> String {
109        with_format_context!(|context| {
110            self.hex_opt(true, Some(context))
111        })
112    }
113}
114
115/// Implementers of this trait define how to be formatted in when output in envelope notation.
116pub trait EnvelopeFormat {
117    fn format_item(&self, context: &FormatContext) -> EnvelopeFormatItem;
118}
119
120/// This type is returned by implementers of the [`EnvelopeFormat`] trait.
121#[derive(Debug, Clone, Eq)]
122pub enum EnvelopeFormatItem {
123    Begin(String),
124    End(String),
125    Item(String),
126    Separator,
127    List(Vec<EnvelopeFormatItem>),
128}
129
130impl EnvelopeFormatItem {
131    fn flatten(&self) -> Vec<EnvelopeFormatItem> {
132        match self {
133            EnvelopeFormatItem::List(items) => items.iter().flat_map(|i| i.flatten()).collect(),
134            _ => vec![self.clone()],
135        }
136    }
137
138    fn nicen(items: &[EnvelopeFormatItem]) -> Vec<EnvelopeFormatItem> {
139        let mut input = items.to_vec();
140        let mut result: Vec<EnvelopeFormatItem> = vec![];
141
142        while !input.is_empty() {
143            let current = input.remove(0);
144            if input.is_empty() {
145                result.push(current);
146                break;
147            }
148            if let EnvelopeFormatItem::End(end_string) = current.clone() {
149                if let EnvelopeFormatItem::Begin(begin_string) = input[0].clone() {
150                    result.push(EnvelopeFormatItem::End(format!("{} {}", end_string, begin_string)));
151                    result.push(EnvelopeFormatItem::Begin("".to_string()));
152                    input.remove(0);
153                } else {
154                    result.push(current);
155                }
156            } else {
157                result.push(current);
158            }
159        }
160
161        result
162    }
163
164    fn indent(level: usize) -> String {
165        " ".repeat(level * 4)
166    }
167
168    fn add_space_at_end_if_needed(s: &str) -> String {
169        if s.is_empty() {
170            " ".to_string()
171        } else if s.ends_with(' ') {
172            s.to_string()
173        } else {
174            s.to_string() + " "
175        }
176    }
177
178    fn format(&self, is_flat: bool) -> String {
179        if is_flat {
180            return self.format_flat();
181        }
182        self.format_hierarchical()
183    }
184
185    fn format_flat(&self) -> String {
186        let mut line: String = "".to_string();
187        let items = self.flatten();
188        for item in items {
189            match item {
190                EnvelopeFormatItem::Begin(s) => {
191                    if !line.ends_with(' ') {
192                        line += " ";
193                    }
194                    line += &s;
195                    line += " ";
196                },
197                EnvelopeFormatItem::End(s) => {
198                    if !line.ends_with(' ') {
199                        line += " ";
200                    }
201                    line += &s;
202                    line += " ";
203                },
204                EnvelopeFormatItem::Item(s) => line += &s,
205                EnvelopeFormatItem::Separator => {
206                    line = line.trim_end().to_string() + ", ";
207                },
208                EnvelopeFormatItem::List(items) => {
209                    for item in items {
210                        line += &item.format_flat();
211                    }
212                }
213            }
214        }
215        line
216    }
217
218    fn format_hierarchical(&self) -> String {
219        let mut lines: Vec<String> = vec![];
220        let mut level = 0;
221        let mut current_line = "".to_string();
222        let items = Self::nicen(&self.flatten());
223        for item in items {
224            match item {
225                EnvelopeFormatItem::Begin(delimiter) => {
226                    if !delimiter.is_empty() {
227                        let c = if current_line.is_empty() {
228                            delimiter
229                        } else {
230                            Self::add_space_at_end_if_needed(&current_line) + &delimiter
231                        };
232                        lines.push(Self::indent(level) + &c + "\n");
233                    }
234                    level += 1;
235                    current_line = "".to_string();
236                }
237                EnvelopeFormatItem::End(delimiter) => {
238                    if !current_line.is_empty() {
239                        lines.push(Self::indent(level) + &current_line + "\n");
240                        current_line = "".to_string();
241                    }
242                    level -= 1;
243                    lines.push(Self::indent(level) + &delimiter + "\n");
244                }
245                EnvelopeFormatItem::Item(string) => {
246                    current_line += &string;
247                }
248                EnvelopeFormatItem::Separator => {
249                    if !current_line.is_empty() {
250                        lines.push(Self::indent(level) + &current_line + "\n");
251                        current_line = "".to_string();
252                    }
253                }
254                EnvelopeFormatItem::List(_) => {
255                    lines.push("<list>".to_string());
256                }
257            }
258        }
259        if !current_line.is_empty() {
260            lines.push(current_line);
261        }
262        lines.join("")
263    }
264}
265
266/// Implements conversion from string slice to EnvelopeFormatItem.
267impl From<&str> for EnvelopeFormatItem {
268    fn from(s: &str) -> Self {
269        Self::Item(s.to_string())
270    }
271}
272
273/// Implements string display for EnvelopeFormatItem.
274impl std::fmt::Display for EnvelopeFormatItem {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match self {
277            EnvelopeFormatItem::Begin(s) => write!(f, ".begin({})", s),
278            EnvelopeFormatItem::End(s) => write!(f, ".end({})", s),
279            EnvelopeFormatItem::Item(s) => write!(f, ".item({})", s),
280            EnvelopeFormatItem::Separator => write!(f, ".separator"),
281            EnvelopeFormatItem::List(items) => write!(f, ".list({:?})", items),
282        }
283    }
284}
285
286/// Implements partial equality for EnvelopeFormatItem.
287impl PartialEq for EnvelopeFormatItem {
288    fn eq(&self, other: &Self) -> bool {
289        match (self, other) {
290            (EnvelopeFormatItem::Begin(s1), EnvelopeFormatItem::Begin(s2)) => s1 == s2,
291            (EnvelopeFormatItem::End(s1), EnvelopeFormatItem::End(s2)) => s1 == s2,
292            (EnvelopeFormatItem::Item(s1), EnvelopeFormatItem::Item(s2)) => s1 == s2,
293            (EnvelopeFormatItem::Separator, EnvelopeFormatItem::Separator) => true,
294            (EnvelopeFormatItem::List(items1), EnvelopeFormatItem::List(items2)) => items1 == items2,
295            _ => false,
296        }
297    }
298}
299
300impl EnvelopeFormatItem {
301    fn index(&self) -> u32 {
302        match self {
303            EnvelopeFormatItem::Begin(_) => 1,
304            EnvelopeFormatItem::End(_) => 2,
305            EnvelopeFormatItem::Item(_) => 3,
306            EnvelopeFormatItem::Separator => 4,
307            EnvelopeFormatItem::List(_) => 5,
308        }
309    }
310}
311
312/// Implements partial ordering for EnvelopeFormatItem.
313impl PartialOrd for EnvelopeFormatItem {
314    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
315        Some(self.cmp(other))
316    }
317}
318
319
320/// Implements total ordering for EnvelopeFormatItem.
321impl Ord for EnvelopeFormatItem {
322    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
323        let l_index = self.index();
324        let r_index = other.index();
325        match l_index.cmp(&r_index) {
326            std::cmp::Ordering::Less => return std::cmp::Ordering::Less,
327            std::cmp::Ordering::Greater => return std::cmp::Ordering::Greater,
328            _ => {}
329        }
330        match (self, other) {
331            (EnvelopeFormatItem::Begin(l), EnvelopeFormatItem::Begin(r)) => l.cmp(r),
332            (EnvelopeFormatItem::End(l), EnvelopeFormatItem::End(r)) => l.cmp(r),
333            (EnvelopeFormatItem::Item(l), EnvelopeFormatItem::Item(r)) => l.cmp(r),
334            (EnvelopeFormatItem::Separator, EnvelopeFormatItem::Separator) => std::cmp::Ordering::Equal,
335            (EnvelopeFormatItem::List(l), EnvelopeFormatItem::List(r)) => l.cmp(r),
336            _ => std::cmp::Ordering::Equal,
337        }
338    }
339}
340
341// impl EnvelopeFormat for Digest {
342//     fn format_item(&self, _context: &FormatContext) -> EnvelopeFormatItem {
343//         EnvelopeFormatItem::Item(hex::encode(self.data()))
344//     }
345// }
346
347// impl EnvelopeFormat for ARID {
348//     fn format_item(&self, _context: &FormatContext) -> EnvelopeFormatItem {
349//         EnvelopeFormatItem::Item(hex::encode(self.data()))
350//     }
351// }
352
353/// Implements formatting for CBOR values in envelope notation.
354impl EnvelopeFormat for CBOR {
355    fn format_item(&self, context: &FormatContext) -> EnvelopeFormatItem {
356        match self.as_case() {
357            CBORCase::Tagged(tag, cbor) if tag == &Envelope::cbor_tags()[0] => {
358                Envelope::from_untagged_cbor(cbor.clone())
359                    .map(|envelope| envelope.format_item(context))
360                    .unwrap_or_else(|_| "<error>".into())
361            }
362            _ => EnvelopeFormatItem::Item(
363                self.envelope_summary(usize::MAX, context)
364                    .unwrap_or_else(|_| "<error>".into())
365            ),
366        }
367    }
368}
369
370/// Implements formatting for Envelope in envelope notation.
371impl EnvelopeFormat for Envelope {
372    fn format_item(&self, context: &FormatContext) -> EnvelopeFormatItem {
373        match self.case() {
374            EnvelopeCase::Leaf { cbor, .. } => cbor.format_item(context),
375            EnvelopeCase::Wrapped { envelope, .. } => EnvelopeFormatItem::List(vec![
376                EnvelopeFormatItem::Begin("{".to_string()),
377                envelope.format_item(context),
378                EnvelopeFormatItem::End("}".to_string()),
379            ]),
380            EnvelopeCase::Assertion(assertion) => assertion.format_item(context),
381            #[cfg(feature = "known_value")]
382            EnvelopeCase::KnownValue { value, .. } => value.format_item(context),
383            #[cfg(feature = "encrypt")]
384            EnvelopeCase::Encrypted(_) => EnvelopeFormatItem::Item("ENCRYPTED".to_string()),
385            #[cfg(feature = "compress")]
386            EnvelopeCase::Compressed(_) => EnvelopeFormatItem::Item("COMPRESSED".to_string()),
387            EnvelopeCase::Node { subject, assertions, .. } => {
388                let mut items: Vec<EnvelopeFormatItem> = Vec::new();
389
390                let subject_item = subject.format_item(context);
391                let mut elided_count = 0;
392                #[cfg(feature = "encrypt")]
393                let mut encrypted_count = 0;
394                #[cfg(feature = "compress")]
395                let mut compressed_count = 0;
396                #[cfg(feature = "known_value")]
397                let mut type_assertion_items: Vec<Vec<EnvelopeFormatItem>> = Vec::new();
398                let mut assertion_items: Vec<Vec<EnvelopeFormatItem>> = Vec::new();
399
400                for assertion in assertions {
401                    match assertion.case() {
402                        EnvelopeCase::Elided(_) => {
403                            elided_count += 1;
404                        },
405                        #[cfg(feature = "encrypt")]
406                        EnvelopeCase::Encrypted(_) => {
407                            encrypted_count += 1;
408                        },
409                        #[cfg(feature = "compress")]
410                        EnvelopeCase::Compressed(_) => {
411                            compressed_count += 1;
412                        },
413                        _ => {
414                            let item = vec![assertion.format_item(context)];
415                            #[cfg(feature = "known_value")]
416                            {
417                                let mut is_type_assertion = false;
418                                if let Some(predicate) = assertion.as_predicate() {
419                                    if let Some(known_value) = predicate.subject().as_known_value() {
420                                        if *known_value == known_values::IS_A {
421                                            is_type_assertion = true;
422                                        }
423                                    }
424                                }
425                                if is_type_assertion {
426                                    type_assertion_items.push(item);
427                                } else {
428                                    assertion_items.push(item);
429                                }
430                            }
431                            #[cfg(not(feature = "known_value"))]
432                            assertion_items.push(item);
433                        },
434                    }
435                }
436                #[cfg(feature = "known_value")]
437                type_assertion_items.sort();
438                assertion_items.sort();
439                #[cfg(feature = "known_value")]
440                assertion_items.splice(0..0, type_assertion_items);
441                #[cfg(feature = "compress")]
442                if compressed_count > 1 {
443                    assertion_items.push(vec![EnvelopeFormatItem::Item(format!("COMPRESSED ({})", compressed_count))]);
444                } else if compressed_count > 0 {
445                    assertion_items.push(vec![EnvelopeFormatItem::Item("COMPRESSED".to_string())]);
446                }
447                if elided_count > 1 {
448                    assertion_items.push(vec![EnvelopeFormatItem::Item(format!("ELIDED ({})", elided_count))]);
449                } else if elided_count > 0 {
450                    assertion_items.push(vec![EnvelopeFormatItem::Item("ELIDED".to_string())]);
451                }
452                #[cfg(feature = "encrypt")]
453                if encrypted_count > 1 {
454                    assertion_items.push(vec![EnvelopeFormatItem::Item(format!("ENCRYPTED ({})", encrypted_count))]);
455                } else if encrypted_count > 0 {
456                    assertion_items.push(vec![EnvelopeFormatItem::Item("ENCRYPTED".to_string())]);
457                }
458                let joined_assertions_items: Vec<Vec<EnvelopeFormatItem>> =
459                    itertools::intersperse_with(assertion_items, || vec![EnvelopeFormatItem::Separator]).collect();
460
461                let needs_braces = subject.is_subject_assertion();
462
463                if needs_braces {
464                    items.push(EnvelopeFormatItem::Begin("{".to_string()));
465                }
466                items.push(subject_item);
467                if needs_braces {
468                    items.push(EnvelopeFormatItem::End("}".to_string()));
469                }
470                items.push(EnvelopeFormatItem::Begin("[".to_string()));
471                items.extend(joined_assertions_items.into_iter().flatten());
472                items.push(EnvelopeFormatItem::End("]".to_string()));
473                EnvelopeFormatItem::List(items)
474            },
475            EnvelopeCase::Elided(_) => EnvelopeFormatItem::Item("ELIDED".to_string()),
476        }
477    }
478}
479
480/// Implements formatting for Assertion in envelope notation.
481impl EnvelopeFormat for Assertion {
482    fn format_item(&self, context: &FormatContext) -> EnvelopeFormatItem {
483        EnvelopeFormatItem::List(vec![
484            self.predicate().format_item(context),
485            EnvelopeFormatItem::Item(": ".to_string()),
486            self.object().format_item(context),
487        ])
488    }
489}
490
491#[cfg(feature = "known_value")]
492/// Implements formatting for KnownValue in envelope notation.
493impl EnvelopeFormat for KnownValue {
494    fn format_item(&self, context: &FormatContext) -> EnvelopeFormatItem {
495        EnvelopeFormatItem::Item(context
496            .known_values()
497            .assigned_name(self)
498            .map(|s| s.to_string())
499            .unwrap_or_else(|| self.name())
500            .flanked_by("'", "'")
501        )
502    }
503}
504
505/// Implements formatting for XID in envelope notation.
506impl EnvelopeFormat for XID {
507    fn format_item(&self, _context: &FormatContext) -> EnvelopeFormatItem {
508        EnvelopeFormatItem::Item(hex::encode(self.data()))
509    }
510}
511
512 impl Envelope {
513    fn description(&self, context: Option<&FormatContext>) -> String {
514        match self.case() {
515            EnvelopeCase::Node { subject, assertions, .. } => {
516                let assertions = assertions
517                    .iter()
518                    .map(|a| a.to_string())
519                    .collect::<Vec<String>>()
520                    .join(", ")
521                    .flanked_by("[", "]");
522                format!(".node({}, {})", subject, assertions)
523            }
524            EnvelopeCase::Leaf { cbor, .. } => format!(".cbor({})", cbor.format_item(context.unwrap_or(&FormatContext::default()))),
525            EnvelopeCase::Wrapped { envelope, .. } => format!(".wrapped({})", envelope),
526            EnvelopeCase::Assertion(assertion) => format!(".assertion({}, {})", assertion.predicate(), assertion.object()),
527            EnvelopeCase::Elided(_) => ".elided".to_string(),
528            #[cfg(feature = "known_value")]
529            EnvelopeCase::KnownValue { value, .. } => format!(".knownValue({})", value),
530            #[cfg(feature = "encrypt")]
531            EnvelopeCase::Encrypted(_) => ".encrypted".to_string(),
532            #[cfg(feature = "compress")]
533            EnvelopeCase::Compressed(_) => ".compressed".to_string(),
534        }
535    }
536}
537
538/// Implements string display for Envelope.
539impl std::fmt::Display for Envelope {
540    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541        f.write_str(&self.description(None))
542    }
543}