1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19
20use soldb_core::{encode_hex, Bytes, EvmWord, StepSnapshot, StepView, TransactionTrace};
21use soldb_ethdebug::{decode_value, parse_word, EthdebugInfo, VariableLocation, Word};
22
23use crate::decode::ValueReader;
24use crate::stepping::calldata_for_step;
25
26pub mod condition;
27pub mod debug_diff;
28mod decode;
29mod locals;
30mod roots;
31pub mod state;
32pub mod stepping;
33mod types;
34
35pub use condition::{Condition, ConditionContext, Evaluation};
36pub use debug_diff::{
37 capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
38 DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
39 DebugTraceSummary,
40};
41pub use roots::{check_root_calls, root_calls, RootCall, RootRecord};
42pub use soldb_ethdebug::{CodeGenerator, StorageLayout};
43pub use state::{
44 short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
45 StateVariable, StorageTape, StorageWords,
46};
47pub use stepping::{
48 address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
49 Frame, FunctionId, InferredVariable, JumpMarker, LineKey, LocalsStatus, ResolvedFunction,
50 ResolvedLine, SourceCache, SourceListing, StepLocation, StepMap, VariableKind,
51 INFERRED_LOCALS_NOTE, INFERRED_LOCALS_WARNING,
52};
53pub use types::{SourceTypes, StructMember};
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct DebugSession {
57 pub trace: TransactionTrace,
58 pub ethdebug: Option<EthdebugInfo>,
59 #[serde(default)]
60 pub source_contents: BTreeMap<u64, String>,
61 #[serde(default)]
62 pub functions: Vec<SourceFunction>,
63}
64
65impl DebugSession {
66 #[must_use]
67 pub fn new(trace: TransactionTrace) -> Self {
68 Self {
69 trace,
70 ethdebug: None,
71 source_contents: BTreeMap::new(),
72 functions: Vec::new(),
73 }
74 }
75
76 #[must_use]
77 pub fn with_ethdebug(
78 trace: TransactionTrace,
79 ethdebug: EthdebugInfo,
80 source_contents: BTreeMap<u64, String>,
81 ) -> Self {
82 let mut session = Self::new(trace);
83 session.attach_ethdebug(ethdebug, source_contents);
84 session
85 }
86
87 pub fn attach_ethdebug(
94 &mut self,
95 ethdebug: EthdebugInfo,
96 source_contents: BTreeMap<u64, String>,
97 ) {
98 self.functions = source_contents
99 .iter()
100 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
101 .collect();
102 self.ethdebug = Some(ethdebug);
103 self.source_contents = source_contents;
104 }
105
106 #[must_use]
107 pub fn step(&self, step_index: usize) -> Option<DebugStep> {
108 let step = self.trace.steps.get(step_index)?;
109 Some(DebugStep {
110 index: step_index,
111 pc: step.pc(),
112 op: step.mnemonic().to_owned(),
113 gas: step.gas(),
114 gas_cost: step.gas_cost(),
115 depth: step.depth(),
116 source: self.source_span(step.pc()),
117 function: self.function_at_pc(step.pc()).cloned(),
118 snapshot: step.to_trace_step().snapshot,
119 variables: self.variables_at_step(step_index),
120 })
121 }
122
123 #[must_use]
124 pub fn steps(&self) -> Vec<DebugStep> {
125 (0..self.trace.steps.len())
126 .filter_map(|step_index| self.step(step_index))
127 .collect()
128 }
129
130 #[must_use]
131 pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
132 let ethdebug = self.ethdebug.as_ref()?;
133 let instruction = ethdebug.instruction_at_pc(pc)?;
134 let location = instruction.source_location()?;
135 let path = ethdebug.sources.get(&location.source_id)?.clone();
136 let position = self
137 .source_contents
138 .get(&location.source_id)
139 .map(|source| line_column_for_offset(source, location.offset as usize));
140 Some(SourceSpan {
141 source_id: location.source_id,
142 path,
143 offset: location.offset,
144 length: location.length,
145 line: position.map_or(0, |position| position.line),
146 column: position.map_or(0, |position| position.column),
147 })
148 }
149
150 #[must_use]
151 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
152 let location = self
153 .ethdebug
154 .as_ref()?
155 .instruction_at_pc(pc)?
156 .source_location()?;
157 self.functions
158 .iter()
159 .filter(|function| {
160 function.source_id == location.source_id
161 && function.declaration_start <= location.offset
162 && location.offset <= function.body_end
163 })
164 .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
165 }
166
167 #[must_use]
168 pub fn variables_at_step(&self, step_index: usize) -> Vec<DebugVariable> {
169 let Some(ethdebug) = &self.ethdebug else {
170 return Vec::new();
171 };
172 variables_for_step(&self.trace, ethdebug, step_index)
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct DebugStep {
178 pub index: usize,
179 pub pc: u64,
180 pub op: String,
181 pub gas: u64,
182 pub gas_cost: u64,
183 pub depth: u64,
184 pub source: Option<SourceSpan>,
185 pub function: Option<SourceFunction>,
186 pub snapshot: StepSnapshot,
187 pub variables: Vec<DebugVariable>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct SourceSpan {
192 pub source_id: u64,
193 pub path: String,
194 pub offset: u64,
195 pub length: u64,
196 pub line: u64,
197 pub column: u64,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct SourceFunction {
202 pub source_id: u64,
203 pub name: String,
206 pub params: Vec<SourceParam>,
207 pub declaration_start: u64,
208 #[serde(default)]
210 pub declaration_line: u64,
211 #[serde(default)]
213 pub body_start: u64,
214 pub body_end: u64,
215 #[serde(default)]
217 pub returns: Vec<SourceLocal>,
218 #[serde(default)]
220 pub locals: Vec<SourceLocal>,
221 #[serde(default)]
224 pub has_modifiers: bool,
225 #[serde(default)]
227 pub modifiers: Vec<String>,
228 #[serde(default)]
230 pub placeholder: Option<u64>,
231}
232
233#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
235pub struct ByteRange {
236 pub start: u64,
237 pub end: u64,
238}
239
240impl ByteRange {
241 #[must_use]
242 pub const fn contains(self, offset: u64) -> bool {
243 self.start <= offset && offset < self.end
244 }
245
246 #[must_use]
248 pub const fn covers(self, offset: u64, length: u64) -> bool {
249 self.start <= offset && offset.saturating_add(length) <= self.end
250 }
251
252 #[must_use]
254 pub const fn encloses(self, other: Self) -> bool {
255 self.start <= other.start && other.end <= self.end
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265pub struct SourceLocal {
266 pub name: String,
267 pub ty: String,
269 #[serde(default)]
271 pub location: Option<String>,
272 pub declaration: ByteRange,
273 pub statement: ByteRange,
275 pub position: usize,
277 pub scope: ByteRange,
278}
279
280impl SourceLocal {
281 #[must_use]
283 pub fn declared_type(&self) -> String {
284 match &self.location {
285 Some(location) => format!("{} {location}", self.ty),
286 None => self.ty.clone(),
287 }
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub struct SourceParam {
293 pub name: String,
294 pub ty: String,
296 #[serde(default)]
299 pub location: Option<String>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct FrameArgument {
305 pub name: String,
306 pub ty: String,
307 pub value: DebugValue,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct DebugVariable {
312 pub name: String,
313 pub ty: String,
314 pub location: DebugLocation,
315 pub value: DebugValue,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
319pub struct DebugLocation {
320 pub kind: String,
321 pub offset: u64,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct DebugValue {
326 pub display: String,
327 pub raw: Option<String>,
328 pub status: DebugValueStatus,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
332#[serde(rename_all = "snake_case")]
333pub enum DebugValueStatus {
334 Decoded,
335 Raw,
336 Unavailable,
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340struct SourcePosition {
341 line: u64,
342 column: u64,
343}
344
345#[must_use]
346pub fn variables_for_step(
347 trace: &TransactionTrace,
348 ethdebug: &EthdebugInfo,
349 step_index: usize,
350) -> Vec<DebugVariable> {
351 let Some(step) = trace.steps.get(step_index) else {
352 return Vec::new();
353 };
354 let variables = ethdebug.variables_at_pc(step.pc());
355 if variables.is_empty() {
356 return Vec::new();
357 }
358 let roots = root_calls(trace);
360 let calldata = calldata_for_step(trace, &roots, step_index);
361 variables
362 .into_iter()
363 .map(|variable| decode_variable(step, calldata, variable))
364 .collect()
365}
366
367fn decode_variable(
368 step: StepView<'_>,
369 calldata: &str,
370 variable: &VariableLocation,
371) -> DebugVariable {
372 let raw = raw_value_for_location(step, calldata, variable);
373 let value = raw.map_or_else(
374 || DebugValue {
375 display: "<unavailable>".to_owned(),
376 raw: None,
377 status: DebugValueStatus::Unavailable,
378 },
379 |raw| decode_debug_value(&raw, &variable.ty),
380 );
381 DebugVariable {
382 name: variable.name.clone(),
383 ty: variable.ty.clone(),
384 location: DebugLocation {
385 kind: variable.location_type.clone(),
386 offset: variable.offset,
387 },
388 value,
389 }
390}
391
392fn raw_value_for_location(
393 step: StepView<'_>,
394 calldata: &str,
395 variable: &VariableLocation,
396) -> Option<String> {
397 match variable.location_type.as_str() {
398 "stack" => step
399 .stack()
400 .get(usize::try_from(variable.offset).ok()?)
401 .map(|value| normalize_hex(&value.to_string())),
402 "memory" => memory_word(&step.memory()?, usize::try_from(variable.offset).ok()?)
403 .map(|word| format!("0x{}", encode_hex(&word))),
404 "calldata" => word_from_hex_bytes(calldata, variable.offset as usize),
405 "storage" => storage_value(step.storage(), variable.offset),
406 _ => None,
407 }
408}
409
410pub(crate) fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
411 let ty = ty.trim();
412 let normalized = normalize_hex(raw);
413 let word = normalized.trim_start_matches("0x");
414 let decoded = decode_static_word(word, ty);
415 match decoded {
416 Some(display) => DebugValue {
417 display,
418 raw: Some(normalized),
419 status: DebugValueStatus::Decoded,
420 },
421 None => DebugValue {
422 display: normalized.clone(),
423 raw: Some(normalized),
424 status: DebugValueStatus::Raw,
425 },
426 }
427}
428
429pub(crate) fn decode_static_word(word: &str, ty: &str) -> Option<String> {
430 if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
431 return None;
432 }
433 if ty.starts_with("uint") {
434 return Some(format_uint_word(word));
435 }
436 if ty.starts_with("int") {
437 return Some(format_int_word(word));
438 }
439 if ty == "address" {
440 let padded = left_pad_word(word);
441 return Some(
442 format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
443 );
444 }
445 if ty == "bool" {
446 let trimmed = word.trim_start_matches('0');
447 return match trimmed {
448 "" | "0" => Some("false".to_owned()),
449 "1" => Some("true".to_owned()),
450 _ => None,
451 };
452 }
453 if ty == "bytes32" {
454 return Some(normalize_hex(word));
455 }
456 if let Some(size) = fixed_bytes_size(ty) {
457 let padded = right_pad_word(word);
458 return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
459 }
460 None
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum ArgumentOrder {
468 FirstOnTop,
471 LastOnTop,
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478pub enum ArgumentLayout {
479 Ordered(ArgumentOrder),
481 TopWords,
484}
485
486impl ArgumentOrder {
487 #[must_use]
490 fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
491 match self {
492 Self::FirstOnTop => stack_len - 1 - index,
493 Self::LastOnTop => stack_len - count + index,
494 }
495 }
496}
497
498#[derive(Debug, Clone, Default)]
501pub struct FrameState<'a> {
502 pub stack: &'a [EvmWord],
504 pub memory: Option<Bytes>,
506}
507
508#[must_use]
517pub fn decode_arguments(
518 params: &[SourceParam],
519 state: FrameState<'_>,
520 layout: ArgumentLayout,
521 types: &SourceTypes,
522 scope: Option<&str>,
523) -> Vec<FrameArgument> {
524 let stack = state.stack;
525 if params.is_empty() || stack.len() < params.len() {
526 return Vec::new();
527 }
528 if !params.iter().all(readable_parameter) {
529 return Vec::new();
530 }
531 let order = match layout {
533 ArgumentLayout::Ordered(order) => order,
534 ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
535 ArgumentLayout::TopWords => return Vec::new(),
536 };
537 params
538 .iter()
539 .enumerate()
540 .map(|(index, param)| {
541 let word = stack[order.word_index(index, params.len(), stack.len())].to_string();
542 FrameArgument {
543 name: param.name.clone(),
544 ty: param.ty.clone(),
545 value: argument_value(param, &word, state.memory.clone(), types, scope),
546 }
547 })
548 .collect()
549}
550
551fn argument_value(
552 param: &SourceParam,
553 word: &str,
554 memory: Option<Bytes>,
555 types: &SourceTypes,
556 scope: Option<&str>,
557) -> DebugValue {
558 let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
559 return DebugValue {
560 display: "<unreadable stack word>".to_owned(),
561 raw: Some(word.to_owned()),
562 status: DebugValueStatus::Unavailable,
563 };
564 };
565 let reader = ValueReader {
566 memory,
567 calldata: "",
568 storage: None,
569 layout: None,
570 types,
571 scope,
572 };
573 let raw = Some(short_hex(&parsed));
574 match param.location.as_deref() {
575 Some("memory") => reader.variable(&format!("{} memory", param.ty), &[word]),
576 Some(location) => DebugValue {
577 display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
578 raw,
579 status: DebugValueStatus::Raw,
580 },
581 None => DebugValue {
582 display: decode_value(value_bytes(&parsed, ¶m.ty), ¶m.ty),
583 raw,
584 status: DebugValueStatus::Decoded,
585 },
586 }
587}
588
589pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
591 match param.location.as_deref() {
592 Some("calldata") => !is_dynamic_type(¶m.ty),
594 Some(_) => true,
595 None => is_value_type(¶m.ty),
596 }
597}
598
599fn is_dynamic_type(ty: &str) -> bool {
601 ty == "string" || ty == "bytes" || ty.ends_with("[]")
602}
603
604pub(crate) fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
607 let inner = ty.strip_suffix(']')?;
608 let open = inner.rfind('[')?;
609 let element = inner[..open].trim();
610 let length = &inner[open + 1..];
611 if length.is_empty() {
612 return Some((element, None));
613 }
614 Some((element, Some(length.parse::<usize>().ok()?)))
615}
616
617pub(crate) fn memory_word(memory: &[u8], offset: usize) -> Option<Word> {
619 memory_bytes(memory, offset, 32)?.try_into().ok()
620}
621
622pub(crate) fn memory_bytes(memory: &[u8], offset: usize, length: usize) -> Option<&[u8]> {
624 memory.get(offset..offset.checked_add(length)?)
625}
626
627pub(crate) fn hex_bytes(hex: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
629 let start = offset.checked_mul(2)?;
630 let end = start.checked_add(length.checked_mul(2)?)?;
631 let digits = hex.get(start..end)?;
632 if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
633 return None;
634 }
635 Some(
636 digits
637 .as_bytes()
638 .chunks(2)
639 .map(|pair| {
640 u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
641 })
642 .collect(),
643 )
644}
645
646pub(crate) fn word_as_usize(word: &Word) -> Option<usize> {
648 if word[..24].iter().any(|byte| *byte != 0) {
649 return None;
650 }
651 usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
652}
653
654pub(crate) fn hex_of(bytes: &[u8]) -> String {
655 encode_hex(bytes)
656}
657
658pub(crate) fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
662 if let Some(bits) = integer_bits(ty, "int") {
663 return &word[32 - bits / 8..];
664 }
665 if let Some(size) = fixed_bytes_size(ty) {
666 return &word[..size];
667 }
668 word
669}
670
671#[must_use]
673pub fn is_value_type(ty: &str) -> bool {
674 matches!(ty, "bool" | "address" | "address payable")
675 || fixed_bytes_size(ty).is_some()
676 || integer_bits(ty, "uint").is_some()
677 || integer_bits(ty, "int").is_some()
678}
679
680fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
682 let rest = ty.strip_prefix(prefix)?;
683 if rest.is_empty() {
685 return Some(256);
686 }
687 let bits = rest.parse::<usize>().ok()?;
688 (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
689}
690
691fn fixed_bytes_size(ty: &str) -> Option<usize> {
692 let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
693 (1..=32).contains(&size).then_some(size)
694}
695
696fn format_uint_word(word: &str) -> String {
697 let trimmed = word.trim_start_matches('0');
698 if trimmed.is_empty() {
699 return "0".to_owned();
700 }
701 if trimmed.len() <= 32 {
702 return u128::from_str_radix(trimmed, 16)
703 .map(|value| value.to_string())
704 .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
705 }
706 format!("0x{}", trimmed.to_ascii_lowercase())
707}
708
709fn format_int_word(word: &str) -> String {
711 let padded = left_pad_word(word);
712 let Ok(bytes) = (0..32)
713 .map(|index| u8::from_str_radix(&padded[index * 2..index * 2 + 2], 16))
714 .collect::<Result<Vec<_>, _>>()
715 else {
716 return format!("0x{}", word.to_ascii_lowercase());
717 };
718 if bytes[0] & 0x80 == 0 {
719 return format_uint_word(word);
720 }
721 let mut magnitude = bytes.iter().map(|byte| !byte).collect::<Vec<_>>();
723 for byte in magnitude.iter_mut().rev() {
724 let (sum, carry) = byte.overflowing_add(1);
725 *byte = sum;
726 if !carry {
727 break;
728 }
729 }
730 let hex = magnitude
731 .iter()
732 .map(|byte| format!("{byte:02x}"))
733 .collect::<String>();
734 let trimmed = hex.trim_start_matches('0');
735 if trimmed.len() <= 32 {
736 if let Ok(value) = u128::from_str_radix(trimmed, 16) {
737 return format!("-{value}");
738 }
739 }
740 format!("-0x{trimmed}")
741}
742
743fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
744 storage
745 .get(&format!("0x{slot:x}"))
746 .or_else(|| storage.get(&format!("{slot:x}")))
747 .or_else(|| storage.get(&format!("0x{slot:064x}")))
748 .or_else(|| storage.get(&format!("{slot:064x}")))
749 .map(|value| normalize_hex(value))
750}
751
752fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
753 let hex = input.trim_start_matches("0x");
754 let start = byte_offset.checked_mul(2)?;
755 let end = start.checked_add(64)?;
756 let word = hex.get(start..end)?;
757 word.bytes()
758 .all(|byte| byte.is_ascii_hexdigit())
759 .then(|| format!("0x{}", word.to_ascii_lowercase()))
760}
761
762fn normalize_hex(value: &str) -> String {
763 let value = value.trim();
764 let hex = value.strip_prefix("0x").unwrap_or(value);
765 format!("0x{}", hex.to_ascii_lowercase())
766}
767
768fn left_pad_word(word: &str) -> String {
769 let word = word.trim_start_matches("0x").to_ascii_lowercase();
770 if word.len() >= 64 {
771 return word;
772 }
773 format!("{:0>64}", word)
774}
775
776fn right_pad_word(word: &str) -> String {
777 let word = word.trim_start_matches("0x").to_ascii_lowercase();
778 if word.len() >= 64 {
779 return word;
780 }
781 format!("{:0<64}", word)
782}
783
784fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
785 let mut line = 1;
786 let mut column = 1;
787 for byte in source.bytes().take(offset.min(source.len())) {
788 if byte == b'\n' {
789 line += 1;
790 column = 1;
791 } else {
792 column += 1;
793 }
794 }
795 SourcePosition { line, column }
796}
797
798const DECLARATION_KEYWORDS: [&str; 5] =
800 ["function", "modifier", "constructor", "fallback", "receive"];
801
802#[must_use]
806pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
807 let mut functions = Vec::new();
808 let found = keyword_positions(source, DECLARATION_KEYWORDS);
809 for (keyword, positions) in DECLARATION_KEYWORDS.into_iter().zip(&found) {
810 collect_declarations(source_id, source, keyword, positions, &mut functions);
811 }
812 functions.sort_by_key(|function| function.declaration_start);
813 functions
814}
815
816fn keyword_positions<const N: usize>(source: &str, keywords: [&str; N]) -> [Vec<usize>; N] {
820 let mut found = std::array::from_fn(|_| Vec::new());
821 let bytes = source.as_bytes();
822 let mut index = 0;
823 while index < bytes.len() {
824 if !is_identifier_byte(bytes[index]) {
825 index += 1;
826 continue;
827 }
828 let start = index;
829 while bytes
830 .get(index)
831 .is_some_and(|byte| is_identifier_byte(*byte))
832 {
833 index += 1;
834 }
835 let word = &bytes[start..index];
836 for (keyword, positions) in keywords.iter().zip(found.iter_mut()) {
837 if word == keyword.as_bytes() {
838 positions.push(start);
839 }
840 }
841 }
842 found
843}
844
845fn collect_declarations(
846 source_id: u64,
847 source: &str,
848 keyword: &str,
849 positions: &[usize],
850 functions: &mut Vec<SourceFunction>,
851) {
852 let named = matches!(keyword, "function" | "modifier");
853 let mut cursor = 0;
854 let mut next = 0;
855 let mut find = |cursor: usize| {
857 while positions.get(next).is_some_and(|at| *at < cursor) {
858 next += 1;
859 }
860 positions.get(next).copied()
861 };
862 let mut counted = (0, 1);
864 let mut line_of = |offset: usize| {
865 let (from, line) = if offset < counted.0 { (0, 1) } else { counted };
866 let newlines = source.as_bytes()[from..offset]
867 .iter()
868 .filter(|byte| **byte == b'\n')
869 .count() as u64;
870 counted = (offset, line + newlines);
871 line + newlines
872 };
873 while let Some(keyword_start) = find(cursor) {
874 let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
875 let name = if named {
876 let Some((name, name_end)) = parse_identifier(source, index) else {
877 cursor = index;
878 continue;
879 };
880 index = skip_ascii_whitespace(source, name_end);
881 name
882 } else {
883 keyword
884 };
885 if source.as_bytes().get(index) != Some(&b'(') {
886 cursor = index;
887 continue;
888 }
889 let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
890 cursor = index + 1;
891 continue;
892 };
893 let params = parse_source_params(&source[index + 1..params_end]);
894 let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
895 cursor = params_end + 1;
896 continue;
897 };
898 if source[params_end + 1..body_start].contains(';') {
901 cursor = params_end + 1;
902 continue;
903 }
904 let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
905 cursor = body_start + 1;
906 continue;
907 };
908 let (returns, modifiers) = parse_header(source, params_end + 1, body_start);
909 let modifiers = if keyword == "modifier" {
910 Vec::new()
911 } else {
912 modifiers
913 };
914 let returns = returns
915 .into_iter()
916 .map(|variable| SourceLocal {
917 name: variable.name,
918 ty: variable.ty,
919 location: variable.location,
920 declaration: variable.span,
921 statement: variable.span,
922 position: 0,
923 scope: ByteRange {
924 start: body_start as u64,
925 end: body_end as u64,
926 },
927 })
928 .collect();
929
930 functions.push(SourceFunction {
931 source_id,
932 name: name.to_owned(),
933 params,
934 declaration_start: keyword_start as u64,
935 declaration_line: line_of(keyword_start),
936 body_start: body_start as u64,
937 body_end: body_end as u64,
938 returns,
939 locals: locals::scan_locals(source, body_start, body_end),
940 has_modifiers: !modifiers.is_empty(),
941 modifiers,
942 placeholder: (keyword == "modifier")
943 .then(|| locals::find_placeholder(source, body_start, body_end))
944 .flatten(),
945 });
946 cursor = body_end + 1;
947 }
948}
949
950const HEADER_KEYWORDS: [&str; 11] = [
953 "public", "private", "internal", "external", "pure", "view", "payable", "virtual", "override",
954 "returns", "constant",
955];
956
957fn parse_header(source: &str, start: usize, end: usize) -> (Vec<locals::Declared>, Vec<String>) {
964 let bytes = source.as_bytes();
965 let mut returns = Vec::new();
966 let mut modifiers = Vec::new();
967 let mut index = start;
968 while index < end {
969 index = locals::skip_trivia(source, index);
970 if index >= end {
971 break;
972 }
973 let Some((word, word_end)) = parse_identifier(source, index) else {
974 index += 1;
975 continue;
976 };
977 let after = locals::skip_trivia(source, word_end);
978 let arguments = (bytes.get(after) == Some(&b'('))
979 .then(|| find_matching_delimiter(source, after, b'(', b')'))
980 .flatten()
981 .filter(|close| *close < end);
982 if word == "returns" {
983 if let Some(close) = arguments {
984 returns = parse_return_list(source, after, close);
985 }
986 } else if !HEADER_KEYWORDS.contains(&word) {
987 modifiers.push(word.to_owned());
988 }
989 index = arguments.map_or(word_end, |close| close + 1);
990 }
991 (returns, modifiers)
992}
993
994fn parse_return_list(source: &str, open: usize, close: usize) -> Vec<locals::Declared> {
996 let mut returns = Vec::new();
997 let mut start = open + 1;
998 let mut depth = 0_i32;
999 for index in open + 1..=close {
1000 match source.as_bytes()[index] {
1001 b'(' | b'[' => depth += 1,
1002 b')' | b']' if depth > 0 => depth -= 1,
1003 b',' | b')' => {
1004 let component = source[start..index].trim();
1005 if !component.is_empty() {
1006 let component_start = start
1007 + (source[start..index].len() - source[start..index].trim_start().len());
1008 let component_start = locals::skip_trivia(source, component_start);
1009 let component_end = component_start + component.len();
1010 if let Some(declared) =
1011 locals::parse_return(source, component_start, component_end, returns.len())
1012 {
1013 returns.push(declared);
1014 }
1015 }
1016 start = index + 1;
1017 }
1018 _ => {}
1019 }
1020 }
1021 returns
1022}
1023
1024const PARAM_KEYWORDS: [&str; 4] = ["memory", "calldata", "storage", "payable"];
1026
1027fn parse_source_params(params: &str) -> Vec<SourceParam> {
1028 let params = strip_param_comments(params);
1029 split_top_level_commas(¶ms)
1030 .into_iter()
1031 .enumerate()
1032 .filter_map(|(index, param)| {
1033 let param = param.trim();
1034 if param.is_empty() {
1035 return None;
1036 }
1037 let mut tokens = param.split_whitespace().collect::<Vec<_>>();
1038 let trailing = tokens.last().copied();
1044 let name = match trailing {
1045 Some(token)
1046 if tokens.len() > 1
1047 && is_identifier(token)
1048 && !PARAM_KEYWORDS.contains(&token) =>
1049 {
1050 tokens.pop();
1051 token.to_owned()
1052 }
1053 _ => format!("arg{index}"),
1054 };
1055 let location = tokens
1056 .iter()
1057 .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
1058 .map(|token| (*token).to_owned());
1059 let ty = tokens
1060 .into_iter()
1061 .filter(|token| !PARAM_KEYWORDS.contains(token))
1062 .collect::<Vec<_>>()
1063 .join(" ");
1064 (!ty.is_empty()).then_some(SourceParam { name, ty, location })
1065 })
1066 .collect()
1067}
1068
1069fn strip_param_comments(params: &str) -> String {
1087 let mut output = String::with_capacity(params.len());
1088 let mut chars = params.chars().peekable();
1089 while let Some(character) = chars.next() {
1090 if character == '/' {
1091 match chars.peek() {
1092 Some('/') => {
1093 for next in chars.by_ref() {
1096 if next == '\n' {
1097 output.push('\n');
1098 break;
1099 }
1100 }
1101 continue;
1102 }
1103 Some('*') => {
1104 chars.next();
1105 let mut previous = None;
1106 for next in chars.by_ref() {
1107 if previous == Some('*') && next == '/' {
1108 break;
1109 }
1110 previous = Some(next);
1111 }
1112 output.push(' ');
1115 continue;
1116 }
1117 _ => {}
1118 }
1119 }
1120 output.push(character);
1121 }
1122 output
1123}
1124
1125fn split_top_level_commas(input: &str) -> Vec<&str> {
1126 let mut parts = Vec::new();
1127 let mut start = 0;
1128 let mut depth = 0_i32;
1129 for (index, byte) in input.bytes().enumerate() {
1130 match byte {
1131 b'(' | b'[' => depth += 1,
1132 b')' | b']' => depth -= 1,
1133 b',' if depth == 0 => {
1134 parts.push(&input[start..index]);
1135 start = index + 1;
1136 }
1137 _ => {}
1138 }
1139 }
1140 parts.push(&input[start..]);
1141 parts
1142}
1143
1144pub(crate) fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
1145 let mut cursor = start;
1146 while let Some(relative) = source[cursor..].find(keyword) {
1147 let absolute = cursor + relative;
1148 let before = absolute
1149 .checked_sub(1)
1150 .and_then(|index| source.as_bytes().get(index))
1151 .copied();
1152 let after = source.as_bytes().get(absolute + keyword.len()).copied();
1153 if before.is_none_or(|byte| !is_identifier_byte(byte))
1154 && after.is_none_or(|byte| !is_identifier_byte(byte))
1155 {
1156 return Some(absolute);
1157 }
1158 cursor = absolute + keyword.len();
1159 }
1160 None
1161}
1162
1163pub(crate) fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
1164 let bytes = source.as_bytes();
1165 let first = *bytes.get(start)?;
1166 if !is_identifier_start_byte(first) {
1167 return None;
1168 }
1169 let mut end = start + 1;
1170 while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
1171 end += 1;
1172 }
1173 Some((&source[start..end], end))
1174}
1175
1176pub(crate) fn is_identifier(input: &str) -> bool {
1177 let mut bytes = input.bytes();
1178 let Some(first) = bytes.next() else {
1179 return false;
1180 };
1181 is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
1182}
1183
1184fn is_identifier_start_byte(byte: u8) -> bool {
1185 byte == b'_' || byte.is_ascii_alphabetic()
1186}
1187
1188pub(crate) fn is_identifier_byte(byte: u8) -> bool {
1189 is_identifier_start_byte(byte) || byte.is_ascii_digit()
1190}
1191
1192pub(crate) fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
1193 while source
1194 .as_bytes()
1195 .get(index)
1196 .is_some_and(u8::is_ascii_whitespace)
1197 {
1198 index += 1;
1199 }
1200 index
1201}
1202
1203fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
1204 source
1205 .as_bytes()
1206 .iter()
1207 .enumerate()
1208 .skip(start)
1209 .find_map(|(index, byte)| (*byte == needle).then_some(index))
1210}
1211
1212pub(crate) fn find_matching_delimiter(
1213 source: &str,
1214 open_index: usize,
1215 open: u8,
1216 close: u8,
1217) -> Option<usize> {
1218 let mut depth = 0_i32;
1219 for (index, byte) in source.bytes().enumerate().skip(open_index) {
1220 if byte == open {
1221 depth += 1;
1222 } else if byte == close {
1223 depth -= 1;
1224 if depth == 0 {
1225 return Some(index);
1226 }
1227 }
1228 }
1229 None
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use std::collections::BTreeMap;
1235
1236 use serde_json::json;
1237 use soldb_core::{ExecutionCall, TraceStep, TransactionTrace};
1238 use soldb_ethdebug::{source_range, EthdebugInfo, Instruction, VariableLocation};
1239
1240 use super::{parse_source_functions, DebugSession, DebugValueStatus};
1241
1242 #[test]
1243 fn builds_source_steps_and_decodes_active_variables() {
1244 let source =
1245 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1246 let offset = source.find("value = x").expect("source offset") as u64;
1247 let trace = sample_trace();
1248 let mut sources = BTreeMap::new();
1249 sources.insert(0, "Counter.sol".to_owned());
1250 let mut source_contents = BTreeMap::new();
1251 source_contents.insert(0, source.to_owned());
1252 let mut variable_locations = BTreeMap::new();
1253 variable_locations.insert(
1254 0,
1255 vec![
1256 VariableLocation {
1257 name: "x".to_owned(),
1258 ty: "uint256".to_owned(),
1259 location_type: "stack".to_owned(),
1260 offset: 0,
1261 pc_range: (0, 1),
1262 },
1263 VariableLocation {
1264 name: "sender".to_owned(),
1265 ty: "address".to_owned(),
1266 location_type: "calldata".to_owned(),
1267 offset: 4,
1268 pc_range: (0, 1),
1269 },
1270 VariableLocation {
1271 name: "tail".to_owned(),
1272 ty: "bytes32".to_owned(),
1273 location_type: "memory".to_owned(),
1274 offset: 32,
1275 pc_range: (0, 1),
1276 },
1277 VariableLocation {
1278 name: "straddle".to_owned(),
1279 ty: "uint256".to_owned(),
1280 location_type: "memory".to_owned(),
1281 offset: 48,
1282 pc_range: (0, 1),
1283 },
1284 ],
1285 );
1286 let info = EthdebugInfo {
1287 compilation: json!({}),
1288 contract_name: "Counter".to_owned(),
1289 environment: "runtime".to_owned(),
1290 instructions: vec![Instruction::new(
1291 0,
1292 "PUSH1",
1293 Some(json!({"code": source_range(0, offset, 9)})),
1294 )],
1295 sources,
1296 variable_locations,
1297 };
1298
1299 let session = DebugSession::with_ethdebug(trace, info, source_contents);
1300 let step = session.step(0).expect("debug step");
1301 assert_eq!(step.source.as_ref().expect("source").line, 3);
1302 assert_eq!(step.function.as_ref().expect("function").name, "set");
1303 assert_eq!(
1304 step.snapshot
1305 .stack
1306 .iter()
1307 .map(|word| &**word)
1308 .collect::<Vec<_>>(),
1309 ["0x2a"]
1310 );
1311 assert_eq!(step.variables[0].name, "x");
1312 assert_eq!(step.variables[0].value.display, "42");
1313 assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1314 assert_eq!(
1315 step.variables[1].value.display,
1316 "0x1111111111111111111111111111111111111111"
1317 );
1318 assert_eq!(
1320 step.variables[2].value.display,
1321 format!("0x{}", "2a".repeat(32))
1322 );
1323 assert_eq!(step.variables[3].value.display, "<unavailable>");
1325 assert_eq!(
1326 step.variables[3].value.status,
1327 DebugValueStatus::Unavailable
1328 );
1329 }
1330
1331 #[test]
1332 fn calldata_variables_read_the_input_of_their_top_level_call() {
1333 let root = |id: usize, entry: usize, input: String| ExecutionCall {
1334 id,
1335 parent_id: None,
1336 depth: 1,
1337 entry_step: Some(entry),
1338 exit_step: Some(entry + 1),
1339 call_type: "CALL".to_owned(),
1340 from: "0x1".to_owned(),
1341 to: "0x2".to_owned(),
1342 bytecode_address: "0x2".to_owned(),
1343 value: "0x0".to_owned(),
1344 input,
1345 gas_limit: 0,
1346 gas_used: None,
1347 output: None,
1348 success: Some(true),
1349 error: None,
1350 code_hash: None,
1351 };
1352 let mut trace = sample_trace();
1353 trace.steps.push(trace.steps.step(0).to_trace_step());
1354 let second_input = format!(
1355 "0x12345678{:0>64}",
1356 "2222222222222222222222222222222222222222"
1357 );
1358 trace.artifacts.calls = vec![
1359 root(0, 0, trace.input_data.clone()),
1360 root(1, 1, second_input),
1361 ];
1362 let info = EthdebugInfo {
1363 compilation: json!({}),
1364 contract_name: "Counter".to_owned(),
1365 environment: "runtime".to_owned(),
1366 instructions: Vec::new(),
1367 sources: BTreeMap::new(),
1368 variable_locations: BTreeMap::from([(
1369 0,
1370 vec![VariableLocation {
1371 name: "sender".to_owned(),
1372 ty: "address".to_owned(),
1373 location_type: "calldata".to_owned(),
1374 offset: 4,
1375 pc_range: (0, 1),
1376 }],
1377 )]),
1378 };
1379 let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1380 let sender = |step: usize| {
1381 session.step(step).expect("debug step").variables[0]
1382 .value
1383 .display
1384 .clone()
1385 };
1386 assert_eq!(sender(0), "0x1111111111111111111111111111111111111111");
1387 assert_eq!(sender(1), "0x2222222222222222222222222222222222222222");
1388 }
1389
1390 #[test]
1391 fn keeps_raw_values_when_type_is_dynamic_or_location_is_unknown() {
1392 let trace = sample_trace();
1393 let mut variable_locations = BTreeMap::new();
1394 variable_locations.insert(
1395 0,
1396 vec![
1397 VariableLocation {
1398 name: "name".to_owned(),
1399 ty: "string".to_owned(),
1400 location_type: "stack".to_owned(),
1401 offset: 0,
1402 pc_range: (0, 0),
1403 },
1404 VariableLocation {
1405 name: "missing".to_owned(),
1406 ty: "uint256".to_owned(),
1407 location_type: "stack".to_owned(),
1408 offset: 99,
1409 pc_range: (0, 0),
1410 },
1411 ],
1412 );
1413 let info = EthdebugInfo {
1414 compilation: json!({}),
1415 contract_name: "Counter".to_owned(),
1416 environment: "runtime".to_owned(),
1417 instructions: Vec::new(),
1418 sources: BTreeMap::new(),
1419 variable_locations,
1420 };
1421
1422 let session = DebugSession::with_ethdebug(trace, info, BTreeMap::new());
1423 let step = session.step(0).expect("debug step");
1424 assert_eq!(step.variables[0].value.status, DebugValueStatus::Raw);
1425 assert_eq!(step.variables[0].value.display, "0x2a");
1426 assert_eq!(
1427 step.variables[1].value.status,
1428 DebugValueStatus::Unavailable
1429 );
1430 }
1431
1432 #[test]
1433 fn attaching_ethdebug_matches_constructing_with_it_and_replaces_prior_info() {
1434 let source =
1435 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1436 let offset = source.find("value = x").expect("source offset") as u64;
1437 let info = EthdebugInfo {
1438 compilation: json!({}),
1439 contract_name: "Counter".to_owned(),
1440 environment: "runtime".to_owned(),
1441 instructions: vec![Instruction::new(
1442 0,
1443 "PUSH1",
1444 Some(json!({"code": source_range(0, offset, 9)})),
1445 )],
1446 sources: BTreeMap::from([(0, "Counter.sol".to_owned())]),
1447 variable_locations: BTreeMap::new(),
1448 };
1449 let source_contents = BTreeMap::from([(0, source.to_owned())]);
1450
1451 let constructed =
1452 DebugSession::with_ethdebug(sample_trace(), info.clone(), source_contents.clone());
1453 let mut attached = DebugSession::new(sample_trace());
1454 assert!(attached.ethdebug.is_none());
1455 assert!(attached.functions.is_empty());
1456 attached.attach_ethdebug(info, source_contents);
1457
1458 assert_eq!(attached, constructed);
1459 assert_eq!(attached.functions.len(), 1);
1460 assert_eq!(attached.functions[0].name, "set");
1461 assert_eq!(
1462 attached
1463 .step(0)
1464 .and_then(|step| step.source)
1465 .map(|span| span.line),
1466 Some(3)
1467 );
1468
1469 let other = EthdebugInfo {
1471 compilation: json!({}),
1472 contract_name: "Other".to_owned(),
1473 environment: "runtime".to_owned(),
1474 instructions: Vec::new(),
1475 sources: BTreeMap::new(),
1476 variable_locations: BTreeMap::new(),
1477 };
1478 attached.attach_ethdebug(other, BTreeMap::new());
1479 assert_eq!(
1480 attached
1481 .ethdebug
1482 .as_ref()
1483 .map(|info| info.contract_name.as_str()),
1484 Some("Other")
1485 );
1486 assert!(attached.functions.is_empty());
1487 assert!(attached.source_contents.is_empty());
1488 assert_eq!(attached.step(0).and_then(|step| step.source), None);
1489 assert_eq!(attached.trace, sample_trace());
1490 }
1491
1492 #[test]
1493 fn parses_source_functions_with_tuple_and_array_params() {
1494 let functions = parse_source_functions(
1495 7,
1496 "contract C { function submit(Person memory p, uint256[2] memory xs) public {} }",
1497 );
1498 assert_eq!(functions[0].source_id, 7);
1499 assert_eq!(functions[0].name, "submit");
1500 assert_eq!(functions[0].params[0].name, "p");
1501 assert_eq!(functions[0].params[0].ty, "Person");
1502 assert_eq!(functions[0].params[1].name, "xs");
1503 assert_eq!(functions[0].params[1].ty, "uint256[2]");
1504 }
1505
1506 #[test]
1507 fn unnamed_params_documented_by_comments_do_not_become_parameters() {
1508 let functions = parse_source_functions(
1512 0,
1513 "contract C {\n function burnFrom_withCaller(\n address caller,\n address, //from\n uint256, //amount\n bytes32 //h\n ) external view returns (bool) {}\n}",
1514 );
1515 let params = &functions[0].params;
1516 assert_eq!(functions[0].name, "burnFrom_withCaller");
1517 assert_eq!(params.len(), 4);
1518 assert_eq!(
1519 (params[0].ty.as_str(), params[0].name.as_str()),
1520 ("address", "caller")
1521 );
1522 assert_eq!(
1523 (params[1].ty.as_str(), params[1].name.as_str()),
1524 ("address", "arg1")
1525 );
1526 assert_eq!(
1527 (params[2].ty.as_str(), params[2].name.as_str()),
1528 ("uint256", "arg2")
1529 );
1530 assert_eq!(
1531 (params[3].ty.as_str(), params[3].name.as_str()),
1532 ("bytes32", "arg3")
1533 );
1534 }
1535
1536 #[test]
1537 fn unnamed_params_take_positional_names_not_their_type() {
1538 let functions = parse_source_functions(
1539 0,
1540 "contract C { function burn(address from, uint256 amount, bytes32, bytes memory signature) public {} }",
1541 );
1542 let params = &functions[0].params;
1543 assert_eq!(
1544 (params[1].ty.as_str(), params[1].name.as_str()),
1545 ("uint256", "amount")
1546 );
1547 assert_eq!(
1549 (params[2].ty.as_str(), params[2].name.as_str()),
1550 ("bytes32", "arg2")
1551 );
1552 assert_eq!(
1553 (params[3].ty.as_str(), params[3].name.as_str()),
1554 ("bytes", "signature")
1555 );
1556 assert_eq!(params[3].location.as_deref(), Some("memory"));
1557 }
1558
1559 #[test]
1560 fn a_trailing_declaration_keyword_does_not_name_a_param() {
1561 let functions = parse_source_functions(
1562 0,
1563 "contract C { function f(string memory, address payable, bytes calldata data) public {} }",
1564 );
1565 let params = &functions[0].params;
1566 assert_eq!(
1567 (params[0].ty.as_str(), params[0].name.as_str()),
1568 ("string", "arg0")
1569 );
1570 assert_eq!(params[0].location.as_deref(), Some("memory"));
1571 assert_eq!(
1572 (params[1].ty.as_str(), params[1].name.as_str()),
1573 ("address", "arg1")
1574 );
1575 assert_eq!(
1576 (params[2].ty.as_str(), params[2].name.as_str()),
1577 ("bytes", "data")
1578 );
1579 assert_eq!(params[2].location.as_deref(), Some("calldata"));
1580 }
1581
1582 #[test]
1583 fn block_comments_in_a_param_list_separate_rather_than_join() {
1584 let functions = parse_source_functions(
1585 0,
1586 "contract C { function f(uint256 /* wei */ amount, address /*to*/) public {} }",
1587 );
1588 let params = &functions[0].params;
1589 assert_eq!(
1590 (params[0].ty.as_str(), params[0].name.as_str()),
1591 ("uint256", "amount")
1592 );
1593 assert_eq!(
1594 (params[1].ty.as_str(), params[1].name.as_str()),
1595 ("address", "arg1")
1596 );
1597 }
1598
1599 #[test]
1600 fn finds_keywords_in_one_pass_as_one_by_one() {
1601 let source = "function f() {} // function in a comment\n\
1602 contract C { function functionX() {} function_ _function 1function function1\n\
1603 modifier m() { _; } \"receive\" fallback() receive() constructor() é function\n\
1604 x.receive(y); functionfunction function";
1605 let found = super::keyword_positions(source, super::DECLARATION_KEYWORDS);
1606 for (keyword, positions) in super::DECLARATION_KEYWORDS.into_iter().zip(&found) {
1607 let mut one_by_one = Vec::new();
1608 let mut cursor = 0;
1609 while let Some(at) = super::find_solidity_keyword(source, keyword, cursor) {
1610 one_by_one.push(at);
1611 cursor = at + 1;
1612 }
1613 assert_eq!(*positions, one_by_one, "{keyword}");
1614 }
1615 assert_eq!(found[0].len(), 5);
1616 }
1617
1618 #[test]
1619 fn stripping_comments_leaves_declaration_offsets_alone() {
1620 let source = "contract C {\n function f(address, //from\n uint256 amount\n ) public {}\n}";
1623 let functions = parse_source_functions(0, source);
1624 let start = functions[0].declaration_start as usize;
1625 assert!(source[start..].starts_with("function f("));
1626 assert_eq!(functions[0].declaration_line, 2);
1627 assert_eq!(source.as_bytes()[functions[0].body_end as usize], b'}');
1628 }
1629
1630 fn sample_trace() -> TransactionTrace {
1631 TransactionTrace {
1632 tx_hash: Some("0xabc".to_owned()),
1633 from_addr: "0x1".to_owned(),
1634 to_addr: Some("0x2".to_owned()),
1635 value: "0".to_owned(),
1636 input_data: format!(
1637 "0x12345678{}",
1638 "0000000000000000000000001111111111111111111111111111111111111111"
1639 ),
1640 gas_used: 1,
1641 output: "0x".to_owned(),
1642 success: true,
1643 error: None,
1644 debug_trace_available: true,
1645 contract_address: None,
1646 backend: Some("debug-rpc".to_owned()),
1647 capabilities: Default::default(),
1648 artifacts: Default::default(),
1649 steps: vec![TraceStep {
1650 pc: 0,
1651 op: "PUSH1".into(),
1652 gas: 1,
1653 gas_cost: 0,
1654 depth: 0,
1655 stack: vec!["0x2a".into()],
1656 memory: Some(soldb_core::Bytes::from([[0_u8; 32], [0x2a; 32]].concat())),
1657 storage: Some(BTreeMap::from([("0x0".to_owned(), "0x2a".to_owned())])),
1658 error: None,
1659 snapshot: Default::default(),
1660 }]
1661 .into(),
1662 }
1663 }
1664 fn memory_image(words: &[&str]) -> Vec<u8> {
1667 let hex = words
1668 .iter()
1669 .map(|word| format!("{:0>64}", word.trim_start_matches("0x")))
1670 .collect::<String>();
1671 soldb_core::decode_hex(&hex).expect("test memory is hex")
1672 }
1673
1674 #[test]
1675 fn reads_arguments_that_live_in_memory() {
1676 let memory = memory_image(&[
1678 "5",
1679 "68656c6c6f000000000000000000000000000000000000000000000000000000",
1680 "3",
1681 "7",
1682 "8",
1683 "9",
1684 ]);
1685 let types = super::SourceTypes::default();
1686 let reader = super::ValueReader {
1687 memory: Some(soldb_core::Bytes::from(memory)),
1688 calldata: "",
1689 storage: None,
1690 layout: None,
1691 types: &types,
1692 scope: None,
1693 };
1694 let read = |offset: usize, ty: &str| reader.read_memory(offset, ty, 0);
1695 assert_eq!(read(0, "string").as_deref(), Some("\"hello\""));
1696 assert_eq!(read(0, "bytes").as_deref(), Some("0x68656c6c6f"));
1697 assert_eq!(read(64, "uint256[]").as_deref(), Some("[7, 8, 9]"));
1698 assert_eq!(read(96, "uint256[2]").as_deref(), Some("[7, 8]"));
1700 assert_eq!(read(4096, "string"), None);
1704 assert_eq!(
1705 read(64, "string[]").as_deref(),
1706 Some("[<string at 0x7>, <string at 0x8>, <string at 0x9>]")
1707 );
1708 }
1709
1710 #[test]
1711 fn a_parameter_is_readable_only_when_its_width_is_known() {
1712 let param = |ty: &str, location: Option<&str>| super::SourceParam {
1713 name: "x".to_owned(),
1714 ty: ty.to_owned(),
1715 location: location.map(str::to_owned),
1716 };
1717 assert!(super::readable_parameter(¶m("uint256", None)));
1718 assert!(super::readable_parameter(¶m("string", Some("memory"))));
1719 assert!(super::readable_parameter(¶m("Config", Some("storage"))));
1720 assert!(super::readable_parameter(¶m(
1722 "uint256[2]",
1723 Some("calldata")
1724 )));
1725 assert!(!super::readable_parameter(¶m(
1727 "bytes",
1728 Some("calldata")
1729 )));
1730 assert!(!super::readable_parameter(¶m(
1731 "uint256[]",
1732 Some("calldata")
1733 )));
1734 assert!(!super::readable_parameter(¶m("Config", None)));
1736 }
1737}