aion_server/config/
file.rs1use std::{fs, path::Path};
4
5use crate::{config::ServerConfig, error::ServerError};
6
7use super::ConfigSource;
8
9const PROJECT_CONFIG_FILE: &str = "aion.toml";
10pub(super) const HOME_CONFIG_FILE: &str = "config.toml";
13
14pub(super) struct DiscoveredFile {
16 pub(super) bytes: Option<Vec<u8>>,
17 pub(super) source: ConfigSource,
18}
19
20pub(super) fn discover(
33 explicit: Option<&Path>,
34 home: &Path,
35 working_dir: &Path,
36) -> Result<DiscoveredFile, ServerError> {
37 match discover_path(explicit, home, working_dir)? {
38 Some((path, source)) => read(&path, source),
39 None => Ok(DiscoveredFile {
40 bytes: None,
41 source: ConfigSource::BuiltInDefaults,
42 }),
43 }
44}
45
46pub(super) fn discover_path(
60 explicit: Option<&Path>,
61 home: &Path,
62 working_dir: &Path,
63) -> Result<Option<(std::path::PathBuf, ConfigSource)>, ServerError> {
64 if let Some(path) = explicit {
65 return Ok(Some((
66 path.to_owned(),
67 ConfigSource::Explicit(path.to_owned()),
68 )));
69 }
70 let project = working_dir.join(PROJECT_CONFIG_FILE);
71 if is_present(&project)? {
72 let source = ConfigSource::ProjectLocal(project.clone());
73 return Ok(Some((project, source)));
74 }
75 let user = home.join(HOME_CONFIG_FILE);
76 if is_present(&user)? {
77 let source = ConfigSource::AionHome(user.clone());
78 return Ok(Some((user, source)));
79 }
80 Ok(None)
81}
82
83fn is_present(path: &Path) -> Result<bool, ServerError> {
84 match fs::symlink_metadata(path) {
85 Ok(_) => Ok(true),
86 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
87 Err(error) => Err(ServerError::Config {
88 message: format!(
89 "failed to inspect config path `{}`: {error}",
90 path.display()
91 ),
92 }),
93 }
94}
95
96fn read(path: &Path, source: ConfigSource) -> Result<DiscoveredFile, ServerError> {
97 let bytes = fs::read(path).map_err(|error| ServerError::Config {
98 message: format!("failed to read config `{}`: {error}", path.display()),
99 })?;
100 Ok(DiscoveredFile {
101 bytes: Some(bytes),
102 source,
103 })
104}
105
106pub fn load_required(path: &Path) -> Result<ServerConfig, ServerError> {
114 let bytes = fs::read(path).map_err(|error| ServerError::Config {
115 message: format!("failed to read config `{}`: {error}", path.display()),
116 })?;
117 ServerConfig::from_slice(&bytes).map_err(|error| ServerError::Config {
118 message: format!("failed to load config `{}`: {error}", path.display()),
119 })
120}
121
122#[cfg(all(test, unix))]
123mod tests {
124 use std::os::unix::fs::{PermissionsExt, symlink};
125
126 use super::*;
127
128 #[test]
129 fn dangling_project_config_is_loud_and_home_is_not_read()
130 -> Result<(), Box<dyn std::error::Error>> {
131 let sandbox = crate::test_support::private_tempdir()?;
132 let working = sandbox.path().join("working");
133 let home = sandbox.path().join("home");
134 fs::create_dir(&working)?;
135 fs::create_dir(&home)?;
136 fs::write(home.join(HOME_CONFIG_FILE), b"not valid toml =")?;
137 symlink(
138 sandbox.path().join("missing"),
139 working.join(PROJECT_CONFIG_FILE),
140 )?;
141
142 let error = discover(None, &home, &working)
143 .err()
144 .ok_or("expected failure")?;
145 assert!(error.to_string().contains(PROJECT_CONFIG_FILE));
146 assert!(!error.to_string().contains("parse"));
147 Ok(())
148 }
149
150 #[test]
151 fn dangling_home_config_is_loud_instead_of_defaults() -> Result<(), Box<dyn std::error::Error>>
152 {
153 let sandbox = crate::test_support::private_tempdir()?;
154 let working = sandbox.path().join("working");
155 let home = sandbox.path().join("home");
156 fs::create_dir(&working)?;
157 fs::create_dir(&home)?;
158 symlink(sandbox.path().join("missing"), home.join(HOME_CONFIG_FILE))?;
159
160 let error = discover(None, &home, &working)
161 .err()
162 .ok_or("expected failure")?;
163 assert!(error.to_string().contains(HOME_CONFIG_FILE));
164 Ok(())
165 }
166
167 #[test]
168 fn unstatable_project_and_home_paths_are_loud() -> Result<(), Box<dyn std::error::Error>> {
169 let sandbox = crate::test_support::private_tempdir()?;
170 let working = sandbox.path().join("working");
171 let home = sandbox.path().join("home");
172 fs::create_dir(&working)?;
173 fs::create_dir(&home)?;
174 fs::write(home.join(HOME_CONFIG_FILE), b"lower layer must not win")?;
175
176 fs::set_permissions(&working, fs::Permissions::from_mode(0o000))?;
177 let project_result = discover(None, &home, &working);
178 fs::set_permissions(&working, fs::Permissions::from_mode(0o700))?;
179 assert!(
180 project_result.is_err(),
181 "project metadata failure was suppressed"
182 );
183
184 fs::set_permissions(&home, fs::Permissions::from_mode(0o000))?;
185 let home_result = discover(None, &home, &working);
186 fs::set_permissions(&home, fs::Permissions::from_mode(0o700))?;
187 assert!(home_result.is_err(), "home metadata failure was suppressed");
188 Ok(())
189 }
190}