cloudiful_config/
paths.rs1use std::ffi::OsString;
2use std::io::{self, ErrorKind};
3use std::path::{Path, PathBuf};
4
5pub fn config_dir(app_name: &str) -> io::Result<PathBuf> {
7 let app_name = Path::new(app_name);
8 validate_relative_path(app_name, "app name")?;
9 Ok(config_root()?.join(app_name))
10}
11
12pub fn config_path<P>(app_name: &str, relative_path: P) -> io::Result<PathBuf>
14where
15 P: AsRef<Path>,
16{
17 let relative_path = relative_path.as_ref();
18 validate_relative_path(relative_path, "config path")?;
19 Ok(config_dir(app_name)?.join(relative_path))
20}
21
22fn validate_relative_path(path: &Path, label: &str) -> io::Result<()> {
23 if path.as_os_str().is_empty() {
24 return Err(io::Error::new(
25 ErrorKind::InvalidInput,
26 format!("{label} must not be empty"),
27 ));
28 }
29
30 if path.is_absolute() {
31 return Err(io::Error::new(
32 ErrorKind::InvalidInput,
33 format!("{label} must be relative, got {}", path.display()),
34 ));
35 }
36
37 Ok(())
38}
39
40fn env_path_with(get_env: impl Fn(&str) -> Option<OsString>, key: &str) -> Option<PathBuf> {
41 get_env(key)
42 .filter(|value| !value.is_empty())
43 .map(PathBuf::from)
44}
45
46#[cfg(windows)]
47fn config_root_from(get_env: impl Fn(&str) -> Option<OsString>) -> io::Result<PathBuf> {
48 env_path_with(&get_env, "APPDATA")
49 .or_else(|| {
50 env_path_with(&get_env, "USERPROFILE")
51 .map(|path| path.join("AppData").join("Roaming"))
52 })
53 .or_else(|| match (get_env("HOMEDRIVE"), get_env("HOMEPATH")) {
54 (Some(drive), Some(path)) if !drive.is_empty() && !path.is_empty() => {
55 Some(PathBuf::from(drive).join(path))
56 }
57 _ => None,
58 })
59 .map(|path| {
60 if path.ends_with("Roaming") {
61 path
62 } else {
63 path.join("AppData").join("Roaming")
64 }
65 })
66 .ok_or_else(|| {
67 io::Error::new(
68 ErrorKind::NotFound,
69 "failed to resolve Windows config directory from APPDATA, USERPROFILE, or HOMEDRIVE/HOMEPATH",
70 )
71 })
72}
73
74#[cfg(target_os = "macos")]
75fn config_root_from(get_env: impl Fn(&str) -> Option<OsString>) -> io::Result<PathBuf> {
76 env_path_with(get_env, "HOME")
77 .map(|path| path.join("Library").join("Application Support"))
78 .ok_or_else(|| {
79 io::Error::new(
80 ErrorKind::NotFound,
81 "failed to resolve macOS config directory from HOME",
82 )
83 })
84}
85
86#[cfg(all(not(windows), not(target_os = "macos")))]
87fn config_root_from(get_env: impl Fn(&str) -> Option<OsString>) -> io::Result<PathBuf> {
88 env_path_with(&get_env, "XDG_CONFIG_HOME")
89 .or_else(|| env_path_with(get_env, "HOME").map(|path| path.join(".config")))
90 .ok_or_else(|| {
91 io::Error::new(
92 ErrorKind::NotFound,
93 "failed to resolve config directory from XDG_CONFIG_HOME or HOME",
94 )
95 })
96}
97
98fn config_root() -> io::Result<PathBuf> {
99 config_root_from(|key| std::env::var_os(key))
100}
101
102#[cfg(test)]
103pub(crate) fn test_config_root_from(
104 get_env: impl Fn(&str) -> Option<OsString>,
105) -> io::Result<PathBuf> {
106 config_root_from(get_env)
107}