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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct ContractMetaEntry {
46 pub key: String,
48 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 #[serde(default)]
63 pub contract_meta: Vec<ContractMetaEntry>,
64 #[serde(default)]
66 pub function_count: u32,
67 #[serde(default)]
69 pub memory: Option<WasmMemory>,
70 #[serde(default)]
72 pub table_count: u32,
73 #[serde(default)]
75 pub global_count: u32,
76 #[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
101pub 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, 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 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 const VALID_EMPTY_WASM: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
230
231 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 let wasm_bytes = [
331 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x02, 0x09, 0x01, 0x01, b'm', 0x03, b'i', b'm', b'p', 0x00, 0x00, 0x07, 0x07, 0x01, 0x03, b'e', b'x', b'p', 0x00, 0x00, ];
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}