Skip to main content

sdkt_wasm/
lib.rs

1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use thiserror::Error;
4use wasmparser::{Parser, Payload};
5
6pub mod abi_decode;
7pub mod client_gen;
8pub mod spec;
9pub mod spec_diff;
10pub use abi_decode::{find_event_abi, find_type_abi, format_scval_abi, DecodedValue};
11pub use client_gen::{
12    generate_client, generate_client_with_options, ClientGenError, GenerateOptions,
13};
14pub use spec::{
15    decode_contract_meta, parse_contract_spec, ContractEvent, ContractFunction, ContractParameter,
16    ContractSpec, ContractType, EventParam, TypeMember,
17};
18pub use spec_diff::{
19    diff_specs, diff_wasm, event_sig, size_delta_bytes, size_delta_pct, type_sig, upgrade_safety,
20    upgrade_safety_wasm, ChangeKind, EventSignatureChange, FunctionSignatureChange, SpecDiff,
21    TypeDefinitionChange, UpgradeVerdict, VerdictChange, WasmSummary,
22};
23
24#[derive(Error, Debug)]
25pub enum WasmError {
26    #[error("WASM parse error: {0}")]
27    Parse(#[from] wasmparser::BinaryReaderError),
28    #[error("Empty WASM bytes")]
29    Empty,
30    #[error("No contractspecv0 section found")]
31    NoContractSpec,
32    #[error("XDR decode error in contract spec: {0}")]
33    SpecXdr(stellar_xdr::Error),
34    #[error("XDR decode error in contractmetav0 metadata: {0}")]
35    MetaXdr(stellar_xdr::Error),
36}
37
38/// A single `contractmetav0` key/value entry.
39///
40/// Contract metadata is an arbitrary, open key/value space (the `SCMetaV0`
41/// XDR struct is literally a `string key` / `string val` pair), so entries are
42/// surfaced verbatim and in section order — no vocabulary is assumed and
43/// repeated keys are preserved as separate entries.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct ContractMetaEntry {
46    /// The metadata key, exactly as declared by the contract (UTF-8).
47    pub key: String,
48    /// The metadata value, exactly as declared by the contract (UTF-8).
49    pub value: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct WasmMetadata {
54    pub hash: String,
55    pub size_bytes: usize,
56    pub version: u16,
57    pub exports: Vec<WasmExport>,
58    pub imports: Vec<WasmImport>,
59    pub custom_sections: Vec<String>,
60    /// Decoded `contractmetav0` entries, in section order (additive; may be
61    /// empty when the module carries no contract metadata).
62    #[serde(default)]
63    pub contract_meta: Vec<ContractMetaEntry>,
64    /// Number of functions declared in the module's function section.
65    #[serde(default)]
66    pub function_count: u32,
67    /// The first module memory, when one is declared.
68    #[serde(default)]
69    pub memory: Option<WasmMemory>,
70    /// Number of tables declared in the module.
71    #[serde(default)]
72    pub table_count: u32,
73    /// Number of globals declared in the module.
74    #[serde(default)]
75    pub global_count: u32,
76    /// Number of data segments declared in the module.
77    #[serde(default)]
78    pub data_segment_count: u32,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82pub struct WasmMemory {
83    pub initial_pages: u64,
84    #[serde(default)]
85    pub maximum_pages: Option<u64>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct WasmExport {
90    pub name: String,
91    pub kind: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95pub struct WasmImport {
96    pub module: String,
97    pub name: String,
98    pub kind: String,
99}
100
101/// Parse metadata (hash, size, exports, imports, custom sections) from raw
102/// Soroban contract WASM bytes.
103///
104/// # Example
105///
106/// ```
107/// use sdkt_wasm::parse_metadata;
108///
109/// // Empty input is rejected with `WasmError::Empty`.
110/// assert!(parse_metadata(b"").is_err());
111/// ```
112pub fn parse_metadata(wasm_bytes: &[u8]) -> Result<WasmMetadata, WasmError> {
113    if wasm_bytes.is_empty() {
114        return Err(WasmError::Empty);
115    }
116
117    let mut hasher = Sha256::new();
118    hasher.update(wasm_bytes);
119    let hash = hex::encode(hasher.finalize());
120
121    let mut meta = WasmMetadata {
122        hash,
123        size_bytes: wasm_bytes.len(),
124        version: 1, // Default, will update if found
125        exports: Vec::new(),
126        imports: Vec::new(),
127        custom_sections: Vec::new(),
128        contract_meta: Vec::new(),
129        function_count: 0,
130        memory: None,
131        table_count: 0,
132        global_count: 0,
133        data_segment_count: 0,
134    };
135
136    let parser = Parser::new(0);
137    for payload in parser.parse_all(wasm_bytes) {
138        match payload? {
139            Payload::Version { num, .. } => {
140                meta.version = num;
141            }
142            Payload::ExportSection(reader) => {
143                for export_res in reader {
144                    let export = export_res?;
145                    meta.exports.push(WasmExport {
146                        name: export.name.to_string(),
147                        kind: export_kind_str(export.kind).to_string(),
148                    });
149                }
150            }
151            Payload::ImportSection(reader) => {
152                for imports_res in reader {
153                    let imports = imports_res?;
154                    for imp_res in imports {
155                        let (_, import) = imp_res?;
156                        meta.imports.push(WasmImport {
157                            module: import.module.to_string(),
158                            name: import.name.to_string(),
159                            kind: import_kind_str(import.ty).to_string(),
160                        });
161                    }
162                }
163            }
164            Payload::CustomSection(reader) => {
165                let name = reader.name().to_string();
166                if name == spec::CONTRACT_META_V0 {
167                    // Decode the payload as ordered SCMetaEntry key/value pairs
168                    // (generic: no vocabulary is assumed). `custom_sections`
169                    // keeps recording the section name exactly as before.
170                    let entries = spec::decode_contract_meta(reader.data())?;
171                    meta.contract_meta.extend(entries);
172                }
173                meta.custom_sections.push(name);
174            }
175            Payload::FunctionSection(reader) => {
176                meta.function_count = reader.count();
177            }
178            Payload::MemorySection(reader) => {
179                if let Some(Ok(memory)) = reader.into_iter().next() {
180                    meta.memory = Some(WasmMemory {
181                        initial_pages: memory.initial,
182                        maximum_pages: memory.maximum,
183                    });
184                }
185            }
186            Payload::TableSection(reader) => {
187                meta.table_count = reader.count();
188            }
189            Payload::GlobalSection(reader) => {
190                meta.global_count = reader.count();
191            }
192            Payload::DataSection(reader) => {
193                meta.data_segment_count = reader.count();
194            }
195            _ => {}
196        }
197    }
198
199    Ok(meta)
200}
201
202fn export_kind_str(kind: wasmparser::ExternalKind) -> &'static str {
203    match kind {
204        wasmparser::ExternalKind::Func => "func",
205        wasmparser::ExternalKind::Table => "table",
206        wasmparser::ExternalKind::Memory => "memory",
207        wasmparser::ExternalKind::Global => "global",
208        wasmparser::ExternalKind::Tag => "tag",
209        wasmparser::ExternalKind::FuncExact => "func_exact",
210    }
211}
212
213fn import_kind_str(ty: wasmparser::TypeRef) -> &'static str {
214    match ty {
215        wasmparser::TypeRef::Func(_) => "func",
216        wasmparser::TypeRef::Table(_) => "table",
217        wasmparser::TypeRef::Memory(_) => "memory",
218        wasmparser::TypeRef::Global(_) => "global",
219        wasmparser::TypeRef::Tag(_) => "tag",
220        wasmparser::TypeRef::FuncExact(_) => "func_exact",
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    // A minimal valid WASM binary (magic + version 1)
229    const VALID_EMPTY_WASM: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
230
231    // Minimal WASM with an empty export section (section id 7, size 1, 0 items)
232    const WASM_WITH_EXPORTS: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 7, 1, 0];
233
234    #[test]
235    fn test_empty_bytes() {
236        let res = parse_metadata(&[]);
237        assert!(matches!(res, Err(WasmError::Empty)));
238    }
239
240    #[test]
241    fn test_invalid_wasm() {
242        let res = parse_metadata(b"not a wasm file");
243        assert!(matches!(res, Err(WasmError::Parse(_))));
244    }
245
246    #[test]
247    fn test_valid_empty_wasm() {
248        let meta = parse_metadata(VALID_EMPTY_WASM).unwrap();
249        assert_eq!(meta.size_bytes, 8);
250        assert_eq!(meta.version, 1);
251        assert!(meta.exports.is_empty());
252        assert!(meta.imports.is_empty());
253        assert!(meta.custom_sections.is_empty());
254
255        let expected_hash = hex::encode(Sha256::digest(VALID_EMPTY_WASM));
256        assert_eq!(meta.hash, expected_hash);
257    }
258
259    #[test]
260    fn test_wasm_exports_parsing() {
261        let meta = parse_metadata(WASM_WITH_EXPORTS).unwrap();
262        assert_eq!(meta.size_bytes, 11);
263        assert!(meta.exports.is_empty());
264    }
265
266    #[test]
267    fn test_export_kind_str() {
268        assert_eq!(export_kind_str(wasmparser::ExternalKind::Func), "func");
269        assert_eq!(export_kind_str(wasmparser::ExternalKind::Table), "table");
270        assert_eq!(export_kind_str(wasmparser::ExternalKind::Memory), "memory");
271        assert_eq!(export_kind_str(wasmparser::ExternalKind::Global), "global");
272        assert_eq!(export_kind_str(wasmparser::ExternalKind::Tag), "tag");
273        assert_eq!(
274            export_kind_str(wasmparser::ExternalKind::FuncExact),
275            "func_exact"
276        );
277    }
278
279    #[test]
280    fn test_import_kind_str() {
281        assert_eq!(import_kind_str(wasmparser::TypeRef::Func(0)), "func");
282        assert_eq!(import_kind_str(wasmparser::TypeRef::Func(42)), "func");
283        assert_eq!(
284            import_kind_str(wasmparser::TypeRef::Table(wasmparser::TableType {
285                element_type: wasmparser::RefType::FUNCREF,
286                table64: false,
287                initial: 0,
288                maximum: None,
289                shared: false,
290            })),
291            "table"
292        );
293        assert_eq!(
294            import_kind_str(wasmparser::TypeRef::Memory(wasmparser::MemoryType {
295                initial: 1,
296                maximum: None,
297                shared: false,
298                memory64: false,
299                page_size_log2: None,
300            })),
301            "memory"
302        );
303        assert_eq!(
304            import_kind_str(wasmparser::TypeRef::Global(wasmparser::GlobalType {
305                content_type: wasmparser::ValType::I32,
306                mutable: false,
307                shared: false,
308            })),
309            "global"
310        );
311        assert_eq!(
312            import_kind_str(wasmparser::TypeRef::Tag(wasmparser::TagType {
313                kind: wasmparser::TagKind::Exception,
314                func_type_idx: 0,
315            })),
316            "tag"
317        );
318        assert_eq!(
319            import_kind_str(wasmparser::TypeRef::FuncExact(10)),
320            "func_exact"
321        );
322    }
323
324    #[test]
325    fn test_parse_metadata_exports_and_imports_kind() {
326        // Minimal WASM binary with:
327        // - Type section (section 1): 1 function type () -> ()
328        // - Import section (section 2): module "m", name "imp", type 0 (func)
329        // - Export section (section 7): name "exp", kind 0 (func), idx 0
330        let wasm_bytes = [
331            0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // header
332            0x01, 0x04, 0x01, 0x60, 0x00, 0x00, // type section: 1 type
333            0x02, 0x09, 0x01, 0x01, b'm', 0x03, b'i', b'm', b'p', 0x00, 0x00, // import: func
334            0x07, 0x07, 0x01, 0x03, b'e', b'x', b'p', 0x00, 0x00, // export: func
335        ];
336        let meta = parse_metadata(&wasm_bytes).expect("valid wasm bytes");
337        assert_eq!(meta.imports.len(), 1);
338        assert_eq!(meta.imports[0].module, "m");
339        assert_eq!(meta.imports[0].name, "imp");
340        assert_eq!(meta.imports[0].kind, "func");
341
342        assert_eq!(meta.exports.len(), 1);
343        assert_eq!(meta.exports[0].name, "exp");
344        assert_eq!(meta.exports[0].kind, "func");
345    }
346}