Skip to main content

soroban_cli/config/
upgrade_check.rs

1use crate::config::locator;
2use chrono::{DateTime, Utc};
3use semver::Version;
4use serde::Deserialize;
5use serde::Serialize;
6use serde_json;
7use std::fs;
8
9use super::data::project_dir;
10
11const FILE_NAME: &str = "upgrade_check.json";
12
13/// The `UpgradeCheck` struct represents the state of the upgrade check.
14/// This state is global and stored in the `upgrade_check.json` file in
15/// the global configuration directory.
16#[derive(Serialize, Deserialize, Debug, PartialEq)]
17pub struct UpgradeCheck {
18    /// The time of the latest check for a new version of the CLI.
19    pub latest_check_time: DateTime<Utc>,
20    /// The latest stable version of the CLI available on crates.io.
21    pub max_stable_version: Version,
22    /// The latest version of the CLI available on crates.io, including pre-releases.
23    pub max_version: Version,
24}
25
26impl Default for UpgradeCheck {
27    fn default() -> Self {
28        Self {
29            latest_check_time: DateTime::<Utc>::UNIX_EPOCH,
30            max_stable_version: Version::new(0, 0, 0),
31            max_version: Version::new(0, 0, 0),
32        }
33    }
34}
35
36impl UpgradeCheck {
37    /// Loads the state of the upgrade check from the global configuration directory.
38    /// If the file doesn't exist, returns a default instance of `UpgradeCheck`.
39    pub fn load() -> Result<Self, locator::Error> {
40        let path = project_dir()
41            .map_err(|_| locator::Error::ProjectDirsError())?
42            .data_dir()
43            .join(FILE_NAME);
44
45        if !path.exists() {
46            return Ok(Self::default());
47        }
48
49        let data = fs::read(&path)
50            .map_err(|error| locator::Error::UpgradeCheckReadFailed { path, error })?;
51
52        Ok(serde_json::from_slice(data.as_slice())?)
53    }
54
55    /// Saves the state of the upgrade check to the `upgrade_check.json` file in the global data directory.
56    pub fn save(&self) -> Result<(), locator::Error> {
57        let path = project_dir()
58            .map_err(|_| locator::Error::ProjectDirsError())?
59            .data_dir()
60            .join(FILE_NAME);
61
62        let path = locator::ensure_directory(path)?;
63        let data = serde_json::to_string(self).map_err(|_| locator::Error::ConfigSerialization)?;
64        locator::write_hardened_file(&path, data.as_bytes())
65            .map_err(|error| locator::Error::UpgradeCheckWriteFailed { path, error })
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use serial_test::serial;
73    use std::env;
74
75    #[test]
76    #[serial]
77    fn test_upgrade_check_load_save() {
78        // Use `STELLAR_DATA_HOME` (cross-platform, highest priority) so that
79        // any `STELLAR_DATA_HOME` or `XDG_DATA_HOME` leaked by parallel tests
80        // cannot shadow our temp dir.
81        let temp_dir = tempfile::tempdir().unwrap();
82        env::remove_var("XDG_DATA_HOME");
83        env::set_var("STELLAR_DATA_HOME", temp_dir.path());
84        // Test default loading
85        let default_check = UpgradeCheck::load().unwrap();
86        assert_eq!(default_check, UpgradeCheck::default());
87        assert_eq!(
88            default_check.latest_check_time,
89            DateTime::<Utc>::from_timestamp_millis(0).unwrap()
90        );
91        assert_eq!(default_check.max_stable_version, Version::new(0, 0, 0));
92
93        // Test saving and loading
94        let saved_check = UpgradeCheck {
95            latest_check_time: DateTime::<Utc>::from_timestamp(1_234_567_890, 0).unwrap(),
96            max_stable_version: Version::new(1, 2, 3),
97            max_version: Version::parse("1.2.4-rc.1").unwrap(),
98        };
99        saved_check.save().unwrap();
100        let loaded_check = UpgradeCheck::load().unwrap();
101        assert_eq!(loaded_check, saved_check);
102    }
103
104    #[cfg(unix)]
105    #[test]
106    #[serial]
107    fn test_upgrade_check_save_uses_0600_permissions() {
108        use crate::test_utils::with_env_set;
109        use std::os::unix::fs::PermissionsExt;
110
111        let temp_dir = tempfile::tempdir().unwrap();
112        with_env_set("STELLAR_DATA_HOME", temp_dir.path(), || {
113            UpgradeCheck::default().save().unwrap();
114
115            let path = project_dir().unwrap().data_dir().join(FILE_NAME);
116            let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
117            assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
118        });
119    }
120}