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}