sui_display/v1/
mod.rs

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