1
//! Base paths for use with arti clients.
2
//!
3
//! This code is defined in `tor-config-path` rather than in `arti-client` so that other programs
4
//! (notably `arti-rpc-client-core`) can use it.
5

            
6
use std::{borrow::Cow, path::PathBuf};
7

            
8
use directories::ProjectDirs;
9
use std::sync::LazyLock;
10

            
11
use crate::{CfgPathError, CfgPathResolver};
12

            
13
/// A [`CfgPathResolver`] with the base variables configured for a `TorClientConfig`
14
/// in `arti-client`.
15
///
16
/// A `TorClientConfig` may set additional variables on its resolver.
17
///
18
/// This should only be used by `TorClient` users
19
/// and others that need to use the same variables.
20
/// Libraries should be written in a
21
/// resolver-agnostic way (shouldn't rely on resolving `ARTI_CONFIG` for example).
22
///
23
/// The supported variables are:
24
///   - `ARTI_CACHE`: an arti-specific cache directory.
25
///   - `ARTI_CONFIG`: an arti-specific configuration directory.
26
///   - `ARTI_SHARED_DATA`: an arti-specific directory in the user's "shared
27
///     data" space. **Note:** Prefer `ARTI_LOCAL_DATA` (see comment below).
28
///   - `ARTI_LOCAL_DATA`: an arti-specific directory in the user's "local
29
///     data" space.
30
///   - `PROGRAM_DIR`: the directory of the currently executing binary.
31
///     See documentation for [`std::env::current_exe`] for security notes.
32
///   - `USER_HOME`: the user's home directory.
33
///
34
/// These variables are implemented using the [`directories`] crate, and
35
/// so should use appropriate system-specific overrides under the
36
/// hood. (Some of those overrides are based on environment variables.)
37
/// For more information, see that crate's documentation.
38
///
39
/// If in doubt, use `ARTI_LOCAL_DATA` rather than `ARTI_SHARED_DATA`.
40
/// These are equivalent on Linux/MacOS, but are different on Windows.
41
/// We should use `ARTI_LOCAL_DATA` for consistency with existing Arti config options,
42
/// unless there's a specific need for roaming data on Windows.
43
7809
pub fn arti_client_base_resolver() -> CfgPathResolver {
44
9709
    let arti_cache = project_dirs().map(|x| Cow::Owned(x.cache_dir().to_owned()));
45
9709
    let arti_config = project_dirs().map(|x| Cow::Owned(x.config_dir().to_owned()));
46
9709
    let arti_shared_data = project_dirs().map(|x| Cow::Owned(x.data_dir().to_owned()));
47
9709
    let arti_local_data = project_dirs().map(|x| Cow::Owned(x.data_local_dir().to_owned()));
48
7809
    let program_dir = get_program_dir().map(Cow::Owned);
49
7809
    let user_home = crate::home().map(Cow::Borrowed);
50

            
51
7809
    let mut resolver = CfgPathResolver::default();
52

            
53
7809
    resolver.set_var("ARTI_CACHE", arti_cache);
54
7809
    resolver.set_var("ARTI_CONFIG", arti_config);
55
7809
    resolver.set_var("ARTI_SHARED_DATA", arti_shared_data);
56
7809
    resolver.set_var("ARTI_LOCAL_DATA", arti_local_data);
57
7809
    resolver.set_var("PROGRAM_DIR", program_dir);
58
7809
    resolver.set_var("USER_HOME", user_home);
59

            
60
7809
    resolver
61
7809
}
62

            
63
/// Return the directory holding the currently executing program.
64
7811
fn get_program_dir() -> Result<PathBuf, CfgPathError> {
65
7811
    let binary = std::env::current_exe().map_err(|_| CfgPathError::NoProgramPath)?;
66
7811
    let directory = binary.parent().ok_or(CfgPathError::NoProgramDir)?;
67
7811
    Ok(directory.to_owned())
68
7811
}
69

            
70
/// Return a ProjectDirs object for the Arti project.
71
31244
fn project_dirs() -> Result<&'static ProjectDirs, CfgPathError> {
72
    /// lazy lock holding the ProjectDirs object.
73
    static PROJECT_DIRS: LazyLock<Option<ProjectDirs>> =
74
2792
        LazyLock::new(|| ProjectDirs::from("org", "torproject", "Arti"));
75

            
76
31244
    PROJECT_DIRS.as_ref().ok_or(CfgPathError::NoProjectDirs)
77
31244
}
78

            
79
#[cfg(test)]
80
mod test {
81
    // @@ begin test lint list maintained by maint/add_warning @@
82
    #![allow(clippy::bool_assert_comparison)]
83
    #![allow(clippy::clone_on_copy)]
84
    #![allow(clippy::dbg_macro)]
85
    #![allow(clippy::mixed_attributes_style)]
86
    #![allow(clippy::print_stderr)]
87
    #![allow(clippy::print_stdout)]
88
    #![allow(clippy::single_char_pattern)]
89
    #![allow(clippy::unwrap_used)]
90
    #![allow(clippy::unchecked_time_subtraction)]
91
    #![allow(clippy::useless_vec)]
92
    #![allow(clippy::needless_pass_by_value)]
93
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
94

            
95
    use super::*;
96
    use crate::CfgPath;
97

            
98
    fn cfg_variables() -> impl IntoIterator<Item = (&'static str, PathBuf)> {
99
        let list = [
100
            ("ARTI_CACHE", project_dirs().unwrap().cache_dir()),
101
            ("ARTI_CONFIG", project_dirs().unwrap().config_dir()),
102
            ("ARTI_SHARED_DATA", project_dirs().unwrap().data_dir()),
103
            ("ARTI_LOCAL_DATA", project_dirs().unwrap().data_local_dir()),
104
            ("PROGRAM_DIR", &get_program_dir().unwrap()),
105
            ("USER_HOME", crate::home().unwrap()),
106
        ];
107

            
108
        list.into_iter()
109
            .map(|(a, b)| (a, b.to_owned()))
110
            .collect::<Vec<_>>()
111
    }
112

            
113
    #[cfg(not(target_family = "windows"))]
114
    #[test]
115
    fn expand_variables() {
116
        let path_resolver = arti_client_base_resolver();
117

            
118
        for (var, val) in cfg_variables() {
119
            let p = CfgPath::new(format!("${{{var}}}/example"));
120
            assert_eq!(p.to_string(), format!("${{{var}}}/example"));
121

            
122
            let expected = val.join("example");
123
            assert_eq!(p.path(&path_resolver).unwrap().to_str(), expected.to_str());
124
        }
125

            
126
        let p = CfgPath::new("${NOT_A_REAL_VAR}/example".to_string());
127
        assert!(p.path(&path_resolver).is_err());
128
    }
129

            
130
    #[cfg(target_family = "windows")]
131
    #[test]
132
    fn expand_variables() {
133
        let path_resolver = arti_client_base_resolver();
134

            
135
        for (var, val) in cfg_variables() {
136
            let p = CfgPath::new(format!("${{{var}}}\\example"));
137
            assert_eq!(p.to_string(), format!("${{{var}}}\\example"));
138

            
139
            let expected = val.join("example");
140
            assert_eq!(p.path(&path_resolver).unwrap().to_str(), expected.to_str());
141
        }
142

            
143
        let p = CfgPath::new("${NOT_A_REAL_VAR}\\example".to_string());
144
        assert!(p.path(&path_resolver).is_err());
145
    }
146
}