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