sui_display/v1/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{collections::BTreeMap, fmt::Write};
5
6use anyhow::{Context, anyhow, bail};
7use move_core_types::{
8    annotated_extractor::Extractor,
9    annotated_value::{MoveTypeLayout, MoveValue},
10};
11use parser::{Parser, Strand};
12use sui_json_rpc_types::SuiMoveValue;
13use sui_types::{
14    collection_types::{Entry, VecMap},
15    object::bounded_visitor::BoundedVisitor,
16};
17
18pub(crate) mod lexer;
19pub(crate) mod parser;
20
21/// Format strings extracted from a `Display` object or `DisplayVersionUpdated` event on-chain.
22pub struct Format<'s> {
23    fields: BTreeMap<&'s str, Vec<Strand<'s>>>,
24}
25
26/// A writer that tracks an output budget (measured in bytes) and fails when that budget is hit
27/// (and from there on out).
28struct BoundedWriter<'b> {
29    output: String,
30    budget: &'b mut usize,
31}
32
33/// Internal error type that distinguishes output budget overflow as a distinct error case.
34#[derive(thiserror::Error, Debug)]
35enum Error {
36    #[error(transparent)]
37    Error(#[from] anyhow::Error),
38
39    #[error("Output budget exceeded")]
40    OutputBudgetExceeded,
41}
42
43impl<'s> Format<'s> {
44    /// Convert the contents of a `Display` object or `DisplayVersionUpdated` event into a
45    /// `Format` string by parsing each of its fields' format strings.
46    ///
47    /// `max_depth` controls how deeply nested a field access expression can be before it is
48    /// considered an error.
49    pub fn parse(
50        max_depth: usize,
51        display_fields: &'s VecMap<String, String>,
52    ) -> anyhow::Result<Self> {
53        let mut fields = BTreeMap::new();
54
55        for Entry { key, value } in &display_fields.contents {
56            let name = key.as_str();
57            let parser = Parser::new(max_depth, value);
58            let strands = parser
59                .parse_format()
60                .with_context(|| format!("Failed to parse format for display field {name:?}"))?;
61
62            fields.insert(name, strands);
63        }
64
65        Ok(Self { fields })
66    }
67
68    /// Interpret the fields of this `Format` structure for the object whose BCS representation is
69    /// `bytes`, and whose type layout is `layout`. The `output_budget` limits how big the display
70    /// output can be overall (it limits the size of fields and values).
71    ///
72    /// Returns a map from field names to their interpreted values. Errors are returned per-field
73    /// (rather than returning the first error encountered), but the function can fail overall if
74    /// the output budget is exceeded.
75    pub fn display(
76        &self,
77        max_output_size: usize,
78        bytes: &[u8],
79        layout: &MoveTypeLayout,
80    ) -> anyhow::Result<BTreeMap<String, anyhow::Result<String>>> {
81        let mut output = BTreeMap::new();
82
83        let mut output_budget = max_output_size;
84        for (name, strands) in &self.fields {
85            match interpolate(&mut output_budget, bytes, layout, strands) {
86                Ok(value) if name.len() <= output_budget => {
87                    output_budget -= name.len();
88                    output.insert(name.to_string(), Ok(value));
89                }
90
91                Err(Error::Error(e)) => {
92                    output.insert(name.to_string(), Err(e));
93                }
94
95                _ => {
96                    bail!("Display output too large");
97                }
98            }
99        }
100
101        Ok(output)
102    }
103}
104
105/// Interpret a single format string, composed of a sequence of `Strand`s, fetching the values
106/// corresponding to any nested field expressions from a Move object, given by `bytes` (its BCS
107/// representation) and `layout` (its type layout).
108fn interpolate(
109    output_budget: &mut usize,
110    bytes: &[u8],
111    layout: &MoveTypeLayout,
112    strands: &[Strand<'_>],
113) -> Result<String, Error> {
114    let mut writer = BoundedWriter::new(output_budget);
115
116    for strand in strands {
117        let res = match strand {
118            Strand::Text(text) => writer.write_str(text.as_ref()),
119            Strand::Expr(path) => {
120                let mut visitor = BoundedVisitor::default();
121                let mut extractor = Extractor::new(&mut visitor, path);
122                let extracted: SuiMoveValue =
123                    MoveValue::visit_deserialize(bytes, layout, &mut extractor)
124                        .with_context(|| format!("Failed to extract '{strand}'"))?
125                        .with_context(|| format!("'{strand}' not found in object"))?
126                        .into();
127
128                match extracted {
129                    SuiMoveValue::Vector(_) => {
130                        return Err(Error::Error(anyhow!(
131                            "'{strand}' is a vector, and is not supported in Display"
132                        )));
133                    }
134
135                    SuiMoveValue::Option(opt) => match opt.as_ref() {
136                        Some(v) => write!(writer, "{v}"),
137                        None => Ok(()),
138                    },
139
140                    v => write!(writer, "{v}"),
141                }
142            }
143        };
144
145        if res.is_err() {
146            return Err(Error::OutputBudgetExceeded);
147        }
148    }
149
150    Ok(writer.finish())
151}
152
153impl<'b> BoundedWriter<'b> {
154    fn new(budget: &'b mut usize) -> Self {
155        Self {
156            output: String::new(),
157            budget,
158        }
159    }
160
161    fn finish(self) -> String {
162        self.output
163    }
164}
165
166impl Write for BoundedWriter<'_> {
167    fn write_str(&mut self, s: &str) -> std::fmt::Result {
168        if s.len() > *self.budget {
169            return Err(std::fmt::Error);
170        }
171
172        self.output.push_str(s);
173        *self.budget -= s.len();
174        Ok(())
175    }
176}