relay_core_runtime/
paths.rs1use 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 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}