1use 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
24pub struct Format<'s> {
26 fields: BTreeMap<&'s str, Vec<Strand<'s>>>,
27}
28
29struct BoundedWriter<'b> {
32 output: String,
33 budget: &'b mut usize,
34}
35
36#[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 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 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
108fn 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}