1use crate::env::{self, SplitPathsRef};
2use crate::ffi::{OsStr, OsString};
3use crate::fs::{self, HomeDirs, MediaDirs};
4use crate::io::{self, ErrorKind, const_error};
5use crate::macros::rustdoc_skip_body;
6use crate::path::{Path, PathBuf};
7
8#[derive(Debug, Default, Clone)]
9#[non_exhaustive]
10pub struct ExtraHomeDirs {
11 runtime: Option<PathBuf>,
12 config_path: Option<OsString>,
13 data_path: Option<OsString>,
14}
15
16#[derive(Debug, Default, Clone)]
17#[non_exhaustive]
18pub struct ExtraMediaDirs {
19 templates: Option<PathBuf>,
20}
21
22#[unstable(feature = "fs_home_dirs", issue = "162082")]
37pub impl(self) trait HomeDirsExt: Sized {
38 #[unstable(feature = "fs_home_dirs", issue = "162082")]
82 fn xdg() -> io::Result<Self>;
83
84 #[unstable(feature = "fs_home_dirs", issue = "162082")]
95 fn runtime_home(&self) -> Option<&Path>;
96
97 #[unstable(feature = "fs_home_dirs", issue = "162082")]
108 fn config_dirs(&self) -> Option<XdgDirs<'_>>;
109
110 #[unstable(feature = "fs_home_dirs", issue = "162082")]
121 fn data_dirs(&self) -> Option<XdgDirs<'_>>;
122
123 #[unstable(feature = "fs_home_dirs", issue = "162082")]
129 fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self;
130
131 #[unstable(feature = "fs_home_dirs", issue = "162082")]
140 fn set_config_dirs(&mut self, paths: OsString) -> &mut Self;
141
142 #[unstable(feature = "fs_home_dirs", issue = "162082")]
151 fn set_data_dirs(&mut self, paths: OsString) -> &mut Self;
152}
153
154#[unstable(feature = "fs_media_dirs", issue = "162083")]
170pub impl(self) trait MediaDirsExt: Sized {
171 #[unstable(feature = "fs_media_dirs", issue = "162083")]
208 fn xdg() -> io::Result<Self>;
209
210 #[unstable(feature = "fs_media_dirs", issue = "162083")]
216 fn templates(&self) -> Option<&Path>;
217
218 #[unstable(feature = "fs_media_dirs", issue = "162083")]
224 fn set_templates(&mut self, path: PathBuf) -> &mut Self;
225}
226
227#[unstable(feature = "fs_home_dirs", issue = "162082")]
228#[derive(Debug)]
229pub struct XdgDirs<'a>(SplitPathsRef<'a>);
230
231#[unstable(feature = "fs_home_dirs", issue = "162082")]
232impl HomeDirsExt for HomeDirs {
233 fn xdg() -> io::Result<Self> {
234 let mut dirs = HomeDirs::empty();
235 let user_home = xdg::user_home()?;
236
237 dirs.set_cache_home(xdg::dir_or_else(|| user_home.join(".cache"), "XDG_CACHE_HOME"));
238 dirs.set_config_home(xdg::dir_or_else(|| user_home.join(".config"), "XDG_CONFIG_HOME"));
239 dirs.set_data_home(xdg::dir_or_else(|| user_home.join(".local/share"), "XDG_DATA_HOME"));
240 dirs.set_state_home(xdg::dir_or_else(|| user_home.join(".local/state"), "XDG_STATE_HOME"));
241 if let Some(runtime) = xdg::dir("XDG_RUNTIME_DIR") {
242 dirs.set_runtime_home(runtime);
243 }
244
245 dirs.set_config_dirs(xdg::dirs_or("/etc/xdg", "XDG_CONFIG_DIRS"));
246 dirs.set_data_dirs(xdg::dirs_or("/usr/local/share/:/usr/share/", "XDG_DATA_DIRS"));
247
248 Ok(dirs)
249 }
250
251 #[rustdoc_skip_body]
252 fn runtime_home(&self) -> Option<&Path> {
253 self.extra.runtime.as_deref()
254 }
255
256 #[rustdoc_skip_body]
257 fn config_dirs(&self) -> Option<XdgDirs<'_>> {
258 self.extra.config_path.as_deref().map(XdgDirs::new)
259 }
260
261 #[rustdoc_skip_body]
262 fn data_dirs(&self) -> Option<XdgDirs<'_>> {
263 self.extra.data_path.as_deref().map(XdgDirs::new)
264 }
265
266 fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self {
267 assert!(path.is_absolute(), "runtime directory path must be absolute");
268 self.extra.runtime = Some(path);
269 self
270 }
271
272 fn set_config_dirs(&mut self, paths: OsString) -> &mut Self {
273 for path in split_paths_ref(&paths) {
274 assert!(path.is_absolute(), "config directory paths must be absolute");
275 }
276 self.extra.config_path = Some(paths);
277 self
278 }
279
280 fn set_data_dirs(&mut self, paths: OsString) -> &mut Self {
281 for path in split_paths_ref(&paths) {
282 assert!(path.is_absolute(), "data directory paths must be absolute");
283 }
284 self.extra.data_path = Some(paths);
285 self
286 }
287}
288
289#[unstable(feature = "fs_media_dirs", issue = "162083")]
290impl MediaDirsExt for MediaDirs {
291 fn xdg() -> io::Result<Self> {
292 let user_home = xdg::user_home()?;
293 let config_home = xdg::dir_or_else(|| user_home.join(".config"), "XDG_CONFIG_HOME");
294
295 let spec = match fs::read(config_home.join("user-dirs.dirs")) {
296 Ok(spec) => spec,
297 Err(e) if e.kind() == ErrorKind::NotFound => {
298 return Err(const_error!(
299 ErrorKind::NotFound,
300 "missing `$XDG_CONFIG_HOME/user-dirs.dirs`",
301 ));
302 }
303 Err(e) => return Err(e),
304 };
305
306 Ok(xdg::parse_user_dirs(&spec, &user_home))
307 }
308
309 #[rustdoc_skip_body]
310 fn templates(&self) -> Option<&Path> {
311 self.extra.templates.as_deref()
312 }
313
314 fn set_templates(&mut self, path: PathBuf) -> &mut Self {
315 assert!(path.is_absolute(), "templates directory path must be absolute");
316 self.extra.templates = Some(path);
317 self
318 }
319}
320
321impl<'a> XdgDirs<'a> {
322 fn new(paths: &'a OsStr) -> Self {
323 XdgDirs(split_paths_ref(paths))
324 }
325}
326
327#[unstable(feature = "fs_home_dirs", issue = "162082")]
328impl<'a> Iterator for XdgDirs<'a> {
329 type Item = &'a Path;
330
331 fn next(&mut self) -> Option<Self::Item> {
332 self.0.next()
333 }
334}
335
336fn split_paths_ref<'a>(paths: &'a OsStr) -> SplitPathsRef<'a> {
337 env::split_paths_ref(paths).unwrap()
339}
340
341mod xdg {
342 use super::{MediaDirs, split_paths_ref};
343 use crate::env::{self, join_paths, var_os};
344 use crate::ffi::OsString;
345 use crate::io::{self, ErrorKind, const_error};
346 use crate::macros::rustdoc_skip_body;
347 use crate::ops::Deref;
348 use crate::os::unix::ffi::{OsStrExt, OsStringExt};
349 use crate::path::{Path, PathBuf};
350
351 pub fn user_home() -> io::Result<PathBuf> {
352 env::home_dir()
353 .filter(|p| p.is_absolute())
354 .ok_or(const_error!(ErrorKind::InvalidData, "user home directory path not absolute"))
355 }
356
357 pub fn dir(env: &str) -> Option<PathBuf> {
358 var_os(env).filter(|s| !s.is_empty()).map(PathBuf::from).filter(|path| path.is_absolute())
359 }
360
361 pub fn dir_or_else(fallback: impl FnOnce() -> PathBuf, env: &str) -> PathBuf {
362 dir(env).unwrap_or_else(fallback)
363 }
364
365 fn dirs(env: &str) -> Option<OsString> {
366 let dirs = var_os(env).filter(|s| !s.is_empty())?;
367 if split_paths_ref(&dirs).all(|p| p.is_absolute()) {
368 return Some(dirs);
369 }
370
371 struct DerefAsRef<T>(pub T);
372 impl<T: Deref<Target: AsRef<U>>, U: ?Sized> AsRef<U> for DerefAsRef<T> {
373 fn as_ref(&self) -> &U {
374 (*self.0).as_ref()
375 }
376 }
377
378 let paths = split_paths_ref(&dirs).filter(|p| p.is_absolute()).map(DerefAsRef);
379 join_paths(paths).ok().filter(|s| !s.is_empty())
380 }
381
382 pub fn dirs_or(fallback: impl Into<OsString>, env: &str) -> OsString {
383 dirs(env).unwrap_or_else(|| fallback.into())
384 }
385
386 #[rustdoc_skip_body]
387 pub fn parse_user_dirs<'a>(spec: &'a [u8], user_home: &Path) -> MediaDirs {
388 let mut dirs = MediaDirs::empty();
389
390 for (xdg, path) in
391 spec.split(|&b| b == b'\n').flat_map(|line| parse_user_dirs_line(line, user_home))
392 {
393 match xdg {
395 b"XDG_DESKTOP_DIR" => dirs.desktop = path,
396 b"XDG_DOCUMENTS_DIR" => dirs.documents = path,
397 b"XDG_DOWNLOAD_DIR" => dirs.downloads = path,
398 b"XDG_MUSIC_DIR" => dirs.music = path,
399 b"XDG_PICTURES_DIR" => dirs.pictures = path,
400 b"XDG_VIDEOS_DIR" => dirs.videos = path,
401 b"XDG_TEMPLATES_DIR" => dirs.extra.templates = path,
402 b"XDG_PUBLICSHARE_DIR" => {
403 }
406 _ => {
407 }
409 }
410 }
411
412 dirs
413 }
414
415 fn parse_user_dirs_line<'a>(
416 line: &'a [u8],
417 user_home: &Path,
418 ) -> Option<(&'a [u8], Option<PathBuf>)> {
419 let trimmed = line.trim_ascii();
421 if trimmed.is_empty() || trimmed.starts_with(&[b'#']) {
423 return None;
424 }
425
426 let mut split = trimmed.splitn(2, |&b| b == b'=');
428 let var = split.next()?;
430 let val = split.next()?;
431 debug_assert_eq!(split.next(), None);
432
433 let path = unquote(val, user_home)?;
435
436 let path = Some(path)
437 .filter(|path| path.is_absolute())
439 .filter(|path| path != user_home);
441
442 Some((var, path))
443 }
444
445 fn unquote(bytes: &[u8], user_home: &Path) -> Option<PathBuf> {
446 let [b'"', rest @ .., b'"'] = bytes else { return None };
447
448 if matches!(rest, b"$HOME" | b"$HOME/") {
451 return None;
452 }
453
454 let mut rest = rest;
455 let mut s = Vec::with_capacity(rest.len());
456
457 if rest.starts_with(b"$HOME/") {
459 s.extend_from_slice(user_home.as_os_str().as_bytes());
460 if !user_home.has_trailing_sep() {
461 s.push(b'/');
462 }
463 rest = &rest[6..];
464 }
465
466 loop {
467 let i = rest
468 .iter()
469 .position(|&b| matches!(b, b'"' | b'\\' | b'$' | b'`'))
470 .unwrap_or(rest.len());
471 s.extend_from_slice(&rest[..i]);
472 match &rest[i..] {
473 [] => break,
474 [b'\\', c @ (b'"' | b'\\' | b'$' | b'`'), tail @ ..] => {
475 s.push(*c);
477 rest = tail;
478 }
479 [b'"' | b'\\' | b'$' | b'`', ..] => {
480 return None;
482 }
483 _ => {
484 if cfg!(debug_assertions) {
485 unreachable!()
486 } else {
487 return None;
488 }
489 }
490 }
491 }
492
493 Some(PathBuf::from(OsString::from_vec(s)))
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 #[test]
502 fn can_fetch_xdg_base_dirs() {
503 let dirs = HomeDirs::xdg().unwrap();
504
505 assert!(dirs.cache_home().is_some());
506 assert!(dirs.config_home().is_some());
507 assert!(dirs.data_home().is_some());
508 assert!(dirs.state_home().is_some());
509 assert!(dirs.config_dirs().is_some());
511 assert!(dirs.data_dirs().is_some());
512 }
513
514 #[test]
515 fn can_fetch_xdg_media_dirs() {
516 let dirs = match MediaDirs::xdg() {
517 Ok(dirs) => dirs,
518 Err(e) if e.kind() == ErrorKind::NotFound => {
519 return;
521 }
522 Err(e) => panic!("failed to fetch xdg user dirs: {e:?}"),
523 };
524
525 let _ = dirs;
528 }
529
530 #[test]
531 fn test_well_user_dirs_parsing() {
532 const TEST_USERDIRS: &'static str = r#"
533# This file is written by xdg-user-dirs-update
534# If you want to change or add directories, just edit the line you're
535# interested in. All local changes will be retained on the next run.
536# Format is XDG_xxx_DIR="$HOME/yyy", where yyy is a shell-escaped
537# homedir-relative path, or XDG_xxx_DIR="/yyy", where /yyy is an
538# absolute path. No other format is supported.
539#
540XDG_DESKTOP_DIR="$HOME/Desktop"
541XDG_DOWNLOAD_DIR="$HOME/Downloads"
542XDG_TEMPLATES_DIR="/pub/Templates"
543XDG_PUBLICSHARE_DIR="/pub"
544XDG_DOCUMENTS_DIR="$HOME/Documents"
545XDG_MUSIC_DIR="$HOME/My \"Music\""
546XDG_PICTURES_DIR="$HOME/Pictures"
547XDG_VIDEOS_DIR="$HOME"
548"#;
549
550 let dirs = xdg::parse_user_dirs(TEST_USERDIRS.as_bytes(), Path::new("/home/ferris"));
551
552 assert_eq!(dirs.desktop(), Some("/home/ferris/Desktop".as_ref()));
553 assert_eq!(dirs.downloads(), Some("/home/ferris/Downloads".as_ref()));
554 assert_eq!(dirs.templates(), Some("/pub/Templates".as_ref()));
555 assert_eq!(dirs.documents(), Some("/home/ferris/Documents".as_ref()));
556 assert_eq!(dirs.music(), Some("/home/ferris/My \"Music\"".as_ref()));
557 assert_eq!(dirs.pictures(), Some("/home/ferris/Pictures".as_ref()));
558 assert_eq!(dirs.videos(), None);
559 }
560
561 #[test]
562 fn test_ill_user_dirs_parsing() {
563 const TEST_USERDIRS: &'static str = r#"
564# unquoted
565XDG_DESKTOP_DIR=$HOME/Desktop
566# mixed quoting
567XDG_DOWNLOAD_DIR="$HOME"/"Downloads"
568# non-HOME env
569XDG_TEMPLATES_DIR="/home/$USER/Templates"
570# missing PUBLICSHARE
571# command substitution
572XDG_DOCUMENTS_DIR="`pwd`/Documents"
573# non-symbolic homedir disabled
574XDG_MUSIC_DIR="/home/ferris"
575# set multiple times
576XDG_PICTURES_DIR="$HOME/Pictures1"
577XDG_PICTURES_DIR="$HOME/Pictures2"
578# relative path
579XDG_VIDEOS_DIR="Videos"
580"#;
581
582 let dirs = xdg::parse_user_dirs(TEST_USERDIRS.as_bytes(), Path::new("/home/ferris"));
583
584 assert_eq!(dirs.desktop(), None);
585 assert_eq!(dirs.downloads(), None);
586 assert_eq!(dirs.templates(), None);
587 assert_eq!(dirs.documents(), None);
588 assert_eq!(dirs.music(), None);
589 assert_eq!(dirs.pictures(), Some("/home/ferris/Pictures2".as_ref()));
590 assert_eq!(dirs.videos(), None);
591 }
592}