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