Skip to main content

dhttp_home/
lib.rs

1pub mod identity;
2
3mod bootstrap;
4
5use std::path::{Path, PathBuf};
6
7use snafu::{OptionExt, Snafu};
8
9const USER_HOME_ENV: &str = "DHTTP_HOME";
10const GLOBAL_HOME_ENV: &str = "DHTTP_GLOBAL_HOME";
11#[cfg(any(target_os = "linux", target_os = "macos"))]
12const DEFAULT_UNIX_GLOBAL_HOME: &str = "/etc/dhttp";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HomeScope {
16    User,
17    Global,
18}
19
20/// A handle to the user's dhttp home directory (e.g. `~/.dhttp/`).
21///
22/// `DhttpHome` describes a directory that contains per-identity profiles and
23/// a global settings file. It does not own any in-memory configuration data;
24/// it is purely a typed path with helpers for resolving the layout inside.
25#[derive(Debug, Clone)]
26pub struct DhttpHome {
27    path: PathBuf,
28}
29
30#[derive(Debug, Snafu)]
31#[snafu(module)]
32pub enum LoadDhttpHomeError {
33    #[cfg(any(unix, windows))]
34    #[snafu(display("cannot locate user home directory"))]
35    NoUserHome {},
36    #[snafu(display("global dhttp home is not configured"))]
37    GlobalHomeNotConfigured {},
38    #[snafu(display(
39        "dhttp home cannot be automatically located on this platform, try setting DHTTP_HOME environment variable"
40    ))]
41    UnsupportedPlatform {},
42}
43
44impl DhttpHome {
45    pub const DIR_NAME: &str = ".dhttp";
46
47    pub fn new(pathbuf: PathBuf) -> Self {
48        Self { path: pathbuf }
49    }
50
51    pub fn for_user_home_dir(home_dir: impl Into<PathBuf>) -> Self {
52        Self::new(home_dir.into().join(Self::DIR_NAME))
53    }
54
55    pub fn load(scope: HomeScope) -> Result<Self, LoadDhttpHomeError> {
56        match scope {
57            HomeScope::User => Ok(Self::new(resolve_user_home_path(
58                std::env::var_os(USER_HOME_ENV).map(PathBuf::from),
59                user_home_dir(),
60            )?)),
61            HomeScope::Global => Ok(Self::new(resolve_global_home_path(
62                std::env::var_os(GLOBAL_HOME_ENV).map(PathBuf::from),
63                bootstrap::DHTTP_GLOBAL_HOME,
64                platform_default_global_home(),
65            )?)),
66        }
67    }
68
69    pub fn as_path(&self) -> &Path {
70        self.path.as_path()
71    }
72
73    pub fn join(&self, path: impl AsRef<Path>) -> PathBuf {
74        self.path.join(path)
75    }
76}
77
78impl AsRef<Path> for DhttpHome {
79    fn as_ref(&self) -> &Path {
80        self.as_path()
81    }
82}
83
84fn resolve_user_home_path(
85    runtime_home: Option<PathBuf>,
86    user_home_dir: Option<PathBuf>,
87) -> Result<PathBuf, LoadDhttpHomeError> {
88    if let Some(path) = runtime_home {
89        return Ok(path);
90    }
91
92    #[cfg(any(unix, windows))]
93    let home_dir = user_home_dir.context(load_dhttp_home_error::NoUserHomeSnafu)?;
94
95    #[cfg(not(any(unix, windows)))]
96    let home_dir = user_home_dir.context(load_dhttp_home_error::UnsupportedPlatformSnafu)?;
97
98    Ok(home_dir.join(DhttpHome::DIR_NAME))
99}
100
101fn resolve_global_home_path(
102    runtime_home: Option<PathBuf>,
103    compiled_home: Option<&str>,
104    default_home: Option<&str>,
105) -> Result<PathBuf, LoadDhttpHomeError> {
106    if let Some(path) = runtime_home {
107        return Ok(path);
108    }
109    if let Some(path) = compiled_home {
110        return Ok(PathBuf::from(path));
111    }
112    if let Some(path) = default_home {
113        return Ok(PathBuf::from(path));
114    }
115
116    load_dhttp_home_error::GlobalHomeNotConfiguredSnafu.fail()
117}
118
119fn user_home_dir() -> Option<PathBuf> {
120    #[cfg(any(unix, windows))]
121    {
122        return dirs::home_dir();
123    }
124
125    #[allow(unreachable_code)]
126    None
127}
128
129fn platform_default_global_home() -> Option<&'static str> {
130    #[cfg(any(target_os = "linux", target_os = "macos"))]
131    {
132        return Some(DEFAULT_UNIX_GLOBAL_HOME);
133    }
134
135    #[allow(unreachable_code)]
136    None
137}
138
139#[cfg(test)]
140mod tests {
141    use std::path::PathBuf;
142
143    use super::{LoadDhttpHomeError, resolve_global_home_path, resolve_user_home_path};
144
145    #[test]
146    fn user_scope_path_prefers_runtime_home_env() {
147        let path = resolve_user_home_path(
148            Some(PathBuf::from("/runtime/dhttp-home")),
149            Some(PathBuf::from("/home/reimu")),
150        )
151        .expect("user path should resolve");
152
153        assert_eq!(path, PathBuf::from("/runtime/dhttp-home"));
154    }
155
156    #[test]
157    fn user_scope_path_falls_back_to_user_home_dir() {
158        let path = resolve_user_home_path(None, Some(PathBuf::from("/home/reimu")))
159            .expect("user path should resolve");
160
161        assert_eq!(path, PathBuf::from("/home/reimu/.dhttp"));
162    }
163
164    #[test]
165    fn global_scope_path_prefers_runtime_env_over_compile_time_and_default() {
166        let path = resolve_global_home_path(
167            Some(PathBuf::from("/runtime/global")),
168            Some("/compiled/global"),
169            Some("/etc/dhttp"),
170        )
171        .expect("global path should resolve");
172
173        assert_eq!(path, PathBuf::from("/runtime/global"));
174    }
175
176    #[test]
177    fn global_scope_path_uses_compile_time_home_when_runtime_is_missing() {
178        let path = resolve_global_home_path(None, Some("/compiled/global"), Some("/etc/dhttp"))
179            .expect("global path should resolve");
180
181        assert_eq!(path, PathBuf::from("/compiled/global"));
182    }
183
184    #[test]
185    fn global_scope_path_uses_platform_default_when_overrides_are_missing() {
186        let path = resolve_global_home_path(None, None, Some("/etc/dhttp"))
187            .expect("global path should resolve");
188
189        assert_eq!(path, PathBuf::from("/etc/dhttp"));
190    }
191
192    #[test]
193    fn global_scope_path_errors_when_no_source_is_available() {
194        let error = resolve_global_home_path(None, None, None)
195            .expect_err("missing global path sources must fail");
196
197        assert!(matches!(
198            error,
199            LoadDhttpHomeError::GlobalHomeNotConfigured {}
200        ));
201    }
202}