1use crate::prelude::{SessionSource, SessionSourceConfig};
7use eyre::Result;
8use foundry_cli::utils::parse_json;
9use foundry_evm::{core::evm::FoundryEvmNetwork, executors::ExecutorBuilder};
10use serde::{Deserialize, Serialize};
11use std::{
12 fs::{self, File},
13 io::{Read, Write},
14 path::{Path, PathBuf},
15};
16use time::{OffsetDateTime, format_description};
17
18#[cfg(unix)]
19use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
20
21fn validate_session_id(id: &str) -> Result<()> {
26 if id.is_empty() || id == "." || id == ".." || id.contains(['/', '\\', ':']) {
27 eyre::bail!(
28 "invalid Chisel session id `{id}`: must not be empty, `.`, `..`, or contain a path \
29 separator or `:`"
30 );
31 }
32 Ok(())
33}
34
35#[derive(Debug, Serialize, Deserialize)]
37#[serde(bound = "")]
38pub struct ChiselSession<FEN: FoundryEvmNetwork> {
39 pub source: SessionSource<FEN>,
41 pub id: Option<String>,
43}
44
45impl<FEN: FoundryEvmNetwork> ChiselSession<FEN> {
47 fn deserialize_cached(contents: &str, executor_builder: ExecutorBuilder<FEN>) -> Result<Self> {
48 let mut session: Self = parse_json(contents)?;
49 session.source.config.clear_credentials();
50 session.source.config.foundry_config.force = false;
52 session.source.config.executor_builder = executor_builder;
53 Ok(session)
54 }
55
56 pub fn new(config: SessionSourceConfig<FEN>) -> Result<Self> {
66 Ok(Self { source: SessionSource::new(config)?, id: None })
68 }
69
70 pub fn contract_source(&self) -> String {
81 self.source.to_repl_source()
82 }
83
84 pub fn clear_cache() -> Result<()> {
91 let cache_dir = Self::cache_dir()?;
92 for entry in std::fs::read_dir(cache_dir)? {
93 let entry = entry?;
94 let path = entry.path();
95 if path.is_dir() {
96 std::fs::remove_dir_all(path)?;
97 } else {
98 std::fs::remove_file(path)?;
99 }
100 }
101 Ok(())
102 }
103
104 pub fn remove_cached_session(id: &str) -> Result<()> {
106 validate_session_id(id)?;
107 let cache_file = format!("{}chisel-{id}.json", Self::cache_dir()?);
108 match std::fs::remove_file(cache_file) {
109 Ok(()) => Ok(()),
110 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
111 Err(error) => Err(error.into()),
112 }
113 }
114
115 pub fn write(&mut self) -> Result<String> {
121 self.write_to(&Self::cache_dir()?)
122 }
123
124 fn write_to(&mut self, cache_dir: &str) -> Result<String> {
125 if let Some(id) = &self.id {
126 validate_session_id(id)?;
127 }
128 Self::secure_cache_dir(cache_dir)?;
129
130 let cache_file_name = match self.id.as_ref() {
131 Some(id) => {
132 format!("{cache_dir}chisel-{id}.json")
134 }
135 None => {
136 let (id, file_name) = Self::next_cached_session_in(cache_dir)?;
138 self.id = Some(id);
140 file_name
142 }
143 };
144
145 let mut file = tempfile::NamedTempFile::new_in(cache_dir)?;
148 #[cfg(unix)]
149 file.as_file().set_permissions(fs::Permissions::from_mode(0o600))?;
150 serde_json::to_writer_pretty(&mut file, self)?;
151 file.flush()?;
152 file.as_file().sync_all()?;
153 file.persist(&cache_file_name).map_err(|err| err.error)?;
154
155 Ok(cache_file_name)
158 }
159
160 pub fn next_cached_session() -> Result<(String, String)> {
168 Self::next_cached_session_in(&Self::cache_dir()?)
169 }
170
171 fn next_cached_session_in(cache_dir: &str) -> Result<(String, String)> {
172 let next_id = std::fs::read_dir(cache_dir)?
173 .filter_map(|entry| entry.ok())
174 .filter_map(|entry| {
175 entry
176 .file_name()
177 .to_str()?
178 .strip_prefix("chisel-")?
179 .strip_suffix(".json")?
180 .parse::<usize>()
181 .ok()
182 })
183 .max()
184 .map_or(Some(0), |max| max.checked_add(1))
185 .ok_or_else(|| eyre::eyre!("no unused chisel session id available"))?;
186
187 Ok((format!("{next_id}"), format!("{cache_dir}chisel-{next_id}.json")))
188 }
189
190 pub fn cache_dir() -> Result<String> {
196 let home_dir =
197 dirs::home_dir().ok_or_else(|| eyre::eyre!("Failed to grab home directory"))?;
198 let home_dir_str = home_dir
199 .to_str()
200 .ok_or_else(|| eyre::eyre!("Failed to convert home directory to string"))?;
201 Ok(format!("{home_dir_str}/.foundry/cache/chisel/"))
202 }
203
204 pub fn create_cache_dir() -> Result<()> {
210 Self::secure_cache_dir(&Self::cache_dir()?)
211 }
212
213 pub fn get_sessions() -> Result<Vec<(String, String)>> {
215 let cache_dir = Self::cache_dir()?;
217 let entries = Self::cached_session_files(&cache_dir)?;
218
219 let mut sessions = Vec::new();
221 for entry in entries {
222 let modified_time = entry.metadata()?.modified()?;
223 let file_name = entry.file_name();
224 let file_name = file_name
225 .into_string()
226 .map_err(|e| eyre::eyre!(format!("{}", e.to_string_lossy())))?;
227 sessions.push((
228 OffsetDateTime::from(modified_time).format(&format_description::parse(
229 "[year]-[month]-[day] [hour]:[minute]:[second]",
230 )?)?,
231 file_name,
232 ));
233 }
234 Ok(sessions)
235 }
236
237 pub fn load(id: &str, executor_builder: ExecutorBuilder<FEN>) -> Result<Self> {
247 Self::load_from(id, &Self::cache_dir()?, executor_builder)
248 }
249
250 fn load_from(
251 id: &str,
252 cache_dir: &str,
253 executor_builder: ExecutorBuilder<FEN>,
254 ) -> Result<Self> {
255 validate_session_id(id)?;
256 Self::secure_cache_dir(cache_dir)?;
257 let contents = Self::read_cached_file(Path::new(&format!("{cache_dir}chisel-{id}.json")))?;
258 let mut session = Self::deserialize_cached(&contents, executor_builder)?;
259 session.id = Some(id.to_string());
261 Ok(session)
262 }
263
264 pub fn latest_cached_session() -> Result<String> {
270 Self::latest_cached_session_in(&Self::cache_dir()?)
271 }
272
273 fn latest_cached_session_in(cache_dir: &str) -> Result<String> {
274 let mut entries = Self::cached_session_files(cache_dir)?.into_iter();
275 let mut latest = entries.next().ok_or_else(|| eyre::eyre!("No entries found!"))?;
276 for entry in entries {
277 if entry.metadata()?.modified()? > latest.metadata()?.modified()? {
278 latest = entry;
279 }
280 }
281 Ok(latest
282 .path()
283 .to_str()
284 .ok_or_else(|| eyre::eyre!("Failed to get session path!"))?
285 .to_string())
286 }
287
288 pub fn latest(executor_builder: ExecutorBuilder<FEN>) -> Result<Self> {
294 Self::latest_from(&Self::cache_dir()?, executor_builder)
295 }
296
297 fn latest_from(cache_dir: &str, executor_builder: ExecutorBuilder<FEN>) -> Result<Self> {
298 let last_session = Self::latest_cached_session_in(cache_dir)?;
299 let last_session_contents = Self::read_cached_file(Path::new(&last_session))?;
300 let mut session = Self::deserialize_cached(&last_session_contents, executor_builder)?;
301 session.id = Self::session_id_from_cache_file_name(&last_session);
303 Ok(session)
304 }
305
306 fn session_id_from_cache_file_name(path: &str) -> Option<String> {
308 Path::new(path).file_stem()?.to_str()?.strip_prefix("chisel-").map(str::to_string)
309 }
310
311 fn secure_cache_dir(cache_dir: &str) -> Result<()> {
313 let path = Path::new(cache_dir).components().collect::<PathBuf>();
315 let mut builder = fs::DirBuilder::new();
316 builder.recursive(true);
317 #[cfg(unix)]
318 builder.mode(0o700);
319 builder.create(&path)?;
320 eyre::ensure!(
321 fs::symlink_metadata(&path)?.is_dir(),
322 "Chisel cache must be a directory, not a symlink"
323 );
324 #[cfg(unix)]
325 fs::set_permissions(&path, fs::Permissions::from_mode(0o700))?;
326 Ok(())
327 }
328
329 fn read_cached_file(path: &Path) -> Result<String> {
331 eyre::ensure!(
332 fs::symlink_metadata(path)?.is_file(),
333 "Chisel session must be a regular file"
334 );
335 let mut file = File::open(path)?;
336 eyre::ensure!(file.metadata()?.is_file(), "Chisel session must be a regular file");
337 #[cfg(unix)]
338 file.set_permissions(fs::Permissions::from_mode(0o600))?;
339 let mut contents = String::new();
340 file.read_to_string(&mut contents)?;
341 Ok(contents)
342 }
343
344 fn cached_session_files(cache_dir: &str) -> Result<Vec<fs::DirEntry>> {
346 Self::secure_cache_dir(cache_dir)?;
347 let mut sessions = Vec::new();
348 for entry in fs::read_dir(cache_dir)? {
349 let entry = entry?;
350 if entry.file_type()?.is_file()
351 && let Some(name) = entry.file_name().to_str()
352 && let Some(id) =
353 name.strip_prefix("chisel-").and_then(|name| name.strip_suffix(".json"))
354 && validate_session_id(id).is_ok()
355 {
356 sessions.push(entry);
357 }
358 }
359 Ok(sessions)
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use foundry_config::{Config, SolcReq};
367 use foundry_evm::core::evm::EthEvmNetwork;
368 use semver::Version;
369
370 #[cfg(unix)]
371 use std::os::unix::fs::symlink;
372
373 #[cfg(feature = "monad")]
374 use foundry_evm::core::{constants::MONAD_CHEATCODE_ADDRESS, evm::MonadEvmNetwork};
375
376 #[test]
378 fn next_cached_session_skips_gaps_left_by_deleted_sessions() {
379 let dir = tempfile::tempdir().unwrap();
380 let cache_dir = format!("{}/", dir.path().to_str().unwrap());
381
382 std::fs::write(format!("{cache_dir}chisel-0.json"), "{\"id\":\"0\"}").unwrap();
384 std::fs::write(format!("{cache_dir}chisel-2.json"), "{\"id\":\"2\"}").unwrap();
385
386 let (next_id, next_file) =
387 ChiselSession::<EthEvmNetwork>::next_cached_session_in(&cache_dir).unwrap();
388
389 assert_eq!(next_id, "3", "must skip past the gap instead of reusing the occupied id 2");
391 assert_eq!(next_file, format!("{cache_dir}chisel-3.json"));
392
393 assert_eq!(
394 std::fs::read_to_string(format!("{cache_dir}chisel-0.json")).unwrap(),
395 "{\"id\":\"0\"}"
396 );
397 assert_eq!(
398 std::fs::read_to_string(format!("{cache_dir}chisel-2.json")).unwrap(),
399 "{\"id\":\"2\"}"
400 );
401 }
402
403 #[test]
404 fn next_cached_session_does_not_overflow_on_a_usize_max_named_session() {
405 let dir = tempfile::tempdir().unwrap();
406 let cache_dir = format!("{}/", dir.path().to_str().unwrap());
407 std::fs::write(format!("{cache_dir}chisel-{}.json", usize::MAX), "{}").unwrap();
408
409 let result = ChiselSession::<EthEvmNetwork>::next_cached_session_in(&cache_dir);
410 assert!(result.is_err(), "must error instead of panicking or wrapping to a reused id");
411 }
412
413 #[test]
414 fn deserialized_sessions_do_not_restore_force() {
415 let session = ChiselSession::<EthEvmNetwork>::new(SessionSourceConfig {
416 foundry_config: Config {
417 force: true,
418 solc: Some(SolcReq::Version(Version::new(0, 8, 29))),
419 ..Default::default()
420 },
421 no_vm: true,
422 ..Default::default()
423 })
424 .unwrap();
425 assert!(session.source.config.foundry_config.force);
426
427 let serialized = serde_json::to_string(&session).unwrap();
428 let session = ChiselSession::<EthEvmNetwork>::deserialize_cached(
429 &serialized,
430 ExecutorBuilder::<EthEvmNetwork>::new(),
431 )
432 .unwrap();
433
434 assert!(!session.source.config.foundry_config.force);
435 }
436
437 #[cfg(feature = "monad")]
438 #[test]
439 fn deserialized_sessions_use_active_monad_tooling() {
440 let session = ChiselSession::<MonadEvmNetwork>::new(SessionSourceConfig {
441 executor_builder: ExecutorBuilder::<MonadEvmNetwork>::new(),
442 ..Default::default()
443 })
444 .unwrap();
445 let serialized = serde_json::to_string(&session).unwrap();
446
447 let session = ChiselSession::<MonadEvmNetwork>::deserialize_cached(
448 &serialized,
449 ExecutorBuilder::<MonadEvmNetwork>::new(),
450 )
451 .unwrap();
452
453 assert_eq!(
454 session.source.config.executor_builder.extra_cheatcode_addresses(),
455 &[MONAD_CHEATCODE_ADDRESS]
456 );
457 }
458
459 #[test]
464 fn path_traversal_ids_are_rejected() {
465 for id in [
466 "../evil",
467 "x/../../../../../../tmp/pwned",
468 "..",
469 ".",
470 "",
471 "sub/dir",
472 "back\\slash",
473 ":colon",
474 "foo:bar",
475 "session:1",
476 ] {
477 let err = validate_session_id(id).unwrap_err();
478 assert!(err.to_string().contains("invalid Chisel session id"), "{id:?}: {err}");
479 }
480
481 for id in ["0", "42", "my-session", "my_session"] {
483 validate_session_id(id).unwrap();
484 }
485 }
486
487 #[test]
488 fn load_rejects_path_traversal_id() {
489 let err = ChiselSession::<EthEvmNetwork>::load(
490 "../../evil",
491 ExecutorBuilder::<EthEvmNetwork>::new(),
492 )
493 .unwrap_err();
494 assert!(err.to_string().contains("invalid Chisel session id"), "{err}");
495 }
496
497 #[test]
498 fn remove_cached_session_rejects_path_traversal_id() {
499 let err = ChiselSession::<EthEvmNetwork>::remove_cached_session("../../evil").unwrap_err();
500 assert!(err.to_string().contains("invalid Chisel session id"), "{err}");
501 }
502
503 fn session_for_normalization_tests() -> ChiselSession<EthEvmNetwork> {
504 ChiselSession::<EthEvmNetwork>::new(SessionSourceConfig {
505 foundry_config: Config {
506 solc: Some(SolcReq::Version(Version::new(0, 8, 29))),
507 ..Default::default()
508 },
509 no_vm: true,
510 ..Default::default()
511 })
512 .unwrap()
513 }
514
515 #[test]
517 fn load_normalizes_id_ignoring_a_stale_or_missing_embedded_id() {
518 let dir = tempfile::tempdir().unwrap();
519 let cache_dir = format!("{}/", dir.path().to_str().unwrap());
520
521 let mut session = session_for_normalization_tests();
522 session.id = Some("stale-name".to_string());
523 let serialized = serde_json::to_string(&session).unwrap();
524 std::fs::write(format!("{cache_dir}chisel-5.json"), &serialized).unwrap();
525
526 let loaded = ChiselSession::<EthEvmNetwork>::load_from(
527 "5",
528 &cache_dir,
529 ExecutorBuilder::<EthEvmNetwork>::new(),
530 )
531 .unwrap();
532 assert_eq!(loaded.id.as_deref(), Some("5"), "must use the requested id, not the stale one");
533
534 let without_id = serialized.replacen("\"stale-name\"", "null", 1);
535 std::fs::write(format!("{cache_dir}chisel-7.json"), without_id).unwrap();
536 let loaded = ChiselSession::<EthEvmNetwork>::load_from(
537 "7",
538 &cache_dir,
539 ExecutorBuilder::<EthEvmNetwork>::new(),
540 )
541 .unwrap();
542 assert_eq!(loaded.id.as_deref(), Some("7"), "a null embedded id must not survive the load");
543 }
544
545 #[test]
546 fn latest_normalizes_id_from_the_resolved_file_name() {
547 let dir = tempfile::tempdir().unwrap();
548 let cache_dir = format!("{}/", dir.path().to_str().unwrap());
549
550 let session = session_for_normalization_tests();
551 let serialized = serde_json::to_string(&session).unwrap();
553 std::fs::write(format!("{cache_dir}chisel-9.json"), serialized).unwrap();
554
555 let loaded = ChiselSession::<EthEvmNetwork>::latest_from(
556 &cache_dir,
557 ExecutorBuilder::<EthEvmNetwork>::new(),
558 )
559 .unwrap();
560 assert_eq!(loaded.id.as_deref(), Some("9"));
561 }
562
563 #[test]
564 fn session_id_from_cache_file_name_strips_prefix_and_extension() {
565 assert_eq!(
566 ChiselSession::<EthEvmNetwork>::session_id_from_cache_file_name(
567 "/home/user/.foundry/cache/chisel/chisel-42.json"
568 ),
569 Some("42".to_string())
570 );
571 assert_eq!(
572 ChiselSession::<EthEvmNetwork>::session_id_from_cache_file_name(
573 "/home/user/.foundry/cache/chisel/not-a-session-file.json"
574 ),
575 None
576 );
577 }
578
579 #[test]
580 fn write_rejects_path_traversal_id() {
581 let mut session = ChiselSession::<EthEvmNetwork>::new(SessionSourceConfig {
582 foundry_config: Config {
583 solc: Some(SolcReq::Version(Version::new(0, 8, 29))),
584 ..Default::default()
585 },
586 no_vm: true,
587 ..Default::default()
588 })
589 .unwrap();
590 session.id = Some("../../evil".to_string());
591
592 let err = session.write().unwrap_err();
593 assert!(err.to_string().contains("invalid Chisel session id"), "{err}");
594 }
595
596 #[cfg(unix)]
597 #[test]
598 fn saved_sessions_are_private() {
599 let dir = tempfile::tempdir().unwrap();
600 let cache = dir.path().join("chisel");
601 std::fs::create_dir(&cache).unwrap();
602 std::fs::set_permissions(&cache, std::fs::Permissions::from_mode(0o755)).unwrap();
603 let file = cache.join("chisel-private.json");
604 std::fs::write(&file, "old session").unwrap();
605 std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
606 let mut session = session_for_normalization_tests();
607 session.id = Some("private".to_string());
608 session.source.run_code = "uint256 privateValue = 42;".to_string();
609
610 session.write_to(&format!("{}/", cache.display())).unwrap();
611
612 assert_eq!(std::fs::metadata(&cache).unwrap().permissions().mode() & 0o777, 0o700);
613 assert_eq!(std::fs::metadata(&file).unwrap().permissions().mode() & 0o777, 0o600);
614 let saved: ChiselSession<EthEvmNetwork> =
615 serde_json::from_str(&std::fs::read_to_string(file).unwrap()).unwrap();
616 assert_eq!(saved.source.run_code, session.source.run_code);
617 }
618
619 #[cfg(unix)]
620 #[test]
621 fn new_cache_directories_are_private_without_changing_existing_parents() {
622 let dir = tempfile::tempdir().unwrap();
623 fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap();
624 let cache = dir.path().join(".foundry/cache/chisel");
625 let mut session = session_for_normalization_tests();
626
627 let file = session.write_to(&format!("{}/", cache.display())).unwrap();
628
629 assert_eq!(fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777, 0o755);
630 for path in [dir.path().join(".foundry"), dir.path().join(".foundry/cache"), cache] {
631 assert_eq!(fs::metadata(path).unwrap().permissions().mode() & 0o777, 0o700);
632 }
633 assert_eq!(fs::metadata(file).unwrap().permissions().mode() & 0o777, 0o600);
634 }
635
636 #[cfg(unix)]
637 #[test]
638 fn saving_replaces_symlinks_without_changing_their_targets() {
639 let dir = tempfile::tempdir().unwrap();
640 let target = dir.path().join("unrelated.json");
641 fs::write(&target, "untouched").unwrap();
642 fs::set_permissions(&target, fs::Permissions::from_mode(0o644)).unwrap();
643 let cache = dir.path().join("chisel");
644 fs::create_dir(&cache).unwrap();
645 let destination = cache.join("chisel-linked.json");
646 symlink(&target, &destination).unwrap();
647 let mut session = session_for_normalization_tests();
648 session.id = Some("linked".to_string());
649
650 session.write_to(&format!("{}/", cache.display())).unwrap();
651
652 assert!(fs::symlink_metadata(&destination).unwrap().is_file());
653 assert_eq!(fs::metadata(destination).unwrap().permissions().mode() & 0o777, 0o600);
654 assert_eq!(fs::read_to_string(&target).unwrap(), "untouched");
655 assert_eq!(fs::metadata(target).unwrap().permissions().mode() & 0o777, 0o644);
656 }
657
658 #[cfg(unix)]
659 #[test]
660 fn cache_directory_symlinks_are_rejected_without_changing_targets() {
661 let dir = tempfile::tempdir().unwrap();
662 let target = dir.path().join("unrelated");
663 fs::create_dir(&target).unwrap();
664 fs::set_permissions(&target, fs::Permissions::from_mode(0o755)).unwrap();
665 let cache = dir.path().join("chisel");
666 symlink(&target, &cache).unwrap();
667 let mut session = session_for_normalization_tests();
668
669 let result = session.write_to(&format!("{}/", cache.display()));
670
671 assert_eq!(
672 result.unwrap_err().to_string(),
673 "Chisel cache must be a directory, not a symlink"
674 );
675 assert_eq!(fs::metadata(&target).unwrap().permissions().mode() & 0o777, 0o755);
676 assert_eq!(fs::read_dir(target).unwrap().count(), 0);
677 }
678
679 #[cfg(unix)]
680 #[test]
681 fn loading_legacy_sessions_restricts_file_and_directory_permissions() {
682 let dir = tempfile::tempdir().unwrap();
683 let cache_dir = format!("{}/", dir.path().display());
684 let path = dir.path().join("chisel-legacy.json");
685 let session = session_for_normalization_tests();
686 fs::write(&path, serde_json::to_vec(&session).unwrap()).unwrap();
687 fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap();
688 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
689
690 let loaded = ChiselSession::<EthEvmNetwork>::load_from(
691 "legacy",
692 &cache_dir,
693 ExecutorBuilder::<EthEvmNetwork>::new(),
694 )
695 .unwrap();
696
697 assert_eq!(loaded.id.as_deref(), Some("legacy"));
698 assert_eq!(fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777, 0o700);
699 assert_eq!(fs::metadata(path).unwrap().permissions().mode() & 0o777, 0o600);
700 }
701
702 #[cfg(unix)]
703 #[test]
704 fn loading_rejects_symlinks_without_changing_their_targets() {
705 let dir = tempfile::tempdir().unwrap();
706 let target = dir.path().join("unrelated.json");
707 fs::write(&target, "untouched").unwrap();
708 fs::set_permissions(&target, fs::Permissions::from_mode(0o644)).unwrap();
709 symlink(&target, dir.path().join("chisel-linked.json")).unwrap();
710
711 let result = ChiselSession::<EthEvmNetwork>::load_from(
712 "linked",
713 &format!("{}/", dir.path().display()),
714 ExecutorBuilder::<EthEvmNetwork>::new(),
715 );
716
717 assert_eq!(result.unwrap_err().to_string(), "Chisel session must be a regular file");
718 assert_eq!(fs::read_to_string(&target).unwrap(), "untouched");
719 assert_eq!(fs::metadata(target).unwrap().permissions().mode() & 0o777, 0o644);
720 }
721
722 #[test]
723 fn session_discovery_ignores_incomplete_and_unrelated_files() {
724 let dir = tempfile::tempdir().unwrap();
725 let cache_dir = format!("{}/", dir.path().display());
726 let mut session = session_for_normalization_tests();
727 session.id = Some("saved".to_string());
728 let saved = session.write_to(&cache_dir).unwrap();
729 fs::write(dir.path().join(".tmp-incomplete"), "{").unwrap();
730 fs::write(dir.path().join("unrelated.json"), "unrelated").unwrap();
731 fs::write(dir.path().join("chisel-.json"), "invalid").unwrap();
732 fs::create_dir(dir.path().join("chisel-directory.json")).unwrap();
733 #[cfg(unix)]
734 symlink(&saved, dir.path().join("chisel-linked.json")).unwrap();
735
736 let sessions = ChiselSession::<EthEvmNetwork>::cached_session_files(&cache_dir).unwrap();
737
738 assert_eq!(sessions.len(), 1);
739 assert_eq!(sessions[0].path(), Path::new(&saved));
740 assert_eq!(
741 ChiselSession::<EthEvmNetwork>::latest_cached_session_in(&cache_dir).unwrap(),
742 saved
743 );
744 let latest = ChiselSession::<EthEvmNetwork>::latest_from(
745 &cache_dir,
746 ExecutorBuilder::<EthEvmNetwork>::new(),
747 )
748 .unwrap();
749 assert_eq!(latest.id.as_deref(), Some("saved"));
750 }
751
752 #[test]
753 fn failed_save_cleans_up_temporary_files() {
754 let dir = tempfile::tempdir().unwrap();
755 let cache_dir = format!("{}/", dir.path().display());
756 let destination = dir.path().join("chisel-blocked.json");
757 fs::create_dir(&destination).unwrap();
758 fs::write(destination.join("sentinel"), "untouched").unwrap();
759 let mut session = session_for_normalization_tests();
760 session.id = Some("blocked".to_string());
761
762 assert!(session.write_to(&cache_dir).is_err());
763
764 assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
765 assert_eq!(fs::read_to_string(destination.join("sentinel")).unwrap(), "untouched");
766 }
767
768 #[test]
769 fn loading_legacy_fork_sessions_discards_cached_credentials() {
770 let dir = tempfile::tempdir().unwrap();
771 let cache_dir = format!("{}/", dir.path().display());
772 let mut session = session_for_normalization_tests();
773 session.source.run_code = "uint256 privateValue = 42;".into();
774 session.source.config.calldata = Some(vec![0xde, 0xad, 0xbe, 0xef]);
775 let mut legacy = serde_json::to_value(&session).unwrap();
776 let config = &mut legacy["source"]["config"];
777 config.as_object_mut().unwrap().remove("fork_url_required");
778 config["foundry_config"]["eth_rpc_url"] = "https://rpc.invalid/legacy-token".into();
779 config["foundry_config"]["eth_rpc_jwt"] = "legacy-jwt".into();
780 config["foundry_config"]["eth_rpc_headers"] =
781 serde_json::json!(["Authorization: legacy-header"]);
782 config["foundry_config"]["etherscan_api_key"] = "legacy-api-key".into();
783 config["foundry_config"]["etherscan"] = serde_json::json!({
784 "mainnet": { "key": "legacy-explorer-key", "chain": 1 }
785 });
786 config["foundry_config"]["rpc_endpoints"] = serde_json::json!({
787 "mainnet": "https://rpc.invalid/legacy-endpoint"
788 });
789 config["evm_opts"]["eth_rpc_url"] = "https://rpc.invalid/legacy-token".into();
790 config["evm_opts"]["eth_rpc_jwt"] = "legacy-jwt".into();
791 config["evm_opts"]["eth_rpc_headers"] = serde_json::json!(["Authorization: legacy-header"]);
792 config["evm_opts"]["fork_headers"] =
793 serde_json::json!(["Authorization: legacy-fork-header"]);
794 let path = dir.path().join("chisel-legacy.json");
795 fs::write(&path, serde_json::to_vec(&legacy).unwrap()).unwrap();
796
797 for loaded in [
798 ChiselSession::<EthEvmNetwork>::load_from(
799 "legacy",
800 &cache_dir,
801 ExecutorBuilder::<EthEvmNetwork>::new(),
802 )
803 .unwrap(),
804 ChiselSession::<EthEvmNetwork>::latest_from(
805 &cache_dir,
806 ExecutorBuilder::<EthEvmNetwork>::new(),
807 )
808 .unwrap(),
809 ] {
810 let config = &loaded.source.config;
811 assert!(config.fork_url_required);
812 assert_eq!(config.foundry_config.eth_rpc_url, None);
813 assert_eq!(config.foundry_config.eth_rpc_jwt, None);
814 assert_eq!(config.foundry_config.eth_rpc_headers, None);
815 assert_eq!(config.foundry_config.etherscan_api_key, None);
816 assert!(config.foundry_config.etherscan.is_empty());
817 assert!(config.foundry_config.rpc_endpoints.is_empty());
818 assert_eq!(config.evm_opts.fork_url, None);
819 assert_eq!(config.evm_opts.rpc_jwt, None);
820 assert_eq!(config.evm_opts.rpc_headers, None);
821 assert_eq!(config.evm_opts.fork_headers, None);
822 assert_eq!(config.calldata, session.source.config.calldata);
823 assert_eq!(loaded.source.run_code, session.source.run_code);
824 }
825 }
826}