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#[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}