Skip to content

Commit

Permalink
Clircle 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasmohrin committed Oct 31, 2020
0 parents commit f214bac
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
25 changes: 25 additions & 0 deletions Cargo.toml
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"] }
45 changes: 45 additions & 0 deletions README.md
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!
21 changes: 21 additions & 0 deletions examples/basic.rs
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() {}
44 changes: 44 additions & 0 deletions src/clircle_unix.rs
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,
}
}
}
121 changes: 121 additions & 0 deletions src/clircle_windows.rs
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
}
}
77 changes: 77 additions & 0 deletions src/lib.rs
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))
}

0 comments on commit f214bac

Please sign in to comment.