1use std::collections::BTreeMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15use revm_bytecode::OpCode;
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use soldb_core::{SoldbError, SoldbResult};
19
20use crate::storage_layout::StorageLayout;
21use crate::{EthdebugInfo, Instruction};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SourceMapEnvironment {
26 Creation,
27 Runtime,
28}
29
30impl SourceMapEnvironment {
31 fn fields(self) -> (&'static str, &'static str, &'static str) {
32 match self {
33 Self::Creation => ("srcmap", "bin", "create"),
34 Self::Runtime => ("srcmap-runtime", "bin-runtime", "call"),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct SourceMapProgram {
42 pub info: EthdebugInfo,
43 pub resources: Value,
44 pub source_contents: BTreeMap<u64, String>,
45 pub storage_layout: Option<StorageLayout>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct SourceMapEntry {
51 pub offset: i64,
52 pub length: i64,
53 pub file_index: i64,
54 pub jump_type: String,
55 pub modifier_depth: i64,
56}
57
58impl SourceMapEntry {
59 #[must_use]
60 pub fn is_valid(&self) -> bool {
61 self.file_index >= 0 && self.offset >= 0
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct SourceMapInfo {
67 pub contract_name: String,
68 pub sources: Vec<String>,
69 pub bytecode: Vec<u8>,
70 pub srcmap_entries: Vec<SourceMapEntry>,
71 pub pc_to_instruction_index: BTreeMap<usize, usize>,
72 pub compiler_version: Option<String>,
73}
74
75impl SourceMapInfo {
76 #[must_use]
77 pub fn source_entry_at_pc(&self, pc: usize) -> Option<&SourceMapEntry> {
78 let instruction_index = self.pc_to_instruction_index.get(&pc)?;
79 self.srcmap_entries.get(*instruction_index)
80 }
81
82 #[must_use]
83 pub fn source_info(&self, pc: usize) -> Option<(&str, i64, i64)> {
84 let entry = self.source_entry_at_pc(pc)?;
85 if !entry.is_valid() {
86 return None;
87 }
88
89 let source = self.sources.get(usize::try_from(entry.file_index).ok()?)?;
90 Some((source, entry.offset, entry.length))
91 }
92}
93
94pub fn build_pc_to_instruction_map(bytecode: &[u8]) -> BTreeMap<usize, usize> {
95 let mut pc_to_index = BTreeMap::new();
96 let mut pc = 0;
97 let mut instruction_index = 0;
98
99 while pc < bytecode.len() {
100 pc_to_index.insert(pc, instruction_index);
101 pc += 1 + push_data_size(bytecode[pc]).unwrap_or(0);
102 instruction_index += 1;
103 }
104
105 pc_to_index
106}
107
108pub fn parse_srcmap(srcmap: &str) -> SoldbResult<Vec<SourceMapEntry>> {
109 if srcmap.is_empty() {
110 return Ok(Vec::new());
111 }
112
113 let mut entries = Vec::new();
114 let mut previous = SourceMapEntry {
115 offset: 0,
116 length: 0,
117 file_index: -1,
118 jump_type: "-".to_owned(),
119 modifier_depth: 0,
120 };
121
122 for part in srcmap.split(';') {
123 if part.is_empty() {
124 entries.push(previous.clone());
125 continue;
126 }
127
128 let fields = part.split(':').collect::<Vec<_>>();
129 let entry = SourceMapEntry {
130 offset: parse_inherited_i64(fields.first().copied(), previous.offset, "offset")?,
131 length: parse_inherited_i64(fields.get(1).copied(), previous.length, "length")?,
132 file_index: parse_inherited_i64(
133 fields.get(2).copied(),
134 previous.file_index,
135 "file index",
136 )?,
137 jump_type: parse_inherited_string(fields.get(3).copied(), &previous.jump_type),
138 modifier_depth: parse_inherited_i64(
139 fields.get(4).copied(),
140 previous.modifier_depth,
141 "modifier depth",
142 )?,
143 };
144
145 previous = entry.clone();
146 entries.push(entry);
147 }
148
149 Ok(entries)
150}
151
152pub fn load_source_map_program(
158 root: &Path,
159 contract_name: &str,
160 environment: SourceMapEnvironment,
161) -> SoldbResult<Option<SourceMapProgram>> {
162 load_source_map_program_with_sources(root, &[], contract_name, environment)
163}
164
165pub fn load_source_map_program_with_sources(
167 root: &Path,
168 source_roots: &[PathBuf],
169 contract_name: &str,
170 environment: SourceMapEnvironment,
171) -> SoldbResult<Option<SourceMapProgram>> {
172 let path = root.join("combined.json");
173 if !path.exists() {
174 return Ok(None);
175 }
176
177 let input = fs::read_to_string(&path).map_err(|error| {
178 SoldbError::Message(format!("failed to read `{}`: {error}", path.display()))
179 })?;
180 let combined = serde_json::from_str::<Value>(&input).map_err(|error| {
181 SoldbError::Message(format!("invalid JSON in `{}`: {error}", path.display()))
182 })?;
183 source_map_program_from_combined(
184 root,
185 source_roots,
186 &path,
187 &combined,
188 contract_name,
189 environment,
190 )
191}
192
193fn source_map_program_from_combined(
194 root: &Path,
195 source_roots: &[PathBuf],
196 artifact_path: &Path,
197 combined: &Value,
198 contract_name: &str,
199 environment: SourceMapEnvironment,
200) -> SoldbResult<Option<SourceMapProgram>> {
201 let contracts = combined
202 .get("contracts")
203 .and_then(Value::as_object)
204 .ok_or_else(|| {
205 SoldbError::Message(format!(
206 "legacy artifact `{}` does not contain a contracts object",
207 artifact_path.display()
208 ))
209 })?;
210 let Some((contract_key, contract)) = find_contract(contracts, contract_name)? else {
211 return Ok(None);
212 };
213 let (source_map_field, bytecode_field, environment_name) = environment.fields();
214 let Some(source_map) = contract.get(source_map_field).and_then(Value::as_str) else {
215 return Ok(None);
216 };
217 let bytecode = contract
218 .get(bytecode_field)
219 .and_then(Value::as_str)
220 .ok_or_else(|| {
221 SoldbError::Message(format!(
222 "legacy artifact `{}` has `{source_map_field}` for `{contract_key}` but no \
223 `{bytecode_field}`; recompile with bytecode output enabled",
224 artifact_path.display()
225 ))
226 })?;
227 let bytecode = decode_bytecode(bytecode, artifact_path, contract_key, bytecode_field)?;
228 let source_map = parse_srcmap(source_map)?;
229 let source_list = combined
230 .get("sourceList")
231 .and_then(Value::as_array)
232 .ok_or_else(|| {
233 SoldbError::Message(format!(
234 "legacy artifact `{}` does not contain a sourceList array",
235 artifact_path.display()
236 ))
237 })?;
238
239 let mut sources = BTreeMap::new();
240 let mut source_contents = BTreeMap::new();
241 let mut compilation_sources = Vec::with_capacity(source_list.len());
242 for (source_id, source) in source_list.iter().enumerate() {
243 let source_path = source.as_str().ok_or_else(|| {
244 SoldbError::Message(format!(
245 "legacy artifact `{}` has a non-string sourceList entry at index {source_id}",
246 artifact_path.display()
247 ))
248 })?;
249 let source_id = u64::try_from(source_id)
250 .map_err(|_| SoldbError::Message("source index does not fit in `u64`".to_owned()))?;
251 sources.insert(source_id, source_path.to_owned());
252
253 let contents = read_source(root, source_roots, source_path)?;
254 if let Some(contents) = &contents {
255 source_contents.insert(source_id, contents.clone());
256 }
257 let mut source = json!({
258 "id": source_id,
259 "path": source_path,
260 "language": "Solidity",
261 });
262 if let Some(contents) = contents {
263 source["contents"] = Value::String(contents);
264 }
265 compilation_sources.push(source);
266 }
267
268 let pc_to_instruction_index = build_pc_to_instruction_map(&bytecode);
269 if source_map.len() > pc_to_instruction_index.len() {
270 return Err(SoldbError::Message(format!(
271 "legacy source map for `{contract_key}` has {} entries but `{bytecode_field}` has \
272 only {} instructions",
273 source_map.len(),
274 pc_to_instruction_index.len()
275 )));
276 }
277
278 let mut instructions = Vec::with_capacity(source_map.len());
279 for (pc, instruction_index) in pc_to_instruction_index {
280 let Some(entry) = source_map.get(instruction_index) else {
281 break;
282 };
283 instructions.push(Instruction {
284 offset: u64::try_from(pc).map_err(|_| {
285 SoldbError::Message("program counter does not fit in `u64`".to_owned())
286 })?,
287 operation: opcode_operation(bytecode[pc]),
288 context: source_context(
289 entry,
290 sources.len(),
291 &source_contents,
292 instruction_index,
293 contract_key,
294 )?,
295 });
296 }
297
298 let compiler_version = combined
299 .get("version")
300 .and_then(Value::as_str)
301 .unwrap_or("unknown");
302 let compilation = json!({
303 "id": format!("legacy-{compiler_version}"),
304 "compiler": {
305 "name": "Solidity compiler",
306 "version": compiler_version,
307 },
308 "sources": compilation_sources,
309 });
310 let resources = json!({
311 "compilation": compilation,
312 "types": {},
313 "pointers": {},
314 });
315 let resolved_contract_name = contract_key
316 .rsplit_once(':')
317 .map_or(contract_key, |(_, name)| name);
318 let info = EthdebugInfo {
319 compilation,
320 contract_name: resolved_contract_name.to_owned(),
321 environment: environment_name.to_owned(),
322 instructions,
323 sources,
324 variable_locations: BTreeMap::new(),
325 };
326
327 let storage_layout = contract
328 .get("storage-layout")
329 .map(StorageLayout::parse)
330 .transpose()
331 .map_err(|error| {
332 SoldbError::Message(format!(
333 "legacy artifact `{}` has an invalid `storage-layout` for `{contract_key}`: {error}",
334 artifact_path.display()
335 ))
336 })?;
337
338 Ok(Some(SourceMapProgram {
339 info,
340 resources,
341 source_contents,
342 storage_layout,
343 }))
344}
345
346fn find_contract<'a>(
347 contracts: &'a serde_json::Map<String, Value>,
348 contract_name: &str,
349) -> SoldbResult<Option<(&'a str, &'a Value)>> {
350 if contract_name.is_empty() {
351 let mut contracts = contracts.iter();
352 let Some((name, contract)) = contracts.next() else {
353 return Ok(None);
354 };
355 if contracts.next().is_some() {
356 return Err(SoldbError::Message(
357 "legacy combined JSON contains multiple contracts; provide `contractName`"
358 .to_owned(),
359 ));
360 }
361 return Ok(Some((name, contract)));
362 }
363 if let Some((name, contract)) = contracts.get_key_value(contract_name) {
364 return Ok(Some((name, contract)));
365 }
366
367 let suffix = format!(":{contract_name}");
368 let mut matches = contracts.iter().filter(|(name, _)| name.ends_with(&suffix));
369 let Some((name, contract)) = matches.next() else {
370 return Ok(None);
371 };
372 if matches.next().is_some() {
373 return Err(SoldbError::Message(format!(
374 "contract name `{contract_name}` is ambiguous in legacy combined JSON"
375 )));
376 }
377 Ok(Some((name, contract)))
378}
379
380fn decode_bytecode(
381 bytecode: &str,
382 artifact_path: &Path,
383 contract_name: &str,
384 bytecode_field: &str,
385) -> SoldbResult<Vec<u8>> {
386 let bytecode = bytecode.strip_prefix("0x").unwrap_or(bytecode);
387 let (byte_pairs, remainder) = bytecode.as_bytes().as_chunks::<2>();
388 if !remainder.is_empty() {
389 return Err(SoldbError::Message(format!(
390 "`{bytecode_field}` for `{contract_name}` in `{}` has an odd number of hex digits",
391 artifact_path.display()
392 )));
393 }
394
395 byte_pairs
396 .iter()
397 .map(|digits| {
398 let high = decode_hex_digit(digits[0]);
399 let low = decode_hex_digit(digits[1]);
400 match (high, low) {
401 (Some(high), Some(low)) => Ok(high << 4 | low),
402 _ => Err(SoldbError::Message(format!(
403 "`{bytecode_field}` for `{contract_name}` in `{}` is not valid hex",
404 artifact_path.display()
405 ))),
406 }
407 })
408 .collect()
409}
410
411fn decode_hex_digit(digit: u8) -> Option<u8> {
412 match digit {
413 b'0'..=b'9' => Some(digit - b'0'),
414 b'a'..=b'f' => Some(digit - b'a' + 10),
415 b'A'..=b'F' => Some(digit - b'A' + 10),
416 _ => None,
417 }
418}
419
420fn opcode_operation(opcode: u8) -> Value {
421 let mnemonic = OpCode::new(opcode).map_or_else(
422 || format!("UNKNOWN(0x{opcode:02x})"),
423 |opcode| opcode.as_str().to_owned(),
424 );
425 json!({"mnemonic": mnemonic})
426}
427
428fn source_context(
433 entry: &SourceMapEntry,
434 source_count: usize,
435 source_contents: &BTreeMap<u64, String>,
436 instruction_index: usize,
437 contract_name: &str,
438) -> SoldbResult<Option<Value>> {
439 let mut context = serde_json::Map::new();
440 context.insert("modifierDepth".to_owned(), json!(entry.modifier_depth));
441 if let Some(code) = source_code(
442 entry,
443 source_count,
444 source_contents,
445 instruction_index,
446 contract_name,
447 )? {
448 context.insert("code".to_owned(), code);
449 }
450 match entry.jump_type.as_str() {
451 "i" => {
452 context.insert("invoke".to_owned(), json!({}));
453 }
454 "o" => {
455 context.insert("return".to_owned(), json!({}));
456 }
457 _ => {}
458 }
459 Ok(Some(Value::Object(context)))
460}
461
462fn source_code(
464 entry: &SourceMapEntry,
465 source_count: usize,
466 source_contents: &BTreeMap<u64, String>,
467 instruction_index: usize,
468 contract_name: &str,
469) -> SoldbResult<Option<Value>> {
470 if entry.file_index < 0 {
471 return Ok(None);
472 }
473 if entry.offset < 0 || entry.length < 0 {
474 return Err(SoldbError::Message(format!(
475 "legacy source-map entry {instruction_index} for `{contract_name}` has a negative \
476 source range"
477 )));
478 }
479 let source_id = u64::try_from(entry.file_index).map_err(|_| {
480 SoldbError::Message(format!(
481 "legacy source-map entry {instruction_index} for `{contract_name}` has an invalid \
482 source index"
483 ))
484 })?;
485 if usize::try_from(source_id).map_or(true, |source_id| source_id >= source_count) {
491 return Ok(None);
492 }
493 let offset = u64::try_from(entry.offset).map_err(|_| {
494 SoldbError::Message(format!(
495 "legacy source-map entry {instruction_index} for `{contract_name}` has an invalid \
496 offset"
497 ))
498 })?;
499 let length = u64::try_from(entry.length).map_err(|_| {
500 SoldbError::Message(format!(
501 "legacy source-map entry {instruction_index} for `{contract_name}` has an invalid \
502 length"
503 ))
504 })?;
505 let end = offset.checked_add(length).ok_or_else(|| {
506 SoldbError::Message(format!(
507 "legacy source-map entry {instruction_index} for `{contract_name}` overflows its \
508 source range"
509 ))
510 })?;
511 if let Some(source) = source_contents.get(&source_id) {
512 let source_len = u64::try_from(source.len())
513 .map_err(|_| SoldbError::Message("source length does not fit in `u64`".to_owned()))?;
514 if end > source_len {
515 return Err(SoldbError::Message(format!(
516 "legacy source-map entry {instruction_index} for `{contract_name}` ends at byte \
517 {end}, beyond source {source_id} length {source_len}"
518 )));
519 }
520 }
521
522 Ok(Some(json!({
523 "source": {"id": source_id},
524 "range": {
525 "offset": offset,
526 "length": length,
527 },
528 })))
529}
530
531fn read_source(
532 root: &Path,
533 source_roots: &[PathBuf],
534 source_path: &str,
535) -> SoldbResult<Option<String>> {
536 let candidates = source_roots
537 .iter()
538 .flat_map(|extra| crate::artifacts::source_candidates(extra, source_path))
539 .chain(crate::artifacts::source_candidates(root, source_path));
540 for candidate in candidates {
541 if candidate.exists() {
542 return fs::read_to_string(&candidate).map(Some).map_err(|error| {
543 SoldbError::Message(format!(
544 "failed to read source `{}`: {error}",
545 candidate.display()
546 ))
547 });
548 }
549 }
550 Ok(None)
551}
552
553#[must_use]
554pub fn is_legacy_compiler(version: &str) -> bool {
555 if version.is_empty() {
556 return true;
557 }
558
559 let version_core = version.split('+').next().unwrap_or(version);
560 let parts = version_core.split('.').collect::<Vec<_>>();
561 if parts.len() < 3 {
562 return true;
563 }
564
565 let Ok(major) = parts[0].parse::<u64>() else {
566 return true;
567 };
568 let Ok(minor) = parts[1].parse::<u64>() else {
569 return true;
570 };
571 let Ok(patch) = parts[2].parse::<u64>() else {
572 return true;
573 };
574
575 major == 0 && (minor < 8 || (minor == 8 && patch < 29))
576}
577
578fn push_data_size(opcode: u8) -> Option<usize> {
579 if (0x60..=0x7f).contains(&opcode) {
580 Some(usize::from(opcode - 0x5f))
581 } else {
582 None
583 }
584}
585
586fn parse_inherited_i64(field: Option<&str>, previous: i64, label: &str) -> SoldbResult<i64> {
587 let Some(field) = field else {
588 return Ok(previous);
589 };
590
591 let trimmed = field.trim();
592 if trimmed.is_empty() {
593 return Ok(previous);
594 }
595
596 trimmed.parse::<i64>().map_err(|error| {
597 SoldbError::Message(format!("Invalid source map {label} '{trimmed}': {error}"))
598 })
599}
600
601fn parse_inherited_string(field: Option<&str>, previous: &str) -> String {
602 field
603 .filter(|value| !value.trim().is_empty())
604 .unwrap_or(previous)
605 .to_owned()
606}
607
608#[cfg(test)]
609mod tests {
610 use crate::metadata::FunctionExit;
611 use std::fs;
612 use std::path::PathBuf;
613 use std::time::{SystemTime, UNIX_EPOCH};
614
615 use serde_json::json;
616
617 use crate::Instruction;
618
619 use super::{
620 build_pc_to_instruction_map, is_legacy_compiler, load_source_map_program, parse_srcmap,
621 SourceMapEnvironment, SourceMapInfo,
622 };
623
624 fn temp_dir(label: &str) -> PathBuf {
625 let unique = SystemTime::now()
626 .duration_since(UNIX_EPOCH)
627 .expect("system time")
628 .as_nanos();
629 let dir = std::env::temp_dir().join(format!("soldb-source-map-{label}-{unique}"));
630 fs::create_dir_all(&dir).expect("create temp dir");
631 dir
632 }
633
634 #[test]
635 fn maps_program_counters_around_push_data() {
636 let bytecode = [0x60, 0xff, 0x61, 0x01, 0x02, 0x00];
637 let pc_map = build_pc_to_instruction_map(&bytecode);
638
639 assert_eq!(pc_map.get(&0), Some(&0));
640 assert_eq!(pc_map.get(&2), Some(&1));
641 assert_eq!(pc_map.get(&5), Some(&2));
642 assert!(!pc_map.contains_key(&1));
643 assert!(!pc_map.contains_key(&3));
644 }
645
646 #[test]
647 fn parses_source_maps_with_inherited_fields() {
648 let entries = parse_srcmap("0:10:0:-:0;:5::i:;20::1").expect("source map");
649
650 assert_eq!(entries[0].offset, 0);
651 assert_eq!(entries[1].offset, 0);
652 assert_eq!(entries[1].length, 5);
653 assert_eq!(entries[1].file_index, 0);
654 assert_eq!(entries[1].jump_type, "i");
655 assert_eq!(entries[2].offset, 20);
656 assert_eq!(entries[2].length, 5);
657 assert_eq!(entries[2].file_index, 1);
658 }
659
660 #[test]
661 fn rejects_invalid_source_map_numbers() {
662 let error = parse_srcmap("abc:10:0").expect_err("invalid number");
663 assert!(error.to_string().contains("offset"));
664 }
665
666 #[test]
667 fn reports_source_info_for_valid_pcs() {
668 let bytecode = vec![0x60, 0xff, 0x00];
669 let srcmap_entries = parse_srcmap("0:4:0;10:2:1").expect("source map");
670 let info = SourceMapInfo {
671 contract_name: "Counter".to_owned(),
672 sources: vec!["A.sol".to_owned(), "B.sol".to_owned()],
673 bytecode: bytecode.clone(),
674 srcmap_entries,
675 pc_to_instruction_index: build_pc_to_instruction_map(&bytecode),
676 compiler_version: Some("0.8.16".to_owned()),
677 };
678
679 assert_eq!(info.source_info(0), Some(("A.sol", 0, 4)));
680 assert_eq!(info.source_info(2), Some(("B.sol", 10, 2)));
681 assert_eq!(info.source_info(1), None);
682 }
683
684 #[test]
685 fn detects_legacy_compiler_versions() {
686 assert!(is_legacy_compiler(""));
687 assert!(is_legacy_compiler("0.8.16"));
688 assert!(is_legacy_compiler("0.8.28+commit.deadbeef"));
689 assert!(!is_legacy_compiler("0.8.29"));
690 assert!(!is_legacy_compiler("0.9.0"));
691 }
692
693 #[test]
694 fn loads_runtime_combined_json_as_debug_info() {
695 let dir = temp_dir("runtime");
696 let source = "contract Counter { function add() external {} }";
697 fs::write(dir.join("Counter.sol"), source).expect("write source");
698 fs::write(
699 dir.join("combined.json"),
700 json!({
701 "version": "0.8.36+commit.test",
702 "sourceList": ["Counter.sol"],
703 "contracts": {
704 "Counter.sol:Counter": {
705 "bin-runtime": "600100",
706 "srcmap-runtime": "0:8:0:-:0;9:3:0",
707 "storage-layout": {
708 "storage": [{
709 "astId": 1,
710 "contract": "Counter.sol:Counter",
711 "label": "value",
712 "offset": 0,
713 "slot": "0",
714 "type": "t_uint256"
715 }],
716 "types": {
717 "t_uint256": {
718 "encoding": "inplace",
719 "label": "uint256",
720 "numberOfBytes": "32"
721 }
722 }
723 }
724 }
725 }
726 })
727 .to_string(),
728 )
729 .expect("write combined JSON");
730
731 let program = load_source_map_program(&dir, "Counter", SourceMapEnvironment::Runtime)
732 .expect("load source map")
733 .expect("runtime program");
734
735 let layout = program.storage_layout.as_ref().expect("storage layout");
738 assert_eq!(layout.variables.len(), 1);
739 assert_eq!(layout.variable("value").expect("value").slot, [0_u8; 32]);
740 assert_eq!(
741 layout.resolve("value").expect("resolve").type_id,
742 "t_uint256"
743 );
744
745 assert_eq!(program.info.contract_name, "Counter");
746 assert_eq!(program.info.environment, "call");
747 assert_eq!(program.info.source_info(0), Some(("Counter.sol", 0, 8)));
748 assert_eq!(program.info.source_info(2), Some(("Counter.sol", 9, 3)));
749 assert_eq!(
750 program
751 .info
752 .instruction_at_pc(0)
753 .and_then(Instruction::mnemonic),
754 Some("PUSH1")
755 );
756 assert_eq!(
757 program
758 .info
759 .instruction_at_pc(2)
760 .and_then(Instruction::mnemonic),
761 Some("STOP")
762 );
763 assert_eq!(
764 program.source_contents.get(&0).map(String::as_str),
765 Some(source)
766 );
767 assert_eq!(
768 program.resources["compilation"]["compiler"]["version"],
769 "0.8.36+commit.test"
770 );
771 }
772
773 #[test]
774 fn jump_types_become_call_and_return_markers() {
775 let dir = temp_dir("jump-types");
776 let source = "contract Counter { function add() external {} }";
777 fs::write(dir.join("Counter.sol"), source).expect("write source");
778 fs::write(
779 dir.join("combined.json"),
780 json!({
781 "version": "0.8.16+commit.test",
782 "sourceList": ["Counter.sol"],
783 "contracts": {
784 "Counter.sol:Counter": {
785 "bin-runtime": "6001600100",
786 "srcmap-runtime": "0:8:0:i;9:3:0:o;0:0:-1:-"
787 }
788 }
789 })
790 .to_string(),
791 )
792 .expect("write combined JSON");
793
794 let program = load_source_map_program(&dir, "Counter", SourceMapEnvironment::Runtime)
795 .expect("load source map")
796 .expect("runtime program");
797 let at = |pc: u64| program.info.instruction_at_pc(pc).expect("instruction");
798 assert_eq!(at(0).function_invocations().len(), 1);
799 assert_eq!(at(0).modifier_depth(), Some(0));
800 assert_eq!(at(0).function_exit(), None);
801 assert!(at(2).function_invocations().is_empty());
802 assert_eq!(at(2).function_exit(), Some(FunctionExit::Return));
803 assert!(at(4).source_location().is_none());
805 assert!(at(4).function_invocations().is_empty());
806 }
807
808 #[test]
809 fn generated_code_past_the_source_list_has_no_source() {
810 let dir = temp_dir("utility-source");
813 let source = "contract Counter { function add() external {} }";
814 fs::write(dir.join("Counter.sol"), source).expect("write source");
815 fs::write(
816 dir.join("combined.json"),
817 json!({
818 "version": "0.8.16+commit.test",
819 "sourceList": ["Counter.sol"],
820 "contracts": {
821 "Counter.sol:Counter": {
822 "bin-runtime": "600100",
823 "srcmap-runtime": "0:8:0:-:0;0:5:1"
824 }
825 }
826 })
827 .to_string(),
828 )
829 .expect("write combined JSON");
830
831 let program = load_source_map_program(&dir, "Counter", SourceMapEnvironment::Runtime)
832 .expect("load source map")
833 .expect("runtime program");
834 assert_eq!(program.info.source_info(0), Some(("Counter.sol", 0, 8)));
835 assert_eq!(program.info.source_info(2), None);
836 assert!(program.info.instruction_at_pc(2).is_some());
837 }
838
839 #[test]
840 fn loads_creation_map_and_infers_single_contract() {
841 let dir = temp_dir("creation");
842 fs::write(
843 dir.join("combined.json"),
844 json!({
845 "sourceList": ["Missing.sol"],
846 "contracts": {
847 "Missing.sol:Created": {
848 "bin": "00",
849 "srcmap": "0:1:0"
850 }
851 }
852 })
853 .to_string(),
854 )
855 .expect("write combined JSON");
856
857 let program = load_source_map_program(&dir, "", SourceMapEnvironment::Creation)
858 .expect("load source map")
859 .expect("creation program");
860
861 assert_eq!(program.info.contract_name, "Created");
862 assert_eq!(program.info.environment, "create");
863 assert!(program.source_contents.is_empty());
864 }
865
866 #[test]
867 fn requires_bytecode_for_program_counter_mapping() {
868 let dir = temp_dir("missing-bytecode");
869 fs::write(
870 dir.join("combined.json"),
871 json!({
872 "sourceList": ["Counter.sol"],
873 "contracts": {
874 "Counter.sol:Counter": {"srcmap-runtime": "0:1:0"}
875 }
876 })
877 .to_string(),
878 )
879 .expect("write combined JSON");
880
881 let error = load_source_map_program(&dir, "Counter", SourceMapEnvironment::Runtime)
882 .expect_err("missing bytecode");
883 assert!(error.to_string().contains("`bin-runtime`"));
884 assert!(error.to_string().contains("bytecode output"));
885 }
886}