clircle/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//! The `clircle` crate helps you detect IO circles in your CLI applications.
//!
//! Imagine you want to read data from a couple of files and output something according to the
//! contents of these files. If the user redirects the output of your program to one of the input
//! files, you might end up in an infinite circle of reading and writing.
//!
//! The crate provides the struct `Identifier` which is a platform dependent type alias, so that
//! you can use it on all platforms and do not need to introduce any conditional compilation
//! yourself. `Identifier` implements the `Clircle` trait, which is where you should look for the
//! public functionality.
//!
//! The `Clircle` trait is implemented on both of these structs and requires `TryFrom` for the
//! `clircle::Stdio` enum and for `File`, so that all possible inputs can be represented as an
//! `Identifier`. Additionally, there are `unsafe` methods for each specific implementation, but
//! they are not recommended to use.
//! Finally, `Clircle` is a subtrait of `Eq`, which allows checking if two `Identifier`s point to
//! the same file, even if they don't conflict. If you only need this last feature, you should
//! use [`same-file`](https://crates.io/crates/same-file) instead of this crate.
//!
//! ## Examples
//!
//! To check if two `Identifier`s conflict, use
//! `Clircle::surely_conflicts_with`:
//!
//! ```rust,no_run
//! # fn example() -> Option<()> {
//! # use clircle::{Identifier, Clircle, Stdio::{Stdin, Stdout}};
//! # use std::convert::TryFrom;
//! let stdin = Identifier::stdin()?;
//! let stdout = Identifier::stdout()?;
//!
//! if stdin.surely_conflicts_with(&stdout) {
//!     eprintln!("stdin and stdout are conflicting!");
//! }
//! # Some(())
//! # }
//! ```
//!
//! On Linux, the above snippet could be used to detect `cat < x > x`, while allowing just
//! `cat`, although stdin and stdout are pointing to the same pty in both cases. On Windows, this
//! code will not print anything, because the same operation is safe there.

#![deny(clippy::all)]
#![deny(missing_docs)]
#![warn(clippy::pedantic)]

cfg_if::cfg_if! {
    if #[cfg(unix)] {
        mod clircle_unix;
        use clircle_unix as imp;
    } else if #[cfg(windows)] {
        mod clircle_windows;
        use clircle_windows as imp;
    } else {
        compile_error!("Neither cfg(unix) nor cfg(windows) was true, aborting.");
    }
}

#[cfg(feature = "serde")]
use serde_derive::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::fs::File;
use std::io;

/// The `Clircle` trait describes the public interface of the crate.
/// It contains all the platform-independent functionality.
/// Additionally, an implementation of `Eq` is required, that gives a simple way to check for
/// conflicts, if using the more elaborate `surely_conflicts_with` method is not wanted.
/// This trait is implemented for the structs `UnixIdentifier` and `WindowsIdentifier`.
pub trait Clircle: Eq + TryFrom<Stdio> + TryFrom<File> {
    /// Returns the `File` that was used for `From<File>`. If the instance was created otherwise,
    /// this may also return `None`.
    fn into_inner(self) -> Option<File>;

    /// Checks whether the two values will without doubt conflict. By default, this always returns
    /// `false`, but implementors can override this method. Currently, only the Unix implementation
    /// overrides `surely_conflicts_with`.
    fn surely_conflicts_with(&self, _other: &Self) -> bool {
        false
    }

    /// Shorthand for `try_from(Stdio::Stdin)`.
    #[must_use]
    fn stdin() -> Option<Self> {
        Self::try_from(Stdio::Stdin).ok()
    }

    #[must_use]
    /// Shorthand for `try_from(Stdio::Stdout)`.
    fn stdout() -> Option<Self> {
        Self::try_from(Stdio::Stdout).ok()
    }

    #[must_use]
    /// Shorthand for `try_from(Stdio::Stderr)`.
    fn stderr() -> Option<Self> {
        Self::try_from(Stdio::Stderr).ok()
    }
}

/// The three stdio streams.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[allow(missing_docs)]
pub enum Stdio {
    Stdin,
    Stdout,
    Stderr,
}

/// Finds a common `Identifier` in the two given slices.
pub fn output_among_inputs<'o, T>(outputs: &'o [T], inputs: &[T]) -> Option<&'o T>
where
    T: Clircle,
{
    outputs.iter().find(|output| inputs.contains(output))
}

/// Checks if `Stdio::Stdout` is in the given slice.
pub fn stdout_among_inputs<T>(inputs: &[T]) -> bool
where
    T: Clircle,
{
    T::stdout().map_or(false, |stdout| inputs.contains(&stdout))
}

/// Identifies a file. The type forwards all methods to the platform implementation.
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Identifier(imp::Identifier);

impl Clircle for Identifier {
    #[must_use]
    fn into_inner(self) -> Option<File> {
        self.0.into_inner()
    }

    fn surely_conflicts_with(&self, other: &Self) -> bool {
        self.0.surely_conflicts_with(&other.0)
    }
}

impl TryFrom<Stdio> for Identifier {
    type Error = io::Error;

    fn try_from(stdio: Stdio) -> Result<Self, Self::Error> {
        imp::Identifier::try_from(stdio).map(Self)
    }
}

impl TryFrom<File> for Identifier {
    type Error = io::Error;

    fn try_from(file: File) -> Result<Self, Self::Error> {
        imp::Identifier::try_from(file).map(Self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;
    use std::hash::Hash;

    fn contains_duplicates<T>(items: Vec<T>) -> bool
    where
        T: Eq + Hash,
    {
        let mut set = HashSet::new();
        items.into_iter().any(|item| !set.insert(item))
    }

    #[test]
    fn test_basic_comparisons() -> Result<(), &'static str> {
        let dir = tempfile::tempdir().expect("Couldn't create tempdir.");
        let dir_path = dir.path().to_path_buf();

        let filenames = ["a", "b", "c", "d"];
        let paths: Vec<_> = filenames
            .iter()
            .map(|filename| dir_path.join(filename))
            .collect();

        let identifiers = paths
            .iter()
            .map(File::create)
            .map(Result::unwrap)
            .map(Identifier::try_from)
            .map(Result::unwrap)
            .collect::<Vec<_>>();

        if contains_duplicates(identifiers) {
            return Err("Duplicate identifier found for set of unique paths.");
        }

        Ok(())
    }
}