1use base64::{engine::general_purpose::STANDARD as base64, Engine as _};
2use std::{
3 fmt::Display,
4 io::{self, Cursor},
5};
6
7use stellar_xdr::curr::{
8 self as xdr, Limited, Limits, ReadXdr, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion,
9 ScMetaEntry, ScMetaV0, ScSpecEntry, ScSpecFunctionV0, ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0,
10 ScSpecUdtStructV0, ScSpecUdtUnionV0, StringM, WriteXdr,
11};
12
13pub struct Spec {
14 pub env_meta_base64: Option<String>,
15 pub env_meta: Vec<ScEnvMetaEntry>,
16 pub meta_base64: Option<String>,
17 pub meta: Vec<ScMetaEntry>,
18 pub spec_base64: Option<String>,
19 pub spec: Vec<ScSpecEntry>,
20}
21
22#[derive(thiserror::Error, Debug)]
23pub enum Error {
24 #[error("reading file {filepath}: {error}")]
25 CannotReadContractFile {
26 filepath: std::path::PathBuf,
27 error: io::Error,
28 },
29 #[error("cannot parse wasm file {file}: {error}")]
30 CannotParseWasm {
31 file: std::path::PathBuf,
32 error: wasmparser::BinaryReaderError,
33 },
34 #[error("xdr processing error: {0}")]
35 Xdr(#[from] xdr::Error),
36
37 #[error(transparent)]
38 Parser(#[from] wasmparser::BinaryReaderError),
39}
40
41impl Spec {
42 pub fn new(bytes: &[u8]) -> Result<Self, Error> {
43 let mut env_meta: Option<Vec<u8>> = None;
44 let mut meta: Option<Vec<u8>> = None;
45 let mut spec: Option<Vec<u8>> = None;
46 for payload in wasmparser::Parser::new(0).parse_all(bytes) {
47 let payload = payload?;
48 if let wasmparser::Payload::CustomSection(section) = payload {
49 let out = match section.name() {
50 "contractenvmetav0" => &mut env_meta,
51 "contractmetav0" => &mut meta,
52 "contractspecv0" => &mut spec,
53 _ => continue,
54 };
55
56 if let Some(existing_data) = out {
57 let combined_data = [existing_data, section.data()].concat();
58 *out = Some(combined_data);
59 } else {
60 *out = Some(section.data().to_vec());
61 }
62 }
63 }
64
65 let mut env_meta_base64 = None;
66 let env_meta = if let Some(env_meta) = env_meta {
67 env_meta_base64 = Some(base64.encode(&env_meta));
68 let cursor = Cursor::new(env_meta);
69 let mut read = Limited::new(cursor, Limits::none());
70 ScEnvMetaEntry::read_xdr_iter(&mut read).collect::<Result<Vec<_>, xdr::Error>>()?
71 } else {
72 vec![]
73 };
74
75 let mut meta_base64 = None;
76 let meta = if let Some(meta) = meta {
77 meta_base64 = Some(base64.encode(&meta));
78 let cursor = Cursor::new(meta);
79 let mut depth_limit_read = Limited::new(cursor, Limits::none());
80 ScMetaEntry::read_xdr_iter(&mut depth_limit_read)
81 .collect::<Result<Vec<_>, xdr::Error>>()?
82 } else {
83 vec![]
84 };
85
86 let (spec_base64, spec) = if let Some(spec) = spec {
87 let (spec_base64, spec) = Spec::spec_to_base64(&spec)?;
88 (Some(spec_base64), spec)
89 } else {
90 (None, vec![])
91 };
92
93 Ok(Spec {
94 env_meta_base64,
95 env_meta,
96 meta_base64,
97 meta,
98 spec_base64,
99 spec,
100 })
101 }
102
103 pub fn spec_as_json_array(&self) -> Result<String, Error> {
104 let spec = self
105 .spec
106 .iter()
107 .map(|e| Ok(format!("\"{}\"", e.to_xdr_base64(Limits::none())?)))
108 .collect::<Result<Vec<_>, Error>>()?
109 .join(",\n");
110 Ok(format!("[{spec}]"))
111 }
112
113 pub fn spec_to_base64(spec: &[u8]) -> Result<(String, Vec<ScSpecEntry>), Error> {
114 let spec_base64 = base64.encode(spec);
115 let cursor = Cursor::new(spec);
116 let mut read = Limited::new(cursor, Limits::none());
117 Ok((
118 spec_base64,
119 ScSpecEntry::read_xdr_iter(&mut read).collect::<Result<Vec<_>, xdr::Error>>()?,
120 ))
121 }
122}
123
124impl Display for Spec {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 if let Some(env_meta) = &self.env_meta_base64 {
127 writeln!(f, "Env Meta: {env_meta}")?;
128 for env_meta_entry in &self.env_meta {
129 match env_meta_entry {
130 ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(
131 ScEnvMetaEntryInterfaceVersion {
132 protocol,
133 pre_release,
134 },
135 ) => {
136 writeln!(f, " • Protocol Version: {protocol}")?;
137 if pre_release != &0 {
138 writeln!(f, " • Pre-release Version: {pre_release})")?;
139 }
140 }
141 }
142 }
143 writeln!(f)?;
144 } else {
145 writeln!(f, "Env Meta: None\n")?;
146 }
147
148 if let Some(_meta) = &self.meta_base64 {
149 writeln!(f, "Contract Meta:")?;
150 for meta_entry in &self.meta {
151 match meta_entry {
152 ScMetaEntry::ScMetaV0(ScMetaV0 { key, val }) => {
153 writeln!(
154 f,
155 " • {}: {}",
156 sanitize(&key.to_utf8_string_lossy()),
157 sanitize(&val.to_utf8_string_lossy())
158 )?;
159 }
160 }
161 }
162 writeln!(f)?;
163 } else {
164 writeln!(f, "Contract Meta: None\n")?;
165 }
166
167 if let Some(_spec_base64) = &self.spec_base64 {
168 writeln!(f, "Contract Spec:")?;
169 for spec_entry in &self.spec {
170 match spec_entry {
171 ScSpecEntry::FunctionV0(func) => write_func(f, func)?,
172 ScSpecEntry::UdtUnionV0(udt) => write_union(f, udt)?,
173 ScSpecEntry::UdtStructV0(udt) => write_struct(f, udt)?,
174 ScSpecEntry::UdtEnumV0(udt) => write_enum(f, udt)?,
175 ScSpecEntry::UdtErrorEnumV0(udt) => write_error(f, udt)?,
176 ScSpecEntry::EventV0(_) => {}
177 }
178 }
179 } else {
180 writeln!(f, "Contract Spec: None")?;
181 }
182 Ok(())
183 }
184}
185
186fn write_func(f: &mut std::fmt::Formatter<'_>, func: &ScSpecFunctionV0) -> std::fmt::Result {
187 writeln!(
188 f,
189 " • Function: {}",
190 sanitize(&func.name.to_utf8_string_lossy())
191 )?;
192 if !func.doc.is_empty() {
193 writeln!(
194 f,
195 " Docs: {}",
196 &indent(&sanitize(&func.doc.to_utf8_string_lossy()), 11).trim()
197 )?;
198 }
199 writeln!(
200 f,
201 " Inputs: {}",
202 indent(&format!("{:#?}", func.inputs), 5).trim()
203 )?;
204 writeln!(
205 f,
206 " Output: {}",
207 indent(&format!("{:#?}", func.outputs), 5).trim()
208 )?;
209 writeln!(f)?;
210 Ok(())
211}
212
213fn write_union(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtUnionV0) -> std::fmt::Result {
214 writeln!(f, " • Union: {}", format_name(&udt.lib, &udt.name))?;
215 if !udt.doc.is_empty() {
216 writeln!(
217 f,
218 " Docs: {}",
219 indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
220 )?;
221 }
222 writeln!(f, " Cases:")?;
223 for case in udt.cases.iter() {
224 writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
225 }
226 writeln!(f)?;
227 Ok(())
228}
229
230fn write_struct(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtStructV0) -> std::fmt::Result {
231 writeln!(f, " • Struct: {}", format_name(&udt.lib, &udt.name))?;
232 if !udt.doc.is_empty() {
233 writeln!(
234 f,
235 " Docs: {}",
236 indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
237 )?;
238 }
239 writeln!(f, " Fields:")?;
240 for field in udt.fields.iter() {
241 writeln!(
242 f,
243 " • {}: {}",
244 sanitize(&field.name.to_utf8_string_lossy()),
245 indent(&format!("{:#?}", field.type_), 8).trim()
246 )?;
247 if !field.doc.is_empty() {
248 writeln!(f, "{}", indent(&format!("{:#?}", field.doc), 8))?;
249 }
250 }
251 writeln!(f)?;
252 Ok(())
253}
254
255fn write_enum(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtEnumV0) -> std::fmt::Result {
256 writeln!(f, " • Enum: {}", format_name(&udt.lib, &udt.name))?;
257 if !udt.doc.is_empty() {
258 writeln!(
259 f,
260 " Docs: {}",
261 indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
262 )?;
263 }
264 writeln!(f, " Cases:")?;
265 for case in udt.cases.iter() {
266 writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
267 }
268 writeln!(f)?;
269 Ok(())
270}
271
272fn write_error(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtErrorEnumV0) -> std::fmt::Result {
273 writeln!(f, " • Error: {}", format_name(&udt.lib, &udt.name))?;
274 if !udt.doc.is_empty() {
275 writeln!(
276 f,
277 " Docs: {}",
278 indent(&sanitize(&udt.doc.to_utf8_string_lossy()), 10).trim()
279 )?;
280 }
281 writeln!(f, " Cases:")?;
282 for case in udt.cases.iter() {
283 writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
284 }
285 writeln!(f)?;
286 Ok(())
287}
288
289pub fn sanitize(s: &str) -> String {
290 escape_bytes::escape(s.as_bytes())
291 .into_iter()
292 .map(char::from)
293 .collect()
294}
295
296fn indent(s: &str, n: usize) -> String {
297 let pad = " ".repeat(n);
298 s.lines()
299 .map(|line| format!("{pad}{line}"))
300 .collect::<Vec<_>>()
301 .join("\n")
302}
303
304fn format_name(lib: &StringM<80>, name: &StringM<60>) -> String {
305 if lib.is_empty() {
306 sanitize(&name.to_utf8_string_lossy())
307 } else {
308 format!(
309 "{}::{}",
310 sanitize(&lib.to_utf8_string_lossy()),
311 sanitize(&name.to_utf8_string_lossy())
312 )
313 }
314}