uapi_config/
lib.rs

1//! Rust implementation of [the UAPI Configuration Files Specification.](https://uapi-group.org/specifications/specs/configuration_files_specification/)
2//!
3//! The tl;dr of the spec is:
4//!
5//! - The search acts on a list of search directories. The directories are considered in the order that they appear in the list.
6//!
7//! - In each search directory, the algorithm looks for a config file with the requested filename.
8//!
9//! - In each search directory, a directory with a name that is the requested name followed by ".d" is considered to be a dropin directory.
10//!   Any files in such dropin directories are also considered, in lexicographic order of filename.
11//!
12//! - If a file with the requested name is present in more than one search directory, the file in the earlier search directory is ignored.
13//!   If a dropin file with the same name is present in more than one dropin directory, the file in the earlier dropin directory is ignored.
14//!
15//! - The config file, if found, is yielded, followed by any dropins that were found. Settings in files yielded later override settings in files yielded earlier.
16//!
17//! - If no filename is requested, then the algorithm only looks for a directory with a name that is the project name followed by ".d" under all search directories,
18//!   and treats those directories as dropin directories.
19//!
20//! ```rust
21//! let files /* : impl Iterator<Item = (PathBuf, File)> */ =
22//!     uapi_config::SearchDirectories::modern_system()
23//!     .with_project("foobar")
24//!     .find_files(".conf")
25//!     .unwrap();
26//! ```
27
28use 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/// A list of search directories that the config files will be searched under.
40#[derive(Clone, Debug)]
41pub struct SearchDirectories<'a> {
42	inner: Vec<Cow<'a, Path>>,
43}
44
45impl<'a> SearchDirectories<'a> {
46	/// Start with an empty list of search directories.
47	pub const fn empty() -> Self {
48		Self {
49			inner: vec![],
50		}
51	}
52
53	/// Start with the default search directory roots for a system application on a classic Linux distribution.
54	///
55	/// The OS vendor ships configuration in `/usr/lib`, ephemeral configuration is defined in `/var/run`,
56	/// and the sysadmin places overrides in `/etc`.
57	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	/// Start with the default search directory roots for a system application on a modern Linux distribution.
68	///
69	/// The OS vendor ships configuration in `/usr/etc`, ephemeral configuration is defined in `/run`,
70	/// and the sysadmin places overrides in `/etc`.
71	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	/// Append the directory for local user config overrides, `$XDG_CONFIG_HOME`.
82	///
83	/// If the `dirs` crate feature is enabled, then `dirs::config_dir()` is used for the implementation of `$XDG_CONFIG_HOME`.
84	/// else a custom implementation is used.
85	#[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			// If the value fails validation, ignore it.
111			_ = self.push(user_config_dir.into());
112		}
113
114		self
115	}
116
117	/// Prepend the specified path to all search directories.
118	///
119	/// # Errors
120	///
121	/// Returns `Err(InvalidPathError)` if `root` does not start with a [`Component::RootDir`] or if it contains [`Component::ParentDir`].
122	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	/// Appends a search directory to the end of the list.
145	/// Files found in this directory will override files found in earlier directories.
146	///
147	/// # Errors
148	///
149	/// Returns `Err(InvalidPathError)` if `path` does not start with a [`Component::RootDir`] or if it contains [`Component::ParentDir`].
150	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	/// Search for configuration files for the given project name.
159	///
160	/// The project name is usually the name of your application.
161	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	/// Search for configuration files with the given config file name.
173	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/// Error returned when a path does not start with [`Component::RootDir`] or when it contains [`Component::ParentDir`].
200#[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/// A list of search directories that the config files will be searched under, scoped to a particular project.
212#[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	/// Search for configuration files of this project with the given config file name.
220	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	/// Returns an [`Iterator`] of `(`[`PathBuf`]`, `[`File`]`)`s for all the files found in the specified search directories.
233	/// The name `format!("{project}.d")` is appended to each search directory, then those directories are searched as if they are
234	/// dropin directories. Only dropin files whose name ends with `dropin_suffix` will be considered.
235	/// Note that if you intend to use a file extension as a suffix, then `dropin_suffix` must include the `.`, such as `".conf"`.
236	///
237	/// You will likely want to parse each file returned by this function according to whatever format they're supposed to contain
238	/// and merge them into a unified config object, with settings from later files overriding settings from earlier files.
239	/// This function does not guarantee that the files are well-formed, only that they exist and could be opened for reading.
240	///
241	/// # Errors
242	///
243	/// Any errors from reading non-existing directories and non-existing files are ignored.
244	/// Apart from that, any I/O errors from walking the directories and from opening the files found within are propagated.
245	///
246	/// # Examples
247	///
248	/// ## Get all config files for the system service `foobar`
249	///
250	/// ... taking into account OS vendor configs, ephemeral overrides and sysadmin overrides.
251	///
252	/// ```rust
253	/// let files =
254	///     uapi_config::SearchDirectories::modern_system()
255	///     .with_project("foobar")
256	///     .find_files(".conf")
257	///     .unwrap();
258	/// ```
259	///
260	/// This will locate all dropins `/usr/etc/foobar.d/*.conf`, `/run/foobar.d/*.conf`, `/etc/foobar.d/*.conf` in lexicographical order.
261	///
262	/// ## Get all config files for the application `foobar`
263	///
264	/// ... taking into account OS vendor configs, ephemeral overrides, sysadmin overrides and local user overrides.
265	///
266	/// ```rust
267	/// let files =
268	///     uapi_config::SearchDirectories::modern_system()
269	///     .with_user_directory()
270	///     .with_project("foobar")
271	///     .find_files(".conf")
272	///     .unwrap();
273	/// ```
274	///
275	/// This will locate all dropins `/usr/etc/foobar.d/*.conf`, `/run/foobar.d/*.conf`, `/etc/foobar.d/*.conf`, `$XDG_CONFIG_HOME/foobar.d/*.conf`
276	/// in lexicographical order.
277	///
278	#[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/// A list of search directories that the config files will be searched under, scoped to a particular config file name.
327#[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	/// Search for configuration files for the given project name and with this config file name.
335	///
336	/// The project name is usually the name of your application.
337	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	/// Returns an [`Iterator`] of `(`[`PathBuf`]`, `[`File`]`)`s for all the files found in the specified search directories.
350	/// Only files named `file_name` under the search directories will be considered.
351	///
352	/// If `dropin_suffix` is provided, then directories named `format!("{file_name}.d")` under the search directories are treated as dropin directories.
353	/// Only dropin files whose name ends with `dropin_suffix` will be considered. Note that if you intend to use a file extension as a suffix,
354	/// then `dropin_suffix` must include the `.`, such as `".conf"`.
355	///
356	/// You will likely want to parse each file returned by this function according to whatever format they're supposed to contain
357	/// and merge them into a unified config object, with settings from later files overriding settings from earlier files.
358	/// This function does not guarantee that the files are well-formed, only that they exist and could be opened for reading.
359	///
360	/// # Errors
361	///
362	/// Any errors from reading non-existing directories and non-existing files are ignored.
363	/// Apart from that, any I/O errors from walking the directories and from opening the files found within are propagated.
364	///
365	/// # Examples
366	///
367	/// ## Get all config files for the system service `foobar`
368	///
369	/// ... taking into account OS vendor configs, ephemeral overrides and sysadmin overrides.
370	///
371	/// ```rust
372	/// let files =
373	///     uapi_config::SearchDirectories::modern_system()
374	///     .with_file_name("foobar.conf")
375	///     .find_files(Some(".conf"))
376	///     .unwrap();
377	/// ```
378	///
379	/// This will locate `/usr/etc/foobar.conf` `/run/foobar.conf`, `/etc/foobar.conf` in that order and return the last one,
380	/// then all dropins `/usr/etc/foobar.d/*.conf`, `/run/foobar.d/*.conf`, `/etc/foobar.d/*.conf` in lexicographical order.
381	///
382	/// ## Get the config files for the "foo.service" systemd system unit like systemd would do
383	///
384	/// ```rust
385	/// let search_directories: uapi_config::SearchDirectories =
386	///     // From `man systemd.unit`
387	///     [
388	///         "/run/systemd/generator.late",
389	///         "/usr/lib/systemd/system",
390	///         "/usr/local/lib/systemd/system",
391	///         "/run/systemd/generator",
392	///         "/run/systemd/system",
393	///         "/etc/systemd/system",
394	///         "/run/systemd/generator.early",
395	///         "/run/systemd/transient",
396	///         "/run/systemd/system.control",
397	///         "/etc/systemd/system.control",
398	///     ].into_iter()
399	///     .map(|path| std::path::Path::new(path).into())
400	///     .collect();
401	/// let files =
402	///     search_directories
403	///     .with_file_name("foo.service")
404	///     .find_files(Some(".conf"))
405	///     .unwrap();
406	/// ```
407	///
408	/// This will locate `/run/systemd/generator.late/foobar.service` `/usr/lib/systemd/system/foo.service`, ... in that order and return the last one,
409	/// then all dropins `/run/systemd/generator.late/foo.service.d/*.conf`, `/usr/lib/systemd/system/foo.service.d/*.conf`, ... in lexicographical order.
410	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/// A list of search directories that the config files will be searched under, scoped to a particular project and config file name.
443#[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	/// Returns an [`Iterator`] of `(`[`PathBuf`]`, `[`File`]`)`s for all the files found in the specified search directories.
452	/// The project name is appended to each search directory, then those directories are searched for files named `file_name`.
453	///
454	/// If `dropin_suffix` is provided, then directories named `format!("{file_name}.d")` under the search directories are treated as dropin directories.
455	/// Only dropin files whose name ends with `dropin_suffix` will be considered. Note that if you intend to use a file extension as a suffix,
456	/// then `dropin_suffix` must include the `.`, such as `".conf"`.
457	///
458	/// You will likely want to parse each file returned by this function according to whatever format they're supposed to contain
459	/// and merge them into a unified config object, with settings from later files overriding settings from earlier files.
460	/// This function does not guarantee that the files are well-formed, only that they exist and could be opened for reading.
461	///
462	/// # Errors
463	///
464	/// Any errors from reading non-existing directories and non-existing files are ignored.
465	/// Apart from that, any I/O errors from walking the directories and from opening the files found within are propagated.
466	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/// The iterator of files returned by [`SearchDirectoriesForProject::find_files`],
590/// [`SearchDirectoriesForFileName::find_files`] and [`SearchDirectoriesForProjectAndFileName::find_files`].
591#[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}