1use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use soldb_core::{SoldbError, SoldbResult};
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct SourceLocation {
20 pub source_id: u64,
21 pub offset: u64,
22 pub length: u64,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct FunctionIdentity {
27 pub identifier: Option<String>,
28 pub declaration: Option<SourceLocation>,
29 pub activation: Option<String>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum FunctionExit {
35 Return,
36 Revert,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Instruction {
41 pub offset: u64,
42 pub operation: Value,
43 #[serde(default)]
44 pub context: Option<Value>,
45}
46
47impl Instruction {
48 #[must_use]
49 pub fn mnemonic(&self) -> Option<&str> {
50 self.operation
51 .get("mnemonic")
52 .and_then(serde_json::Value::as_str)
53 }
54
55 #[must_use]
56 pub fn arguments(&self) -> Vec<&str> {
57 self.operation
58 .get("arguments")
59 .and_then(serde_json::Value::as_array)
60 .map(|arguments| {
61 arguments
62 .iter()
63 .filter_map(serde_json::Value::as_str)
64 .collect()
65 })
66 .unwrap_or_default()
67 }
68
69 #[must_use]
70 pub fn source_location(&self) -> Option<SourceLocation> {
71 let mut locations = self.source_locations();
72 (locations.len() == 1).then(|| locations.remove(0))
73 }
74
75 #[must_use]
77 pub fn source_locations(&self) -> Vec<SourceLocation> {
78 let mut locations = Vec::new();
79 if let Some(context) = &self.context {
80 collect_source_locations(context, &mut locations);
81 }
82 locations
83 }
84
85 #[must_use]
87 pub fn function_invocations(&self) -> Vec<FunctionIdentity> {
88 let mut functions = Vec::new();
89 if let Some(context) = &self.context {
90 collect_function_invocations(context, &mut functions);
91 }
92 functions
93 }
94
95 #[must_use]
97 pub fn function_exit(&self) -> Option<FunctionExit> {
98 let context = self.context.as_ref()?;
99 if context.get("return").is_some() {
100 Some(FunctionExit::Return)
101 } else if context.get("revert").is_some() {
102 Some(FunctionExit::Revert)
103 } else {
104 None
105 }
106 }
107}
108
109fn collect_source_locations(context: &Value, locations: &mut Vec<SourceLocation>) {
110 if let Some(location) = context.get("code").and_then(parse_source_location) {
111 if !locations.contains(&location) {
112 locations.push(location);
113 }
114 }
115 if let Some(pick) = context.get("pick").and_then(Value::as_array) {
116 for alternative in pick {
117 collect_source_locations(alternative, locations);
118 }
119 }
120}
121
122fn collect_function_invocations(context: &Value, functions: &mut Vec<FunctionIdentity>) {
123 if let Some(invoke) = context.get("invoke") {
124 let function = FunctionIdentity {
125 identifier: invoke
126 .get("identifier")
127 .and_then(Value::as_str)
128 .map(str::to_owned),
129 declaration: invoke.get("declaration").and_then(parse_source_location),
130 activation: invoke
131 .get("activation")
132 .and_then(Value::as_str)
133 .map(str::to_owned),
134 };
135 if !functions.contains(&function) {
136 functions.push(function);
137 }
138 }
139 if let Some(pick) = context.get("pick").and_then(Value::as_array) {
140 for alternative in pick {
141 collect_function_invocations(alternative, functions);
142 }
143 }
144}
145
146fn parse_source_location(value: &Value) -> Option<SourceLocation> {
147 let source_id = value.get("source")?.get("id")?.as_u64()?;
148 let range = value.get("range")?;
149 Some(SourceLocation {
150 source_id,
151 offset: range.get("offset")?.as_u64()?,
152 length: range.get("length")?.as_u64()?,
153 })
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct VariableLocation {
158 pub name: String,
159 pub ty: String,
160 pub location_type: String,
161 pub offset: u64,
162 pub pc_range: (u64, u64),
163}
164
165impl VariableLocation {
166 #[must_use]
167 pub fn is_active_at_pc(&self, pc: u64) -> bool {
168 self.pc_range.0 <= pc && pc <= self.pc_range.1
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct EthdebugInfo {
174 pub compilation: Value,
175 pub contract_name: String,
176 pub environment: String,
177 pub instructions: Vec<Instruction>,
178 pub sources: BTreeMap<u64, String>,
179 #[serde(default)]
180 pub variable_locations: BTreeMap<u64, Vec<VariableLocation>>,
181}
182
183impl EthdebugInfo {
184 #[must_use]
185 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
186 self.instructions
187 .iter()
188 .find(|instruction| instruction.offset == pc)
189 }
190
191 #[must_use]
192 pub fn source_info(&self, pc: u64) -> Option<(&str, u64, u64)> {
193 let instruction = self.instruction_at_pc(pc)?;
194 let source_location = instruction.source_location()?;
195 let source_path = self.sources.get(&source_location.source_id)?;
196 Some((source_path, source_location.offset, source_location.length))
197 }
198
199 #[must_use]
200 pub fn variables_at_pc(&self, pc: u64) -> Vec<&VariableLocation> {
201 if let Some(exact) = self.variable_locations.get(&pc) {
202 return exact.iter().collect();
203 }
204
205 self.variable_locations
206 .values()
207 .flat_map(|variables| variables.iter())
208 .filter(|variable| variable.is_active_at_pc(pc))
209 .collect()
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct EthdebugSpec {
215 pub address: Option<String>,
216 pub name: Option<String>,
217 pub path: String,
218}
219
220pub fn parse_ethdebug_spec(input: &str) -> EthdebugSpec {
221 if let Ok(spec) = parse_single_contract_spec(input) {
222 return spec;
223 }
224
225 parse_multi_contract_spec(input).unwrap_or_else(|_| EthdebugSpec {
226 address: None,
227 name: None,
228 path: input.to_owned(),
229 })
230}
231
232pub fn parse_single_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
233 if !input.starts_with("0x") {
234 return Err(SoldbError::Message(format!(
235 "Must use format 'address:name:path' (got: {input})"
236 )));
237 }
238
239 let parts = input.splitn(3, ':').collect::<Vec<_>>();
240 if parts.len() != 3 || parts[1].is_empty() || parts[2].is_empty() {
241 return Err(SoldbError::Message(format!(
242 "Must use format 'address:name:path' (got: {input})"
243 )));
244 }
245
246 Ok(EthdebugSpec {
247 address: Some(parts[0].to_owned()),
248 name: Some(parts[1].to_owned()),
249 path: parts[2].to_owned(),
250 })
251}
252
253pub fn parse_multi_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
254 if input.is_empty() {
255 return Err(SoldbError::Message("Path cannot be empty".to_owned()));
256 }
257
258 if input.starts_with("0x") && input.contains(':') {
259 let parts = input.splitn(2, ':').collect::<Vec<_>>();
260 if parts.len() != 2 || parts[1].is_empty() {
261 return Err(SoldbError::Message(format!(
262 "Must use format 'address:path' (got: {input})"
263 )));
264 }
265
266 return Ok(EthdebugSpec {
267 address: Some(parts[0].to_owned()),
268 name: None,
269 path: parts[1].to_owned(),
270 });
271 }
272
273 Ok(EthdebugSpec {
274 address: None,
275 name: None,
276 path: input.to_owned(),
277 })
278}
279
280pub fn parse_variable_locations(
281 contract_data: &Value,
282) -> SoldbResult<BTreeMap<u64, Vec<VariableLocation>>> {
283 let mut variable_locations = BTreeMap::<u64, Vec<VariableLocation>>::new();
284
285 if let Some(instructions) = contract_data
286 .get("instructions")
287 .and_then(serde_json::Value::as_array)
288 {
289 for instruction in instructions {
290 let pc = instruction
291 .get("offset")
292 .and_then(serde_json::Value::as_u64)
293 .ok_or_else(|| SoldbError::Message("Instruction missing offset".to_owned()))?;
294
295 let Some(variables) = instruction
296 .get("context")
297 .and_then(|context| context.get("variables"))
298 .and_then(serde_json::Value::as_array)
299 else {
300 continue;
301 };
302
303 for variable in variables {
304 variable_locations
305 .entry(pc)
306 .or_default()
307 .push(parse_context_variable(variable, pc));
308 }
309 }
310 }
311
312 if let Some(variables) = contract_data
313 .get("variables")
314 .and_then(serde_json::Value::as_array)
315 {
316 for variable in variables {
317 let variable_location = parse_top_level_variable(variable);
318 for pc in variable_location.pc_range.0..=variable_location.pc_range.1 {
319 variable_locations
320 .entry(pc)
321 .or_default()
322 .push(variable_location.clone());
323 }
324 }
325 }
326
327 Ok(variable_locations)
328}
329
330fn parse_context_variable(variable: &Value, pc: u64) -> VariableLocation {
331 let location = variable.get("location");
332 let scope = variable.get("scope");
333
334 VariableLocation {
335 name: get_string(variable, "name", "unknown"),
336 ty: get_string(variable, "type", "unknown"),
337 location_type: location
338 .and_then(|value| value.get("type"))
339 .and_then(serde_json::Value::as_str)
340 .unwrap_or("stack")
341 .to_owned(),
342 offset: location
343 .and_then(|value| value.get("offset"))
344 .and_then(serde_json::Value::as_u64)
345 .unwrap_or(0),
346 pc_range: (
347 scope
348 .and_then(|value| value.get("start"))
349 .and_then(serde_json::Value::as_u64)
350 .unwrap_or(pc),
351 scope
352 .and_then(|value| value.get("end"))
353 .and_then(serde_json::Value::as_u64)
354 .unwrap_or(pc),
355 ),
356 }
357}
358
359fn parse_top_level_variable(variable: &Value) -> VariableLocation {
360 VariableLocation {
361 name: get_string(variable, "name", "unknown"),
362 ty: get_string(variable, "type", "unknown"),
363 location_type: get_string(variable, "location_type", "stack"),
364 offset: variable
365 .get("offset")
366 .and_then(serde_json::Value::as_u64)
367 .unwrap_or(0),
368 pc_range: (
369 variable
370 .get("pc_start")
371 .and_then(serde_json::Value::as_u64)
372 .unwrap_or(0),
373 variable
374 .get("pc_end")
375 .and_then(serde_json::Value::as_u64)
376 .unwrap_or(0),
377 ),
378 }
379}
380
381fn get_string(value: &Value, key: &str, default: &str) -> String {
382 value
383 .get(key)
384 .and_then(serde_json::Value::as_str)
385 .unwrap_or(default)
386 .to_owned()
387}
388
389#[cfg(test)]
390mod tests {
391 use std::collections::BTreeMap;
392
393 use super::{
394 parse_ethdebug_spec, parse_multi_contract_spec, parse_single_contract_spec,
395 parse_variable_locations, EthdebugInfo, Instruction,
396 };
397 use serde_json::json;
398
399 #[test]
400 fn parses_single_and_multi_contract_specs() {
401 let spec = parse_single_contract_spec("0xabc:Token:out").expect("single spec");
402 assert_eq!(spec.address.as_deref(), Some("0xabc"));
403 assert_eq!(spec.name.as_deref(), Some("Token"));
404 assert_eq!(spec.path, "out");
405
406 let multi = parse_multi_contract_spec("0xabc:out").expect("multi spec");
407 assert_eq!(multi.address.as_deref(), Some("0xabc"));
408 assert_eq!(multi.name, None);
409 assert_eq!(multi.path, "out");
410
411 let plain = parse_ethdebug_spec("out");
412 assert_eq!(plain.address, None);
413 assert_eq!(plain.path, "out");
414
415 assert!(parse_single_contract_spec("out").is_err());
416 assert!(parse_multi_contract_spec("").is_err());
417 }
418
419 #[test]
420 fn extracts_instruction_source_locations() {
421 let instruction: Instruction = serde_json::from_value(json!({
422 "offset": 12,
423 "operation": {"mnemonic": "PUSH1", "arguments": ["0x2a"]},
424 "context": {
425 "code": {
426 "source": {"id": 7},
427 "range": {"offset": 20, "length": 4}
428 }
429 }
430 }))
431 .expect("instruction");
432
433 let source_location = instruction.source_location().expect("source location");
434 assert_eq!(instruction.mnemonic(), Some("PUSH1"));
435 assert_eq!(instruction.arguments(), ["0x2a"]);
436 assert_eq!(source_location.source_id, 7);
437 assert_eq!(source_location.offset, 20);
438 assert_eq!(source_location.length, 4);
439 }
440
441 #[test]
442 fn extracts_source_picks_and_function_events() {
443 let invoke: Instruction = serde_json::from_value(json!({
444 "offset": 12,
445 "operation": {"mnemonic": "JUMPDEST"},
446 "context": {
447 "invoke": {
448 "identifier": "calculate",
449 "declaration": {
450 "source": {"id": 7},
451 "range": {"offset": 100, "length": 80}
452 },
453 "jump": true
454 }
455 }
456 }))
457 .expect("invoke instruction");
458 let shared: Instruction = serde_json::from_value(json!({
459 "offset": 13,
460 "operation": {"mnemonic": "SSTORE"},
461 "context": {
462 "pick": [
463 {"code": {
464 "source": {"id": 7},
465 "range": {"offset": 20, "length": 4}
466 }},
467 {"code": {
468 "source": {"id": 7},
469 "range": {"offset": 140, "length": 4}
470 }}
471 ]
472 }
473 }))
474 .expect("shared instruction");
475 let returned: Instruction = serde_json::from_value(json!({
476 "offset": 14,
477 "operation": {"mnemonic": "STOP"},
478 "context": {"return": {}}
479 }))
480 .expect("return instruction");
481
482 let function = invoke.function_invocations().remove(0);
483 assert_eq!(function.identifier.as_deref(), Some("calculate"));
484 assert_eq!(function.declaration.map(|range| range.offset), Some(100));
485 assert_eq!(shared.source_location(), None);
486 assert_eq!(
487 shared
488 .source_locations()
489 .into_iter()
490 .map(|location| location.offset)
491 .collect::<Vec<_>>(),
492 [20, 140]
493 );
494 assert_eq!(returned.function_exit(), Some(super::FunctionExit::Return));
495 }
496
497 #[test]
498 fn reports_source_and_variable_info_at_pc() {
499 let instruction: Instruction = serde_json::from_value(json!({
500 "offset": 3,
501 "operation": {"mnemonic": "SLOAD"},
502 "context": {
503 "code": {
504 "source": {"id": 0},
505 "range": {"offset": 9, "length": 5}
506 }
507 }
508 }))
509 .expect("instruction");
510
511 let variable = super::VariableLocation {
512 name: "stored".to_owned(),
513 ty: "uint256".to_owned(),
514 location_type: "storage".to_owned(),
515 offset: 0,
516 pc_range: (2, 5),
517 };
518 let mut sources = BTreeMap::new();
519 sources.insert(0, "Counter.sol".to_owned());
520 let mut variable_locations = BTreeMap::new();
521 variable_locations.insert(2, vec![variable]);
522 let info = EthdebugInfo {
523 compilation: json!({}),
524 contract_name: "Counter".to_owned(),
525 environment: "runtime".to_owned(),
526 instructions: vec![instruction],
527 sources,
528 variable_locations,
529 };
530
531 assert_eq!(info.source_info(3), Some(("Counter.sol", 9, 5)));
532 assert_eq!(info.variables_at_pc(3)[0].name, "stored");
533 assert!(info.variables_at_pc(9).is_empty());
534 }
535
536 #[test]
537 fn parses_variable_locations_from_contract_data() {
538 let variables = parse_variable_locations(&json!({
539 "instructions": [
540 {
541 "offset": 10,
542 "context": {
543 "variables": [
544 {
545 "name": "amount",
546 "type": "uint256",
547 "location": {"type": "stack", "offset": 1},
548 "scope": {"start": 10, "end": 12}
549 }
550 ]
551 }
552 }
553 ],
554 "variables": [
555 {
556 "name": "stored",
557 "type": "uint256",
558 "location_type": "storage",
559 "offset": 0,
560 "pc_start": 20,
561 "pc_end": 21
562 }
563 ]
564 }))
565 .expect("variables");
566
567 assert_eq!(variables.get(&10).expect("pc 10")[0].name, "amount");
568 assert_eq!(
569 variables.get(&20).expect("pc 20")[0].location_type,
570 "storage"
571 );
572 assert_eq!(variables.get(&21).expect("pc 21")[0].name, "stored");
573 }
574}