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::{
36		ffi::{OsStrExt as _, OsStringExt as _},
37		fs::FileTypeExt as _,
38	},
39	path::{Component, Path, PathBuf},
40};
41
42/// A list of search directories that the config files will be searched under.
43#[derive(Clone, Debug)]
44pub struct SearchDirectories<'a> {
45	inner: Vec<Cow<'a, Path>>,
46}
47
48impl<'a> SearchDirectories<'a> {
49	/// Start with an empty list of search directories.
50	pub const fn empty() -> Self {
51		Self {
52			inner: vec![],
53		}
54	}
55
56	/// Start with the default search directory roots for a system application on a classic Linux distribution.
57	///
58	/// The OS vendor ships configuration in `/usr/lib`, ephemeral configuration is defined in `/var/run`,
59	/// and the sysadmin places overrides in `/etc`.
60	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	/// Start with the default search directory roots for a system application on a modern Linux distribution.
71	///
72	/// The OS vendor ships configuration in `/usr/etc`, ephemeral configuration is defined in `/run`,
73	/// and the sysadmin places overrides in `/etc`.
74	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	/// Append the directory for local user config overrides, `$XDG_CONFIG_HOME`.
85	///
86	/// If the `dirs` crate feature is enabled, then `dirs::config_dir()` is used for the implementation of `$XDG_CONFIG_HOME`,
87	/// else a custom implementation is used.
88	#[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			// If the value fails validation, ignore it.
114			_ = self.push(user_config_dir.into());
115		}
116
117		self
118	}
119
120	/// Prepend the specified path to all search directories.
121	///
122	/// # Errors
123	///
124	/// Returns `Err(InvalidPathError)` if `root` does not start with a [`Component::RootDir`] or if it contains [`Component::ParentDir`].
125	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	/// Appends a search directory to the end of the list.
148	/// Files found in this directory will override files found in earlier directories.
149	///
150	/// # Errors
151	///
152	/// Returns `Err(InvalidPathError)` if `path` does not start with a [`Component::RootDir`] or if it contains [`Component::ParentDir`].
153	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	/// Search for configuration files for the given project name.
162	///
163	/// The project name is usually the name of your application.
164	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	/// Search for configuration files with the given config file name.
176	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/// Error returned when a path does not start with [`Component::RootDir`] or when it contains [`Component::ParentDir`].
203#[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/// A list of search directories that the config files will be searched under, scoped to a particular project.
215///
216/// Created using [`SearchDirectories::with_project`].
217#[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	/// Search for configuration files of this project with the given config file name.
225	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	/// Returns an [`Iterator`] of `(`[`PathBuf`]`, `[`File`]`)`s for all the files found in the specified search directories.
238	/// The name `format!("{project}.d")` is appended to each search directory, then those directories are searched as if they are
239	/// dropin directories. Only dropin files whose name ends with `dropin_suffix` will be considered.
240	/// Note that if you intend to use a file extension as a suffix, then `dropin_suffix` must include the `.`, such as `".conf"`.
241	///
242	/// You will likely want to parse each file returned by this function according to whatever format they're supposed to contain
243	/// and merge them into a unified config object, with settings from later files overriding settings from earlier files.
244	/// This function does not guarantee that the files are well-formed, only that they exist and could be opened for reading.
245	///
246	/// # Errors
247	///
248	/// Any errors from reading non-existing directories and non-existing files are ignored.
249	/// Apart from that, any I/O errors from walking the directories and from opening the files found within are propagated.
250	///
251	/// # Examples
252	///
253	/// ## Get all config files for the system service `foobar`
254	///
255	/// ... taking into account OS vendor configs, ephemeral overrides and sysadmin overrides.
256	///
257	/// ```rust
258	/// let files =
259	///     uapi_config::SearchDirectories::modern_system()
260	///     .with_project("foobar")
261	///     .find_files(".conf")
262	///     .unwrap();
263	/// ```
264	///
265	/// This will locate all dropins `/usr/etc/foobar.d/*.conf`, `/run/foobar.d/*.conf`, `/etc/foobar.d/*.conf` in lexicographical order.
266	///
267	/// ## Get all config files for the application `foobar`
268	///
269	/// ... taking into account OS vendor configs, ephemeral overrides, sysadmin overrides and local user overrides.
270	///
271	/// ```rust
272	/// let files =
273	///     uapi_config::SearchDirectories::modern_system()
274	///     .with_user_directory()
275	///     .with_project("foobar")
276	///     .find_files(".conf")
277	///     .unwrap();
278	/// ```
279	///
280	/// This will locate all dropins `/usr/etc/foobar.d/*.conf`, `/run/foobar.d/*.conf`, `/etc/foobar.d/*.conf`, `$XDG_CONFIG_HOME/foobar.d/*.conf`
281	/// in lexicographical order.
282	///
283	#[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/// A list of search directories that the config files will be searched under, scoped to a particular config file name.
332///
333/// Created using [`SearchDirectories::with_file_name`].
334#[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	/// Search for configuration files for the given project name and with this config file name.
342	///
343	/// The project name is usually the name of your application.
344	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	/// Returns an [`Iterator`] of `(`[`PathBuf`]`, `[`File`]`)`s for all the files found in the specified search directories.
357	/// Only files named `file_name` under the search directories will be considered.
358	///
359	/// If `dropin_suffix` is provided, then directories named `format!("{file_name}.d")` under the search directories are treated as dropin directories.
360	/// 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,
361	/// then `dropin_suffix` must include the `.`, such as `".conf"`.
362	///
363	/// You will likely want to parse each file returned by this function according to whatever format they're supposed to contain
364	/// and merge them into a unified config object, with settings from later files overriding settings from earlier files.
365	/// This function does not guarantee that the files are well-formed, only that they exist and could be opened for reading.
366	///
367	/// # Errors
368	///
369	/// Any errors from reading non-existing directories and non-existing files are ignored.
370	/// Apart from that, any I/O errors from walking the directories and from opening the files found within are propagated.
371	///
372	/// # Examples
373	///
374	/// ## Get all config files for the system service `foobar`
375	///
376	/// ... taking into account OS vendor configs, ephemeral overrides and sysadmin overrides.
377	///
378	/// ```rust
379	/// let files =
380	///     uapi_config::SearchDirectories::modern_system()
381	///     .with_file_name("foobar.conf")
382	///     .find_files(Some(".conf"))
383	///     .unwrap();
384	/// ```
385	///
386	/// This will locate `/usr/etc/foobar.conf` `/run/foobar.conf`, `/etc/foobar.conf` in that order and return the last one,
387	/// then all dropins `/usr/etc/foobar.d/*.conf`, `/run/foobar.d/*.conf`, `/etc/foobar.d/*.conf` in lexicographical order.
388	///
389	/// ## Get the config files for the "foo.service" systemd system unit like systemd would do
390	///
391	/// ```rust
392	/// let search_directories: uapi_config::SearchDirectories =
393	///     // From `man systemd.unit`
394	///     [
395	///         "/run/systemd/generator.late",
396	///         "/usr/lib/systemd/system",
397	///         "/usr/local/lib/systemd/system",
398	///         "/run/systemd/generator",
399	///         "/run/systemd/system",
400	///         "/etc/systemd/system",
401	///         "/run/systemd/generator.early",
402	///         "/run/systemd/transient",
403	///         "/run/systemd/system.control",
404	///         "/etc/systemd/system.control",
405	///     ].into_iter()
406	///     .map(|path| std::path::Path::new(path).into())
407	///     .collect();
408	/// let files =
409	///     search_directories
410	///     .with_file_name("foo.service")
411	///     .find_files(Some(".conf"))
412	///     .unwrap();
413	/// ```
414	///
415	/// This will locate `/run/systemd/generator.late/foobar.service` `/usr/lib/systemd/system/foo.service`, ... in that order and return the last one,
416	/// then all dropins `/run/systemd/generator.late/foo.service.d/*.conf`, `/usr/lib/systemd/system/foo.service.d/*.conf`, ... in lexicographical order.
417	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/// A list of search directories that the config files will be searched under, scoped to a particular project and config file name.
450///
451/// Created using [`SearchDirectoriesForProject::with_file_name`] or [`SearchDirectoriesForFileName::with_project`].
452#[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	/// Returns an [`Iterator`] of `(`[`PathBuf`]`, `[`File`]`)`s for all the files found in the specified search directories.
461	/// The project name is appended to each search directory, then those directories are searched for files named `file_name`.
462	///
463	/// If `dropin_suffix` is provided, then directories named `format!("{file_name}.d")` under the search directories are treated as dropin directories.
464	/// 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,
465	/// then `dropin_suffix` must include the `.`, such as `".conf"`.
466	///
467	/// You will likely want to parse each file returned by this function according to whatever format they're supposed to contain
468	/// and merge them into a unified config object, with settings from later files overriding settings from earlier files.
469	/// This function does not guarantee that the files are well-formed, only that they exist and could be opened for reading.
470	///
471	/// # Errors
472	///
473	/// Any errors from reading non-existing directories and non-existing files are ignored.
474	/// Apart from that, any I/O errors from walking the directories and from opening the files found within are propagated.
475	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		// `/dev/null` is a character device.
544		// We could allow FIFOs and block devices here too, but it doesn't seem likely anyone would want to use them.
545		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			// `/dev/null` is a character device.
592			// We could allow FIFOs and block devices here too, but it doesn't seem likely anyone would want to use them.
593			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/// The iterator of files returned by [`SearchDirectoriesForProject::find_files`],
605/// [`SearchDirectoriesForFileName::find_files`] and [`SearchDirectoriesForProjectAndFileName::find_files`].
606#[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}