clircle/lib.rs
1//! The `clircle` crate helps you detect IO circles in your CLI applications.
2//!
3//! Imagine you want to read data from a couple of files and output something according to the
4//! contents of these files. If the user redirects the output of your program to one of the input
5//! files, you might end up in an infinite circle of reading and writing.
6//!
7//! The crate provides the struct `Identifier` which is a platform dependent type alias, so that
8//! you can use it on all platforms and do not need to introduce any conditional compilation
9//! yourself. `Identifier` implements the `Clircle` trait, which is where you should look for the
10//! public functionality.
11//!
12//! The `Clircle` trait is implemented on both of these structs and requires `TryFrom` for the
13//! `clircle::Stdio` enum and for `File`, so that all possible inputs can be represented as an
14//! `Identifier`. Additionally, there are `unsafe` methods for each specific implementation, but
15//! they are not recommended to use.
16//! Finally, `Clircle` is a subtrait of `Eq`, which allows checking if two `Identifier`s point to
17//! the same file, even if they don't conflict. If you only need this last feature, you should
18//! use [`same-file`](https://crates.io/crates/same-file) instead of this crate.
19//!
20//! ## Examples
21//!
22//! To check if two `Identifier`s conflict, use
23//! `Clircle::surely_conflicts_with`:
24//!
25//! ```rust,no_run
26//! # fn example() -> Option<()> {
27//! # use clircle::{Identifier, Clircle, Stdio::{Stdin, Stdout}};
28//! # use std::convert::TryFrom;
29//! let stdin = Identifier::stdin()?;
30//! let stdout = Identifier::stdout()?;
31//!
32//! if stdin.surely_conflicts_with(&stdout) {
33//! eprintln!("stdin and stdout are conflicting!");
34//! }
35//! # Some(())
36//! # }
37//! ```
38//!
39//! On Linux, the above snippet could be used to detect `cat < x > x`, while allowing just
40//! `cat`, although stdin and stdout are pointing to the same pty in both cases. On Windows, this
41//! code will not print anything, because the same operation is safe there.
42
43#![deny(clippy::all)]
44#![deny(missing_docs)]
45#![warn(clippy::pedantic)]
46
47cfg_if::cfg_if! {
48 if #[cfg(unix)] {
49 mod clircle_unix;
50 use clircle_unix as imp;
51 } else if #[cfg(windows)] {
52 mod clircle_windows;
53 use clircle_windows as imp;
54 } else {
55 compile_error!("Neither cfg(unix) nor cfg(windows) was true, aborting.");
56 }
57}
58
59#[cfg(feature = "serde")]
60use serde_derive::{Deserialize, Serialize};
61use std::convert::TryFrom;
62use std::fs::File;
63use std::io;
64
65/// The `Clircle` trait describes the public interface of the crate.
66/// It contains all the platform-independent functionality.
67/// Additionally, an implementation of `Eq` is required, that gives a simple way to check for
68/// conflicts, if using the more elaborate `surely_conflicts_with` method is not wanted.
69/// This trait is implemented for the structs `UnixIdentifier` and `WindowsIdentifier`.
70pub trait Clircle: Eq + TryFrom<Stdio> + TryFrom<File> {
71 /// Returns the `File` that was used for `From<File>`. If the instance was created otherwise,
72 /// this may also return `None`.
73 fn into_inner(self) -> Option<File>;
74
75 /// Checks whether the two values will without doubt conflict. By default, this always returns
76 /// `false`, but implementors can override this method. Currently, only the Unix implementation
77 /// overrides `surely_conflicts_with`.
78 fn surely_conflicts_with(&self, _other: &Self) -> bool {
79 false
80 }
81
82 /// Shorthand for `try_from(Stdio::Stdin)`.
83 #[must_use]
84 fn stdin() -> Option<Self> {
85 Self::try_from(Stdio::Stdin).ok()
86 }
87
88 #[must_use]
89 /// Shorthand for `try_from(Stdio::Stdout)`.
90 fn stdout() -> Option<Self> {
91 Self::try_from(Stdio::Stdout).ok()
92 }
93
94 #[must_use]
95 /// Shorthand for `try_from(Stdio::Stderr)`.
96 fn stderr() -> Option<Self> {
97 Self::try_from(Stdio::Stderr).ok()
98 }
99}
100
101/// The three stdio streams.
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
104#[allow(missing_docs)]
105pub enum Stdio {
106 Stdin,
107 Stdout,
108 Stderr,
109}
110
111/// Finds a common `Identifier` in the two given slices.
112pub fn output_among_inputs<'o, T>(outputs: &'o [T], inputs: &[T]) -> Option<&'o T>
113where
114 T: Clircle,
115{
116 outputs.iter().find(|output| inputs.contains(output))
117}
118
119/// Checks if `Stdio::Stdout` is in the given slice.
120pub fn stdout_among_inputs<T>(inputs: &[T]) -> bool
121where
122 T: Clircle,
123{
124 T::stdout().map_or(false, |stdout| inputs.contains(&stdout))
125}
126
127/// Identifies a file. The type forwards all methods to the platform implementation.
128#[derive(Debug, PartialEq, Eq, Hash)]
129pub struct Identifier(imp::Identifier);
130
131impl Clircle for Identifier {
132 #[must_use]
133 fn into_inner(self) -> Option<File> {
134 self.0.into_inner()
135 }
136
137 fn surely_conflicts_with(&self, other: &Self) -> bool {
138 self.0.surely_conflicts_with(&other.0)
139 }
140}
141
142impl TryFrom<Stdio> for Identifier {
143 type Error = io::Error;
144
145 fn try_from(stdio: Stdio) -> Result<Self, Self::Error> {
146 imp::Identifier::try_from(stdio).map(Self)
147 }
148}
149
150impl TryFrom<File> for Identifier {
151 type Error = io::Error;
152
153 fn try_from(file: File) -> Result<Self, Self::Error> {
154 imp::Identifier::try_from(file).map(Self)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::collections::HashSet;
162 use std::hash::Hash;
163
164 fn contains_duplicates<T>(items: Vec<T>) -> bool
165 where
166 T: Eq + Hash,
167 {
168 let mut set = HashSet::new();
169 items.into_iter().any(|item| !set.insert(item))
170 }
171
172 #[test]
173 fn test_basic_comparisons() -> Result<(), &'static str> {
174 let dir = tempfile::tempdir().expect("Couldn't create tempdir.");
175 let dir_path = dir.path().to_path_buf();
176
177 let filenames = ["a", "b", "c", "d"];
178 let paths: Vec<_> = filenames
179 .iter()
180 .map(|filename| dir_path.join(filename))
181 .collect();
182
183 let identifiers = paths
184 .iter()
185 .map(File::create)
186 .map(Result::unwrap)
187 .map(Identifier::try_from)
188 .map(Result::unwrap)
189 .collect::<Vec<_>>();
190
191 if contains_duplicates(identifiers) {
192 return Err("Duplicate identifier found for set of unique paths.");
193 }
194
195 Ok(())
196 }
197}