Skip to main content

soroban_cli/commands/cache/actionlog/
read.rs

1use 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    /// ID of the cache entry
23    #[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}