aion_server/config/
home.rs1use std::ffi::OsString;
4use std::path::{Component, Path, PathBuf};
5
6use crate::error::ServerError;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum HomeSource {
18 Explicit,
20 Derived,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct AionHome {
27 pub path: PathBuf,
29 pub source: HomeSource,
31}
32
33pub fn aion_home() -> Result<AionHome, ServerError> {
51 let configured = std::env::var_os("AION_HOME");
52 let (path, source) = match configured {
53 Some(value) if value.is_empty() => {
54 return Err(ServerError::Config {
55 message: "AION_HOME must not be empty".to_owned(),
56 });
57 }
58 Some(value) => (expand_tilde(Path::new(&value))?, HomeSource::Explicit),
59 None => {
60 let Some(home) = std::env::var_os("HOME").filter(|value| !value.is_empty()) else {
61 return Err(ServerError::Config {
62 message: "cannot resolve Aion home: set AION_HOME or HOME".to_owned(),
63 });
64 };
65 (PathBuf::from(home).join(".aion"), HomeSource::Derived)
66 }
67 };
68 let path = if path.is_absolute() {
69 path
70 } else {
71 std::env::current_dir()
72 .map(|current| current.join(path))
73 .map_err(|source| ServerError::Config {
74 message: format!(
75 "cannot resolve relative AION_HOME against the current directory: {source}"
76 ),
77 })?
78 };
79 Ok(AionHome { path, source })
80}
81
82pub(super) fn expand_tilde(path: &Path) -> Result<PathBuf, ServerError> {
92 expand_tilde_against(path, std::env::var_os("HOME"))
93}
94
95fn expand_tilde_against(path: &Path, home: Option<OsString>) -> Result<PathBuf, ServerError> {
96 let mut components = path.components();
97 let Some(Component::Normal(first)) = components.next() else {
98 return Ok(path.to_owned());
99 };
100 if first == "~" {
101 let Some(home) = home.filter(|value| !value.is_empty()) else {
102 return Err(ServerError::Config {
103 message: format!(
104 "cannot expand `~` in path `{}`: HOME is not set",
105 path.display()
106 ),
107 });
108 };
109 let rest = components.as_path();
110 return Ok(if rest.as_os_str().is_empty() {
111 PathBuf::from(home)
112 } else {
113 PathBuf::from(home).join(rest)
114 });
115 }
116 if first.as_encoded_bytes().starts_with(b"~") {
117 return Err(ServerError::Config {
118 message: format!(
119 "cannot resolve `{}`: `~user` expansion is not supported; use an absolute \
120 path (a directory literally named `{}` can be spelled `./{}`)",
121 path.display(),
122 Path::new(first).display(),
123 Path::new(first).display()
124 ),
125 });
126 }
127 Ok(path.to_owned())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn home() -> OsString {
135 OsString::from("/users/probe")
136 }
137
138 #[test]
139 fn a_bare_tilde_expands_to_home() -> Result<(), Box<dyn std::error::Error>> {
140 assert_eq!(
141 expand_tilde_against(Path::new("~"), Some(home()))?,
142 PathBuf::from("/users/probe")
143 );
144 Ok(())
145 }
146
147 #[test]
148 fn a_tilde_slash_prefix_expands_under_home() -> Result<(), Box<dyn std::error::Error>> {
149 assert_eq!(
150 expand_tilde_against(Path::new("~/.aion/config.toml"), Some(home()))?,
151 PathBuf::from("/users/probe/.aion/config.toml")
152 );
153 Ok(())
154 }
155
156 #[test]
157 fn non_tilde_paths_pass_through_untouched() -> Result<(), Box<dyn std::error::Error>> {
158 for literal in ["/absolute/path", "relative/path", "./~escaped", ""] {
159 assert_eq!(
160 expand_tilde_against(Path::new(literal), Some(home()))?,
161 PathBuf::from(literal),
162 "`{literal}` must not be rewritten"
163 );
164 }
165 Ok(())
166 }
167
168 #[test]
171 fn an_interior_tilde_is_a_literal_name() -> Result<(), Box<dyn std::error::Error>> {
172 assert_eq!(
173 expand_tilde_against(Path::new("/srv/~backup"), Some(home()))?,
174 PathBuf::from("/srv/~backup")
175 );
176 Ok(())
177 }
178
179 #[test]
180 fn a_tilde_user_form_is_a_loud_typed_refusal() -> Result<(), Box<dyn std::error::Error>> {
181 let error = expand_tilde_against(Path::new("~alice/.aion"), Some(home()))
182 .err()
183 .ok_or("`~alice` was accepted")?;
184 let message = error.to_string();
185 assert!(message.contains("~user"));
186 assert!(message.contains("~alice"));
187 assert!(message.contains("./~alice"));
188 Ok(())
189 }
190
191 #[test]
192 fn a_tilde_without_home_is_a_typed_error() -> Result<(), Box<dyn std::error::Error>> {
193 for absent in [None, Some(OsString::new())] {
194 let error = expand_tilde_against(Path::new("~/.aion"), absent)
195 .err()
196 .ok_or("`~` expanded without HOME")?;
197 assert!(error.to_string().contains("HOME is not set"));
198 }
199 Ok(())
200 }
201}