-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f214bac
Showing
7 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
[package] | ||
name = "clircle" | ||
version = "0.1.0" | ||
authors = ["Niklas Mohrin <[email protected]>"] | ||
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"] } | ||
|
||
[target.'cfg(not(windows))'.dependencies] | ||
nix = "0.19.0" | ||
|
||
[target.'cfg(windows)'.dependencies] | ||
winapi = { version = "0.3.9", features = ["winnt", "winbase", "processenv", "handleapi", "ntdef", "fileapi"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Clircle | ||
|
||
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#[cfg(unix)] | ||
fn main() { | ||
use std::convert::TryFrom; | ||
|
||
let cli_input_args = ["/some/file", "~/myFile"] | ||
.iter() | ||
.map(AsRef::as_ref) | ||
.flat_map(clircle::Identifier::try_from) | ||
.collect::<Vec<_>>(); | ||
let cli_output_args = ["/another/file"] | ||
.iter() | ||
.map(AsRef::as_ref) | ||
.flat_map(clircle::Identifier::try_from) | ||
.collect::<Vec<_>>(); | ||
|
||
let common = clircle::output_among_inputs(&cli_input_args, &cli_output_args); | ||
assert_eq!(common, None); | ||
} | ||
|
||
#[cfg(windows)] | ||
fn main() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
use crate::Stdio; | ||
use nix::libc; | ||
use nix::sys::stat::{fstat, stat, FileStat}; | ||
use std::convert::TryFrom; | ||
use std::path::Path; | ||
|
||
/// Implementation of `Clircle` for Unix. | ||
#[derive(Clone, Copy, PartialEq, Eq, Debug)] | ||
pub struct UnixIdentifier { | ||
/// The `st_dev` of a `FileStat` (returned by the `stat` family of functions). | ||
pub device: u64, | ||
/// The `st_ino` of a `FileStat` (returned by the `stat` family of functions). | ||
pub inode: u64, | ||
} | ||
|
||
impl TryFrom<Stdio> for UnixIdentifier { | ||
type Error = nix::Error; | ||
|
||
fn try_from(stdio: Stdio) -> Result<Self, Self::Error> { | ||
let fd = match stdio { | ||
Stdio::Stdin => libc::STDIN_FILENO, | ||
Stdio::Stdout => libc::STDOUT_FILENO, | ||
Stdio::Stderr => libc::STDERR_FILENO, | ||
}; | ||
fstat(fd).map(UnixIdentifier::from) | ||
} | ||
} | ||
|
||
impl<'a> TryFrom<&'a Path> for UnixIdentifier { | ||
type Error = nix::Error; | ||
|
||
fn try_from(path: &'a Path) -> Result<Self, Self::Error> { | ||
stat(path).map(UnixIdentifier::from) | ||
} | ||
} | ||
|
||
impl From<FileStat> for UnixIdentifier { | ||
fn from(stats: FileStat) -> Self { | ||
UnixIdentifier { | ||
device: stats.st_dev, | ||
inode: stats.st_ino, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
use crate::Stdio; | ||
use std::convert::TryFrom; | ||
use std::io; | ||
use std::iter; | ||
use std::mem::MaybeUninit; | ||
use std::os::windows::ffi::OsStrExt; | ||
use std::path::Path; | ||
use std::ptr::null_mut; | ||
use winapi::shared::ntdef::NULL; | ||
use winapi::um::fileapi::{ | ||
CreateFileW, GetFileInformationByHandle, GetFileType, BY_HANDLE_FILE_INFORMATION, OPEN_EXISTING, | ||
}; | ||
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; | ||
use winapi::um::processenv::GetStdHandle; | ||
use winapi::um::winbase::{FILE_TYPE_DISK, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}; | ||
use winapi::um::winnt::{FILE_ATTRIBUTE_NORMAL, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, HANDLE}; | ||
|
||
/// Implementation of `Clircle` for Windows. | ||
#[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
pub struct WindowsIdentifier { | ||
volume_serial: u32, | ||
file_index: u64, | ||
} | ||
|
||
impl TryFrom<HANDLE> for WindowsIdentifier { | ||
type Error = io::Error; | ||
|
||
fn try_from(handle: HANDLE) -> Result<Self, Self::Error> { | ||
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 unsafe { 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::<BY_HANDLE_FILE_INFORMATION>::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 = unsafe { 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 = unsafe { fi.assume_init() }; | ||
|
||
Ok(WindowsIdentifier { | ||
volume_serial: fi.dwVolumeSerialNumber, | ||
file_index: u64::from(fi.nFileIndexHigh) << 32 | u64::from(fi.nFileIndexLow), | ||
}) | ||
} | ||
} | ||
} | ||
|
||
impl TryFrom<Stdio> for WindowsIdentifier { | ||
type Error = io::Error; | ||
|
||
fn try_from(stdio: Stdio) -> Result<Self, Self::Error> { | ||
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()); | ||
} | ||
|
||
Self::try_from(handle) | ||
} | ||
} | ||
|
||
impl TryFrom<&'_ Path> for WindowsIdentifier { | ||
type Error = io::Error; | ||
|
||
fn try_from(path: &Path) -> Result<Self, Self::Error> { | ||
// Convert to C-style UTF-16 | ||
let path: Vec<u16> = path | ||
.as_os_str() | ||
.encode_wide() | ||
.chain(iter::once(0)) | ||
.collect(); | ||
|
||
// SAFETY: Arguments are specified according to documentation and failure is caught below. | ||
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew | ||
let handle = unsafe { | ||
CreateFileW( | ||
path.as_ptr(), | ||
FILE_READ_ATTRIBUTES, | ||
// Other processes can still read the file, but cannot write to it | ||
FILE_SHARE_READ, | ||
// No extra security attributes needed | ||
null_mut(), | ||
OPEN_EXISTING, | ||
FILE_ATTRIBUTE_NORMAL, | ||
// No meaning in this mode | ||
null_mut(), | ||
) | ||
}; | ||
|
||
if handle == INVALID_HANDLE_VALUE || handle == NULL { | ||
return Err(io::Error::last_os_error()); | ||
} | ||
|
||
let ret = WindowsIdentifier::try_from(handle); | ||
// SAFETY: The handle is valid by the above comparison. | ||
// https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle | ||
unsafe { CloseHandle(handle) }; | ||
ret | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
//! 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. | ||
//! 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. | ||
#![deny(clippy::all)] | ||
#![deny(missing_docs)] | ||
#![warn(clippy::pedantic)] | ||
|
||
#[cfg(unix)] | ||
mod clircle_unix; | ||
#[cfg(unix)] | ||
pub use clircle_unix::UnixIdentifier; | ||
|
||
#[cfg(windows)] | ||
mod clircle_windows; | ||
#[cfg(windows)] | ||
pub use clircle_windows::WindowsIdentifier; | ||
|
||
#[cfg(unix)] | ||
/// Identifies a file. The type is aliased according to the target platform. | ||
pub type Identifier = UnixIdentifier; | ||
#[cfg(windows)] | ||
/// Identifies a file. The type is aliased according to the target platform. | ||
pub type Identifier = WindowsIdentifier; | ||
|
||
#[cfg(feature = "serde")] | ||
use serde::{Deserialize, Serialize}; | ||
use std::convert::TryFrom; | ||
use std::path::Path; | ||
|
||
/// The `Clircle` trait describes the public interface of the crate. | ||
/// It contains all the platform-independent functionality. | ||
/// This trait is implemented for the structs `UnixIdentifier` and `WindowsIdentifier`. | ||
pub trait Clircle: Eq + TryFrom<Stdio> + for<'a> TryFrom<&'a Path> {} | ||
|
||
/// 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, | ||
} | ||
|
||
impl<T> Clircle for T where T: Eq + TryFrom<Stdio> + for<'a> TryFrom<&'a Path> {} | ||
|
||
/// 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)) | ||
} | ||
|
||
/// Finds `Stdio::Stdout` in the given slice. | ||
pub fn stdout_among_inputs<T>(inputs: &[T]) -> bool | ||
where | ||
T: Clircle, | ||
{ | ||
T::try_from(Stdio::Stdout).map_or(false, |stdout| inputs.contains(&stdout)) | ||
} |