Skip to main content

relay_core_runtime/
paths.rs

1use std::path::PathBuf;
2
3pub const RELAY_DATA_DIR_ENV: &str = "RELAY_DATA_DIR";
4pub const RELAY_CA_CERT_ENV: &str = "RELAY_CA_CERT";
5pub const RELAY_CA_KEY_ENV: &str = "RELAY_CA_KEY";
6pub const DEFAULT_DATA_DIR_NAME: &str = ".relay-core";
7pub const DEFAULT_CA_CERT_FILE: &str = "ca_cert.pem";
8pub const DEFAULT_CA_KEY_FILE: &str = "ca_key.pem";
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct CaPaths {
12    pub cert: PathBuf,
13    pub key: PathBuf,
14}
15
16impl CaPaths {
17    pub fn default_from_data_dir() -> Self {
18        let data_dir = resolve_data_dir();
19        Self {
20            cert: data_dir.join(DEFAULT_CA_CERT_FILE),
21            key: data_dir.join(DEFAULT_CA_KEY_FILE),
22        }
23    }
24
25    /// Resolve CA paths with a single precedence chain:
26    /// 1) CLI args, 2) environment variables, 3) config directory defaults.
27    pub fn resolve(
28        ca_cert_arg: Option<PathBuf>,
29        ca_key_arg: Option<PathBuf>,
30    ) -> Result<Self, String> {
31        match (ca_cert_arg, ca_key_arg) {
32            (Some(cert), Some(key)) => return Ok(Self { cert, key }),
33            (Some(_), None) | (None, Some(_)) => {
34                return Err(
35                    "CA path arguments must be provided as a pair: --ca-cert and --ca-key"
36                        .to_string(),
37                );
38            }
39            (None, None) => {}
40        }
41
42        let env_cert = std::env::var(RELAY_CA_CERT_ENV).ok().map(PathBuf::from);
43        let env_key = std::env::var(RELAY_CA_KEY_ENV).ok().map(PathBuf::from);
44        match (env_cert, env_key) {
45            (Some(cert), Some(key)) => Ok(Self { cert, key }),
46            (Some(_), None) | (None, Some(_)) => Err(format!(
47                "Environment variables must be provided as a pair: {} and {}",
48                RELAY_CA_CERT_ENV, RELAY_CA_KEY_ENV
49            )),
50            (None, None) => Ok(Self::default_from_data_dir()),
51        }
52    }
53}
54
55pub fn resolve_data_dir() -> PathBuf {
56    std::env::var(RELAY_DATA_DIR_ENV)
57        .ok()
58        .map(PathBuf::from)
59        .unwrap_or_else(default_data_dir)
60}
61
62pub fn default_data_dir() -> PathBuf {
63    std::env::var_os("HOME")
64        .map(PathBuf::from)
65        .unwrap_or_else(|| PathBuf::from("."))
66        .join(DEFAULT_DATA_DIR_NAME)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use std::sync::{Mutex, OnceLock};
73
74    fn env_lock() -> &'static Mutex<()> {
75        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
76        LOCK.get_or_init(|| Mutex::new(()))
77    }
78
79    #[test]
80    fn resolve_prefers_args_over_env_and_default() {
81        let _guard = env_lock().lock().expect("lock");
82        unsafe {
83            std::env::set_var(RELAY_DATA_DIR_ENV, "/tmp/relay-default");
84            std::env::set_var(RELAY_CA_CERT_ENV, "/tmp/env-cert.pem");
85            std::env::set_var(RELAY_CA_KEY_ENV, "/tmp/env-key.pem");
86        }
87        let resolved = CaPaths::resolve(
88            Some(PathBuf::from("/tmp/arg-cert.pem")),
89            Some(PathBuf::from("/tmp/arg-key.pem")),
90        )
91        .expect("resolve");
92        assert_eq!(resolved.cert, PathBuf::from("/tmp/arg-cert.pem"));
93        assert_eq!(resolved.key, PathBuf::from("/tmp/arg-key.pem"));
94        unsafe {
95            std::env::remove_var(RELAY_DATA_DIR_ENV);
96            std::env::remove_var(RELAY_CA_CERT_ENV);
97            std::env::remove_var(RELAY_CA_KEY_ENV);
98        }
99    }
100
101    #[test]
102    fn resolve_prefers_env_over_default_dir() {
103        let _guard = env_lock().lock().expect("lock");
104        unsafe {
105            std::env::set_var(RELAY_DATA_DIR_ENV, "/tmp/relay-default");
106            std::env::set_var(RELAY_CA_CERT_ENV, "/tmp/env-cert.pem");
107            std::env::set_var(RELAY_CA_KEY_ENV, "/tmp/env-key.pem");
108        }
109        let resolved = CaPaths::resolve(None, None).expect("resolve");
110        assert_eq!(resolved.cert, PathBuf::from("/tmp/env-cert.pem"));
111        assert_eq!(resolved.key, PathBuf::from("/tmp/env-key.pem"));
112        unsafe {
113            std::env::remove_var(RELAY_DATA_DIR_ENV);
114            std::env::remove_var(RELAY_CA_CERT_ENV);
115            std::env::remove_var(RELAY_CA_KEY_ENV);
116        }
117    }
118
119    #[test]
120    fn resolve_falls_back_to_data_dir_defaults() {
121        let _guard = env_lock().lock().expect("lock");
122        unsafe {
123            std::env::set_var(RELAY_DATA_DIR_ENV, "/tmp/relay-default");
124            std::env::remove_var(RELAY_CA_CERT_ENV);
125            std::env::remove_var(RELAY_CA_KEY_ENV);
126        }
127        let resolved = CaPaths::resolve(None, None).expect("resolve");
128        assert_eq!(
129            resolved.cert,
130            PathBuf::from("/tmp/relay-default/ca_cert.pem")
131        );
132        assert_eq!(resolved.key, PathBuf::from("/tmp/relay-default/ca_key.pem"));
133        unsafe {
134            std::env::remove_var(RELAY_DATA_DIR_ENV);
135        }
136    }
137
138    #[test]
139    fn resolve_rejects_single_arg_override() {
140        let _guard = env_lock().lock().expect("lock");
141        let err = CaPaths::resolve(Some(PathBuf::from("/tmp/only-cert.pem")), None)
142            .expect_err("should fail");
143        assert!(err.contains("--ca-cert"));
144    }
145
146    #[test]
147    fn resolve_rejects_single_env_override() {
148        let _guard = env_lock().lock().expect("lock");
149        unsafe {
150            std::env::set_var(RELAY_CA_CERT_ENV, "/tmp/env-cert.pem");
151            std::env::remove_var(RELAY_CA_KEY_ENV);
152        }
153        let err = CaPaths::resolve(None, None).expect_err("should fail");
154        assert!(err.contains(RELAY_CA_CERT_ENV));
155        unsafe {
156            std::env::remove_var(RELAY_CA_CERT_ENV);
157        }
158    }
159}