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