1use 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
21pub struct Format<'s> {
23 fields: BTreeMap<&'s str, Vec<Strand<'s>>>,
24}
25
26struct BoundedWriter<'b> {
29 output: String,
30 budget: &'b mut usize,
31}
32
33#[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 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 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
105fn 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}