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