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!(f, " • {key}: {val}")?;
154 }
155 }
156 }
157 writeln!(f)?;
158 } else {
159 writeln!(f, "Contract Meta: None\n")?;
160 }
161
162 if let Some(_spec_base64) = &self.spec_base64 {
163 writeln!(f, "Contract Spec:")?;
164 for spec_entry in &self.spec {
165 match spec_entry {
166 ScSpecEntry::FunctionV0(func) => write_func(f, func)?,
167 ScSpecEntry::UdtUnionV0(udt) => write_union(f, udt)?,
168 ScSpecEntry::UdtStructV0(udt) => write_struct(f, udt)?,
169 ScSpecEntry::UdtEnumV0(udt) => write_enum(f, udt)?,
170 ScSpecEntry::UdtErrorEnumV0(udt) => write_error(f, udt)?,
171 ScSpecEntry::EventV0(_) => {}
172 }
173 }
174 } else {
175 writeln!(f, "Contract Spec: None")?;
176 }
177 Ok(())
178 }
179}
180
181fn write_func(f: &mut std::fmt::Formatter<'_>, func: &ScSpecFunctionV0) -> std::fmt::Result {
182 writeln!(f, " • Function: {}", func.name.to_utf8_string_lossy())?;
183 if !func.doc.is_empty() {
184 writeln!(
185 f,
186 " Docs: {}",
187 &indent(&func.doc.to_utf8_string_lossy(), 11).trim()
188 )?;
189 }
190 writeln!(
191 f,
192 " Inputs: {}",
193 indent(&format!("{:#?}", func.inputs), 5).trim()
194 )?;
195 writeln!(
196 f,
197 " Output: {}",
198 indent(&format!("{:#?}", func.outputs), 5).trim()
199 )?;
200 writeln!(f)?;
201 Ok(())
202}
203
204fn write_union(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtUnionV0) -> std::fmt::Result {
205 writeln!(f, " • Union: {}", format_name(&udt.lib, &udt.name))?;
206 if !udt.doc.is_empty() {
207 writeln!(
208 f,
209 " Docs: {}",
210 indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
211 )?;
212 }
213 writeln!(f, " Cases:")?;
214 for case in udt.cases.iter() {
215 writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
216 }
217 writeln!(f)?;
218 Ok(())
219}
220
221fn write_struct(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtStructV0) -> std::fmt::Result {
222 writeln!(f, " • Struct: {}", format_name(&udt.lib, &udt.name))?;
223 if !udt.doc.is_empty() {
224 writeln!(
225 f,
226 " Docs: {}",
227 indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
228 )?;
229 }
230 writeln!(f, " Fields:")?;
231 for field in udt.fields.iter() {
232 writeln!(
233 f,
234 " • {}: {}",
235 field.name.to_utf8_string_lossy(),
236 indent(&format!("{:#?}", field.type_), 8).trim()
237 )?;
238 if !field.doc.is_empty() {
239 writeln!(f, "{}", indent(&format!("{:#?}", field.doc), 8))?;
240 }
241 }
242 writeln!(f)?;
243 Ok(())
244}
245
246fn write_enum(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtEnumV0) -> std::fmt::Result {
247 writeln!(f, " • Enum: {}", format_name(&udt.lib, &udt.name))?;
248 if !udt.doc.is_empty() {
249 writeln!(
250 f,
251 " Docs: {}",
252 indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
253 )?;
254 }
255 writeln!(f, " Cases:")?;
256 for case in udt.cases.iter() {
257 writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
258 }
259 writeln!(f)?;
260 Ok(())
261}
262
263fn write_error(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtErrorEnumV0) -> std::fmt::Result {
264 writeln!(f, " • Error: {}", format_name(&udt.lib, &udt.name))?;
265 if !udt.doc.is_empty() {
266 writeln!(
267 f,
268 " Docs: {}",
269 indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
270 )?;
271 }
272 writeln!(f, " Cases:")?;
273 for case in udt.cases.iter() {
274 writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
275 }
276 writeln!(f)?;
277 Ok(())
278}
279
280fn indent(s: &str, n: usize) -> String {
281 let pad = " ".repeat(n);
282 s.lines()
283 .map(|line| format!("{pad}{line}"))
284 .collect::<Vec<_>>()
285 .join("\n")
286}
287
288fn format_name(lib: &StringM<80>, name: &StringM<60>) -> String {
289 if lib.is_empty() {
290 name.to_utf8_string_lossy()
291 } else {
292 format!(
293 "{}::{}",
294 lib.to_utf8_string_lossy(),
295 name.to_utf8_string_lossy()
296 )
297 }
298}