1use alloy_primitives::{Address, Bytes, TxHash};
50use eyre::Result;
51use foundry_block_explorers::Client;
52use foundry_compilers::{
53 artifacts::{Contract, SolcInput},
54 solc::Solc,
55};
56use indicatif::{ProgressBar, ProgressStyle};
57use revm::{
58 context::{
59 result::{ExecutionResult, HaltReason},
60 Host, TxEnv,
61 },
62 database::CacheDB,
63 Database, DatabaseCommit, DatabaseRef, InspectEvm, MainBuilder,
64};
65use semver::Version;
66use std::{
67 collections::{HashMap, HashSet},
68 fs,
69 path::PathBuf,
70 time::Duration,
71};
72use tracing::{debug, error, info, warn};
73
74use edb_common::{
75 relax_evm_constraints, types::Trace, CachePath, EdbCachePath, EdbContext, ForkResult,
76 DEFAULT_ETHERSCAN_CACHE_TTL,
77};
78
79use crate::{
80 analysis::AnalysisResult,
81 analyze,
82 inspector::{CallTracer, TraceReplayResult},
83 instrument,
84 rpc::RpcServerHandle,
85 start_debug_server,
86 utils::{next_etherscan_api_key, Artifact, OnchainCompiler},
87 CodeTweaker, EngineContext, HookSnapshotInspector, HookSnapshots, OpcodeSnapshotInspector,
88 OpcodeSnapshots, SnapshotAnalysis, Snapshots,
89};
90
91#[derive(Debug, Clone)]
96pub struct EngineConfig {
97 pub rpc_proxy_url: String,
99 pub etherscan_api_key: Option<String>,
101 pub quick: bool,
103}
104
105impl Default for EngineConfig {
106 fn default() -> Self {
107 Self {
108 rpc_proxy_url: "http://localhost:8545".into(),
109 etherscan_api_key: None,
110 quick: false,
111 }
112 }
113}
114
115impl EngineConfig {
116 pub fn with_etherscan_api_key(mut self, key: String) -> Self {
118 self.etherscan_api_key = Some(key);
119 self
120 }
121
122 pub fn with_quick_mode(mut self, quick: bool) -> Self {
124 self.quick = quick;
125 self
126 }
127
128 pub fn with_rpc_proxy_url(mut self, url: String) -> Self {
130 self.rpc_proxy_url = url;
131 self
132 }
133}
134
135#[derive(Debug)]
137pub struct Engine {
138 pub rpc_proxy_url: String,
140 pub host_port: Option<u16>,
142 pub etherscan_api_key: Option<String>,
144 pub quick: bool,
146}
147
148impl Default for Engine {
149 fn default() -> Self {
150 Self::new(EngineConfig::default())
151 }
152}
153
154impl Engine {
155 pub fn new(config: EngineConfig) -> Self {
157 let EngineConfig { rpc_proxy_url, etherscan_api_key, quick } = config;
158 Self { rpc_proxy_url, host_port: None, etherscan_api_key, quick }
159 }
160
161 pub async fn prepare<DB>(&self, fork_result: ForkResult<DB>) -> Result<RpcServerHandle>
173 where
174 DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
175 <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
176 <DB as Database>::Error: Clone + Send + Sync,
177 {
178 info!("Starting engine preparation for transaction: {:?}", fork_result.target_tx_hash);
179
180 let ForkResult { context: mut ctx, target_tx_env: tx, target_tx_hash: tx_hash, fork_info } =
182 fork_result;
183
184 info!("Replaying transaction to collect call trace and touched contracts");
186 let replay_result = self.replay_and_collect_trace(ctx.clone(), tx.clone())?;
187
188 info!("Downloading verified source code for each contract");
190 let artifacts =
191 self.download_verified_source_code(&replay_result, ctx.chain_id().to::<u64>()).await?;
192
193 info!("Analyzing source code");
195 let analysis_results = self.analyze_source_code(&artifacts)?;
196
197 info!("Instrumenting source code");
199 let recompiled_artifacts =
200 self.instrument_and_recompile_source_code(&artifacts, &analysis_results)?;
201
202 info!("Collecting opcode-level step execution results");
204 let opcode_snapshots = self.capture_opcode_level_snapshots(
205 ctx.clone(),
206 tx.clone(),
207 artifacts.keys().cloned().collect(),
208 &replay_result.execution_trace,
209 )?;
210
211 info!("Tweaking bytecode");
213 let contracts_in_tx =
214 self.tweak_bytecode(&mut ctx, &artifacts, &recompiled_artifacts, tx_hash).await?;
215
216 info!("Re-executing transaction with snapshot collection");
218 let hook_creation =
219 self.collect_creation_hooks(&artifacts, &recompiled_artifacts, contracts_in_tx)?;
220 let hook_snapshots = self.capture_hook_snapshots(
221 ctx.clone(),
222 tx.clone(),
223 hook_creation,
224 &replay_result.execution_trace,
225 &analysis_results,
226 )?;
227
228 info!("Starting RPC server with analysis results and snapshots");
230 let mut snapshots = self.get_time_travel_snapshots(opcode_snapshots, hook_snapshots)?;
231 snapshots.analyze(&replay_result.execution_trace, &analysis_results)?;
232 let context = EngineContext::build(
234 fork_info,
235 ctx.cfg.clone(),
236 ctx.block.clone(),
237 tx,
238 tx_hash,
239 snapshots,
240 artifacts,
241 recompiled_artifacts,
242 analysis_results,
243 replay_result.execution_trace,
244 )?;
245
246 let rpc_handle = start_debug_server(context).await?;
247 info!("Debug RPC server started on port {}", rpc_handle.port());
248
249 Ok(rpc_handle)
250 }
251
252 fn replay_and_collect_trace<DB>(
254 &self,
255 ctx: EdbContext<DB>,
256 tx: TxEnv,
257 ) -> Result<TraceReplayResult>
258 where
259 DB: Database + DatabaseCommit + DatabaseRef + Clone,
260 <CacheDB<DB> as Database>::Error: Clone,
261 <DB as Database>::Error: Clone,
262 {
263 info!("Replaying transaction to collect call trace and touched addresses");
264
265 let mut tracer = CallTracer::new();
266 let mut evm = ctx.build_mainnet_with_inspector(&mut tracer);
267
268 let result = evm
269 .inspect_one_tx(tx)
270 .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
271
272 if let ExecutionResult::Halt { reason, .. } = result {
273 if matches!(reason, HaltReason::OutOfGas { .. }) {
274 error!("EDB cannot debug out-of-gas errors. Proceed at your own risk.")
275 }
276 }
277
278 let result = tracer.into_replay_result();
279
280 for (address, deployed) in &result.visited_addresses {
281 if *deployed {
282 debug!("Contract {} was deployed during transaction replay", address);
283 } else {
284 debug!("Address {} was touched during transaction replay", address);
285 }
286 }
287
288 result.execution_trace.print_trace_tree();
290
291 Ok(result)
292 }
293
294 async fn download_verified_source_code(
296 &self,
297 replay_result: &TraceReplayResult,
298 chain_id: u64,
299 ) -> Result<HashMap<Address, Artifact>> {
300 info!("Downloading verified source code for touched contracts");
301
302 let compiler = OnchainCompiler::new(None)?;
303
304 let compiler_cache_root =
305 EdbCachePath::new(None as Option<PathBuf>).compiler_chain_cache_dir(chain_id);
306
307 let addresses: Vec<_> = replay_result.visited_addresses.keys().copied().collect();
312 let total_contracts = addresses.len();
313
314 let console_bar = std::sync::Arc::new(ProgressBar::new(total_contracts as u64));
315 console_bar.set_style(
316 ProgressStyle::with_template(
317 "{spinner:.green} 📜 Downloading & compiling contracts [{bar:40.cyan/blue}] {pos:>3}/{len:3} 🔧 {msg}"
318 )?
319 .progress_chars("🟩🟦⬜")
320 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
321 );
322
323 let mut artifacts = HashMap::new();
324 for (i, address) in addresses.iter().enumerate() {
325 let short_addr = &address.to_string()[2..10]; console_bar.set_message(format!("contract {}: 0x{}...", i + 1, short_addr));
327
328 let api_key = self.get_etherscan_api_key();
330
331 let etherscan = Client::builder()
332 .with_api_key(api_key)
333 .with_cache(
334 compiler_cache_root.clone(),
335 Duration::from_secs(DEFAULT_ETHERSCAN_CACHE_TTL),
336 ) .chain(chain_id.into())?
338 .build()?;
339
340 match compiler.compile(ðerscan, *address).await {
341 Ok(Some(artifact)) => {
342 console_bar.set_message(format!("✅ 0x{short_addr}... compiled"));
343 artifacts.insert(*address, artifact);
344 }
345 Ok(None) => {
346 console_bar.set_message(format!("⚠️ 0x{short_addr}... no source"));
347 debug!("No source code available for contract {}", address);
348 }
349 Err(e) => {
350 console_bar.set_message(format!("❌ 0x{short_addr}... failed"));
351 warn!("Failed to compile contract {}: {:?}", address, e);
352 }
353 }
354
355 console_bar.inc(1);
356 }
357
358 console_bar.finish_with_message(format!(
359 "✨ Done! Compiled {} out of {} contracts",
360 artifacts.len(),
361 total_contracts
362 ));
363
364 Ok(artifacts)
365 }
366
367 fn analyze_source_code(
369 &self,
370 artifacts: &HashMap<Address, Artifact>,
371 ) -> Result<HashMap<Address, AnalysisResult>> {
372 info!("Analyzing source code to identify instrumentation points");
373
374 let mut analysis_result = HashMap::new();
375 for (address, artifact) in artifacts {
376 debug!("Analyzing contract at address: {}", address);
377 let analysis = analyze(artifact)?;
378 analysis_result.insert(*address, analysis);
379 }
380
381 Ok(analysis_result)
382 }
383
384 fn capture_hook_snapshots<'a, DB>(
386 &self,
387 mut ctx: EdbContext<DB>,
388 mut tx: TxEnv,
389 creation_hooks: Vec<(&'a Contract, &'a Contract, &'a Bytes)>,
390 trace: &Trace,
391 analysis_results: &HashMap<Address, AnalysisResult>,
392 ) -> Result<HookSnapshots<DB>>
393 where
394 DB: Database + DatabaseCommit + DatabaseRef + Clone,
395 <CacheDB<DB> as Database>::Error: Clone,
396 <DB as Database>::Error: Clone,
397 {
398 relax_evm_constraints(&mut ctx, &mut tx);
400
401 info!("Collecting hook snapshots for source code contracts");
402
403 let mut inspector = HookSnapshotInspector::new(trace, analysis_results);
404 inspector.with_creation_hooks(creation_hooks)?;
405 let mut evm = ctx.build_mainnet_with_inspector(&mut inspector);
406
407 evm.inspect_one_tx(tx)
408 .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
409
410 let snapshots = inspector.into_snapshots();
411
412 snapshots.print_summary();
413
414 Ok(snapshots)
415 }
416
417 fn capture_opcode_level_snapshots<DB>(
420 &self,
421 ctx: EdbContext<DB>,
422 tx: TxEnv,
423 excluded_addresses: HashSet<Address>,
424 trace: &Trace,
425 ) -> Result<OpcodeSnapshots<DB>>
426 where
427 DB: Database + DatabaseCommit + DatabaseRef + Clone,
428 <CacheDB<DB> as Database>::Error: Clone,
429 <DB as Database>::Error: Clone,
430 {
431 info!("Collecting opcode-level step execution results");
432
433 let mut inspector = OpcodeSnapshotInspector::new(&ctx, trace);
434 inspector.with_excluded_addresses(excluded_addresses);
435 let mut evm = ctx.build_mainnet_with_inspector(&mut inspector);
436
437 evm.inspect_one_tx(tx)
438 .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
439
440 let snapshots = inspector.into_snapshots();
441
442 snapshots.print_summary();
443
444 Ok(snapshots)
445 }
446
447 fn instrument_and_recompile_source_code(
449 &self,
450 artifacts: &HashMap<Address, Artifact>,
451 analysis_result: &HashMap<Address, AnalysisResult>,
452 ) -> Result<HashMap<Address, Artifact>> {
453 info!("Instrumenting source code based on analysis results");
454
455 let mut recompiled_artifacts = HashMap::new();
456 for (address, artifact) in artifacts {
457 let compiler_version =
458 Version::parse(artifact.compiler_version().trim_start_matches('v'))?;
459
460 let analysis = analysis_result
461 .get(address)
462 .ok_or_else(|| eyre::eyre!("No analysis result found for address {}", address))?;
463
464 let input = instrument(&compiler_version, &artifact.input, analysis)?;
465 let meta = artifact.meta.clone();
466
467 let version = meta.compiler_version()?;
469 let compiler = Solc::find_or_install(&version)?;
470
471 let output = match compiler.compile_exact(&input) {
473 Ok(output) => output,
474 Err(e) => {
475 let (original_dir, instrumented_dir) =
477 dump_source_for_debugging(address, &artifact.input, &input)?;
478
479 return Err(eyre::eyre!(
480 "Failed to recompile contract {}\n\nCompiler error: {}\n\nDebug info:\n Original source: {}\n Instrumented source: {}",
481 address,
482 e,
483 original_dir.display(),
484 instrumented_dir.display()
485 ));
486 }
487 };
488 if output.errors.iter().any(|e| e.is_error()) {
489 let (original_dir, instrumented_dir) =
491 dump_source_for_debugging(address, &artifact.input, &input)?;
492
493 let formatted_errors = format_compiler_errors(&output.errors, &instrumented_dir);
495
496 return Err(eyre::eyre!(
497 "Recompilation failed for contract {}\n\nCompilation errors:{}\n\nDebug info:\n Original source: {}\n Instrumented source: {}",
498 address,
499 formatted_errors,
500 original_dir.display(),
501 instrumented_dir.display()
502 ));
503 }
504
505 debug!(
506 "Recompiled Contract {}: {} vs {}",
507 address,
508 artifact.output.contracts.len(),
509 output.contracts.len()
510 );
511
512 recompiled_artifacts.insert(*address, Artifact { meta, input, output });
513 }
514
515 Ok(recompiled_artifacts)
516 }
517
518 async fn tweak_bytecode<DB>(
520 &self,
521 ctx: &mut EdbContext<DB>,
522 artifacts: &HashMap<Address, Artifact>,
523 recompiled_artifacts: &HashMap<Address, Artifact>,
524 tx_hash: TxHash,
525 ) -> Result<Vec<Address>>
526 where
527 DB: Database + DatabaseCommit + DatabaseRef + Clone,
528 <CacheDB<DB> as Database>::Error: Clone,
529 <DB as Database>::Error: Clone,
530 {
531 let mut tweaker =
532 CodeTweaker::new(ctx, self.rpc_proxy_url.clone(), self.etherscan_api_key.clone());
533
534 let mut contracts_in_tx = Vec::new();
535
536 for (address, recompiled_artifact) in recompiled_artifacts {
537 let creation_tx_hash = tweaker.get_creation_tx(address).await?;
538 if creation_tx_hash == tx_hash {
539 debug!("Skip tweaking contract {}, since it was created by the transaction under investigation", address);
540 contracts_in_tx.push(*address);
541 continue;
542 }
543
544 let artifact = artifacts
545 .get(address)
546 .ok_or_else(|| eyre::eyre!("No original artifact found for address {}", address))?;
547
548 tweaker.tweak(address, artifact, recompiled_artifact, self.quick).await.map_err(
549 |e| eyre::eyre!("Failed to tweak bytecode for contract {}: {}", address, e),
550 )?;
551 }
552
553 Ok(contracts_in_tx)
554 }
555
556 fn collect_creation_hooks<'a>(
558 &self,
559 artifacts: &'a HashMap<Address, Artifact>,
560 recompiled_artifacts: &'a HashMap<Address, Artifact>,
561 contracts_in_tx: Vec<Address>,
562 ) -> Result<Vec<(&'a Contract, &'a Contract, &'a Bytes)>> {
563 info!("Collecting creation hooks for contracts in transaction");
564
565 let mut hook_creation = Vec::new();
566 for address in contracts_in_tx {
567 let Some(artifact) = artifacts.get(&address) else {
568 eyre::bail!("No original artifact found for address {}", address);
569 };
570
571 let Some(recompiled_artifact) = recompiled_artifacts.get(&address) else {
572 eyre::bail!("No recompiled artifact found for address {}", address);
573 };
574
575 hook_creation.extend(artifact.find_creation_hooks(recompiled_artifact));
576 }
577
578 Ok(hook_creation)
579 }
580
581 fn get_time_travel_snapshots<DB>(
583 &self,
584 opcode_snapshots: OpcodeSnapshots<DB>,
585 hook_snapshots: HookSnapshots<DB>,
586 ) -> Result<Snapshots<DB>>
587 where
588 DB: Database + DatabaseCommit + DatabaseRef + Clone,
589 <CacheDB<DB> as Database>::Error: Clone,
590 <DB as Database>::Error: Clone,
591 {
592 let snapshots = Snapshots::merge(opcode_snapshots, hook_snapshots);
593 snapshots.print_summary();
594
595 Ok(snapshots)
596 }
597}
598
599impl Engine {
601 fn get_etherscan_api_key(&self) -> String {
602 self.etherscan_api_key.clone().unwrap_or(next_etherscan_api_key())
603 }
604}
605
606fn sanitize_path(path: &std::path::Path) -> PathBuf {
608 use std::path::Component;
609
610 let mut sanitized = PathBuf::new();
611
612 for component in path.components() {
613 match component {
614 Component::Normal(name) => {
615 sanitized.push(name);
617 }
618 Component::CurDir => {
619 }
621 Component::ParentDir => {
622 warn!("Skipping parent directory component in path: {:?}", path);
624 }
625 Component::RootDir => {
626 warn!("Skipping root directory component in path: {:?}", path);
628 }
629 Component::Prefix(_) => {
630 warn!("Skipping prefix component in path: {:?}", path);
632 }
633 }
634 }
635
636 if sanitized.as_os_str().is_empty() {
638 sanitized.push("unnamed_source");
639 }
640
641 sanitized
642}
643
644fn extract_code_context(
646 file_path: &std::path::Path,
647 start_pos: i32,
648 end_pos: i32,
649 context_lines: usize,
650) -> Option<String> {
651 use std::io::{BufRead, BufReader};
652
653 let file = fs::File::open(file_path).ok()?;
654 let reader = BufReader::new(file);
655 let lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
656
657 let mut current_pos = 0i32;
659 let mut start_line = 0;
660 let mut start_col = 0;
661 let mut end_line = 0;
662 let mut end_col = 0;
663
664 for (line_num, line) in lines.iter().enumerate() {
665 let line_start = current_pos;
666 let line_end = current_pos + line.len() as i32 + 1; if start_pos >= line_start && start_pos < line_end {
669 start_line = line_num;
670 start_col = (start_pos - line_start) as usize;
671 }
672
673 if end_pos >= line_start && end_pos <= line_end {
674 end_line = line_num;
675 end_col = (end_pos - line_start) as usize;
676 }
677
678 current_pos = line_end;
679 }
680
681 let mut context = String::new();
683 let context_start = start_line.saturating_sub(context_lines);
684 let context_end = (end_line + context_lines + 1).min(lines.len());
685
686 for line_num in context_start..context_end {
687 if line_num >= lines.len() {
688 break;
689 }
690
691 let line_number = line_num + 1; let line = &lines[line_num];
693
694 if line_num >= start_line && line_num <= end_line {
696 context.push_str(&format!(" {line_number} | {line}\n"));
698
699 if line_num == start_line {
701 let padding = format!(" {line_number} | ").len();
702 let mut underline = " ".repeat(padding + start_col);
703 let underline_len = if start_line == end_line {
704 (end_col - start_col).max(1)
705 } else {
706 line.len() - start_col
707 };
708 underline.push_str(&"^".repeat(underline_len));
709 context.push_str(&format!("{underline}\n"));
710 }
711 } else {
712 context.push_str(&format!(" {line_number} | {line}\n"));
714 }
715 }
716
717 Some(context)
718}
719
720fn format_compiler_errors(
722 errors: &[foundry_compilers::artifacts::Error],
723 dump_dir: &std::path::Path,
724) -> String {
725 let mut formatted = String::new();
726
727 for error in errors.iter().filter(|e| e.is_error()) {
728 formatted.push_str("\n\n");
729
730 if let Some(error_code) = &error.error_code {
732 formatted.push_str(&format!("Error [{error_code}]: "));
733 } else {
734 formatted.push_str("Error: ");
735 }
736
737 formatted.push_str(&error.message);
739
740 if let Some(loc) = &error.source_location {
742 formatted.push_str(&format!("\n --> {}:{}:{}", loc.file.as_str(), loc.start, loc.end));
743
744 let sanitized_path = sanitize_path(std::path::Path::new(&loc.file));
746 let source_file = dump_dir.join(&sanitized_path);
747
748 if let Some(context) = extract_code_context(&source_file, loc.start, loc.end, 5) {
749 formatted.push_str("\n\n");
750 formatted.push_str(&context);
751 }
752 }
753
754 if let Some(formatted_msg) = &error.formatted_message {
757 if !formatted_msg.trim().is_empty() {
758 formatted.push_str("\n\nCompiler's formatted output:\n");
759 formatted.push_str(formatted_msg);
760 }
761 }
762
763 if !error.secondary_source_locations.is_empty() {
765 for sec_loc in &error.secondary_source_locations {
766 if let Some(msg) = &sec_loc.message {
767 formatted.push_str(&format!("\n Note: {msg}"));
768 }
769 if let Some(file) = &sec_loc.file {
770 formatted.push_str(&format!(
771 "\n --> {}:{}:{}",
772 file,
773 sec_loc.start.map(|s| s.to_string()).unwrap_or_else(|| "?".to_string()),
774 sec_loc.end.map(|e| e.to_string()).unwrap_or_else(|| "?".to_string())
775 ));
776
777 if let (Some(start), Some(end)) = (sec_loc.start, sec_loc.end) {
779 let sanitized_path = sanitize_path(std::path::Path::new(file));
780 let source_file = dump_dir.join(&sanitized_path);
781
782 if let Some(context) = extract_code_context(&source_file, start, end, 1) {
783 formatted.push('\n');
784 formatted.push_str(&context);
785 }
786 }
787 }
788 }
789 }
790 }
791
792 if formatted.is_empty() {
793 formatted.push_str("\nNo specific error details available");
794 }
795
796 formatted
797}
798
799fn dump_source_for_debugging(
801 address: &Address,
802 original_input: &SolcInput,
803 instrumented_input: &SolcInput,
804) -> Result<(PathBuf, PathBuf)> {
805 use std::io::Write;
806
807 let temp_dir = std::env::temp_dir();
809 let debug_dir = temp_dir.join(format!("edb_debug_{address}"));
810 let original_dir = debug_dir.join("original");
811 let instrumented_dir = debug_dir.join("instrumented");
812
813 fs::create_dir_all(&original_dir)?;
815 fs::create_dir_all(&instrumented_dir)?;
816
817 for (path_str, source) in &original_input.sources {
819 let path = std::path::Path::new(path_str);
820 let sanitized_path = sanitize_path(path);
821 let file_path = original_dir.join(&sanitized_path);
822
823 if !file_path.starts_with(&original_dir) {
826 return Err(eyre::eyre!(
827 "Path traversal detected in source path: {}",
828 path_str.display()
829 ));
830 }
831
832 if let Some(parent) = file_path.parent() {
834 fs::create_dir_all(parent)?;
835 }
836
837 let mut file = fs::File::create(&file_path)?;
838 file.write_all(source.content.as_bytes())?;
839 }
840
841 let settings_path = original_dir.join("settings.json");
843 let mut settings_file = fs::File::create(&settings_path)?;
844 settings_file.write_all(serde_json::to_string_pretty(&original_input.settings)?.as_bytes())?;
845
846 for (path_str, source) in &instrumented_input.sources {
848 let path = std::path::Path::new(path_str);
849 let sanitized_path = sanitize_path(path);
850 let file_path = instrumented_dir.join(&sanitized_path);
851
852 if !file_path.starts_with(&instrumented_dir) {
855 return Err(eyre::eyre!(
856 "Path traversal detected in source path: {}",
857 path_str.display()
858 ));
859 }
860
861 if let Some(parent) = file_path.parent() {
863 fs::create_dir_all(parent)?;
864 }
865
866 let mut file = fs::File::create(&file_path)?;
867 file.write_all(source.content.as_bytes())?;
868 }
869
870 let settings_path = instrumented_dir.join("settings.json");
872 let mut settings_file = fs::File::create(&settings_path)?;
873 settings_file
874 .write_all(serde_json::to_string_pretty(&instrumented_input.settings)?.as_bytes())?;
875
876 Ok((original_dir, instrumented_dir))
877}