clircle-0.3.0/.cargo_vcs_info.json0000644000000001120000000000000124420ustar { "git": { "sha1": "4228092d8a8b5124d879d0480efc6c72a12eb417" } } clircle-0.3.0/.github/workflows/ci.yml000064400000000000000000000036660000000000000157440ustar 00000000000000name: CI on: push: branches: - main pull_request: schedule: - cron: '0 9 * * MON' jobs: build: name: Build and test runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] rust: ["1.40.0", stable] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} default: true profile: minimal components: clippy - uses: actions-rs/cargo@v1 with: command: build - uses: actions-rs/cargo@v1 with: command: test - uses: actions-rs/cargo@v1 with: command: clippy build_extra: name: Build on extra platforms runs-on: ubuntu-latest strategy: fail-fast: false matrix: target: - aarch64-unknown-linux-gnu - arm-unknown-linux-gnueabihf - i586-unknown-linux-gnu - i686-unknown-linux-gnu - arm-linux-androideabi - aarch64-unknown-linux-gnu - aarch64-linux-android - i686-linux-android - x86_64-linux-android steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable target: ${{ matrix.target }} override: true default: true profile: minimal - uses: actions-rs/cargo@v1 with: command: build args: --target ${{ matrix.target }} use-cross: true rustfmt: name: Check rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable default: true profile: minimal components: rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check clircle-0.3.0/.gitignore000064400000000000000000000000230000000000000132010ustar 00000000000000/target Cargo.lock clircle-0.3.0/Cargo.lock0000644000000130100000000000000104160ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "cc" version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clircle" version = "0.3.0" dependencies = [ "cfg-if", "libc", "nix", "serde", "tempfile", "winapi", ] [[package]] name = "getrandom" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "libc" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" [[package]] name = "nix" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" dependencies = [ "bitflags", "cc", "cfg-if", "libc", ] [[package]] name = "ppv-lite86" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", "rand_chacha", "rand_core", "rand_hc", ] [[package]] name = "rand_chacha" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ "rand_core", ] [[package]] name = "redox_syscall" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" dependencies = [ "bitflags", ] [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "serde" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "tempfile" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if", "libc", "rand", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" clircle-0.3.0/Cargo.toml0000644000000027130000000000000104510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "clircle" version = "0.3.0" authors = ["Niklas Mohrin "] description = "Detect IO circles in your CLI apps arguments." homepage = "https://github.com/niklasmohrin/clircle" documentation = "https://docs.rs/clircle" readme = "README.md" keywords = ["cycle", "arguments", "argv", "io"] categories = ["command-line-interface", "filesystem", "os"] license = "MIT OR Apache-2.0" repository = "https://github.com/niklasmohrin/clircle" [dependencies.cfg-if] version = "1.0.0" [dependencies.serde] version = "1.0.117" features = ["derive"] optional = true [dev-dependencies.tempfile] version = "3.1.0" [features] default = ["serde"] [target."cfg(not(windows))".dependencies.libc] version = "0.2" [target."cfg(not(windows))".dev-dependencies.nix] version = "0.19.1" [target."cfg(windows)".dependencies.winapi] version = "0.3.9" features = ["winnt", "winbase", "processenv", "handleapi", "ntdef", "fileapi"] clircle-0.3.0/Cargo.toml.orig000064400000000000000000000016420000000000000141100ustar 00000000000000[package] name = "clircle" version = "0.3.0" authors = ["Niklas Mohrin "] edition = "2018" license = "MIT OR Apache-2.0" description = "Detect IO circles in your CLI apps arguments." homepage = "https://github.com/niklasmohrin/clircle" repository = "https://github.com/niklasmohrin/clircle" documentation = "https://docs.rs/clircle" readme = "README.md" categories = ["command-line-interface", "filesystem", "os"] keywords = ["cycle", "arguments", "argv", "io"] [features] default = ["serde"] [dependencies] serde = { version = "1.0.117", optional = true, features = ["derive"] } cfg-if = "1.0.0" [target.'cfg(not(windows))'.dependencies] libc = "0.2" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["winnt", "winbase", "processenv", "handleapi", "ntdef", "fileapi"] } [dev-dependencies] tempfile = "3.1.0" [target.'cfg(not(windows))'.dev-dependencies] nix = "0.19.1" clircle-0.3.0/README.md000064400000000000000000000045470000000000000125070ustar 00000000000000# Clircle [![CI](https://github.com/niklasmohrin/clircle/workflows/CI/badge.svg?branch=main)](https://github.com/niklasmohrin/clircle/actions) [![crates.io version](https://img.shields.io/crates/v/clircle)](https://crates.io/crates/clircle) [![MSRV](https://img.shields.io/badge/MSRV-1.40.0-blue)](https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html) Clircle provides a cross-platform API to detect read / write cycles from your user-supplied arguments. You can get the important identifiers of a file (from a path) and for all three stdio streams, if they are piped from or to a file as well. ## Why? 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. On both Unix and Windows systems, `Identifier` holds information to identify a file on a disk. The `Clircle` trait is implemented on both of these structs and requires `TryFrom` for the `clircle::Stdio` enum and for `&Path`, so that all possible inputs can be represented as an `Identifier`. Finally, `Clircle` is a subtrait of `Eq`, so that the identifiers can be conveniently compared and circles can be detected. The `clircle` crate also provides some convenience functions around the comparison of `Clircle` implementors. ## Why should I use this and not just `fs::Metadata`? The `clircle` crate seamlessly works on Linux **and** Windows through a single API, so no conditional compilation is needed at all. Furthermore, `MetadataExt` is not stable on Windows yet, meaning you would have to dig into the Windows APIs yourself to get the information needed to identify a file. ## Where did this crate come from? This crate originated in a pull request to the [`bat`](github.com/sharkdp/bat) project. The `bat` tool strives to be a drop-in replacement for the unix tool `cat`. Since `cat` detects these cycles, `bat` has to do so too, which is where most of this code came into play. However, it was decided, that the new logic was - useful for other projects and - too platform specific for `bat`s scope. So now, you can use `clircle` too! clircle-0.3.0/examples/return_code.rs000064400000000000000000000016040000000000000157140ustar 00000000000000//! This example collects all given arguments, interprets them as paths, gathers information //! about them and checks if stdout is being redirected to one of the given files. //! If that's the case, the return code will be set to 1. In both cases, there is an according //! output to stderr. use clircle::{stdout_among_inputs, Identifier}; use std::convert::TryFrom; use std::fs::File; fn main() { let inputs: Vec<_> = std::env::args().collect(); let inputs: Result, _> = inputs .iter() .map(File::open) .map(Result::unwrap) .map(Identifier::try_from) .collect(); let inputs = inputs.expect("There was an argument that could not be converted to an Idenfier!"); if stdout_among_inputs(&inputs) { eprintln!("Cycle detected!"); std::process::exit(1); } else { eprintln!("No cycle detected.") } } clircle-0.3.0/src/clircle_unix.rs000064400000000000000000000155070000000000000150430ustar 00000000000000use crate::{Clircle, Stdio}; use std::convert::TryFrom; use std::fs::File; use std::io::{self, Seek, SeekFrom}; use std::os::unix::fs::MetadataExt; use std::os::unix::io::{FromRawFd, IntoRawFd, RawFd}; use std::{cmp, hash, ops}; /// Re-export of libc pub use libc; /// Implementation of `Clircle` for Unix. #[derive(Debug)] pub struct UnixIdentifier { device: u64, inode: u64, size: u64, is_regular_file: bool, file: Option, owns_fd: bool, } impl UnixIdentifier { fn file(&self) -> &File { self.file.as_ref().expect("Called file() on an identifier that has already been destroyed, this should never happen! Please file a bug!") } fn current_file_offset(&self) -> io::Result { self.file().seek(SeekFrom::Current(0)) } fn has_content_left_to_read(&self) -> io::Result { Ok(self.current_file_offset()? < self.size) } /// Creates a `UnixIdentifier` from a raw file descriptor. The preferred way to create a /// `UnixIdentifier` is through one of the `TryFrom` implementations. /// /// # Safety /// /// The `owns_fd` argument should only be true, if the given file descriptor owns the resource /// it points to (for example a file). /// If it is true, a `File` can be obtained back with `Clircle::into_inner`, or it will be /// closed when the `UnixIdentifier` is dropped. /// /// # Errors /// /// The underlying call to `File::metadata` fails. pub unsafe fn try_from_raw_fd(fd: RawFd, owns_fd: bool) -> io::Result { Self::try_from(File::from_raw_fd(fd)).map(|mut ident| { ident.owns_fd = owns_fd; ident }) } } impl Clircle for UnixIdentifier { #[must_use] fn into_inner(mut self) -> Option { if self.owns_fd { self.owns_fd = false; self.file.take() } else { None } } /// This method implements the conflict check that is used in the GNU coreutils program `cat`. #[must_use] fn surely_conflicts_with(&self, other: &Self) -> bool { PartialEq::eq(self, other) && self.is_regular_file && other.has_content_left_to_read().unwrap_or(true) } } impl TryFrom for UnixIdentifier { type Error = >::Error; fn try_from(stdio: Stdio) -> Result { let fd = match stdio { Stdio::Stdin => libc::STDIN_FILENO, Stdio::Stdout => libc::STDOUT_FILENO, Stdio::Stderr => libc::STDERR_FILENO, }; // Safety: It is okay to create the file, because it won't be dropped later since the // `owns_fd` field is not set. unsafe { Self::try_from_raw_fd(fd, false) } } } impl ops::Drop for UnixIdentifier { fn drop(&mut self) { if !self.owns_fd { let _ = self.file.take().map(IntoRawFd::into_raw_fd); } } } impl TryFrom for UnixIdentifier { type Error = io::Error; fn try_from(file: File) -> Result { file.metadata().map(|metadata| Self { device: metadata.dev(), inode: metadata.ino(), size: metadata.size(), is_regular_file: metadata.file_type().is_file(), file: Some(file), owns_fd: true, }) } } impl cmp::PartialEq for UnixIdentifier { #[must_use] fn eq(&self, other: &Self) -> bool { self.device == other.device && self.inode == other.inode } } impl Eq for UnixIdentifier {} impl hash::Hash for UnixIdentifier { fn hash(&self, state: &mut H) { self.device.hash(state); self.inode.hash(state); } } #[cfg(test)] mod tests { use super::*; use std::error::Error; use std::io::Write; use nix::pty::{openpty, OpenptyResult}; use nix::unistd::close; #[test] fn test_fd_closing() -> Result<(), Box> { let dir = tempfile::tempdir().expect("Couldn't create tempdir."); let dir_path = dir.path().to_path_buf(); // 1) Check that the file returned by into_inner is still valid let file = File::create(dir_path.join("myfile"))?; let ident = UnixIdentifier::try_from(file)?; let mut file = ident .into_inner() .ok_or("Did not get file back from identifier")?; // Check if file can be written to without weird errors file.write_all(b"Some test content")?; // 2) Check that dropping the Identifier does not close the file, if owns_fd is false let fd = file.into_raw_fd(); let ident = unsafe { UnixIdentifier::try_from_raw_fd(fd, false) }; if let Err(e) = ident { let _ = dbg!(close(fd)); return Err(Box::new(e)); } let ident = ident.unwrap(); drop(ident); close(fd).map_err(|e| { format!( "Error closing file, that I told UnixIdentifier not to close: {}", e ) })?; // 3) Check that the file is closed on drop, if owns_fd is true let fd = File::open(dir_path.join("myfile"))?.into_raw_fd(); let ident = unsafe { UnixIdentifier::try_from_raw_fd(fd, true) }; if let Err(e) = ident { let _ = dbg!(close(fd)); return Err(Box::new(e)); } let ident = ident.unwrap(); drop(ident); close(fd).expect_err("This file descriptor should have been closed already!"); Ok(()) } #[test] fn test_pty_equal_but_not_conflicting() -> Result<(), &'static str> { let OpenptyResult { master, slave } = openpty(None, None).expect("Could not open pty."); let res = unsafe { UnixIdentifier::try_from_raw_fd(slave, false) } .map_err(|_| "Error creating UnixIdentifier from pty fd") .and_then(|ident| { if !ident.eq(&ident) { return Err("ident != ident"); } if ident.surely_conflicts_with(&ident) { return Err("pty fd does not conflict with itself, but conflict detected"); } let second_ident = unsafe { UnixIdentifier::try_from_raw_fd(slave, false) } .map_err(|_| "Error creating second Identifier to pty")?; if !ident.eq(&second_ident) { return Err("ident != second_ident"); } if ident.surely_conflicts_with(&second_ident) { return Err( "Two Identifiers to the same pty should not conflict, but they do.", ); } Ok(()) }); let r1 = close(master); let r2 = close(slave); r1.expect("Error closing master end of pty"); r2.expect("Error closing slave end of pty"); res } } clircle-0.3.0/src/clircle_windows.rs000064400000000000000000000105430000000000000155450ustar 00000000000000use crate::{Clircle, Stdio}; use winapi::shared::ntdef::NULL; use winapi::um::{ fileapi::{GetFileInformationByHandle, GetFileType, BY_HANDLE_FILE_INFORMATION}, handleapi::INVALID_HANDLE_VALUE, processenv::GetStdHandle, winbase::{FILE_TYPE_DISK, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}, }; use std::convert::TryFrom; use std::fs::File; use std::mem::MaybeUninit; use std::os::windows::io::{FromRawHandle, IntoRawHandle, RawHandle}; use std::{cmp, hash, io, mem, ops}; /// Re-export of winapi pub use winapi; /// Implementation of `Clircle` for Windows. #[derive(Debug)] pub struct WindowsIdentifier { volume_serial: u32, file_index: u64, handle: RawHandle, owns_handle: bool, } impl WindowsIdentifier { unsafe fn try_from_raw_handle(handle: RawHandle, owns_handle: bool) -> Result { if handle == INVALID_HANDLE_VALUE || handle == NULL { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Tried to convert handle to WindowsIdentifier that was invalid or null.", )); } // SAFETY: This function can be called with any valid handle. // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfiletype if GetFileType(handle) != FILE_TYPE_DISK { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Tried to convert handle to WindowsIdentifier that was not a file handle.", )); } let mut fi = MaybeUninit::::uninit(); // SAFETY: This function is safe to call, if the handle is valid and a handle to a file. // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle let success = GetFileInformationByHandle(handle, fi.as_mut_ptr()); if success == 0 { Err(io::Error::last_os_error()) } else { // SAFETY: If the return value of GetFileInformationByHandle is non-zero, the struct // has successfully been initialized (see link above). let fi = fi.assume_init(); Ok(Self { volume_serial: fi.dwVolumeSerialNumber, file_index: u64::from(fi.nFileIndexHigh) << 32 | u64::from(fi.nFileIndexLow), handle, owns_handle, }) } } unsafe fn take_handle(&mut self) -> Option { if self.owns_handle { self.owns_handle = false; Some(mem::replace(&mut self.handle, INVALID_HANDLE_VALUE)) } else { None } } } impl Clircle for WindowsIdentifier { #[must_use] fn into_inner(mut self) -> Option { Some(unsafe { File::from_raw_handle(self.take_handle()?) }) } } impl TryFrom for WindowsIdentifier { type Error = io::Error; fn try_from(stdio: Stdio) -> Result { let std_handle_id = match stdio { Stdio::Stdin => STD_INPUT_HANDLE, Stdio::Stdout => STD_OUTPUT_HANDLE, Stdio::Stderr => STD_ERROR_HANDLE, }; // SAFETY: This method can safely be called with one of the above constants. // https://docs.microsoft.com/en-us/windows/console/getstdhandle let handle = unsafe { GetStdHandle(std_handle_id) }; if handle == INVALID_HANDLE_VALUE || handle == NULL { return Err(io::Error::last_os_error()); } unsafe { Self::try_from_raw_handle(handle, false) } } } impl TryFrom for WindowsIdentifier { type Error = io::Error; fn try_from(file: File) -> Result { unsafe { Self::try_from_raw_handle(file.into_raw_handle(), true) } } } impl ops::Drop for WindowsIdentifier { fn drop(&mut self) { unsafe { if let Some(handle) = self.take_handle() { drop(File::from_raw_handle(handle)); } } } } impl cmp::PartialEq for WindowsIdentifier { #[must_use] fn eq(&self, other: &Self) -> bool { self.volume_serial == other.volume_serial && self.file_index == other.file_index } } impl Eq for WindowsIdentifier {} impl hash::Hash for WindowsIdentifier { fn hash(&self, state: &mut H) { self.volume_serial.hash(state); self.file_index.hash(state); } } clircle-0.3.0/src/lib.rs000064400000000000000000000134320000000000000131240ustar 00000000000000//! 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 + TryFrom { /// Returns the `File` that was used for `From`. If the instance was created otherwise, /// this may also return `None`. fn into_inner(self) -> Option; /// 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::try_from(Stdio::Stdin).ok() } #[must_use] /// Shorthand for `try_from(Stdio::Stdout)`. fn stdout() -> Option { Self::try_from(Stdio::Stdout).ok() } #[must_use] /// Shorthand for `try_from(Stdio::Stderr)`. fn stderr() -> Option { 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(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(items: Vec) -> 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::>(); if contains_duplicates(identifiers) { return Err("Duplicate identifier found for set of unique paths."); } Ok(()) } }