1use crate::error::PathError;
17use crate::expand::{ExpandOptions, expand_input};
18use crate::inspect::is_existing_directory;
19use crate::internal::validation::validate_app_name;
20use crate::normalize::normalize;
21use crate::resolve::absolute;
22use std::env;
23use std::fs;
24use std::path::{Path, PathBuf};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct PlatformDirs {
52 pub home: PathBuf,
54 pub config: PathBuf,
56 pub data: PathBuf,
58 pub cache: PathBuf,
60 pub state: Option<PathBuf>,
62 pub runtime: Option<PathBuf>,
64 pub temp: PathBuf,
66}
67
68pub fn platform_dirs() -> Result<PlatformDirs, PathError> {
74 let home = dirs::home_dir().ok_or(PathError::HomeDirectoryUnavailable)?;
75 let config = dirs::config_dir()
76 .ok_or_else(|| PathError::invalid("config directory is unavailable on this platform"))?;
77 let data = dirs::data_dir()
78 .ok_or_else(|| PathError::invalid("data directory is unavailable on this platform"))?;
79 let cache = dirs::cache_dir()
80 .ok_or_else(|| PathError::invalid("cache directory is unavailable on this platform"))?;
81 let state = dirs::state_dir();
82 let runtime = dirs::runtime_dir();
83 let temp = std::env::temp_dir();
84
85 Ok(PlatformDirs {
86 home,
87 config,
88 data,
89 cache,
90 state,
91 runtime,
92 temp,
93 })
94}
95
96pub fn config_dir(app_name: &str) -> Result<PathBuf, PathError> {
101 validate_app_name(app_name)?;
102 let base = dirs::config_dir()
103 .ok_or_else(|| PathError::invalid("config directory is unavailable on this platform"))?;
104 Ok(base.join(app_name))
105}
106
107pub fn data_dir(app_name: &str) -> Result<PathBuf, PathError> {
111 validate_app_name(app_name)?;
112 let base = dirs::data_dir()
113 .ok_or_else(|| PathError::invalid("data directory is unavailable on this platform"))?;
114 Ok(base.join(app_name))
115}
116
117pub fn cache_dir(app_name: &str) -> Result<PathBuf, PathError> {
121 validate_app_name(app_name)?;
122 let base = dirs::cache_dir()
123 .ok_or_else(|| PathError::invalid("cache directory is unavailable on this platform"))?;
124 Ok(base.join(app_name))
125}
126
127pub fn temp_dir() -> PathBuf {
129 std::env::temp_dir()
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct AppPaths {
138 pub application_name: String,
140 pub root_dir: PathBuf,
142 pub config_dir: PathBuf,
144 pub data_dir: PathBuf,
146 pub cache_dir: PathBuf,
148 pub state_dir: Option<PathBuf>,
150 pub temp_dir: PathBuf,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum AppRootPolicy {
160 PlatformDefault,
162 EnvironmentOverride {
164 variable: String,
166 },
167 Explicit {
169 path: PathBuf,
171 },
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct AppPathsOptions {
177 pub application_name: String,
179 pub environment_override: Option<String>,
183 pub create_directories: bool,
185 pub root_policy: Option<AppRootPolicy>,
188}
189
190impl AppPathsOptions {
191 pub fn new(application_name: impl Into<String>) -> Self {
193 Self {
194 application_name: application_name.into(),
195 environment_override: None,
196 create_directories: false,
197 root_policy: None,
198 }
199 }
200}
201
202pub fn app_paths(application_name: &str) -> Result<AppPaths, PathError> {
209 app_paths_with_options(AppPathsOptions {
210 application_name: application_name.to_owned(),
211 environment_override: None,
212 create_directories: false,
213 root_policy: Some(AppRootPolicy::PlatformDefault),
214 })
215}
216
217pub fn app_paths_with_options(options: AppPathsOptions) -> Result<AppPaths, PathError> {
230 validate_app_name(&options.application_name)?;
231
232 let policy = options.root_policy.clone().unwrap_or_else(|| {
233 if let Some(var) = &options.environment_override {
234 AppRootPolicy::EnvironmentOverride {
235 variable: var.clone(),
236 }
237 } else {
238 AppRootPolicy::PlatformDefault
239 }
240 });
241
242 let mut paths = match policy {
243 AppRootPolicy::PlatformDefault => platform_app_paths(&options.application_name)?,
244 AppRootPolicy::Explicit { path } => rooted_app_paths(&options.application_name, path)?,
245 AppRootPolicy::EnvironmentOverride { variable } => match env::var(&variable) {
246 Ok(value) if !value.trim().is_empty() => {
247 let expanded = expand_input(
248 value.trim(),
249 &ExpandOptions {
250 reject_undefined_variables: true,
251 ..ExpandOptions::default()
252 },
253 )?;
254 let abs = if expanded.is_absolute() {
255 normalize(expanded)?
256 } else {
257 absolute(expanded)?
258 };
259 rooted_app_paths(&options.application_name, abs)?
260 }
261 Ok(_) | Err(env::VarError::NotPresent) => {
262 platform_app_paths(&options.application_name)?
263 }
264 Err(env::VarError::NotUnicode(_)) => {
265 return Err(PathError::invalid(format!(
266 "environment variable {variable} is not valid Unicode"
267 )));
268 }
269 },
270 };
271
272 if options.create_directories {
273 create_app_directories(&paths)?;
274 } else {
275 if paths.root_dir.exists() && !is_existing_directory(&paths.root_dir) {
277 return Err(PathError::invalid(format!(
278 "application root exists and is not a directory: {}",
279 paths.root_dir.to_string_lossy()
280 )));
281 }
282 }
283
284 if options.create_directories && !is_existing_directory(&paths.root_dir) {
286 return Err(PathError::invalid(format!(
287 "failed to materialize application root as a directory: {}",
288 paths.root_dir.to_string_lossy()
289 )));
290 }
291
292 let _ = &mut paths;
293 Ok(paths)
294}
295
296pub fn app_paths_with_policy(
298 application_name: &str,
299 policy: AppRootPolicy,
300 create_directories: bool,
301) -> Result<AppPaths, PathError> {
302 app_paths_with_options(AppPathsOptions {
303 application_name: application_name.to_owned(),
304 environment_override: None,
305 create_directories,
306 root_policy: Some(policy),
307 })
308}
309
310fn platform_app_paths(application_name: &str) -> Result<AppPaths, PathError> {
311 let data = data_dir(application_name)?;
312 let config = config_dir(application_name)?;
313 let cache = cache_dir(application_name)?;
314 let state = dirs::state_dir().map(|s| s.join(application_name));
315 Ok(AppPaths {
316 application_name: application_name.to_owned(),
317 root_dir: data.clone(),
319 config_dir: config,
320 data_dir: data,
321 cache_dir: cache,
322 state_dir: state,
323 temp_dir: temp_dir(),
324 })
325}
326
327fn rooted_app_paths(application_name: &str, root: PathBuf) -> Result<AppPaths, PathError> {
328 let root = normalize(root)?;
329 if root.as_os_str().is_empty() {
330 return Err(PathError::EmptyInput);
331 }
332 Ok(AppPaths {
333 application_name: application_name.to_owned(),
334 config_dir: root.clone(),
335 data_dir: root.clone(),
336 cache_dir: root.join("cache"),
337 state_dir: Some(root.join("state")),
338 temp_dir: temp_dir(),
339 root_dir: root,
340 })
341}
342
343fn create_app_directories(paths: &AppPaths) -> Result<(), PathError> {
344 for dir in [
345 Some(paths.root_dir.as_path()),
346 Some(paths.config_dir.as_path()),
347 Some(paths.data_dir.as_path()),
348 Some(paths.cache_dir.as_path()),
349 paths.state_dir.as_deref(),
350 ]
351 .into_iter()
352 .flatten()
353 {
354 create_dir_if_needed(dir)?;
355 }
356 Ok(())
357}
358
359fn create_dir_if_needed(path: &Path) -> Result<(), PathError> {
360 if path.exists() {
361 if is_existing_directory(path) {
362 return Ok(());
363 }
364 return Err(PathError::invalid(format!(
365 "path exists and is not a directory: {}",
366 path.to_string_lossy()
367 )));
368 }
369 fs::create_dir_all(path).map_err(|e| PathError::filesystem(path, e))
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn platform_dirs_available() {
378 let d = platform_dirs().unwrap();
379 assert!(!d.home.as_os_str().is_empty());
380 assert!(!d.temp.as_os_str().is_empty());
381 }
382
383 #[test]
384 fn app_name_validation() {
385 assert!(config_dir("").is_err());
386 assert!(config_dir("a/b").is_err());
387 assert!(config_dir("..").is_err());
388 assert!(config_dir("CON").is_err());
389 assert!(config_dir("my-app").is_ok());
390 }
391
392 #[test]
393 fn app_paths_platform() {
394 let p = app_paths("path-rs-test-app").unwrap();
395 assert_eq!(p.application_name, "path-rs-test-app");
396 assert!(p.root_dir.ends_with("path-rs-test-app"));
397 }
398
399 #[test]
400 fn app_paths_explicit_and_create() {
401 let dir = tempfile::tempdir().unwrap();
402 let root = dir.path().join("app-root");
403 let p = app_paths_with_policy(
404 "my-tool",
405 AppRootPolicy::Explicit { path: root.clone() },
406 true,
407 )
408 .unwrap();
409 assert_eq!(p.root_dir, normalize(&root).unwrap());
410 assert!(p.root_dir.is_dir());
411 assert!(p.cache_dir.is_dir());
412 }
413}