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
//! 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;
        pub use clircle_unix::{libc, UnixIdentifier};
        /// Identifies a file. The type is aliased according to the target platform.
        pub type Identifier = UnixIdentifier;
    } else if #[cfg(windows)] {
        mod clircle_windows;
        pub use clircle_windows::{winapi, WindowsIdentifier};
        /// Identifies a file. The type is aliased according to the target platform.
        pub type Identifier = WindowsIdentifier;
    } else {
        compile_error!("Neither cfg(unix) nor cfg(windows) was true, aborting.");
    }
}

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

/// 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 `UnixIdentifier`
    /// 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))
}

#[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(())
    }
}