soroban_cli/commands/cache/actionlog/
read.rs1use std::io;
2
3use crate::config::{data, locator};
4
5#[derive(thiserror::Error, Debug)]
6pub enum Error {
7 #[error(transparent)]
8 Config(#[from] locator::Error),
9 #[error(transparent)]
10 Data(#[from] data::Error),
11 #[error("failed to find cache entry {0}")]
12 NotFound(String),
13 #[error("invalid cache entry ID \"{0}\": expected a ULID")]
14 InvalidId(String),
15 #[error(transparent)]
16 Io(#[from] std::io::Error),
17}
18
19#[derive(Debug, clap::Parser, Clone)]
20#[group(skip)]
21pub struct Cmd {
22 #[arg(long)]
24 pub id: String,
25}
26
27impl Cmd {
28 pub fn run(&self) -> Result<(), Error> {
29 let id: ulid::Ulid = self
30 .id
31 .parse()
32 .map_err(|_| Error::InvalidId(self.id.clone()))?;
33 let file = data::actions_dir()?
34 .join(id.to_string())
35 .with_extension("json");
36 tracing::debug!("reading file {}", file.display());
37 let mut f = std::fs::File::open(&file).map_err(|e| {
38 if e.kind() == io::ErrorKind::NotFound {
39 Error::NotFound(self.id.clone())
40 } else {
41 Error::Io(e)
42 }
43 })?;
44 io::copy(&mut f, &mut io::stdout())?;
45 Ok(())
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::test_utils::with_env_set;
53 use serial_test::serial;
54
55 #[test]
56 #[serial]
57 fn path_traversal_via_dotdot_is_rejected() {
58 let tmp = tempfile::tempdir().unwrap();
59
60 with_env_set("STELLAR_DATA_HOME", tmp.path(), || {
61 let outside = tmp.path().join("outside.json");
62 std::fs::write(&outside, r#"{"leaked":true}"#).unwrap();
63
64 let cmd = Cmd {
65 id: "../outside".to_string(),
66 };
67
68 let err = cmd.run().expect_err("expected error for path-traversal ID");
69 assert!(
70 matches!(err, Error::InvalidId(_)),
71 "expected InvalidId, got {err:?}"
72 );
73 });
74 }
75
76 #[test]
77 #[serial]
78 fn absolute_path_id_is_rejected() {
79 let tmp = tempfile::tempdir().unwrap();
80
81 with_env_set("STELLAR_DATA_HOME", tmp.path(), || {
82 let outside = tmp.path().join("outside.json");
83 std::fs::write(&outside, r#"{"leaked":true}"#).unwrap();
84
85 let abs_id = outside
86 .to_str()
87 .unwrap()
88 .trim_end_matches(".json")
89 .to_string();
90 let cmd = Cmd { id: abs_id };
91
92 let err = cmd.run().expect_err("expected error for absolute-path ID");
93 assert!(
94 matches!(err, Error::InvalidId(_)),
95 "expected InvalidId, got {err:?}"
96 );
97 });
98 }
99}