1use std::{
29 borrow::Cow,
30 collections::BTreeMap,
31 ffi::{OsStr, OsString},
32 fs::{self, File},
33 io,
34 ops::Deref,
35 os::unix::ffi::{OsStrExt as _, OsStringExt as _},
36 path::{Component, Path, PathBuf},
37};
38
39#[derive(Clone, Debug)]
41pub struct SearchDirectories<'a> {
42 inner: Vec<Cow<'a, Path>>,
43}
44
45impl<'a> SearchDirectories<'a> {
46 pub const fn empty() -> Self {
48 Self {
49 inner: vec![],
50 }
51 }
52
53 pub fn classic_system() -> Self {
58 Self {
59 inner: vec![
60 Path::new("/usr/lib").into(),
61 Path::new("/var/run").into(),
62 Path::new("/etc").into(),
63 ],
64 }
65 }
66
67 pub fn modern_system() -> Self {
72 Self {
73 inner: vec![
74 Path::new("/usr/etc").into(),
75 Path::new("/run").into(),
76 Path::new("/etc").into(),
77 ],
78 }
79 }
80
81 #[must_use]
86 pub fn with_user_directory(mut self) -> Self {
87 let user_config_dir;
88 #[cfg(feature = "dirs")]
89 {
90 user_config_dir = dirs::config_dir();
91 }
92 #[cfg(not(feature = "dirs"))]
93 match std::env::var_os("XDG_CONFIG_HOME") {
94 Some(value) if !value.is_empty() => user_config_dir = Some(value.into()),
95
96 _ => match std::env::var_os("HOME") {
97 Some(value) if !value.is_empty() => {
98 let mut value: PathBuf = value.into();
99 value.push(".config");
100 user_config_dir = Some(value);
101 },
102
103 _ => {
104 user_config_dir = None;
105 },
106 },
107 }
108
109 if let Some(user_config_dir) = user_config_dir {
110 _ = self.push(user_config_dir.into());
112 }
113
114 self
115 }
116
117 pub fn chroot(mut self, root: &Path) -> Result<Self, InvalidPathError> {
123 validate_path(root)?;
124
125 for dir in &mut self.inner {
126 let mut new_dir = root.to_owned();
127 for component in dir.components() {
128 match component {
129 Component::Prefix(_) => unreachable!("this variant is Windows-only"),
130 Component::RootDir |
131 Component::CurDir => (),
132 Component::ParentDir => unreachable!("all paths in self.inner went through validate_path or were hard-coded to be valid"),
133 Component::Normal(component) => {
134 new_dir.push(component);
135 },
136 }
137 }
138 *dir = new_dir.into();
139 }
140
141 Ok(self)
142 }
143
144 pub fn push(&mut self, path: Cow<'a, Path>) -> Result<(), InvalidPathError> {
151 validate_path(&path)?;
152
153 self.inner.push(path);
154
155 Ok(())
156 }
157
158 pub fn with_project<TProject>(
162 self,
163 project: TProject,
164 ) -> SearchDirectoriesForProject<'a, TProject>
165 {
166 SearchDirectoriesForProject {
167 inner: self.inner,
168 project,
169 }
170 }
171
172 pub fn with_file_name<TFileName>(
174 self,
175 file_name: TFileName,
176 ) -> SearchDirectoriesForFileName<'a, TFileName>
177 {
178 SearchDirectoriesForFileName {
179 inner: self.inner,
180 file_name,
181 }
182 }
183}
184
185impl Default for SearchDirectories<'_> {
186 fn default() -> Self {
187 Self::empty()
188 }
189}
190
191impl<'a> FromIterator<Cow<'a, Path>> for SearchDirectories<'a> {
192 fn from_iter<T>(iter: T) -> Self where T: IntoIterator<Item = Cow<'a, Path>> {
193 Self {
194 inner: FromIterator::from_iter(iter),
195 }
196 }
197}
198
199#[derive(Debug)]
201pub struct InvalidPathError;
202
203impl std::fmt::Display for InvalidPathError {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.write_str("path contains Component::ParentDir")
206 }
207}
208
209impl std::error::Error for InvalidPathError {}
210
211#[derive(Clone, Debug)]
213pub struct SearchDirectoriesForProject<'a, TProject> {
214 inner: Vec<Cow<'a, Path>>,
215 project: TProject,
216}
217
218impl<'a, TProject> SearchDirectoriesForProject<'a, TProject> {
219 pub fn with_file_name<TFileName>(
221 self,
222 file_name: TFileName,
223 ) -> SearchDirectoriesForProjectAndFileName<'a, TProject, TFileName>
224 {
225 SearchDirectoriesForProjectAndFileName {
226 inner: self.inner,
227 project: self.project,
228 file_name,
229 }
230 }
231
232 #[cfg_attr(feature = "dirs", doc = r#"## Get all config files for the application `foobar`"#)]
279 #[cfg_attr(feature = "dirs", doc = r#""#)]
280 #[cfg_attr(feature = "dirs", doc = r#"... with custom paths for the OS vendor configs, sysadmin overrides and local user overrides."#)]
281 #[cfg_attr(feature = "dirs", doc = r#""#)]
282 #[cfg_attr(feature = "dirs", doc = r#"```rust"#)]
283 #[cfg_attr(feature = "dirs", doc = r#"// OS and sysadmin configs"#)]
284 #[cfg_attr(feature = "dirs", doc = r#"let mut search_directories: uapi_config::SearchDirectories = ["#)]
285 #[cfg_attr(feature = "dirs", doc = r#" std::path::Path::new("/usr/share").into(),"#)]
286 #[cfg_attr(feature = "dirs", doc = r#" std::path::Path::new("/etc").into(),"#)]
287 #[cfg_attr(feature = "dirs", doc = r#"].into_iter().collect();"#)]
288 #[cfg_attr(feature = "dirs", doc = r#""#)]
289 #[cfg_attr(feature = "dirs", doc = r#"// Local user configs under `${XDG_CONFIG_HOME:-$HOME/.config}`"#)]
290 #[cfg_attr(feature = "dirs", doc = r#"if let Some(user_config_dir) = dirs::config_dir() {"#)]
291 #[cfg_attr(feature = "dirs", doc = r#" search_directories.push(user_config_dir.into());"#)]
292 #[cfg_attr(feature = "dirs", doc = r#"}"#)]
293 #[cfg_attr(feature = "dirs", doc = r#""#)]
294 #[cfg_attr(feature = "dirs", doc = r#"let files ="#)]
295 #[cfg_attr(feature = "dirs", doc = r#" search_directories"#)]
296 #[cfg_attr(feature = "dirs", doc = r#" .with_project("foobar")"#)]
297 #[cfg_attr(feature = "dirs", doc = r#" .find_files(".conf")"#)]
298 #[cfg_attr(feature = "dirs", doc = r#" .unwrap();"#)]
299 #[cfg_attr(feature = "dirs", doc = r#"```"#)]
300 #[cfg_attr(feature = "dirs", doc = r#""#)]
301 #[cfg_attr(feature = "dirs", doc = r#"This will locate `/usr/share/foobar.d/*.conf`, `/etc/foobar.d/*.conf`, `$XDG_CONFIG_HOME/foobar.d/*.conf` in that order and return the last one."#)]
302 pub fn find_files<TDropinSuffix>(
303 self,
304 dropin_suffix: TDropinSuffix,
305 ) -> io::Result<Files>
306 where
307 TProject: AsRef<OsStr>,
308 TDropinSuffix: AsRef<OsStr>,
309 {
310 let project = self.project.as_ref().as_bytes();
311
312 let dropins = find_dropins(dropin_suffix.as_ref(), self.inner.into_iter().map(|path| {
313 let mut path_bytes = path.into_owned().into_os_string().into_vec();
314 path_bytes.push(b'/');
315 path_bytes.extend_from_slice(project);
316 path_bytes.extend_from_slice(b".d");
317 PathBuf::from(OsString::from_vec(path_bytes))
318 }))?;
319
320 Ok(Files {
321 inner: None.into_iter().chain(dropins),
322 })
323 }
324}
325
326#[derive(Clone, Debug)]
328pub struct SearchDirectoriesForFileName<'a, TFileName> {
329 inner: Vec<Cow<'a, Path>>,
330 file_name: TFileName,
331}
332
333impl<'a, TFileName> SearchDirectoriesForFileName<'a, TFileName> {
334 pub fn with_project<TProject>(
338 self,
339 project: TProject,
340 ) -> SearchDirectoriesForProjectAndFileName<'a, TProject, TFileName>
341 {
342 SearchDirectoriesForProjectAndFileName {
343 inner: self.inner,
344 project,
345 file_name: self.file_name,
346 }
347 }
348
349 pub fn find_files<TDropinSuffix>(
411 self,
412 dropin_suffix: Option<TDropinSuffix>,
413 ) -> io::Result<Files>
414 where
415 TFileName: AsRef<OsStr>,
416 TDropinSuffix: AsRef<OsStr>,
417 {
418 let file_name = self.file_name.as_ref();
419
420 let main_file = find_main_file(file_name, self.inner.iter().map(Deref::deref))?;
421
422 let dropins =
423 if let Some(dropin_suffix) = dropin_suffix {
424 find_dropins(dropin_suffix.as_ref(), self.inner.into_iter().map(|path| {
425 let mut path_bytes = path.into_owned().into_os_string().into_vec();
426 path_bytes.push(b'/');
427 path_bytes.extend_from_slice(file_name.as_bytes());
428 path_bytes.extend_from_slice(b".d");
429 PathBuf::from(OsString::from_vec(path_bytes))
430 }))?
431 }
432 else {
433 Default::default()
434 };
435
436 Ok(Files {
437 inner: main_file.into_iter().chain(dropins),
438 })
439 }
440}
441
442#[derive(Clone, Debug)]
444pub struct SearchDirectoriesForProjectAndFileName<'a, TProject, TFileName> {
445 inner: Vec<Cow<'a, Path>>,
446 project: TProject,
447 file_name: TFileName,
448}
449
450impl<'a, TProject, TFileName> SearchDirectoriesForProjectAndFileName<'a, TProject, TFileName> {
451 pub fn find_files<TDropinSuffix>(
467 self,
468 dropin_suffix: Option<TDropinSuffix>,
469 ) -> io::Result<Files>
470 where
471 TProject: AsRef<OsStr>,
472 TFileName: AsRef<OsStr>,
473 TDropinSuffix: AsRef<OsStr>,
474 {
475 let project = self.project.as_ref();
476
477 let file_name = self.file_name.as_ref();
478
479 let main_file = find_main_file(file_name, self.inner.iter().map(|path| path.join(project)))?;
480
481 let dropins =
482 if let Some(dropin_suffix) = dropin_suffix {
483 find_dropins(dropin_suffix.as_ref(), self.inner.into_iter().map(|path| {
484 let mut path_bytes = path.into_owned().into_os_string().into_vec();
485 path_bytes.push(b'/');
486 path_bytes.extend_from_slice(project.as_bytes());
487 path_bytes.push(b'/');
488 path_bytes.extend_from_slice(file_name.as_bytes());
489 path_bytes.extend_from_slice(b".d");
490 PathBuf::from(OsString::from_vec(path_bytes))
491 }))?
492 }
493 else {
494 Default::default()
495 };
496
497 Ok(Files {
498 inner: main_file.into_iter().chain(dropins),
499 })
500 }
501}
502
503fn validate_path(path: &Path) -> Result<(), InvalidPathError> {
504 let mut components = path.components();
505
506 if components.next() != Some(Component::RootDir) {
507 return Err(InvalidPathError);
508 }
509
510 if components.any(|component| matches!(component, Component::ParentDir)) {
511 return Err(InvalidPathError);
512 }
513
514 Ok(())
515}
516
517fn find_main_file<I>(
518 file_name: &OsStr,
519 search_directories: I,
520) -> io::Result<Option<(PathBuf, File)>>
521where
522 I: DoubleEndedIterator,
523 I::Item: Deref<Target = Path>,
524{
525 for search_directory in search_directories.rev() {
526 let path = search_directory.join(file_name);
527 let file = match File::open(&path) {
528 Ok(file) => file,
529 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
530 Err(err) => return Err(err),
531 };
532
533 if !file.metadata()?.file_type().is_file() {
534 continue;
535 }
536
537 return Ok(Some((path, file)));
538 }
539
540 Ok(None)
541}
542
543fn find_dropins<I>(
544 suffix: &OsStr,
545 search_directories: I,
546) -> io::Result<std::collections::btree_map::IntoValues<Vec<u8>, (PathBuf, File)>>
547where
548 I: DoubleEndedIterator,
549 I::Item: Deref<Target = Path>,
550{
551 let mut result: BTreeMap<_, _> = Default::default();
552
553 for search_directory in search_directories.rev() {
554 let entries = match fs::read_dir(&*search_directory) {
555 Ok(entries) => entries,
556 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
557 Err(err) => return Err(err),
558 };
559 for entry in entries {
560 let entry = entry?;
561
562 let file_name = entry.file_name();
563 if !file_name.as_bytes().ends_with(suffix.as_bytes()) {
564 continue;
565 }
566
567 if result.contains_key(file_name.as_bytes()) {
568 continue;
569 }
570
571 let path = search_directory.join(&file_name);
572 let file = match File::open(&path) {
573 Ok(file) => file,
574 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
575 Err(err) => return Err(err),
576 };
577
578 if !file.metadata()?.file_type().is_file() {
579 continue;
580 }
581
582 result.insert(file_name.into_vec(), (path, file));
583 }
584 }
585
586 Ok(result.into_values())
587}
588
589#[derive(Debug)]
592#[repr(transparent)]
593pub struct Files {
594 inner: FilesInner,
595}
596
597type FilesInner =
598 std::iter::Chain<
599 std::option::IntoIter<(PathBuf, File)>,
600 std::collections::btree_map::IntoValues<Vec<u8>, (PathBuf, File)>,
601 >;
602
603impl Iterator for Files {
604 type Item = (PathBuf, File);
605
606 fn next(&mut self) -> Option<Self::Item> {
607 self.inner.next()
608 }
609}
610
611impl DoubleEndedIterator for Files {
612 fn next_back(&mut self) -> Option<Self::Item> {
613 self.inner.next_back()
614 }
615}
616
617const _STATIC_ASSERT_FILES_INNER_IS_FUSED_ITERATOR: () = {
618 const fn is_fused_iterator<T>() where T: std::iter::FusedIterator {}
619 is_fused_iterator::<FilesInner>();
620};
621impl std::iter::FusedIterator for Files {}
622
623#[cfg(test)]
624mod tests {
625 use std::path::{Path, PathBuf};
626
627 use crate::SearchDirectories;
628
629 #[test]
630 fn search_directory_precedence() {
631 for include_usr_etc in [false, true] {
632 for include_run in [false, true] {
633 for include_etc in [false, true] {
634 let mut search_directories = vec![];
635 if include_usr_etc {
636 search_directories.push(concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/usr/etc"));
637 }
638 if include_run {
639 search_directories.push(concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/run"));
640 }
641 if include_etc {
642 search_directories.push(concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/etc"));
643 }
644 let search_directories: SearchDirectories<'_> =
645 search_directories
646 .into_iter()
647 .map(|path| Path::new(path).into())
648 .collect();
649 let files: Vec<_> =
650 search_directories
651 .with_project("foo")
652 .with_file_name("a.conf")
653 .find_files(Some(".conf"))
654 .unwrap()
655 .map(|(path, _)| path)
656 .collect();
657 if include_etc {
658 assert_eq!(files, [
659 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/etc/foo/a.conf"),
660 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/etc/foo/a.conf.d/b.conf"),
661 ].into_iter().map(Into::into).collect::<Vec<PathBuf>>());
662 }
663 else if include_run {
664 assert_eq!(files, [
665 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/run/foo/a.conf"),
666 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/run/foo/a.conf.d/b.conf"),
667 ].into_iter().map(Into::into).collect::<Vec<PathBuf>>());
668 }
669 else if include_usr_etc {
670 assert_eq!(files, [
671 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/usr/etc/foo/a.conf"),
672 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/search_directory_precedence/usr/etc/foo/a.conf.d/b.conf"),
673 ].into_iter().map(Into::into).collect::<Vec<PathBuf>>());
674 }
675 else {
676 assert_eq!(files, Vec::<PathBuf>::new());
677 }
678 }
679 }
680 }
681 }
682
683 #[test]
684 fn only_project() {
685 let files: Vec<_> =
686 SearchDirectories::modern_system()
687 .chroot(Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project")))
688 .unwrap()
689 .with_project("foo")
690 .find_files(".conf")
691 .unwrap()
692 .map(|(path, _)| path)
693 .collect();
694 assert_eq!(files, [
695 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project/etc/foo.d/a.conf"),
696 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project/usr/etc/foo.d/b.conf"),
697 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project/run/foo.d/c.conf"),
698 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project/etc/foo.d/d.conf"),
699 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project/run/foo.d/e.conf"),
700 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_project/usr/etc/foo.d/f.conf"),
701 ].into_iter().map(Into::into).collect::<Vec<PathBuf>>());
702 }
703
704 #[test]
705 fn only_file_name() {
706 let files: Vec<_> =
707 SearchDirectories::modern_system()
708 .chroot(Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name")))
709 .unwrap()
710 .with_file_name("foo.service")
711 .find_files(Some(".conf"))
712 .unwrap()
713 .map(|(path, _)| path)
714 .collect();
715 assert_eq!(files, [
716 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/etc/foo.service"),
717 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/etc/foo.service.d/a.conf"),
718 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/usr/etc/foo.service.d/b.conf"),
719 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/run/foo.service.d/c.conf"),
720 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/etc/foo.service.d/d.conf"),
721 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/run/foo.service.d/e.conf"),
722 concat!(env!("CARGO_MANIFEST_DIR"), "/test-files/only_file_name/usr/etc/foo.service.d/f.conf"),
723 ].into_iter().map(Into::into).collect::<Vec<PathBuf>>());
724 }
725}