Skip to content

Commit e5f8eff

Browse files
leighmccullochCopilotmootz12
authored
Return errors instead of panicking in soroban-spec-rust (#1810)
### What Convert all code-generation functions in `soroban-spec-rust` (`generate_struct`, `generate_union`, `generate_enum`, `generate_error_enum`, `generate_event`, `generate_function`, and their `_with_options` variants) from infallible to fallible, returning `GenerateError`. Propagate `GenerateError` up through `generate_from_wasm_with_options` and other fns. ### Why The generator panicked on Wasm contracts with invalid UTF-8 bytes or non-identifier strings in spec entry names. Existing TODOs in the source acknowledged this. Close #1765 ### Targeting v26 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: mootz12 <38118608+mootz12@users.noreply.github.com>
1 parent 5e36de5 commit e5f8eff

4 files changed

Lines changed: 324 additions & 167 deletions

File tree

soroban-spec-rust/src/lib.rs

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod syn_ext;
12
pub mod r#trait;
23
pub mod types;
34

@@ -12,11 +13,11 @@ use syn::Error;
1213

1314
use soroban_spec::read::{from_wasm, FromWasmError};
1415

15-
pub use types::GenerateOptions;
1616
use types::{
1717
generate_enum_with_options, generate_error_enum_with_options, generate_event_with_options,
1818
generate_struct_with_options, generate_union_with_options,
1919
};
20+
pub use types::{GenerateError, GenerateOptions};
2021

2122
// IMPORTANT: The "docs" fields of spec entries are not output in Rust token
2223
// streams as rustdocs, because rustdocs can contain Rust code, and that code
@@ -33,6 +34,8 @@ pub enum GenerateFromFileError {
3334
Parse(stellar_xdr::Error),
3435
#[error("getting contract spec: {0}")]
3536
GetSpec(FromWasmError),
37+
#[error("generating code: {0}")]
38+
Generate(GenerateError),
3639
}
3740

3841
pub fn generate_from_file(
@@ -70,11 +73,16 @@ pub fn generate_from_wasm_with_options(
7073
}
7174

7275
let spec = from_wasm(wasm).map_err(GenerateFromFileError::GetSpec)?;
73-
let code = generate_with_options(&spec, file, &sha256, opts);
76+
let code = generate_with_options(&spec, file, &sha256, opts)
77+
.map_err(GenerateFromFileError::Generate)?;
7478
Ok(code)
7579
}
7680

77-
pub fn generate(specs: &[ScSpecEntry], file: &str, sha256: &str) -> TokenStream {
81+
pub fn generate(
82+
specs: &[ScSpecEntry],
83+
file: &str,
84+
sha256: &str,
85+
) -> Result<TokenStream, GenerateError> {
7886
generate_with_options(specs, file, sha256, &GenerateOptions::default())
7987
}
8088

@@ -83,22 +91,22 @@ pub fn generate_with_options(
8391
file: &str,
8492
sha256: &str,
8593
opts: &GenerateOptions,
86-
) -> TokenStream {
87-
let generated = generate_without_file_with_options(specs, opts);
88-
quote! {
94+
) -> Result<TokenStream, GenerateError> {
95+
let generated = generate_without_file_with_options(specs, opts)?;
96+
Ok(quote! {
8997
pub const WASM: &[u8] = soroban_sdk::contractfile!(file = #file, sha256 = #sha256);
9098
#generated
91-
}
99+
})
92100
}
93101

94-
pub fn generate_without_file(specs: &[ScSpecEntry]) -> TokenStream {
102+
pub fn generate_without_file(specs: &[ScSpecEntry]) -> Result<TokenStream, GenerateError> {
95103
generate_without_file_with_options(specs, &GenerateOptions::default())
96104
}
97105

98106
pub fn generate_without_file_with_options(
99107
specs: &[ScSpecEntry],
100108
opts: &GenerateOptions,
101-
) -> TokenStream {
109+
) -> Result<TokenStream, GenerateError> {
102110
let mut spec_fns = Vec::new();
103111
let mut spec_structs = Vec::new();
104112
let mut spec_unions = Vec::new();
@@ -118,24 +126,29 @@ pub fn generate_without_file_with_options(
118126

119127
let trait_name = "Contract";
120128

121-
let trait_ = r#trait::generate_trait(trait_name, &spec_fns);
129+
let trait_ = r#trait::generate_trait(trait_name, &spec_fns)?;
122130
let structs = spec_structs
123131
.iter()
124-
.map(|s| generate_struct_with_options(s, opts));
132+
.map(|s| generate_struct_with_options(s, opts))
133+
.collect::<Result<Vec<_>, _>>()?;
125134
let unions = spec_unions
126135
.iter()
127-
.map(|s| generate_union_with_options(s, opts));
136+
.map(|s| generate_union_with_options(s, opts))
137+
.collect::<Result<Vec<_>, _>>()?;
128138
let enums = spec_enums
129139
.iter()
130-
.map(|s| generate_enum_with_options(s, opts));
140+
.map(|s| generate_enum_with_options(s, opts))
141+
.collect::<Result<Vec<_>, _>>()?;
131142
let error_enums = spec_error_enums
132143
.iter()
133-
.map(|s| generate_error_enum_with_options(s, opts));
144+
.map(|s| generate_error_enum_with_options(s, opts))
145+
.collect::<Result<Vec<_>, _>>()?;
134146
let events = spec_events
135147
.iter()
136-
.map(|s| generate_event_with_options(s, opts));
148+
.map(|s| generate_event_with_options(s, opts))
149+
.collect::<Result<Vec<_>, _>>()?;
137150

138-
quote! {
151+
Ok(quote! {
139152
#[soroban_sdk::contractargs(name = "Args")]
140153
#[soroban_sdk::contractclient(name = "Client")]
141154
#trait_
@@ -145,7 +158,7 @@ pub fn generate_without_file_with_options(
145158
#(#enums)*
146159
#(#error_enums)*
147160
#(#events)*
148-
}
161+
})
149162
}
150163

151164
/// Implemented by types that can be converted into pretty formatted Strings of
@@ -177,6 +190,7 @@ mod test {
177190
fn example() {
178191
let entries = from_wasm(EXAMPLE_WASM).unwrap();
179192
let rust = generate(&entries, "<file>", "<sha256>")
193+
.unwrap()
180194
.to_formatted_string()
181195
.unwrap();
182196
assert_eq!(

soroban-spec-rust/src/syn_ext.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use proc_macro2::Ident;
2+
use stellar_xdr::curr::{ScSymbol, StringM};
3+
4+
use crate::types::GenerateError;
5+
6+
pub trait IntoIdent {
7+
fn into_ident(&self) -> Result<Ident, GenerateError>;
8+
}
9+
10+
impl IntoIdent for str {
11+
fn into_ident(&self) -> Result<Ident, GenerateError> {
12+
syn::parse_str::<Ident>(self).map_err(|_| GenerateError::InvalidIdent(self.to_string()))
13+
}
14+
}
15+
16+
impl<const N: u32> IntoIdent for StringM<N> {
17+
fn into_ident(&self) -> Result<Ident, GenerateError> {
18+
let s = self
19+
.to_utf8_string()
20+
.map_err(|_| GenerateError::InvalidUtf8)?;
21+
s.as_str().into_ident()
22+
}
23+
}
24+
25+
impl IntoIdent for ScSymbol {
26+
fn into_ident(&self) -> Result<Ident, GenerateError> {
27+
self.0.into_ident()
28+
}
29+
}
30+
31+
/// Creates a Rust identifier from a string or spec name, returning an error if
32+
/// it contains invalid UTF-8 or is not a valid identifier.
33+
pub fn str_to_ident(s: &(impl IntoIdent + ?Sized)) -> Result<Ident, GenerateError> {
34+
s.into_ident()
35+
}

soroban-spec-rust/src/trait.rs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use proc_macro2::TokenStream;
2-
use quote::{format_ident, quote};
2+
use quote::quote;
33
use stellar_xdr::curr as stellar_xdr;
44
use stellar_xdr::ScSpecFunctionV0;
55

6-
use super::types::generate_type_ident;
6+
use super::syn_ext::str_to_ident;
7+
use super::types::{generate_type_ident, GenerateError};
78

89
// IMPORTANT: The "docs" fields of spec entries are not output in Rust token
910
// streams as rustdocs, because rustdocs can contain Rust code, and that code
@@ -12,12 +13,18 @@ use super::types::generate_type_ident;
1213

1314
/// Constructs a token stream containing a single trait that has a function for
1415
/// every function spec.
15-
pub fn generate_trait(name: &str, specs: &[&ScSpecFunctionV0]) -> TokenStream {
16-
let trait_ident = format_ident!("{}", name);
17-
let fns: Vec<_> = specs.iter().map(|s| generate_function(*s)).collect();
18-
quote! {
16+
pub fn generate_trait(
17+
name: &str,
18+
specs: &[&ScSpecFunctionV0],
19+
) -> Result<TokenStream, GenerateError> {
20+
let trait_ident = str_to_ident(name)?;
21+
let fns = specs
22+
.iter()
23+
.map(|s| generate_function(s))
24+
.collect::<Result<Vec<_>, _>>()?;
25+
Ok(quote! {
1926
pub trait #trait_ident { #(#fns;)* }
20-
}
27+
})
2128
}
2229

2330
/// Constructs a token stream representing a single function definition based on the provided
@@ -28,19 +35,24 @@ pub fn generate_trait(name: &str, specs: &[&ScSpecFunctionV0]) -> TokenStream {
2835
///
2936
/// # Returns
3037
/// A `TokenStream` containing the generated function definition.
31-
pub fn generate_function(s: &ScSpecFunctionV0) -> TokenStream {
32-
let fn_ident = format_ident!("{}", s.name.to_utf8_string().unwrap());
33-
let fn_inputs = s.inputs.iter().map(|input| {
34-
let name = format_ident!("{}", input.name.to_utf8_string().unwrap());
35-
let type_ident = generate_type_ident(&input.type_);
36-
quote! { #name: #type_ident }
37-
});
38+
pub fn generate_function(s: &ScSpecFunctionV0) -> Result<TokenStream, GenerateError> {
39+
let fn_ident = str_to_ident(&s.name)?;
40+
let fn_inputs = s
41+
.inputs
42+
.iter()
43+
.map(|input| {
44+
let name = str_to_ident(&input.name)?;
45+
let type_ident = generate_type_ident(&input.type_)?;
46+
Ok(quote! { #name: #type_ident })
47+
})
48+
.collect::<Result<Vec<_>, GenerateError>>()?;
3849
let fn_output = s
3950
.outputs
4051
.to_option()
4152
.map(|t| generate_type_ident(&t))
53+
.transpose()?
4254
.map(|t| quote! { -> #t });
43-
quote! {
55+
Ok(quote! {
4456
fn #fn_ident(env: soroban_sdk::Env, #(#fn_inputs),*) #fn_output
45-
}
57+
})
4658
}

0 commit comments

Comments
 (0)