sui_display/v1/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::{collections::BTreeMap, fmt::Write};

use anyhow::{anyhow, bail, Context};
use move_core_types::{
    annotated_extractor::Extractor,
    annotated_value::{MoveTypeLayout, MoveValue},
};
use parser::{Parser, Strand};
use sui_json_rpc_types::SuiMoveValue;
use sui_types::{
    collection_types::{Entry, VecMap},
    object::bounded_visitor::BoundedVisitor,
};

pub(crate) mod lexer;
pub(crate) mod parser;

/// Format strings extracted from a `Display` object or `DisplayVersionUpdated` event on-chain.
pub struct Format<'s> {
    fields: BTreeMap<&'s str, Vec<Strand<'s>>>,
}

/// A writer that tracks an output budget (measured in bytes) and fails when that budget is hit
/// (and from there on out).
struct BoundedWriter<'b> {
    output: String,
    budget: &'b mut usize,
}

/// Internal error type that distinguishes output budget overflow as a distinct error case.
#[derive(thiserror::Error, Debug)]
enum Error {
    #[error(transparent)]
    Error(#[from] anyhow::Error),

    #[error("Output budget exceeded")]
    OutputBudgetExceeded,
}

impl<'s> Format<'s> {
    /// Convert the contents of a `Display` object or `DisplayVersionUpdated` event into a
    /// `Format` string by parsing each of its fields' format strings.
    ///
    /// `max_depth` controls how deeply nested a field access expression can be before it is
    /// considered an error.
    pub fn parse(
        max_depth: usize,
        display_fields: &'s VecMap<String, String>,
    ) -> anyhow::Result<Self> {
        let mut fields = BTreeMap::new();

        for Entry { key, value } in &display_fields.contents {
            let name = key.as_str();
            let parser = Parser::new(max_depth, value);
            let strands = parser
                .parse_format()
                .with_context(|| format!("Failed to parse format for display field {name:?}"))?;

            fields.insert(name, strands);
        }

        Ok(Self { fields })
    }

    /// Interpret the fields of this `Format` structure for the object whose BCS representation is
    /// `bytes`, and whose type layout is `layout`. The `output_budget` limits how big the display
    /// output can be overall (it limits the size of fields and values).
    ///
    /// Returns a map from field names to their interpreted values. Errors are returned per-field
    /// (rather than returning the first error encountered), but the function can fail overall if
    /// the output budget is exceeded.
    pub fn display(
        &self,
        max_output_size: usize,
        bytes: &[u8],
        layout: &MoveTypeLayout,
    ) -> anyhow::Result<BTreeMap<String, anyhow::Result<String>>> {
        let mut output = BTreeMap::new();

        let mut output_budget = max_output_size;
        for (name, strands) in &self.fields {
            match interpolate(&mut output_budget, bytes, layout, strands) {
                Ok(value) if name.len() <= output_budget => {
                    output_budget -= name.len();
                    output.insert(name.to_string(), Ok(value));
                }

                Err(Error::Error(e)) => {
                    output.insert(name.to_string(), Err(e));
                }

                _ => {
                    bail!("Display output too large");
                }
            }
        }

        Ok(output)
    }
}

/// Interpret a single format string, composed of a sequence of `Strand`s, fetching the values
/// corresponding to any nested field expressions from a Move object, given by `bytes` (its BCS
/// representation) and `layout` (its type layout).
fn interpolate(
    output_budget: &mut usize,
    bytes: &[u8],
    layout: &MoveTypeLayout,
    strands: &[Strand<'_>],
) -> Result<String, Error> {
    let mut writer = BoundedWriter::new(output_budget);

    for strand in strands {
        let res = match strand {
            Strand::Text(text) => writer.write_str(text.as_ref()),
            Strand::Expr(path) => {
                let mut visitor = BoundedVisitor::default();
                let mut extractor = Extractor::new(&mut visitor, path);
                let extracted: SuiMoveValue =
                    MoveValue::visit_deserialize(bytes, layout, &mut extractor)
                        .with_context(|| format!("Failed to extract '{strand}'"))?
                        .with_context(|| format!("'{strand}' not found in object"))?
                        .into();

                match extracted {
                    SuiMoveValue::Vector(_) => {
                        return Err(Error::Error(anyhow!(
                            "'{strand}' is a vector, and is not supported in Display"
                        )));
                    }

                    SuiMoveValue::Option(opt) => match opt.as_ref() {
                        Some(v) => write!(writer, "{v}"),
                        None => Ok(()),
                    },

                    v => write!(writer, "{v}"),
                }
            }
        };

        if res.is_err() {
            return Err(Error::OutputBudgetExceeded);
        }
    }

    Ok(writer.finish())
}

impl<'b> BoundedWriter<'b> {
    fn new(budget: &'b mut usize) -> Self {
        Self {
            output: String::new(),
            budget,
        }
    }

    fn finish(self) -> String {
        self.output
    }
}

impl Write for BoundedWriter<'_> {
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        if s.len() > *self.budget {
            return Err(std::fmt::Error);
        }

        self.output.push_str(s);
        *self.budget -= s.len();
        Ok(())
    }
}