Skip to main content

sov_universal_wallet/visitors/
display.rs

1use std::collections::BTreeMap;
2use std::fmt::Write;
3
4use thiserror::Error;
5
6use crate::schema::Primitive;
7use crate::ty::visitor::{ResolutionError, TypeResolver, TypeVisitor};
8use crate::ty::{
9    byte_display, ByteDisplay, Enum, FixedPointDisplay, IntegerDisplay, IntegerType, LinkingScheme,
10    Struct, Tuple,
11};
12
13type Delimiters = (&'static str, &'static str);
14
15pub type Result<T, E = FormatError> = core::result::Result<T, E>;
16
17/// The largest input size to display (in bytes)
18pub const MAX_INPUT_CHUNK: usize = 65;
19
20#[derive(Debug, Error, Clone)]
21pub enum FormatError {
22    #[error("Core error: {0}")]
23    Core(#[from] core::fmt::Error),
24    #[error("The byte sequence could not be formatted for display: {0}")]
25    InvalidBytes(#[from] byte_display::ByteFormatError),
26    #[error("The input is not a valid utf-8 string: {0}")]
27    InvalidString(#[from] core::str::Utf8Error),
28    #[error("Invalid discriminant `{discriminant}` for {type_name}")]
29    InvalidDiscriminant { type_name: String, discriminant: u8 },
30    #[error(transparent)]
31    UnresolvedType(#[from] ResolutionError),
32    #[error("A discriminant is required for items of type `{type_name}` but the input ended without providing one.")]
33    MissingDiscriminant { type_name: String },
34    #[error("The input claimed to provide an integer {claimed_size} bytes wide, but the maximum allowed size is 16 bytes.")]
35    IntegerTooLarge { claimed_size: u8 },
36    #[error("The input claimed to provide an integer {claimed_size} bytes wide, but only provided {bytes_available} additional bytes of input.")]
37    MissingIntegerInput {
38        claimed_size: u8,
39        bytes_available: u8,
40    },
41    #[error("The input claimed to provide a byte array {claimed_size} bytes wide, but only provided {bytes_available} additional bytes of input.")]
42    MissingBytesInput {
43        claimed_size: usize,
44        bytes_available: usize,
45    },
46    #[error("The input's attributes reference a sibling field with a byte offset, but offset was outside the range of the field's value.")]
47    FieldReferenceOffsetOutOfBounds(usize),
48    #[error("Fixed-point integer formatting specified more decimals than is plausible for an integer: {0}")]
49    TooManyDecimalsForInt(u8),
50    #[error("The input should have contained a vector but did not provide one.")]
51    MissingVecLength,
52    #[error("The input should have contained a string but did not provide one.")]
53    MissingStringLength,
54    #[error("The provided input had leftover bytes that weren't displayed.")]
55    UnusedInput,
56    #[error(
57        "A structs's display template did not provide sufficient slots to display its fields."
58    )]
59    InsufficientTemplateSlots,
60    #[error("A struct's display template had more slots than the struct had fields.")]
61    UnusedTemplateSlots,
62}
63
64pub struct Output<'a, W> {
65    f: &'a mut W,
66    silent: bool,
67    // Incremented every time a peek starts, decremented when it ends.
68    // We are peeking IFF `peeking > 0`.
69    peeking: u32,
70}
71
72impl<'a, W> Output<'a, W> {
73    pub fn new(f: &'a mut W) -> Self {
74        Self {
75            f,
76            silent: false,
77            peeking: 0,
78        }
79    }
80
81    pub fn start_peek(&mut self) {
82        self.peeking = self.peeking.checked_add(1).unwrap();
83    }
84
85    pub fn end_peek(&mut self) {
86        self.peeking = self
87            .peeking
88            .checked_sub(1)
89            .expect("Underflow when ending peek. This is a bug in the schema display logic.")
90    }
91
92    pub fn peeking(&self) -> bool {
93        self.peeking > 0
94    }
95}
96
97impl<W: core::fmt::Write> core::fmt::Write for Output<'_, W> {
98    fn write_str(&mut self, s: &str) -> core::fmt::Result {
99        if self.peeking() {
100            return Ok(());
101        }
102        if self.silent {
103            return Ok(());
104        }
105        self.f.write_str(s)
106    }
107}
108
109pub struct Input<'a> {
110    buf: &'a mut &'a [u8],
111    peeking: bool,
112    /// A stack of origins for nested peek processing, to calculate relative field offsets. First
113    /// element is always 0 for the outermost peek.
114    peek_origins_stack: Vec<usize>,
115    /// The current point we're reading at, while peeking, from the absolute start of the buffer
116    /// (the current start, i.e. from the point we started peeking).
117    peek_cursor: usize,
118}
119
120impl<'a> Input<'a> {
121    pub fn new(buf: &'a mut &'a [u8]) -> Self {
122        Self {
123            buf,
124            peeking: false,
125            peek_origins_stack: Vec::new(),
126            peek_cursor: 0,
127        }
128    }
129
130    pub fn is_empty(&self) -> bool {
131        self.buf.is_empty()
132    }
133
134    pub fn len(&self) -> usize {
135        self.buf.len()
136    }
137
138    pub(crate) fn check_remaining_bytes(&self, mut len: usize) -> Result<(), FormatError> {
139        if self.peeking {
140            len += self.peek_cursor;
141        }
142        if self.buf.len() < len {
143            return Err(FormatError::MissingBytesInput {
144                claimed_size: len,
145                bytes_available: self.buf.len(),
146            });
147        }
148        Ok(())
149    }
150
151    /// Splits the first `len` bytes from the input, returning them as a slice and updating the input buffer.
152    /// Returns an error if there are not enough bytes remaining to fulfill the request.
153    pub fn advance(&mut self, len: usize) -> Result<&[u8], FormatError> {
154        self.check_remaining_bytes(len)?;
155        if self.peeking {
156            let end_cursor = self.peek_cursor + len;
157            let slice = &self.buf[self.peek_cursor..end_cursor];
158            self.peek_cursor = end_cursor;
159            Ok(slice)
160        } else {
161            let (leading, rest) = self.buf.split_at(len);
162            *self.buf = rest;
163            Ok(leading)
164        }
165    }
166
167    /// Similar to `advance()`, but does not advance the input, only returns a reference to the next `len` bytes.
168    ///
169    /// Not to be confused with the struct-wide peeking functionality that switches between
170    /// consuming the buffer and advancing a cursor.
171    pub fn local_peek_bytes(&self, len: usize) -> Result<&[u8], FormatError> {
172        self.check_remaining_bytes(len)?;
173        // When not in a global peek, peek_cursor == 0 so this is a no-op
174        let end = self.peek_cursor + len;
175        Ok(&self.buf[self.peek_cursor..end])
176    }
177
178    /// Similar to `local_peek_bytes()`, but returns a single byte.
179    ///
180    /// Not to be confused with the struct-wide peeking functionality that switches between
181    /// consuming the buffer and advancing a cursor.
182    pub fn local_peek_byte(&self, offset: usize) -> Result<u8, FormatError> {
183        self.check_remaining_bytes(offset)?;
184        // When not in a global peek, peek_cursor == 0 so this is a no-op
185        let offset = self.peek_cursor + offset;
186        Ok(self.buf[offset])
187    }
188
189    pub fn start_peek(&mut self) {
190        self.peeking = true;
191        self.peek_origins_stack.push(self.peek_cursor);
192    }
193
194    /// The peek cursor _from the view of the current innermost peek_.
195    pub fn peek_cursor(&self) -> usize {
196        self.peek_cursor.checked_sub(*self.peek_origins_stack.last().expect("peek_cursor() was called while no peek is ongoing. This is a bug in the schema display logic.")).expect("The peek cursor offset calculation underflowed. This is a bug in the schema display logic.")
197    }
198
199    /// End a peek and proceed with normal input processing.
200    /// This method can only be used after a peek is started. If there was no ongoing peek, the
201    /// method will panic.
202    ///
203    /// Returns true if the peek pass is over, false if there is still an outer peek being
204    /// processed.
205    pub fn end_peek(&mut self) -> bool {
206        self.peek_origins_stack.pop().expect("end_peek() was called but the peek stack was empty. This is a bug in the schema display logic.");
207        // Only actually end peeking if there are no more nested peeks
208        if self.peek_origins_stack.is_empty() {
209            self.peeking = false;
210            self.peek_cursor = 0;
211            true
212        } else {
213            false
214        }
215    }
216}
217
218/// In case an enum variant contains nested trivial tuples, propagate the virtual-ness transitively
219/// to the actual content
220fn tuple_displays_as_enum_contents(context: &Context) -> bool {
221    context.parent_type == ParentType::Enum(IsHideTag::No)
222        || context.parent_type == ParentType::Tuple(IsVirtual::Yes, IsTrivial::Yes)
223}
224
225pub struct DisplayVisitor<'a, 'fmt, W> {
226    input: Input<'a>,
227    output: Output<'fmt, W>,
228}
229
230// To avoid unnecessary nested brackets, we apply an optimization to the display of tuples where
231// tuples with a single variant do not wrap their contents in parentheses by default.
232// This creates a special case when a tuple-variant of an enum has a single field. In that case,
233// we need to "make up" the removed delimiter later on in the rendering process.
234//
235// The "make-up" delimiters are as follows:
236// - Inner enum: "."
237// - Inner tuple: N/A
238// - String-like item: Wrap in parentheses
239// - Other: Add an extra " " after the usual delimiter
240impl<'a, 'fmt, W> DisplayVisitor<'a, 'fmt, W> {
241    pub fn new(input: &'a mut &'a [u8], f: &'fmt mut W) -> Self {
242        Self {
243            input: Input::new(input),
244            output: Output::new(f),
245        }
246    }
247
248    pub fn has_displayed_whole_input(&self) -> bool {
249        self.input.is_empty()
250    }
251
252    fn start_peek(&mut self) {
253        self.output.start_peek();
254        self.input.start_peek();
255    }
256
257    fn end_peek(&mut self) -> bool {
258        self.output.end_peek();
259        self.input.end_peek()
260    }
261
262    fn tuple_delimiters<L: LinkingScheme>(
263        &self,
264        tuple: &Tuple<L>,
265        context: &Context,
266        schema: &impl TypeResolver<LinkingScheme = L>,
267    ) -> Delimiters {
268        let first_child_is_primitive = schema
269            .resolve_or_err(&tuple.fields[0].value)
270            .is_ok_and(|v| v.is_primitive());
271
272        if tuple.fields.len() == 1
273            && !(tuple_displays_as_enum_contents(context) && first_child_is_primitive)
274        {
275            ("", "")
276        } else {
277            ("(", ")")
278        }
279    }
280
281    fn enum_delimiters<L: LinkingScheme>(&mut self, _e: &Enum<L>, context: &Context) -> Delimiters {
282        match context.parent_type {
283            ParentType::Tuple(_, IsTrivial::Yes)
284            | ParentType::Enum(_)
285            | ParentType::Vec
286            | ParentType::Map => (".", ""),
287            ParentType::Tuple(_, _) | ParentType::Struct(_) | ParentType::None => ("", ""),
288        }
289    }
290
291    fn struct_delimiters<L: LinkingScheme>(&self, s: &Struct<L>, context: &Context) -> Delimiters {
292        if s.fields.is_empty() {
293            return ("", "");
294        }
295        match context.parent_type {
296            ParentType::Tuple(IsVirtual::Yes, _) => (" { ", " }"),
297            ParentType::Struct(_)
298            | ParentType::None
299            | ParentType::Tuple(_, _)
300            | ParentType::Vec
301            | ParentType::Map => ("{ ", " }"),
302            ParentType::Enum(_) => (" { ", " }"),
303        }
304    }
305
306    /// Generates a template/format string to display a struct if none was provided as an
307    /// attribute. The default template either displays the typename if the struct has no
308    /// fields, or looks like this (for a hypothetical struct with three fields named as below):
309    /// ```text
310    /// "{ field_one: {}, field_two: {}, field_three: {} }"
311    /// ```
312    /// In other words, simply lists out the field names followed by an unnamed substitution (that
313    /// will be filled in with the field's values, in order), enclosed in braces and
314    /// comma-separated.
315    fn struct_default_template<L: LinkingScheme>(
316        &self,
317        s: &Struct<L>,
318        context: &Context,
319        schema: &impl TypeResolver<LinkingScheme = L>,
320    ) -> String {
321        let mut template = String::new();
322        let (opener, closer) = self.struct_delimiters(s, context);
323        template.push_str(opener);
324        if s.fields.is_empty() {
325            template.push_str(&s.type_name);
326        } else {
327            for (i, field) in s.fields.iter().enumerate() {
328                if field.silent {
329                    continue;
330                }
331                if schema
332                    .resolve_or_err(&field.value)
333                    .is_ok_and(|inner| inner.is_skip())
334                {
335                    continue;
336                }
337                if i > 0 {
338                    template.push_str(self.item_separator());
339                }
340                template.push_str(&field.display_name);
341                template.push_str(": {}");
342            }
343        }
344        template.push_str(closer);
345        template
346    }
347
348    /// Generates a template/format string to display a tuple if none was provided as an
349    /// attribute. The default template looks like this, for example for a tuple with three values:
350    /// ```text
351    /// "({}, {}, {})"
352    /// ```
353    /// In other words, simply lists out the fields as unnamed substitutions (which will be filled in
354    /// with the tuple's values in order, during display), enclosed in brackets and comma-separated.
355    /// The brackets are sometimes omitted - see the implementation of `tuple_delimiters` for the
356    /// logic.
357    fn tuple_default_template<L: LinkingScheme>(
358        &self,
359        t: &Tuple<L>,
360        context: &Context,
361        schema: &impl TypeResolver<LinkingScheme = L>,
362    ) -> String {
363        let mut template = String::new();
364        let (opener, closer) = self.tuple_delimiters(t, context, schema);
365        template.push_str(opener);
366        for (i, field) in t.fields.iter().enumerate() {
367            if field.silent {
368                continue;
369            }
370            if i > 0 {
371                template.push_str(self.item_separator());
372            }
373            template.push_str("{}");
374        }
375        template.push_str(closer);
376        template
377    }
378
379    fn option_none_delimiters(&self) -> Delimiters {
380        ("None", "")
381    }
382
383    fn option_some_delimiters(&self) -> Delimiters {
384        ("", "")
385    }
386
387    fn map_delimiters(&mut self, context: &Context) -> Delimiters {
388        match context.parent_type {
389            ParentType::Tuple(IsVirtual::Yes, _) => (" { ", " }"),
390            ParentType::Struct(_)
391            | ParentType::None
392            | ParentType::Tuple(_, _)
393            | ParentType::Vec
394            | ParentType::Enum(_)
395            | ParentType::Map => ("{ ", " }"),
396        }
397    }
398
399    fn vec_delimiters(&mut self, context: &Context) -> Delimiters {
400        match context.parent_type {
401            ParentType::Tuple(IsVirtual::Yes, _) => (" [", "]"),
402            ParentType::None
403            | ParentType::Struct(_)
404            | ParentType::Tuple(_, _)
405            | ParentType::Vec
406            | ParentType::Enum(_)
407            | ParentType::Map => ("[", "]"),
408        }
409    }
410
411    fn item_separator(&self) -> &'static str {
412        ", "
413    }
414}
415
416impl<W: Write> DisplayVisitor<'_, '_, W> {
417    pub fn read_usize_borsh(&mut self) -> Result<usize, FormatError> {
418        if self.input.len() < 4 {
419            return Err(FormatError::MissingIntegerInput {
420                claimed_size: 4,
421                bytes_available: self.input.len() as u8,
422            });
423        }
424        let len = u32::from_le_bytes(
425            self.input
426                .advance(4)?
427                .try_into()
428                .expect("Converting [u8;4] to u32 is infallible"),
429        ) as usize;
430        Ok(len)
431    }
432
433    pub fn display_byte_sequence(
434        &mut self,
435        len: usize,
436        display: ByteDisplay,
437        _context: Context,
438    ) -> Result<(), FormatError> {
439        self.input.check_remaining_bytes(len)?;
440
441        if len > MAX_INPUT_CHUNK {
442            display.format(self.input.advance(MAX_INPUT_CHUNK)?, &mut self.output)?;
443            self.output.write_fmt(format_args!(
444                " (trailing {} bytes truncated)",
445                len - MAX_INPUT_CHUNK
446            ))?;
447            self.input.advance(len - MAX_INPUT_CHUNK)?;
448        } else {
449            display.format(self.input.advance(len)?, &mut self.output)?;
450        }
451        Ok(())
452    }
453}
454
455#[derive(Clone, Debug)]
456pub struct Context {
457    parent_type: ParentType,
458    /// True if this is a first pre-pass that runs to determine field byte lengths before the
459    /// actual display pass
460    is_peek_pass: bool,
461    /// Bytes referenced by other fields.
462    /// Maps by field index and offset inside the field, because that is how child fields will
463    /// access it.
464    peek_bytes: BTreeMap<usize, BTreeMap<usize, u8>>,
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum IsVirtual {
469    Yes,
470    No,
471}
472
473/// Tuple wrapping a single value
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum IsTrivial {
476    Yes,
477    No,
478}
479
480/// An enum should display its tags
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub enum IsHideTag {
483    Yes,
484    No,
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum ParentType {
489    None,
490    Struct(IsVirtual),
491    Tuple(IsVirtual, IsTrivial),
492    Enum(IsHideTag),
493    Vec,
494    Map,
495}
496
497impl Default for Context {
498    fn default() -> Self {
499        Self {
500            parent_type: ParentType::None,
501            is_peek_pass: false,
502            peek_bytes: BTreeMap::new(),
503        }
504    }
505}
506
507/// Format an integer number string as a fixed point with the specified number of decimals.
508/// E.g.
509///  * `format_fixed_point("81", 4)` -> `"0.0081"`
510///  * `format_fixed_point("810", 4)` -> `"0.081"`
511///  * `format_fixed_point("-81", 4)` -> `"-0.0081"`
512///  * `format_fixed_point("24215", 3)` -> `"24.215"`
513///  * `format_fixed_point("24200", 3)` -> `"24.2"`
514///  * `format_fixed_point("24000", 3)` -> `"24"`
515///
516/// We pass the number as a string to be able to dynamically support both u128 and
517/// i128 in the same function (by stripping the first char of the string if it's a '-').
518fn format_fixed_point(number_str: String, decimals: u8) -> String {
519    // 1. Strip the negative sign if necessary, to simplify processing
520    let (number_str, is_negative) = match number_str.strip_prefix('-') {
521        Some(str) => (str.to_string(), true),
522        None => (number_str, false),
523    };
524    // 2. Pad the string to at least decimals + 1, to give a 0 before the '.'
525    let mut number_str = format!("{:0>pad$}", number_str, pad = (decimals + 1) as usize);
526    // 3. Insert the decimal point `decimals` from the right
527    let decimal_idx = number_str.len().checked_sub(decimals.into()).expect("We just formatted the string to be wider than the number of decimals - should never underflow");
528    number_str.insert(decimal_idx, '.');
529    // 3. Trim trailing 0s, and if necessary, the trailing '.' (if the number ended up being whole)
530    let mut number_str = number_str
531        .trim_end_matches('0')
532        .trim_end_matches('.')
533        .to_string();
534    // 4. Finally, restore the negative sign if there was one
535    if is_negative {
536        number_str.insert(0, '-');
537    }
538    number_str
539}
540
541// TODO: this would be nicer for devex if it were a function. The `<$t>::from_le_bytes` is what
542// makes it non-trivial to convert though
543macro_rules! display_int {
544    ($t:ident, $input:expr, $disp:expr, $f:expr, $ctx:expr) => {{
545        let size = IntegerType::$t.size();
546        if $input.len() < size {
547            return Err(FormatError::MissingIntegerInput {
548                claimed_size: size as u8,
549                bytes_available: $input.len() as u8,
550            });
551        }
552        let buf = $input.advance(size)?;
553        match $disp {
554            IntegerDisplay::Hex => {
555                write!($f, "{:#x}", <$t>::from_le_bytes(buf.try_into().unwrap()))?
556            }
557            IntegerDisplay::Decimal => {
558                write!($f, "{}", <$t>::from_le_bytes(buf.try_into().unwrap()))?
559            }
560            IntegerDisplay::FixedPoint(decimal_spec) => {
561                let decimals = match decimal_spec {
562                    FixedPointDisplay::Decimals(d) => d,
563                    FixedPointDisplay::FromSiblingField {
564                        field_index,
565                        byte_offset,
566                    } => {
567                        if $ctx.is_peek_pass {
568                            0
569                        } else {
570                            *$ctx
571                                .peek_bytes
572                                .get(&field_index)
573                                .expect("Fixed point display attempted to get field that was not provided in context - this is a bug in the schema display implementation")
574                                .get(&byte_offset)
575                                .expect("Fixed point display attempted to get byte that was not provided in context - this is a bug in the schema display implementation")
576                        }
577                    }
578                };
579                // 39 = log_10(128::MAX)
580                if decimals > 39 {
581                    return Err(FormatError::TooManyDecimalsForInt (decimals));
582                }
583                let t = <$t>::from_le_bytes(buf.try_into().unwrap());
584                write!($f, "{}", format_fixed_point(t.to_string(), decimals))?
585            }
586        }
587        Ok(())
588    }};
589}
590
591impl<W: Write, L: LinkingScheme, M> TypeVisitor<L, M> for DisplayVisitor<'_, '_, W> {
592    type Arg = Context;
593    type ReturnType = Result<(), FormatError>;
594    fn visit_enum(
595        &mut self,
596        e: &Enum<L>,
597        schema: &impl TypeResolver<LinkingScheme = L>,
598        mut context: Context,
599    ) -> Self::ReturnType {
600        if self.input.is_empty() && !e.variants.is_empty() {
601            return Err(FormatError::MissingDiscriminant {
602                type_name: e.type_name.clone(),
603            });
604        }
605
606        // If the enum is displayed as part of a tuple, the user doesn't have any context about what the type is,
607        // since there's no field name. In that case we display the full type name.
608        if matches!(context.parent_type, ParentType::Tuple(IsVirtual::No, _))
609            || context.parent_type == ParentType::Vec
610        {
611            self.output.write_str(&e.type_name)?;
612        }
613
614        let (open, close) = self.enum_delimiters(e, &context);
615        self.output.write_str(open)?;
616
617        let discriminant = self.input.advance(1)?[0];
618        let mut variants_by_discriminant =
619            e.variants.iter().filter(|v| v.discriminant == discriminant);
620        let variant = variants_by_discriminant
621            .next()
622            .ok_or(FormatError::InvalidDiscriminant {
623                type_name: e.type_name.clone(),
624                discriminant,
625            })?;
626        assert!(variants_by_discriminant.next().is_none(), "Found two enum variants with the same discriminant - the schema is malformed, cannot proceed!");
627        if variant.template.is_none() && !e.hide_tag {
628            write!(self.output, "{}", variant.name)?;
629        }
630        let is_hide_tag = if e.hide_tag {
631            IsHideTag::Yes
632        } else {
633            IsHideTag::No
634        };
635        context.parent_type = ParentType::Enum(is_hide_tag);
636        if let Some(maybe_resolved) = &variant.value {
637            let inner = schema.resolve_or_err(maybe_resolved)?;
638            inner.visit(schema, self, context)?;
639        }
640        self.output.write_str(close)?;
641        Ok(())
642    }
643
644    fn visit_struct(
645        &mut self,
646        s: &Struct<L>,
647        schema: &impl TypeResolver<LinkingScheme = L>,
648        mut context: Context,
649    ) -> Self::ReturnType {
650        let template = s
651            .template
652            .clone()
653            .unwrap_or_else(|| self.struct_default_template(s, &context, schema));
654        let mut template = template.as_str();
655
656        context.parent_type = ParentType::Struct(IsVirtual::No);
657
658        // If we need to get the field offset indices, we perform a peek pass.
659        // However, recursive peeks are currently not supported. If a parent struct is currently
660        // peeking, skip this: we will perform our pass once it's our turn to display for real.
661        // This is to simplify implementation, as being `peekable` is expected to not be used
662        // often, and nested peeks should be even more rare. If it becomes a problem, this
663        // can be optimised into having a single recursive pre-pass for all structs that need it.
664        if s.peekable && !context.is_peek_pass {
665            // At which byte does every field start?
666            let mut field_offsets: Vec<usize> = Vec::new();
667            // For every field, check which bytes need to be provided in the Context.
668            // Vec<(field idx, byte offset)>
669            let mut bytes_needed: Vec<(usize, usize)> = Vec::new();
670            self.start_peek();
671            context.is_peek_pass = true;
672            for field in s.fields.iter() {
673                field_offsets.push(self.input.peek_cursor());
674                // TODO: if optimisation is required, this resolution can be cached for the "real"
675                // display below to save a clone(). (Need to handle the !peekable case.)
676                let inner_ty = schema.resolve_or_err(&field.value)?;
677                // TODO: if time should be traded for space, the Output can cache the result of
678                // this and reuse it for the "real" display below instead of dropping it
679                inner_ty.visit(schema, self, context.clone())?;
680
681                // resolve all bytes necessary from this field
682                bytes_needed.extend(inner_ty.parent_byte_references());
683            }
684            // Save the end of the entire struct, for bounds checking.
685            field_offsets.push(self.input.peek_cursor());
686            // Rewind the input cursor
687            let still_peeking = !self.end_peek();
688            context.is_peek_pass = still_peeking;
689
690            // Save, into the Context, those bytes which were found to be needed
691            let mut peek_bytes: BTreeMap<usize, BTreeMap<usize, u8>> = BTreeMap::new();
692            for (field, offset) in bytes_needed {
693                // The upper allowed bound on the offset.
694                let field_end = *field_offsets
695                    .get(field + 1)
696                    .expect("A struct's child field attribute referenced a sibling field by index that is out of bounds of the parent struct. This should not be possible in a well-constructed schema.");
697                // unwrap: we just asserted that field_offsets[field+1] exists, so field_offsets[field] must exist too
698                let byte_offset = field_offsets.get(field).unwrap() + offset;
699                if byte_offset > field_end {
700                    return Err(FormatError::FieldReferenceOffsetOutOfBounds(byte_offset));
701                }
702                let byte = self.input.local_peek_byte(byte_offset)?;
703
704                let field_bytes = peek_bytes.entry(field).or_default();
705                field_bytes.insert(offset, byte);
706            }
707            context.peek_bytes = peek_bytes;
708        }
709
710        for field in &s.fields {
711            // Save the previous state of the silent flag so we can restore it after displaying the field.
712            let was_silent = self.output.silent;
713            if field.silent {
714                self.output.silent = true;
715            }
716
717            let inner_ty = schema.resolve_or_err(&field.value)?;
718            if !field.silent && !inner_ty.is_skip() {
719                let Some((before_next_field, rest)) = template.split_once("{}") else {
720                    return Err(FormatError::InsufficientTemplateSlots);
721                };
722                self.output.write_str(before_next_field)?;
723                template = rest;
724            }
725
726            inner_ty.visit(schema, self, context.clone())?;
727            // Restore the silent flag to its previous state
728            self.output.silent = was_silent;
729        }
730        if template.contains("{}") {
731            return Err(FormatError::UnusedTemplateSlots);
732        }
733        self.output.write_str(template)?;
734        Ok(())
735    }
736
737    fn visit_tuple(
738        &mut self,
739        t: &Tuple<L>,
740        schema: &impl TypeResolver<LinkingScheme = L>,
741        mut context: Context,
742    ) -> Self::ReturnType {
743        let template = t
744            .template
745            .clone()
746            .unwrap_or_else(|| self.tuple_default_template(t, &context, schema));
747        let mut template = template.as_str();
748
749        let is_virtual = if tuple_displays_as_enum_contents(&context) {
750            IsVirtual::Yes
751        } else {
752            IsVirtual::No
753        };
754        let trivial = if t.fields.len() == 1 {
755            IsTrivial::Yes
756        } else {
757            IsTrivial::No
758        };
759        context.parent_type = ParentType::Tuple(is_virtual, trivial);
760
761        // In case we require any field offsets, perform a peeking pass before proper display.
762        // Refer to the `visit_struct()` implementation for full documentation.
763        if t.peekable && !context.is_peek_pass {
764            let mut field_offsets: Vec<usize> = Vec::new();
765            // Vec<(field idx, byte offset)>
766            let mut bytes_needed: Vec<(usize, usize)> = Vec::new();
767            self.start_peek();
768            context.is_peek_pass = true;
769            for field in t.fields.iter() {
770                field_offsets.push(self.input.peek_cursor());
771                let inner_ty = schema.resolve_or_err(&field.value)?;
772                inner_ty.visit(schema, self, context.clone())?;
773
774                bytes_needed.extend(inner_ty.parent_byte_references());
775            }
776            field_offsets.push(self.input.peek_cursor());
777            let still_peeking = !self.end_peek();
778            context.is_peek_pass = still_peeking;
779
780            let mut peek_bytes: BTreeMap<usize, BTreeMap<usize, u8>> = BTreeMap::new();
781            for (field, offset) in bytes_needed {
782                let field_end = *field_offsets
783                    .get(field + 1)
784                    .expect("A tuple's child field attribute referenced a sibling field by index that is out of bounds of the parent struct. This should not be possible in a well-constructed schema.");
785                // unwrap: we just asserted that field_offsets[field+1] exists, so field_offsets[field] must exist too
786                let byte_offset = field_offsets.get(field).unwrap() + offset;
787                if byte_offset > field_end {
788                    return Err(FormatError::FieldReferenceOffsetOutOfBounds(byte_offset));
789                }
790                let byte = self.input.local_peek_byte(byte_offset)?;
791
792                let field_bytes = peek_bytes.entry(field).or_default();
793                field_bytes.insert(offset, byte);
794            }
795            context.peek_bytes = peek_bytes;
796        }
797
798        for field in &t.fields {
799            // Save the previous state of the silent flag so we can restore it after displaying the field.
800            let was_silent = self.output.silent;
801            if field.silent {
802                self.output.silent = true;
803            }
804            if !field.silent {
805                let Some((before_next_field, rest)) = template.split_once("{}") else {
806                    return Err(FormatError::InsufficientTemplateSlots);
807                };
808                self.output.write_str(before_next_field)?;
809                template = rest;
810            }
811
812            schema
813                .resolve_or_err(&field.value)?
814                .visit(schema, self, context.clone())?;
815            // Restore the silent flag to its previous state
816            self.output.silent = was_silent;
817        }
818        if template.contains("{}") {
819            return Err(FormatError::UnusedTemplateSlots);
820        }
821        self.output.write_str(template)?;
822        Ok(())
823    }
824
825    fn visit_option(
826        &mut self,
827        value: &L::TypeLink,
828        schema: &impl TypeResolver<LinkingScheme = L>,
829        context: Self::Arg,
830    ) -> Self::ReturnType {
831        let discriminant = self.input.advance(1)?[0];
832
833        match discriminant {
834            0 => {
835                let (open, close) = self.option_none_delimiters();
836                self.output.write_str(open)?;
837                self.output.write_str(close)?;
838            }
839            1 => {
840                let (open, close) = self.option_some_delimiters();
841                self.output.write_str(open)?;
842                schema.resolve_or_err(value)?.visit(schema, self, context)?;
843                self.output.write_str(close)?;
844            }
845            _ => {
846                return Err(FormatError::InvalidDiscriminant {
847                    type_name: "Option".to_string(),
848                    discriminant,
849                })
850            }
851        }
852
853        Ok(())
854    }
855
856    fn visit_primitive(
857        &mut self,
858        p: crate::schema::Primitive,
859        _schema: &impl TypeResolver<LinkingScheme = L>,
860        context: Context,
861    ) -> Self::ReturnType {
862        match p {
863            Primitive::Float32 => {
864                let value = self.input.advance(4)?;
865                let value = f32::from_le_bytes(value.try_into().unwrap());
866                write!(self.output, "{value}")?;
867                Ok(())
868            }
869            Primitive::Float64 => {
870                let value = self.input.advance(8)?;
871                let value = f64::from_le_bytes(value.try_into().unwrap());
872                write!(self.output, "{value}")?;
873                Ok(())
874            }
875            Primitive::Boolean => {
876                let value = self.input.advance(1)?;
877                match value[0] {
878                    0 => self.output.write_str("false")?,
879                    1 => self.output.write_str("true")?,
880                    _ => {
881                        return Err(FormatError::InvalidDiscriminant {
882                            type_name: "bool".to_string(),
883                            discriminant: value[0],
884                        });
885                    }
886                }
887                Ok(())
888            }
889            Primitive::Integer(int, display) => match int {
890                IntegerType::i8 => display_int!(i8, self.input, display, self.output, context),
891                IntegerType::i16 => display_int!(i16, self.input, display, self.output, context),
892                IntegerType::i32 => display_int!(i32, self.input, display, self.output, context),
893                IntegerType::i64 => display_int!(i64, self.input, display, self.output, context),
894                IntegerType::i128 => display_int!(i128, self.input, display, self.output, context),
895                IntegerType::u8 => display_int!(u8, self.input, display, self.output, context),
896                IntegerType::u16 => display_int!(u16, self.input, display, self.output, context),
897                IntegerType::u32 => display_int!(u32, self.input, display, self.output, context),
898                IntegerType::u64 => display_int!(u64, self.input, display, self.output, context),
899                IntegerType::u128 => display_int!(u128, self.input, display, self.output, context),
900            },
901            Primitive::ByteArray { len, display } => {
902                self.display_byte_sequence(len, display, context)
903            }
904            Primitive::ByteVec { display } => {
905                let len = self
906                    .read_usize_borsh()
907                    .or(Err(FormatError::MissingVecLength))?;
908                self.display_byte_sequence(len, display, context)
909            }
910            Primitive::String => {
911                let len = self
912                    .read_usize_borsh()
913                    .or(Err(FormatError::MissingStringLength))?;
914                let content = self.input.advance(len)?;
915                let content = std::str::from_utf8(content)?;
916                write!(self.output, "\"{content}\"")?;
917                Ok(())
918            }
919            Primitive::Skip { len } => {
920                self.input.advance(len)?;
921                Ok(())
922            }
923        }
924    }
925
926    fn visit_array(
927        &mut self,
928        len: &usize,
929        value: &L::TypeLink,
930        schema: &impl TypeResolver<LinkingScheme = L>,
931        mut context: Context,
932    ) -> Self::ReturnType {
933        let inner = schema.resolve_or_err(value)?;
934        let (open, close) = self.vec_delimiters(&context);
935        self.output.write_str(open)?;
936        context.parent_type = ParentType::Vec;
937        for i in 0..*len {
938            if i > 0 {
939                self.output.write_str(", ")?;
940            }
941            inner.visit(schema, self, context.clone())?;
942        }
943        self.output.write_str(close)?;
944        Ok(())
945    }
946
947    fn visit_vec(
948        &mut self,
949        value: &L::TypeLink,
950        schema: &impl TypeResolver<LinkingScheme = L>,
951        mut context: Context,
952    ) -> Self::ReturnType {
953        let len = self.read_usize_borsh()?;
954        let inner = schema.resolve_or_err(value)?;
955        let (open, close) = self.vec_delimiters(&context);
956        self.output.write_str(open)?;
957        context.parent_type = ParentType::Vec;
958        for i in 0..len {
959            if i > 0 {
960                self.output.write_str(", ")?;
961            }
962            inner.visit(schema, self, context.clone())?;
963        }
964        self.output.write_str(close)?;
965        Ok(())
966    }
967
968    fn visit_map(
969        &mut self,
970        key: &L::TypeLink,
971        value: &L::TypeLink,
972        schema: &impl TypeResolver<LinkingScheme = L>,
973        mut context: Context,
974    ) -> Self::ReturnType {
975        let len = self.read_usize_borsh()?;
976        let key = schema.resolve_or_err(key)?;
977        let value = schema.resolve_or_err(value)?;
978        let (open, close) = self.map_delimiters(&context);
979        self.output.write_str(open)?;
980        context.parent_type = ParentType::Map;
981        for i in 0..len {
982            if i > 0 {
983                self.output.write_str(", ")?;
984            }
985            key.visit(schema, self, context.clone())?;
986            self.output.write_str(": ")?;
987            value.visit(schema, self, context.clone())?;
988        }
989        self.output.write_str(close)?;
990        Ok(())
991    }
992}