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::roots::root_calls;
25use crate::stepping::calldata_for_step;
26
27pub mod condition;
28pub mod debug_diff;
29mod decode;
30mod locals;
31mod roots;
32pub mod state;
33pub mod stepping;
34mod types;
35
36pub use condition::{Condition, ConditionContext, Evaluation};
37pub use debug_diff::{
38 capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode, DebugDiffReport,
39 DebugDifference, DebugDifferenceKind, DebugExecution, DebugTrace, DebugTraceEvent,
40 DebugTraceSummary,
41};
42pub use roots::check_root_calls;
43pub use soldb_ethdebug::{CodeGenerator, StorageLayout};
44pub use state::{
45 short_hex, state_value, state_variables, CachedChain, ChainRead, ChainStorage, StateSource,
46 StateVariable, StorageTape, StorageWords,
47};
48pub use stepping::{
49 address_from_word, call_target, normalize_address, source_path_matches, ContractDebugInfo,
50 Frame, FunctionId, InferredVariable, JumpMarker, LineKey, LocalsStatus, ResolvedFunction,
51 ResolvedLine, SourceCache, SourceListing, StepLocation, StepMap, VariableKind,
52 INFERRED_LOCALS_NOTE, INFERRED_LOCALS_WARNING,
53};
54pub use types::{SourceTypes, StructMember};
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct DebugSession {
58 pub trace: TransactionTrace,
59 pub ethdebug: Option<EthdebugInfo>,
60 #[serde(default)]
61 pub source_contents: BTreeMap<u64, String>,
62 #[serde(default)]
63 pub functions: Vec<SourceFunction>,
64}
65
66impl DebugSession {
67 #[must_use]
68 pub fn new(trace: TransactionTrace) -> Self {
69 Self {
70 trace,
71 ethdebug: None,
72 source_contents: BTreeMap::new(),
73 functions: Vec::new(),
74 }
75 }
76
77 #[must_use]
78 pub fn with_ethdebug(
79 trace: TransactionTrace,
80 ethdebug: EthdebugInfo,
81 source_contents: BTreeMap<u64, String>,
82 ) -> Self {
83 let mut session = Self::new(trace);
84 session.attach_ethdebug(ethdebug, source_contents);
85 session
86 }
87
88 pub fn attach_ethdebug(
95 &mut self,
96 ethdebug: EthdebugInfo,
97 source_contents: BTreeMap<u64, String>,
98 ) {
99 self.functions = source_contents
100 .iter()
101 .flat_map(|(source_id, source)| parse_source_functions(*source_id, source))
102 .collect();
103 self.ethdebug = Some(ethdebug);
104 self.source_contents = source_contents;
105 }
106
107 #[must_use]
108 pub fn step(&self, step_index: usize) -> Option<DebugStep> {
109 let step = self.trace.steps.get(step_index)?;
110 Some(DebugStep {
111 index: step_index,
112 pc: step.pc(),
113 op: step.mnemonic().to_owned(),
114 gas: step.gas(),
115 gas_cost: step.gas_cost(),
116 depth: step.depth(),
117 source: self.source_span(step.pc()),
118 function: self.function_at_pc(step.pc()).cloned(),
119 snapshot: step.to_trace_step().snapshot,
120 variables: self.variables_at_step(step_index),
121 })
122 }
123
124 #[must_use]
125 pub fn steps(&self) -> Vec<DebugStep> {
126 (0..self.trace.steps.len())
127 .filter_map(|step_index| self.step(step_index))
128 .collect()
129 }
130
131 #[must_use]
132 pub fn source_span(&self, pc: u64) -> Option<SourceSpan> {
133 let ethdebug = self.ethdebug.as_ref()?;
134 let instruction = ethdebug.instruction_at_pc(pc)?;
135 let location = instruction.source_location()?;
136 let path = ethdebug.sources.get(&location.source_id)?.clone();
137 let position = self
138 .source_contents
139 .get(&location.source_id)
140 .map(|source| line_column_for_offset(source, location.offset as usize));
141 Some(SourceSpan {
142 source_id: location.source_id,
143 path,
144 offset: location.offset,
145 length: location.length,
146 line: position.map_or(0, |position| position.line),
147 column: position.map_or(0, |position| position.column),
148 })
149 }
150
151 #[must_use]
152 pub fn function_at_pc(&self, pc: u64) -> Option<&SourceFunction> {
153 let location = self
154 .ethdebug
155 .as_ref()?
156 .instruction_at_pc(pc)?
157 .source_location()?;
158 self.functions
159 .iter()
160 .filter(|function| {
161 function.source_id == location.source_id
162 && function.declaration_start <= location.offset
163 && location.offset <= function.body_end
164 })
165 .min_by_key(|function| function.body_end.saturating_sub(function.declaration_start))
166 }
167
168 #[must_use]
169 pub fn variables_at_step(&self, step_index: usize) -> Vec<DebugVariable> {
170 let Some(ethdebug) = &self.ethdebug else {
171 return Vec::new();
172 };
173 variables_for_step(&self.trace, ethdebug, step_index)
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct DebugStep {
179 pub index: usize,
180 pub pc: u64,
181 pub op: String,
182 pub gas: u64,
183 pub gas_cost: u64,
184 pub depth: u64,
185 pub source: Option<SourceSpan>,
186 pub function: Option<SourceFunction>,
187 pub snapshot: StepSnapshot,
188 pub variables: Vec<DebugVariable>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct SourceSpan {
193 pub source_id: u64,
194 pub path: String,
195 pub offset: u64,
196 pub length: u64,
197 pub line: u64,
198 pub column: u64,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct SourceFunction {
203 pub source_id: u64,
204 pub name: String,
207 pub params: Vec<SourceParam>,
208 pub declaration_start: u64,
209 #[serde(default)]
211 pub declaration_line: u64,
212 #[serde(default)]
214 pub body_start: u64,
215 pub body_end: u64,
216 #[serde(default)]
218 pub returns: Vec<SourceLocal>,
219 #[serde(default)]
221 pub locals: Vec<SourceLocal>,
222 #[serde(default)]
225 pub has_modifiers: bool,
226 #[serde(default)]
228 pub modifiers: Vec<String>,
229 #[serde(default)]
231 pub placeholder: Option<u64>,
232}
233
234#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
236pub struct ByteRange {
237 pub start: u64,
238 pub end: u64,
239}
240
241impl ByteRange {
242 #[must_use]
243 pub const fn contains(self, offset: u64) -> bool {
244 self.start <= offset && offset < self.end
245 }
246
247 #[must_use]
249 pub const fn covers(self, offset: u64, length: u64) -> bool {
250 self.start <= offset && offset.saturating_add(length) <= self.end
251 }
252
253 #[must_use]
255 pub const fn encloses(self, other: Self) -> bool {
256 self.start <= other.start && other.end <= self.end
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct SourceLocal {
267 pub name: String,
268 pub ty: String,
270 #[serde(default)]
272 pub location: Option<String>,
273 pub declaration: ByteRange,
274 pub statement: ByteRange,
276 pub position: usize,
278 pub scope: ByteRange,
279}
280
281impl SourceLocal {
282 #[must_use]
284 pub fn declared_type(&self) -> String {
285 match &self.location {
286 Some(location) => format!("{} {location}", self.ty),
287 None => self.ty.clone(),
288 }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293pub struct SourceParam {
294 pub name: String,
295 pub ty: String,
297 #[serde(default)]
300 pub location: Option<String>,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct FrameArgument {
306 pub name: String,
307 pub ty: String,
308 pub value: DebugValue,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub struct DebugVariable {
313 pub name: String,
314 pub ty: String,
315 pub location: DebugLocation,
316 pub value: DebugValue,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320pub struct DebugLocation {
321 pub kind: String,
322 pub offset: u64,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct DebugValue {
327 pub display: String,
328 pub raw: Option<String>,
329 pub status: DebugValueStatus,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "snake_case")]
334pub enum DebugValueStatus {
335 Decoded,
336 Raw,
337 Unavailable,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341struct SourcePosition {
342 line: u64,
343 column: u64,
344}
345
346#[must_use]
347pub fn variables_for_step(
348 trace: &TransactionTrace,
349 ethdebug: &EthdebugInfo,
350 step_index: usize,
351) -> Vec<DebugVariable> {
352 let Some(step) = trace.steps.get(step_index) else {
353 return Vec::new();
354 };
355 let variables = ethdebug.variables_at_pc(step.pc());
356 if variables.is_empty() {
357 return Vec::new();
358 }
359 let roots = root_calls(trace);
361 let calldata = calldata_for_step(trace, &roots, step_index);
362 variables
363 .into_iter()
364 .map(|variable| decode_variable(step, calldata, variable))
365 .collect()
366}
367
368fn decode_variable(
369 step: StepView<'_>,
370 calldata: &str,
371 variable: &VariableLocation,
372) -> DebugVariable {
373 let raw = raw_value_for_location(step, calldata, variable);
374 let value = raw.map_or_else(
375 || DebugValue {
376 display: "<unavailable>".to_owned(),
377 raw: None,
378 status: DebugValueStatus::Unavailable,
379 },
380 |raw| decode_debug_value(&raw, &variable.ty),
381 );
382 DebugVariable {
383 name: variable.name.clone(),
384 ty: variable.ty.clone(),
385 location: DebugLocation {
386 kind: variable.location_type.clone(),
387 offset: variable.offset,
388 },
389 value,
390 }
391}
392
393fn raw_value_for_location(
394 step: StepView<'_>,
395 calldata: &str,
396 variable: &VariableLocation,
397) -> Option<String> {
398 match variable.location_type.as_str() {
399 "stack" => step
400 .stack()
401 .get(usize::try_from(variable.offset).ok()?)
402 .map(|value| normalize_hex(&value.to_string())),
403 "memory" => memory_word(&step.memory()?, usize::try_from(variable.offset).ok()?)
404 .map(|word| format!("0x{}", encode_hex(&word))),
405 "calldata" => word_from_hex_bytes(calldata, variable.offset as usize),
406 "storage" => storage_value(step.storage(), variable.offset),
407 _ => None,
408 }
409}
410
411pub(crate) fn decode_debug_value(raw: &str, ty: &str) -> DebugValue {
412 let ty = ty.trim();
413 let normalized = normalize_hex(raw);
414 let word = normalized.trim_start_matches("0x");
415 let decoded = decode_static_word(word, ty);
416 match decoded {
417 Some(display) => DebugValue {
418 display,
419 raw: Some(normalized),
420 status: DebugValueStatus::Decoded,
421 },
422 None => DebugValue {
423 display: normalized.clone(),
424 raw: Some(normalized),
425 status: DebugValueStatus::Raw,
426 },
427 }
428}
429
430pub(crate) fn decode_static_word(word: &str, ty: &str) -> Option<String> {
431 if !word.bytes().all(|byte| byte.is_ascii_hexdigit()) {
432 return None;
433 }
434 if ty.starts_with("uint") {
435 return Some(format_uint_word(word));
436 }
437 if ty.starts_with("int") {
438 return Some(format_int_word(word));
439 }
440 if ty == "address" {
441 let padded = left_pad_word(word);
442 return Some(
443 format!("0x{}", &padded[padded.len().saturating_sub(40)..]).to_ascii_lowercase(),
444 );
445 }
446 if ty == "bool" {
447 let trimmed = word.trim_start_matches('0');
448 return match trimmed {
449 "" | "0" => Some("false".to_owned()),
450 "1" => Some("true".to_owned()),
451 _ => None,
452 };
453 }
454 if ty == "bytes32" {
455 return Some(normalize_hex(word));
456 }
457 if let Some(size) = fixed_bytes_size(ty) {
458 let padded = right_pad_word(word);
459 return Some(format!("0x{}", &padded[..size * 2]).to_ascii_lowercase());
460 }
461 None
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum ArgumentOrder {
469 FirstOnTop,
472 LastOnTop,
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub enum ArgumentLayout {
480 Ordered(ArgumentOrder),
482 TopWords,
485}
486
487impl ArgumentOrder {
488 #[must_use]
491 fn word_index(self, index: usize, count: usize, stack_len: usize) -> usize {
492 match self {
493 Self::FirstOnTop => stack_len - 1 - index,
494 Self::LastOnTop => stack_len - count + index,
495 }
496 }
497}
498
499#[derive(Debug, Clone, Default)]
502pub struct FrameState<'a> {
503 pub stack: &'a [EvmWord],
505 pub memory: Option<Bytes>,
507}
508
509#[must_use]
518pub fn decode_arguments(
519 params: &[SourceParam],
520 state: FrameState<'_>,
521 layout: ArgumentLayout,
522 types: &SourceTypes,
523 scope: Option<&str>,
524) -> Vec<FrameArgument> {
525 let stack = state.stack;
526 if params.is_empty() || stack.len() < params.len() {
527 return Vec::new();
528 }
529 if !params.iter().all(readable_parameter) {
530 return Vec::new();
531 }
532 let order = match layout {
534 ArgumentLayout::Ordered(order) => order,
535 ArgumentLayout::TopWords if params.len() == 1 => ArgumentOrder::FirstOnTop,
536 ArgumentLayout::TopWords => return Vec::new(),
537 };
538 params
539 .iter()
540 .enumerate()
541 .map(|(index, param)| {
542 let word = stack[order.word_index(index, params.len(), stack.len())].to_string();
543 FrameArgument {
544 name: param.name.clone(),
545 ty: param.ty.clone(),
546 value: argument_value(param, &word, state.memory.clone(), types, scope),
547 }
548 })
549 .collect()
550}
551
552fn argument_value(
553 param: &SourceParam,
554 word: &str,
555 memory: Option<Bytes>,
556 types: &SourceTypes,
557 scope: Option<&str>,
558) -> DebugValue {
559 let Ok(parsed) = parse_word(&format!("0x{}", word.trim_start_matches("0x"))) else {
560 return DebugValue {
561 display: "<unreadable stack word>".to_owned(),
562 raw: Some(word.to_owned()),
563 status: DebugValueStatus::Unavailable,
564 };
565 };
566 let reader = ValueReader {
567 memory,
568 calldata: "",
569 storage: None,
570 layout: None,
571 types,
572 scope,
573 };
574 let raw = Some(short_hex(&parsed));
575 match param.location.as_deref() {
576 Some("memory") => reader.variable(&format!("{} memory", param.ty), &[word]),
577 Some(location) => DebugValue {
578 display: format!("<{} in {location} at {}>", param.ty, short_hex(&parsed)),
579 raw,
580 status: DebugValueStatus::Raw,
581 },
582 None => DebugValue {
583 display: decode_value(value_bytes(&parsed, ¶m.ty), ¶m.ty),
584 raw,
585 status: DebugValueStatus::Decoded,
586 },
587 }
588}
589
590pub(crate) fn readable_parameter(param: &SourceParam) -> bool {
592 match param.location.as_deref() {
593 Some("calldata") => !is_dynamic_type(¶m.ty),
595 Some(_) => true,
596 None => is_value_type(¶m.ty),
597 }
598}
599
600fn is_dynamic_type(ty: &str) -> bool {
602 ty == "string" || ty == "bytes" || ty.ends_with("[]")
603}
604
605pub(crate) fn array_shape(ty: &str) -> Option<(&str, Option<usize>)> {
608 let inner = ty.strip_suffix(']')?;
609 let open = inner.rfind('[')?;
610 let element = inner[..open].trim();
611 let length = &inner[open + 1..];
612 if length.is_empty() {
613 return Some((element, None));
614 }
615 Some((element, Some(length.parse::<usize>().ok()?)))
616}
617
618pub(crate) fn memory_word(memory: &[u8], offset: usize) -> Option<Word> {
620 memory_bytes(memory, offset, 32)?.try_into().ok()
621}
622
623pub(crate) fn memory_bytes(memory: &[u8], offset: usize, length: usize) -> Option<&[u8]> {
625 memory.get(offset..offset.checked_add(length)?)
626}
627
628pub(crate) fn hex_bytes(hex: &str, offset: usize, length: usize) -> Option<Vec<u8>> {
630 let start = offset.checked_mul(2)?;
631 let end = start.checked_add(length.checked_mul(2)?)?;
632 let digits = hex.get(start..end)?;
633 if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
634 return None;
635 }
636 Some(
637 digits
638 .as_bytes()
639 .chunks(2)
640 .map(|pair| {
641 u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16).expect("hex")
642 })
643 .collect(),
644 )
645}
646
647pub(crate) fn word_as_usize(word: &Word) -> Option<usize> {
649 if word[..24].iter().any(|byte| *byte != 0) {
650 return None;
651 }
652 usize::try_from(u64::from_be_bytes(word[24..].try_into().expect("8 bytes"))).ok()
653}
654
655pub(crate) fn hex_of(bytes: &[u8]) -> String {
656 encode_hex(bytes)
657}
658
659pub(crate) fn value_bytes<'a>(word: &'a Word, ty: &str) -> &'a [u8] {
663 if let Some(bits) = integer_bits(ty, "int") {
664 return &word[32 - bits / 8..];
665 }
666 if let Some(size) = fixed_bytes_size(ty) {
667 return &word[..size];
668 }
669 word
670}
671
672#[must_use]
674pub fn is_value_type(ty: &str) -> bool {
675 matches!(ty, "bool" | "address" | "address payable")
676 || fixed_bytes_size(ty).is_some()
677 || integer_bits(ty, "uint").is_some()
678 || integer_bits(ty, "int").is_some()
679}
680
681fn integer_bits(ty: &str, prefix: &str) -> Option<usize> {
683 let rest = ty.strip_prefix(prefix)?;
684 if rest.is_empty() {
686 return Some(256);
687 }
688 let bits = rest.parse::<usize>().ok()?;
689 (bits % 8 == 0 && (8..=256).contains(&bits)).then_some(bits)
690}
691
692fn fixed_bytes_size(ty: &str) -> Option<usize> {
693 let size = ty.strip_prefix("bytes")?.parse::<usize>().ok()?;
694 (1..=32).contains(&size).then_some(size)
695}
696
697fn format_uint_word(word: &str) -> String {
698 let trimmed = word.trim_start_matches('0');
699 if trimmed.is_empty() {
700 return "0".to_owned();
701 }
702 if trimmed.len() <= 32 {
703 return u128::from_str_radix(trimmed, 16)
704 .map(|value| value.to_string())
705 .unwrap_or_else(|_| format!("0x{}", trimmed.to_ascii_lowercase()));
706 }
707 format!("0x{}", trimmed.to_ascii_lowercase())
708}
709
710fn format_int_word(word: &str) -> String {
712 let padded = left_pad_word(word);
713 let Ok(bytes) = (0..32)
714 .map(|index| u8::from_str_radix(&padded[index * 2..index * 2 + 2], 16))
715 .collect::<Result<Vec<_>, _>>()
716 else {
717 return format!("0x{}", word.to_ascii_lowercase());
718 };
719 if bytes[0] & 0x80 == 0 {
720 return format_uint_word(word);
721 }
722 let mut magnitude = bytes.iter().map(|byte| !byte).collect::<Vec<_>>();
724 for byte in magnitude.iter_mut().rev() {
725 let (sum, carry) = byte.overflowing_add(1);
726 *byte = sum;
727 if !carry {
728 break;
729 }
730 }
731 let hex = magnitude
732 .iter()
733 .map(|byte| format!("{byte:02x}"))
734 .collect::<String>();
735 let trimmed = hex.trim_start_matches('0');
736 if trimmed.len() <= 32 {
737 if let Ok(value) = u128::from_str_radix(trimmed, 16) {
738 return format!("-{value}");
739 }
740 }
741 format!("-0x{trimmed}")
742}
743
744fn storage_value(storage: &BTreeMap<String, String>, slot: u64) -> Option<String> {
745 storage
746 .get(&format!("0x{slot:x}"))
747 .or_else(|| storage.get(&format!("{slot:x}")))
748 .or_else(|| storage.get(&format!("0x{slot:064x}")))
749 .or_else(|| storage.get(&format!("{slot:064x}")))
750 .map(|value| normalize_hex(value))
751}
752
753fn word_from_hex_bytes(input: &str, byte_offset: usize) -> Option<String> {
754 let hex = input.trim_start_matches("0x");
755 let start = byte_offset.checked_mul(2)?;
756 let end = start.checked_add(64)?;
757 let word = hex.get(start..end)?;
758 word.bytes()
759 .all(|byte| byte.is_ascii_hexdigit())
760 .then(|| format!("0x{}", word.to_ascii_lowercase()))
761}
762
763fn normalize_hex(value: &str) -> String {
764 let value = value.trim();
765 let hex = value.strip_prefix("0x").unwrap_or(value);
766 format!("0x{}", hex.to_ascii_lowercase())
767}
768
769fn left_pad_word(word: &str) -> String {
770 let word = word.trim_start_matches("0x").to_ascii_lowercase();
771 if word.len() >= 64 {
772 return word;
773 }
774 format!("{:0>64}", word)
775}
776
777fn right_pad_word(word: &str) -> String {
778 let word = word.trim_start_matches("0x").to_ascii_lowercase();
779 if word.len() >= 64 {
780 return word;
781 }
782 format!("{:0<64}", word)
783}
784
785fn line_column_for_offset(source: &str, offset: usize) -> SourcePosition {
786 let mut line = 1;
787 let mut column = 1;
788 for byte in source.bytes().take(offset.min(source.len())) {
789 if byte == b'\n' {
790 line += 1;
791 column = 1;
792 } else {
793 column += 1;
794 }
795 }
796 SourcePosition { line, column }
797}
798
799const DECLARATION_KEYWORDS: [&str; 5] =
801 ["function", "modifier", "constructor", "fallback", "receive"];
802
803#[must_use]
807pub fn parse_source_functions(source_id: u64, source: &str) -> Vec<SourceFunction> {
808 let mut functions = Vec::new();
809 let found = keyword_positions(source, DECLARATION_KEYWORDS);
810 for (keyword, positions) in DECLARATION_KEYWORDS.into_iter().zip(&found) {
811 collect_declarations(source_id, source, keyword, positions, &mut functions);
812 }
813 functions.sort_by_key(|function| function.declaration_start);
814 functions
815}
816
817fn keyword_positions<const N: usize>(source: &str, keywords: [&str; N]) -> [Vec<usize>; N] {
821 let mut found = std::array::from_fn(|_| Vec::new());
822 let bytes = source.as_bytes();
823 let mut index = 0;
824 while index < bytes.len() {
825 if !is_identifier_byte(bytes[index]) {
826 index += 1;
827 continue;
828 }
829 let start = index;
830 while bytes
831 .get(index)
832 .is_some_and(|byte| is_identifier_byte(*byte))
833 {
834 index += 1;
835 }
836 let word = &bytes[start..index];
837 for (keyword, positions) in keywords.iter().zip(found.iter_mut()) {
838 if word == keyword.as_bytes() {
839 positions.push(start);
840 }
841 }
842 }
843 found
844}
845
846fn collect_declarations(
847 source_id: u64,
848 source: &str,
849 keyword: &str,
850 positions: &[usize],
851 functions: &mut Vec<SourceFunction>,
852) {
853 let named = matches!(keyword, "function" | "modifier");
854 let mut cursor = 0;
855 let mut next = 0;
856 let mut find = |cursor: usize| {
858 while positions.get(next).is_some_and(|at| *at < cursor) {
859 next += 1;
860 }
861 positions.get(next).copied()
862 };
863 let mut counted = (0, 1);
865 let mut line_of = |offset: usize| {
866 let (from, line) = if offset < counted.0 { (0, 1) } else { counted };
867 let newlines = source.as_bytes()[from..offset]
868 .iter()
869 .filter(|byte| **byte == b'\n')
870 .count() as u64;
871 counted = (offset, line + newlines);
872 line + newlines
873 };
874 while let Some(keyword_start) = find(cursor) {
875 let mut index = skip_ascii_whitespace(source, keyword_start + keyword.len());
876 let name = if named {
877 let Some((name, name_end)) = parse_identifier(source, index) else {
878 cursor = index;
879 continue;
880 };
881 index = skip_ascii_whitespace(source, name_end);
882 name
883 } else {
884 keyword
885 };
886 if source.as_bytes().get(index) != Some(&b'(') {
887 cursor = index;
888 continue;
889 }
890 let Some(params_end) = find_matching_delimiter(source, index, b'(', b')') else {
891 cursor = index + 1;
892 continue;
893 };
894 let params = parse_source_params(&source[index + 1..params_end]);
895 let Some(body_start) = find_next_byte(source, params_end + 1, b'{') else {
896 cursor = params_end + 1;
897 continue;
898 };
899 if source[params_end + 1..body_start].contains(';') {
902 cursor = params_end + 1;
903 continue;
904 }
905 let Some(body_end) = find_matching_delimiter(source, body_start, b'{', b'}') else {
906 cursor = body_start + 1;
907 continue;
908 };
909 let (returns, modifiers) = parse_header(source, params_end + 1, body_start);
910 let modifiers = if keyword == "modifier" {
911 Vec::new()
912 } else {
913 modifiers
914 };
915 let returns = returns
916 .into_iter()
917 .map(|variable| SourceLocal {
918 name: variable.name,
919 ty: variable.ty,
920 location: variable.location,
921 declaration: variable.span,
922 statement: variable.span,
923 position: 0,
924 scope: ByteRange {
925 start: body_start as u64,
926 end: body_end as u64,
927 },
928 })
929 .collect();
930
931 functions.push(SourceFunction {
932 source_id,
933 name: name.to_owned(),
934 params,
935 declaration_start: keyword_start as u64,
936 declaration_line: line_of(keyword_start),
937 body_start: body_start as u64,
938 body_end: body_end as u64,
939 returns,
940 locals: locals::scan_locals(source, body_start, body_end),
941 has_modifiers: !modifiers.is_empty(),
942 modifiers,
943 placeholder: (keyword == "modifier")
944 .then(|| locals::find_placeholder(source, body_start, body_end))
945 .flatten(),
946 });
947 cursor = body_end + 1;
948 }
949}
950
951const HEADER_KEYWORDS: [&str; 11] = [
954 "public", "private", "internal", "external", "pure", "view", "payable", "virtual", "override",
955 "returns", "constant",
956];
957
958fn parse_header(source: &str, start: usize, end: usize) -> (Vec<locals::Declared>, Vec<String>) {
965 let bytes = source.as_bytes();
966 let mut returns = Vec::new();
967 let mut modifiers = Vec::new();
968 let mut index = start;
969 while index < end {
970 index = locals::skip_trivia(source, index);
971 if index >= end {
972 break;
973 }
974 let Some((word, word_end)) = parse_identifier(source, index) else {
975 index += 1;
976 continue;
977 };
978 let after = locals::skip_trivia(source, word_end);
979 let arguments = (bytes.get(after) == Some(&b'('))
980 .then(|| find_matching_delimiter(source, after, b'(', b')'))
981 .flatten()
982 .filter(|close| *close < end);
983 if word == "returns" {
984 if let Some(close) = arguments {
985 returns = parse_return_list(source, after, close);
986 }
987 } else if !HEADER_KEYWORDS.contains(&word) {
988 modifiers.push(word.to_owned());
989 }
990 index = arguments.map_or(word_end, |close| close + 1);
991 }
992 (returns, modifiers)
993}
994
995fn parse_return_list(source: &str, open: usize, close: usize) -> Vec<locals::Declared> {
997 let mut returns = Vec::new();
998 let mut start = open + 1;
999 let mut depth = 0_i32;
1000 for index in open + 1..=close {
1001 match source.as_bytes()[index] {
1002 b'(' | b'[' => depth += 1,
1003 b')' | b']' if depth > 0 => depth -= 1,
1004 b',' | b')' => {
1005 let component = source[start..index].trim();
1006 if !component.is_empty() {
1007 let component_start = start
1008 + (source[start..index].len() - source[start..index].trim_start().len());
1009 let component_start = locals::skip_trivia(source, component_start);
1010 let component_end = component_start + component.len();
1011 if let Some(declared) =
1012 locals::parse_return(source, component_start, component_end, returns.len())
1013 {
1014 returns.push(declared);
1015 }
1016 }
1017 start = index + 1;
1018 }
1019 _ => {}
1020 }
1021 }
1022 returns
1023}
1024
1025const PARAM_KEYWORDS: [&str; 4] = ["memory", "calldata", "storage", "payable"];
1027
1028fn parse_source_params(params: &str) -> Vec<SourceParam> {
1029 let params = strip_param_comments(params);
1030 split_top_level_commas(¶ms)
1031 .into_iter()
1032 .enumerate()
1033 .filter_map(|(index, param)| {
1034 let param = param.trim();
1035 if param.is_empty() {
1036 return None;
1037 }
1038 let mut tokens = param.split_whitespace().collect::<Vec<_>>();
1039 let trailing = tokens.last().copied();
1045 let name = match trailing {
1046 Some(token)
1047 if tokens.len() > 1
1048 && is_identifier(token)
1049 && !PARAM_KEYWORDS.contains(&token) =>
1050 {
1051 tokens.pop();
1052 token.to_owned()
1053 }
1054 _ => format!("arg{index}"),
1055 };
1056 let location = tokens
1057 .iter()
1058 .find(|token| matches!(**token, "memory" | "calldata" | "storage"))
1059 .map(|token| (*token).to_owned());
1060 let ty = tokens
1061 .into_iter()
1062 .filter(|token| !PARAM_KEYWORDS.contains(token))
1063 .collect::<Vec<_>>()
1064 .join(" ");
1065 (!ty.is_empty()).then_some(SourceParam { name, ty, location })
1066 })
1067 .collect()
1068}
1069
1070fn strip_param_comments(params: &str) -> String {
1088 let mut output = String::with_capacity(params.len());
1089 let mut chars = params.chars().peekable();
1090 while let Some(character) = chars.next() {
1091 if character == '/' {
1092 match chars.peek() {
1093 Some('/') => {
1094 for next in chars.by_ref() {
1097 if next == '\n' {
1098 output.push('\n');
1099 break;
1100 }
1101 }
1102 continue;
1103 }
1104 Some('*') => {
1105 chars.next();
1106 let mut previous = None;
1107 for next in chars.by_ref() {
1108 if previous == Some('*') && next == '/' {
1109 break;
1110 }
1111 previous = Some(next);
1112 }
1113 output.push(' ');
1116 continue;
1117 }
1118 _ => {}
1119 }
1120 }
1121 output.push(character);
1122 }
1123 output
1124}
1125
1126fn split_top_level_commas(input: &str) -> Vec<&str> {
1127 let mut parts = Vec::new();
1128 let mut start = 0;
1129 let mut depth = 0_i32;
1130 for (index, byte) in input.bytes().enumerate() {
1131 match byte {
1132 b'(' | b'[' => depth += 1,
1133 b')' | b']' => depth -= 1,
1134 b',' if depth == 0 => {
1135 parts.push(&input[start..index]);
1136 start = index + 1;
1137 }
1138 _ => {}
1139 }
1140 }
1141 parts.push(&input[start..]);
1142 parts
1143}
1144
1145pub(crate) fn find_solidity_keyword(source: &str, keyword: &str, start: usize) -> Option<usize> {
1146 let mut cursor = start;
1147 while let Some(relative) = source[cursor..].find(keyword) {
1148 let absolute = cursor + relative;
1149 let before = absolute
1150 .checked_sub(1)
1151 .and_then(|index| source.as_bytes().get(index))
1152 .copied();
1153 let after = source.as_bytes().get(absolute + keyword.len()).copied();
1154 if before.is_none_or(|byte| !is_identifier_byte(byte))
1155 && after.is_none_or(|byte| !is_identifier_byte(byte))
1156 {
1157 return Some(absolute);
1158 }
1159 cursor = absolute + keyword.len();
1160 }
1161 None
1162}
1163
1164pub(crate) fn parse_identifier(source: &str, start: usize) -> Option<(&str, usize)> {
1165 let bytes = source.as_bytes();
1166 let first = *bytes.get(start)?;
1167 if !is_identifier_start_byte(first) {
1168 return None;
1169 }
1170 let mut end = start + 1;
1171 while bytes.get(end).is_some_and(|byte| is_identifier_byte(*byte)) {
1172 end += 1;
1173 }
1174 Some((&source[start..end], end))
1175}
1176
1177pub(crate) fn is_identifier(input: &str) -> bool {
1178 let mut bytes = input.bytes();
1179 let Some(first) = bytes.next() else {
1180 return false;
1181 };
1182 is_identifier_start_byte(first) && bytes.all(is_identifier_byte)
1183}
1184
1185fn is_identifier_start_byte(byte: u8) -> bool {
1186 byte == b'_' || byte.is_ascii_alphabetic()
1187}
1188
1189pub(crate) fn is_identifier_byte(byte: u8) -> bool {
1190 is_identifier_start_byte(byte) || byte.is_ascii_digit()
1191}
1192
1193pub(crate) fn skip_ascii_whitespace(source: &str, mut index: usize) -> usize {
1194 while source
1195 .as_bytes()
1196 .get(index)
1197 .is_some_and(u8::is_ascii_whitespace)
1198 {
1199 index += 1;
1200 }
1201 index
1202}
1203
1204fn find_next_byte(source: &str, start: usize, needle: u8) -> Option<usize> {
1205 source
1206 .as_bytes()
1207 .iter()
1208 .enumerate()
1209 .skip(start)
1210 .find_map(|(index, byte)| (*byte == needle).then_some(index))
1211}
1212
1213pub(crate) fn find_matching_delimiter(
1214 source: &str,
1215 open_index: usize,
1216 open: u8,
1217 close: u8,
1218) -> Option<usize> {
1219 let mut depth = 0_i32;
1220 for (index, byte) in source.bytes().enumerate().skip(open_index) {
1221 if byte == open {
1222 depth += 1;
1223 } else if byte == close {
1224 depth -= 1;
1225 if depth == 0 {
1226 return Some(index);
1227 }
1228 }
1229 }
1230 None
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use std::collections::BTreeMap;
1236
1237 use serde_json::json;
1238 use soldb_core::{ExecutionCall, TraceStep, TransactionTrace};
1239 use soldb_ethdebug::{source_range, EthdebugInfo, Instruction, VariableLocation};
1240
1241 use super::{parse_source_functions, DebugSession, DebugValueStatus};
1242
1243 #[test]
1244 fn builds_source_steps_and_decodes_active_variables() {
1245 let source =
1246 "contract Counter {\n function set(uint256 x) public {\n value = x;\n }\n}\n";
1247 let offset = source.find("value = x").expect("source offset") as u64;
1248 let trace = sample_trace();
1249 let mut sources = BTreeMap::new();
1250 sources.insert(0, "Counter.sol".to_owned());
1251 let mut source_contents = BTreeMap::new();
1252 source_contents.insert(0, source.to_owned());
1253 let mut variable_locations = BTreeMap::new();
1254 variable_locations.insert(
1255 0,
1256 vec![
1257 VariableLocation {
1258 name: "x".to_owned(),
1259 ty: "uint256".to_owned(),
1260 location_type: "stack".to_owned(),
1261 offset: 0,
1262 pc_range: (0, 1),
1263 },
1264 VariableLocation {
1265 name: "sender".to_owned(),
1266 ty: "address".to_owned(),
1267 location_type: "calldata".to_owned(),
1268 offset: 4,
1269 pc_range: (0, 1),
1270 },
1271 VariableLocation {
1272 name: "tail".to_owned(),
1273 ty: "bytes32".to_owned(),
1274 location_type: "memory".to_owned(),
1275 offset: 32,
1276 pc_range: (0, 1),
1277 },
1278 VariableLocation {
1279 name: "straddle".to_owned(),
1280 ty: "uint256".to_owned(),
1281 location_type: "memory".to_owned(),
1282 offset: 48,
1283 pc_range: (0, 1),
1284 },
1285 ],
1286 );
1287 let info = EthdebugInfo {
1288 compilation: json!({}),
1289 contract_name: "Counter".to_owned(),
1290 environment: "runtime".to_owned(),
1291 instructions: vec![Instruction::new(
1292 0,
1293 "PUSH1",
1294 Some(json!({"code": source_range(0, offset, 9)})),
1295 )],
1296 sources,
1297 variable_locations,
1298 };
1299
1300 let session = DebugSession::with_ethdebug(trace, info, source_contents);
1301 let step = session.step(0).expect("debug step");
1302 assert_eq!(step.source.as_ref().expect("source").line, 3);
1303 assert_eq!(step.function.as_ref().expect("function").name, "set");
1304 assert_eq!(
1305 step.snapshot
1306 .stack
1307 .iter()
1308 .map(|word| &**word)
1309 .collect::<Vec<_>>(),
1310 ["0x2a"]
1311 );
1312 assert_eq!(step.variables[0].name, "x");
1313 assert_eq!(step.variables[0].value.display, "42");
1314 assert_eq!(step.variables[0].value.status, DebugValueStatus::Decoded);
1315 assert_eq!(
1316 step.variables[1].value.display,
1317 "0x1111111111111111111111111111111111111111"
1318 );
1319 assert_eq!(
1321 step.variables[2].value.display,
1322 format!("0x{}", "2a".repeat(32))
1323 );
1324 assert_eq!(step.variables[3].value.display, "<unavailable>");
1326 assert_eq!(
1327 step.variables[3].value.status,
1328 DebugValueStatus::Unavailable
1329 );
1330 }
1331
1332 #[test]
1333 fn calldata_variables_read_the_input_of_their_top_level_call() {
1334 let root = |id: usize, entry: usize, input: String| ExecutionCall {
1335 id,
1336 parent_id: None,
1337 depth: 1,
1338 entry_step: Some(entry),
1339 exit_step: Some(entry + 1),
1340 call_type: "CALL".to_owned(),
1341 from: "0x1".to_owned(),
1342 to: "0x2".to_owned(),
1343 bytecode_address: "0x2".to_owned(),
1344 value: "0x0".to_owned(),
1345 input,
1346 gas_limit: 0,
1347 gas_used: None,
1348 output: None,
1349 success: Some(true),
1350 error: 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}