From 923f85d98840e8b94ca68996103c9e3bce822cfe Mon Sep 17 00:00:00 2001 From: Marcel Schneider Date: Tue, 25 Feb 2020 19:35:58 +0100 Subject: [PATCH] add first steps --- Cargo.lock | 408 +++++++++++++++++++++++++++++++++ justfile | 4 +- src/app.rs | 58 +++-- src/errors.rs | 10 +- src/helpers.rs | 61 ++++- src/main.rs | 36 ++- src/validators.rs | 563 ---------------------------------------------- 7 files changed, 533 insertions(+), 607 deletions(-) create mode 100644 Cargo.lock delete mode 100644 src/validators.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..871a5ab --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,408 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" +dependencies = [ + "libc", + "termion", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "error-chain" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" +dependencies = [ + "version_check", +] + +[[package]] +name = "gitig" +version = "0.1.0" +dependencies = [ + "error-chain", + "libc", + "log", + "stderrlog", + "structopt", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "proc-macro-error" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052b3c9af39c7e5e94245f820530487d19eb285faedcb40e0c3275132293f242" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn", + "syn-mid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "rustversion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stderrlog" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25" +dependencies = [ + "atty", + "chrono", + "log", + "termcolor", + "thread_local", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98" +dependencies = [ + "clap", + "lazy_static 1.4.0", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termion" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14" +dependencies = [ + "lazy_static 0.2.11", + "unreachable", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +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-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/justfile b/justfile index 7caee50..fb4713d 100644 --- a/justfile +++ b/justfile @@ -4,10 +4,10 @@ # --== Variables to be customized/overridden by the user ==-- # The target for `cargo` commands to use and `install-rustup-deps` to install -export CARGO_BUILD_TARGET = "i686-unknown-linux-musl" +export CARGO_BUILD_TARGET = "x86_64-unknown-linux-gnu" # An easy way to override the `cargo` channel for just this project -channel = "stable" +channel = "nightly" # Extra cargo features to enable features = "" diff --git a/src/app.rs b/src/app.rs index bc94f87..ce34f49 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,8 +11,7 @@ use log::{debug, error, info, trace, warn}; // Local Imports use crate::errors::*; -use crate::helpers::{BoilerplateOpts, HELP_TEMPLATE}; -use crate::validators::path_readable_file; +use crate::helpers::{git_dir, BoilerplateOpts, HELP_TEMPLATE}; /// The verbosity level when no `-q` or `-v` arguments are given, with `0` being `-q` pub const DEFAULT_VERBOSITY: u64 = 1; @@ -23,8 +22,8 @@ pub const DEFAULT_VERBOSITY: u64 = 1; /// /// * Make sure that there is a blank space between the `` `` line and the /// description text or the `--help` output won't comply with the platform conventions that -/// `help2man` depends on to generate your manpage. -/// (Specifically, it will mistake the ` ` line for part of the description.) +/// `help2man` depends on to generate your manpage. (Specifically, it will mistake the ` +/// ` line for part of the description.) /// * `StructOpt`'s default behaviour of including the author name in the `--help` output is an /// oddity among Linux commands and, if you don't disable it, you run the risk of people /// unfamiliar with `StructOpt` assuming that you are an egotistical person who made a conscious @@ -34,14 +33,13 @@ pub const DEFAULT_VERBOSITY: u64 = 1; /// can read about by typing `man help2man`. /// /// ## Cautions: -/// * Subcommands do not inherit `template` and it must be re-specified for each one. -/// ([clap-rs/clap#1184](https://github.com/clap-rs/clap/issues/1184)) +/// * Subcommands do not inherit `template` and it must be re-specified for each one. ([clap-rs/clap#1184](https://github.com/clap-rs/clap/issues/1184)) /// * Double-check that your choice of `about` or `long_about` is actually overriding this /// doc comment. The precedence is affected by things you wouldn't expect, such as the presence /// or absence of `template` and it's easy to wind up with this doc-comment as your `--help` /// ([TeXitoi/structopt#173](https://github.com/TeXitoi/structopt/issues/173)) -/// * Do not begin the description text for subcommands with `\n`. It will break the formatting -/// in the top-level help output's list of subcommands. +/// * Do not begin the description text for subcommands with `\n`. It will break the formatting in +/// the top-level help output's list of subcommands. #[derive(StructOpt, Debug)] #[structopt(template = HELP_TEMPLATE, about = "TODO: Replace me with the description text for the command", @@ -51,22 +49,44 @@ pub struct CliOpts { #[structopt(flatten)] pub boilerplate: BoilerplateOpts, - // -- Arguments used by application-specific logic -- + #[structopt(subcommand)] + cmd: Command, +} - /// File(s) to use as input - /// - /// **TODO:** Figure out if there's a way to only enforce constraints on this when not asking - /// to dump completions. - #[structopt(parse(from_os_str), - validator_os = path_readable_file)] - inpath: Vec, +#[derive(StructOpt, Debug)] +pub enum Command { + /// Add a line to the gitignore + Add { + glob: String, + }, + Get { + lang: String, + }, +} + +/// Runs the command `add` +fn run_add(glob: &str) -> Result<()> { + trace!("running command `add` with glob '{}'", &glob); + let root = match git_dir()? { + Some(r) => r, + None => return Err(ErrorKind::NoGitRootFound.into()), + }; + info!("Working with git root in {:?}", root); + + Ok(()) +} + +/// Runs the command `get` +fn run_get(lang: &str) -> Result<()> { + unimplemented!(); } /// The actual `main()` pub fn main(opts: CliOpts) -> Result<()> { - for inpath in opts.inpath { - unimplemented!() - } + match opts.cmd { + Command::Add { glob } => run_add(&glob)?, + Command::Get { lang } => run_get(&lang)?, + }; Ok(()) } diff --git a/src/errors.rs b/src/errors.rs index 94fa79f..3da0bff 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,11 @@ /*! `error-chain` boilerplate and custom `Error` types */ // Copyright 2020, Marcel Schneider -use error_chain::*; - // Create the Error, ErrorKind, ResultExt, and Result types -error_chain! {} +error_chain! { + errors{ + NoGitRootFound { + description("no git root found"), + } + } +} diff --git a/src/helpers.rs b/src/helpers.rs index 02985a3..a255611 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -5,7 +5,13 @@ // for .expect() instead) in my two Option fields and the `allow` gets ignored unless I // `#![...]` it onto the entire module. #![allow(clippy::result_unwrap_used)] +use log::{info, trace}; +use crate::errors::*; +use std::fs::DirEntry; +use std::fs::ReadDir; +use std::path::Path; +use std::path::PathBuf; use structopt::{clap, StructOpt}; /// Modified version of Clap's default template for proper help2man compatibility @@ -26,17 +32,9 @@ USAGE: "; /// Options used by boilerplate code -// TODO: Move these into a struct of their own in something like helpers.rs #[derive(StructOpt, Debug)] #[structopt(rename_all = "kebab-case")] pub struct BoilerplateOpts { - - // -- Arguments used by main.rs -- - // TODO: Move these into a struct of their own in something like helpers.rs - - // FIXME: Report that StructOpt trips Clippy's `cast_possible_truncation` lint unless I use - // `u64` for my `from_occurrences` inputs, which is a ridiculous state of things. - /// Decrease verbosity (-q, -qq, -qqq, etc.) #[structopt(short, long, parse(from_occurrences))] pub quiet: u64, @@ -54,3 +52,50 @@ pub struct BoilerplateOpts { pub dump_completions: Option, } +/// Checks if the given dir contains the root `.git` dir +/// +/// # Errors +/// +/// Returns an `Err` if any of the fs related methods return an error +fn has_git_dir(path: &PathBuf) -> Result { + trace!("Checking for git root in {:?}", &path); + let mut git: PathBuf = path.clone(); + git.push(".git"); + for entry in path.read_dir().chain_err(|| "Reading contents of dir")? { + if let Ok(entry) = entry { + // skip non-directories + if let Ok(ft) = entry.file_type() { + if !ft.is_dir() { + continue; + } + } + + // check if this is `.git` + if entry.path() == git { + return Ok(true); + } + } + } + Ok(false) +} + +/// Returns the root git directory for the current directory if there is one +/// +/// # Errors +/// +/// Returns an [`Err`](std::Err) if the current cwd is invalid (refer to +/// [`current_dir`](std::env::current_dir)) +pub fn git_dir() -> Result> { + let mut cwd: Option = + Some(std::env::current_dir().chain_err(|| "Error with current dir")?); + while cwd.is_some() { + let c = cwd.ok_or("Should not have been none, as checked before in if")?; + if has_git_dir(&c)? { + return Ok(Some(c)); + } + cwd = c.parent().map(PathBuf::from); + } + + info!("Arrived at filesystem root while checking for git folder"); + Ok(None) +} diff --git a/src/main.rs b/src/main.rs index b8fbdc8..e07a12a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,42 @@ /*! TODO: Application description here -This file provided by [rust-cli-boilerplate](https://github.com/ssokolow/rust-cli-boilerplate) +This project used [rust-cli-boilerplate](https://github.com/ssokolow/rust-cli-boilerplate) */ // Copyright 2017-2019, Stephan Sokolow // `error_chain` recursion adjustment #![recursion_limit = "1024"] - // Make rustc's built-in lints more strict and set clippy into a whitelist-based configuration so // we see new lints as they get written (We'll opt back out selectively) -#![warn(warnings, rust_2018_idioms, clippy::all, clippy::complexity, clippy::correctness, - clippy::pedantic, clippy::perf, clippy::style, clippy::restriction)] - +#![warn( + warnings, + rust_2018_idioms, + clippy::all, + clippy::complexity, + clippy::correctness, + clippy::pedantic, + clippy::perf, + clippy::style, + clippy::restriction +)] // Opt out of the lints I've seen and don't want #![allow(clippy::float_arithmetic, clippy::implicit_return)] +#[macro_use] +extern crate error_chain; + // stdlib imports -use std::io; use std::convert::TryInto; +use std::io; // 3rd-party imports mod errors; -use structopt::{clap, StructOpt}; use log::error; +use structopt::{clap, StructOpt}; // Local imports mod app; mod helpers; -mod validators; /// Boilerplate to parse command-line arguments, set up logging, and handle bubbled-up `Error`s. /// @@ -43,9 +52,11 @@ fn main() { let opts = app::CliOpts::from_args(); // Configure logging output so that -q is "decrease verbosity" rather than instant silence - let verbosity = opts.boilerplate.verbose - .saturating_add(app::DEFAULT_VERBOSITY) - .saturating_sub(opts.boilerplate.quiet); + let verbosity = opts + .boilerplate + .verbose + .saturating_add(app::DEFAULT_VERBOSITY) + .saturating_sub(opts.boilerplate.quiet); stderrlog::new() .module(module_path!()) .quiet(verbosity == 0) @@ -59,7 +70,8 @@ fn main() { app::CliOpts::clap().gen_completions_to( app::CliOpts::clap().get_bin_name().unwrap_or_else(|| clap::crate_name!()), shell, - &mut io::stdout()); + &mut io::stdout(), + ); std::process::exit(0); }; diff --git a/src/validators.rs b/src/validators.rs deleted file mode 100644 index 8b2a4a5..0000000 --- a/src/validators.rs +++ /dev/null @@ -1,563 +0,0 @@ -/*! Validator functions suitable for use with `Clap` and `StructOpt` */ -// Copyright 2017-2019, Stephan Sokolow - -use std::ffi::OsString; -use std::fs::File; -use std::path::{Component, Path}; - -/// Special filenames which cannot be used for real files under Win32 -/// -/// (Unless your app uses the `\\?\` path prefix to bypass legacy Win32 API compatibility -/// limitations) -/// -/// **NOTE:** These are still reserved if you append an extension to them. -/// -/// Source: [Boost Path Name Portability Guide -/// ](https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm) -pub const RESERVED_DOS_FILENAMES: &[&str] = &["AUX", "CON", "NUL", "PRN", // Comments for rustfmt - "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", // Serial Ports - "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", // Parallel Ports - "CLOCK$" ]; // https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm -// TODO: Add the rest of the disallowed names from -// https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations - -/// Module to contain the unsafety of an `unsafe` call to `access()` -#[cfg(unix)] -mod access { - /// TODO: Make this wrapper portable - /// - /// TODO: Consider making `wrapped_access` typesafe using the `bitflags` - /// crate `clap` pulled in - use libc::{access, c_int, W_OK}; - use std::ffi::CString; - use std::os::unix::ffi::OsStrExt; - use std::path::Path; - - /// Lower-level safety wrapper shared by all probably_* functions I define - /// TODO: Unit test **HEAVILY** (Has unsafe block. Here be dragons!) - fn wrapped_access(abs_path: &Path, mode: c_int) -> bool { - // Debug-time check that we're using the API properly - // (Debug-only because relying on it in a release build grants a false - // sense of security and, besides, access() is only really safe to use - // as a way to abort early for convenience on errors that would still - // be safe anyway.) - debug_assert!(abs_path.is_absolute()); - - // Make a null-terminated copy of the path for libc - match CString::new(abs_path.as_os_str().as_bytes()) { - // If we succeed, call access(2), convert the result into bool, and return it - Ok(cstr) => unsafe { access(cstr.as_ptr(), mode) == 0 }, - // If we fail, return false because it can't be an access()ible path - Err(_) => false, - } - } - - /// API suitable for a lightweight "fail early" check for whether a target - /// directory is writable without worry that a fancy filesystem may be - /// configured to allow write but deny deletion for the resulting test file. - /// (It's been seen in the wild) - /// - /// Uses a name which helps to drive home the security hazard in access() - /// abuse and hide the mode flag behind an abstraction so the user can't - /// mess up unsafe{} (eg. On my system, "/" erroneously returns success) - pub fn probably_writable + ?Sized>(path: &P) -> bool { - wrapped_access(path.as_ref(), W_OK) - } - - #[cfg(test)] - mod tests { - use std::ffi::OsStr; - use std::os::unix::ffi::OsStrExt; // TODO: Find a better way to produce invalid UTF-8 - use super::probably_writable; - - #[test] - fn probably_writable_basic_functionality() { - assert!(probably_writable(OsStr::new("/tmp"))); // OK Folder - assert!(probably_writable(OsStr::new("/dev/null"))); // OK File - assert!(!probably_writable(OsStr::new("/etc/shadow"))); // Denied File - assert!(!probably_writable(OsStr::new("/etc/ssl/private"))); // Denied Folder - assert!(!probably_writable(OsStr::new("/nonexistant_test_path"))); // Missing Path - assert!(!probably_writable(OsStr::new("/tmp\0with\0null"))); // Bad CString - assert!(!probably_writable(OsStr::from_bytes(b"/not\xffutf8"))); // Bad UTF-8 - assert!(!probably_writable(OsStr::new("/"))); // Root - // TODO: Relative path - // TODO: Non-UTF8 path that actually does exist and is writable - } - } -} - -/// Test that the given path **should** be writable -/// -/// **TODO:** Implement a Windows version of this. -/// -/// Given that every relevant Windows API I can find seems to be a complex mess compared to -/// `access(2)`, I'll probably just want to settle for the compromise I rejected and just try -/// writing and then deleting a test file. -#[cfg(unix)] -pub fn path_output_dir + ?Sized>(value: &P) -> Result<(), OsString> { - let path = value.as_ref(); - - // Test that the path is a directory - // (Check before, not after, as an extra safety guard on the unsafe block) - if !path.is_dir() { - return Err(format!("Not a directory: {}", path.display()).into()); - } - - // TODO: Think about how to code this more elegantly (try! perhaps?) - if let Ok(abs_pathbuf) = path.canonicalize() { - if let Some(abs_path) = abs_pathbuf.to_str() { - if self::access::probably_writable(abs_path) { - return Ok(()); - } - } - } - - Err(format!("Would be unable to write to destination directory: {}", path.display()).into()) -} - -/// The given path is a file that can be opened for reading -/// -/// ## Use For: -/// * Input file paths -/// -/// ## Relevant Conventions: -/// * Commands which read from `stdin` by default should use `-f` to specify the input path. -/// [[1]](http://www.catb.org/esr/writings/taoup/html/ch10s05.html) -/// * Commands which read from files by default should use positional arguments to specify input -/// paths. -/// * Allow an arbitrary number of input paths if feasible. -/// * Interpret a value of `-` to mean "read from `stdin`" if feasible. -/// [[2]](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) -/// -/// **Note:** The following command-lines, which interleave files and `stdin`, are a good test of -/// how the above conventions should interact: -/// -/// data_source | my_utility_a header.dat - footer.dat > output.dat -/// data_source | my_utility_b -f header.dat -f - -f footer.dat > output.dat -/// -/// ## Cautions: -/// * This will momentarily open the given path for reading to verify that it is readable. -/// However, relying on this to remain true will introduce a race condition. This validator is -/// intended only to allow your program to exit as quickly as possible in the case of obviously -/// bad input. -/// * As a more reliable validity check, you are advised to open a handle to the file in question -/// as early in your program's operation as possible, use it for all your interactions with the -/// file, and keep it open until you are finished. This will both verify its validity and -/// minimize the window in which another process could render the path invalid. -pub fn path_readable_file + ?Sized>(value: &P) - -> std::result::Result<(), OsString> { - let path = value.as_ref(); - - if path.is_dir() { - return Err(format!("{}: Input path must be a file, not a directory", - path.display()).into()); - } - - // TODO: Why does this not fail on Linux? I forget what reading a directory actually does. - File::open(path).map(|_| ()).map_err(|e| format!("{}: {}", path.display(), e).into()) -} - -// TODO: Implement path_readable_dir and path_readable for --recurse use-cases - -/// The given path is valid on all major filesystems and OSes -/// -/// ## Use For: -/// * Output file or directory paths -/// -/// ## Relevant Conventions: -/// * Use `-o` to specify the output path. -/// [[1]](http://www.catb.org/esr/writings/taoup/html/ch10s05.html) -/// [[2]](http://tldp.org/LDP/abs/html/standard-options.html) -/// * Interpret a value of `-` to mean "Write output to stdout". -/// [[3]](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) -/// * Because `-o` does not inherently indicate whether it expects a file or a directory, consider -/// also providing a GNU-style long version with a name like `--outfile` to allow scripts which -/// depend on your tool to be more self-documenting. -/// -/// ## Cautions: -/// * To ensure files can be copied/moved without issue, this validator may impose stricter -/// restrictions on filenames than your filesystem. Do *not* use it for input paths. -/// * Other considerations, such as paths containing symbolic links with longer target names, may -/// still cause your system to reject paths which pass this check. -/// * As a more reliable validity check, you are advised to open a handle to the file in question -/// as early in your program's operation as possible and keep it open until you are finished. -/// This will both verify its validity and minimize the window in which another process could -/// render the path invalid. -/// -/// ## Design Considerations: [[4]](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits) -/// * Many popular Linux filesystems impose no total length limit. -/// * This function imposes a 32,760-character limit for compatibility with flash drives formatted -/// FAT32 or exFAT. -/// * Some POSIX API functions, such as `getcwd()` and `realpath()` rely on the `PATH_MAX` -/// constant, which typically specifies a length of 4096 bytes including terminal `NUL`, but -/// this is not enforced by the filesystem itself. -/// [[4]](https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html) -/// -/// Programs which rely on libc for this functionality but do not attempt to canonicalize paths -/// will usually work if you change the working directory and use relative paths. -/// * The following lengths were considered too limiting to be enforced by this function: -/// * The UDF filesystem used on DVDs imposes a 1023-byte length limit on paths. -/// * When not using the `\\?\` prefix to disable legacy compatibility, Windows paths are -/// limited to 260 characters, which was arrived at as `A:\MAX_FILENAME_LENGTH`. -/// [[5]](https://stackoverflow.com/a/1880453/435253) -/// * ISO 9660 without Joliet or Rock Ridge extensions does not permit periods in directory -/// names, directory trees more than 8 levels deep, or filenames longer than 32 characters. -/// [[6]](https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm) -/// -/// **TODO:** -/// * Write another function for enforcing the limits imposed by targeting optical media. -pub fn path_valid_portable + ?Sized>(value: &P) -> Result<(), OsString> { - #![allow(clippy::match_same_arms, clippy::decimal_literal_representation)] - let path = value.as_ref(); - - if path.as_os_str().is_empty() { - Err("Path is empty".into()) - } else if path.as_os_str().len() > 32760 { - // Limit length to fit on VFAT/exFAT when using the `\\?\` prefix to disable legacy limits - // Source: https://en.wikipedia.org/wiki/Comparison_of_file_systems - Err(format!("Path is too long ({} chars): {:?}", path.as_os_str().len(), path).into()) - } else { - for component in path.components() { - if let Component::Normal(string) = component { - filename_valid_portable(string)? - } - } - Ok(()) - } -} - -/// The string is a valid file/folder name on all major filesystems and OSes -/// -/// ## Use For: -/// * Output file or directory names within a parent directory specified through other means. -/// -/// ## Relevant Conventions: -/// * Most of the time, you want to let users specify a full path via [`path_valid_portable` -/// ](fn.path_valid_portable.html)instead. -/// -/// ## Cautions: -/// * To ensure files can be copied/moved without issue, this validator may impose stricter -/// restrictions on filenames than your filesystem. Do *not* use it for input filenames. -/// * This validator cannot guarantee that a given filename will be valid once other -/// considerations such as overall path length limits are taken into account. -/// * As a more reliable validity check, you are advised to open a handle to the file in question -/// as early in your program's operation as possible, use it for all your interactions with the -/// file, and keep it open until you are finished. This will both verify its validity and -/// minimize the window in which another process could render the path invalid. -/// -/// ## Design Considerations: [[3]](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits) -/// * In the interest of not inconveniencing users in the most common case, this validator imposes -/// a 255-character length limit. -/// * The eCryptFS home directory encryption offered by Ubuntu Linux imposes a 143-character -/// length limit when filename encryption is enabled. -/// [[4]](https://bugs.launchpad.net/ecryptfs/+bug/344878) -/// * the Joliet extensions for ISO 9660 are specified to support only 64-character filenames and -/// tested to support either 103 or 110 characters depending whether you ask the mkisofs -/// developers or Microsoft. [[5]](https://en.wikipedia.org/wiki/Joliet_(file_system)) -/// * The [POSIX Portable Filename Character Set -/// ](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282) -/// is too restrictive to be baked into a general-purpose validator. -/// -/// **TODO:** Consider converting this to a private function that just exists as a helper for the -/// path validator in favour of more specialized validators for filename patterns, prefixes, and/or -/// suffixes, to properly account for how "you can specify a name bu not a path" generally -/// comes about. -pub fn filename_valid_portable + ?Sized>(value: &P) -> Result<(), OsString> { - #![allow(clippy::match_same_arms, clippy::else_if_without_else)] - let path = value.as_ref(); - - // TODO: Should I refuse incorrect Unicode normalization since Finder doesn't like it or just - // advise users to run a normalization pass? - // Source: https://news.ycombinator.com/item?id=16993687 - - // Check that the length is within range - let os_str = path.as_os_str(); - if os_str.len() > 255 { - return Err(format!("File/folder name is too long ({} chars): {:?}", - path.as_os_str().len(), path).into()); - } else if os_str.is_empty() { - return Err("Path component is empty".into()); - } - - // Check for invalid characters - let lossy_str = os_str.to_string_lossy(); - let last_char = lossy_str.chars().last().expect("getting last character"); - if [' ', '.'].iter().any(|&x| x == last_char) { - // The Windows shell and UI don't support component names ending in periods or spaces - // Source: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file - return Err("Windows forbids path components ending with spaces/periods".into()); - } else if lossy_str.as_bytes().iter().any(|c| match c { - // invalid on all APIs which don't use counted strings like inside the NT kernel - b'\0' => true, - // invalid under FAT*, VFAT, exFAT, and NTFS - 0x0..=0x1f | 0x7f | b'"' | b'*' | b'<' | b'>' | b'?' | b'|' => true, - // POSIX path separator (invalid on Unixy platforms like Linux and BSD) - b'/' => true, - // HFS/Carbon path separator (invalid in filenames on MacOS and Mac filesystems) - // DOS/Win32 drive separator (invalid in filenames on Windows and Windows filesystems) - b':' => true, - // DOS/Windows path separator (invalid in filenames on Windows and Windows filesystems) - b'\\' => true, - // let everything else through - _ => false, - }) { - #[allow(clippy::use_debug)] - return Err(format!("Path component contains invalid characters: {:?}", path).into()); - } - - // Reserved DOS filenames that still can't be used on modern Windows for compatibility - if let Some(file_stem) = path.file_stem() { - let stem = file_stem.to_string_lossy().to_uppercase(); - if RESERVED_DOS_FILENAMES.iter().any(|&x| x == stem) { - Err(format!("Filename is reserved on Windows: {:?}", file_stem).into()) - } else { - Ok(()) - } - } else { - Ok(()) - } -} - - -#[cfg(test)] -mod tests { - use super::*; - use std::ffi::OsStr; - - #[cfg(unix)] - use std::os::unix::ffi::OsStrExt; - #[cfg(windows)] - use std::os::windows::ffi::OsStringExt; - - #[test] - #[cfg(unix)] - fn path_output_dir_basic_functionality() { - assert!(path_output_dir(OsStr::new("/")).is_err()); // Root - assert!(path_output_dir(OsStr::new("/tmp")).is_ok()); // OK Folder - assert!(path_output_dir(OsStr::new("/dev/null")).is_err()); // OK File - assert!(path_output_dir(OsStr::new("/etc/shadow")).is_err()); // Denied File - assert!(path_output_dir(OsStr::new("/etc/ssl/private")).is_err()); // Denied Folder - assert!(path_output_dir(OsStr::new("/nonexistant_test_path")).is_err()); // Missing Path - assert!(path_output_dir(OsStr::new("/tmp\0with\0null")).is_err()); // Invalid CString - // TODO: is_dir but fails to canonicalize() - // TODO: Not-already-canonicalized paths - - assert!(path_output_dir(OsStr::from_bytes(b"/not\xffutf8")).is_err()); // Invalid UTF-8 - // TODO: Non-UTF8 path that actually does exist and is writable - } - - #[test] - #[cfg(windows)] - fn path_output_dir_basic_functionality() { - unimplemented!("TODO: Implement Windows version of path_output_dir"); - } - - // ---- path_readable_file ---- - - #[cfg(unix)] - #[test] - fn path_readable_file_basic_functionality() { - // Existing paths - assert!(path_readable_file(OsStr::new("/bin/sh")).is_ok()); // OK File - assert!(path_readable_file(OsStr::new("/bin/../etc/.././bin/sh")).is_ok()); // Non-canonic. - assert!(path_readable_file(OsStr::new("/../../../../bin/sh")).is_ok()); // Above root - - // Inaccessible, nonexistent, or invalid paths - assert!(path_readable_file(OsStr::new("")).is_err()); // Empty String - assert!(path_readable_file(OsStr::new("/")).is_err()); // OK Folder - assert!(path_readable_file(OsStr::new("/etc/shadow")).is_err()); // Denied File - assert!(path_readable_file(OsStr::new("/etc/ssl/private")).is_err()); // Denied Foldr - assert!(path_readable_file(OsStr::new("/nonexistant_test_path")).is_err()); // Missing Path - assert!(path_readable_file(OsStr::new("/null\0containing")).is_err()); // Invalid CStr - - } - #[cfg(windows)] - #[test] - fn path_readable_file_basic_functionality() { - unimplemented!("TODO: Pick some appropriate equivalent test paths for Windows"); - } - - #[cfg(unix)] - #[test] - fn path_readable_file_invalid_utf8() { - assert!(path_readable_file(OsStr::from_bytes(b"/not\xffutf8")).is_err()); // Invalid UTF-8 - // TODO: Non-UTF8 path that actually IS valid - } - #[cfg(windows)] - #[test] - fn path_readable_file_unpaired_surrogates() { - assert!(path_readable_file(&OsString::from_wide( - &['C' as u16, ':' as u16, '\\' as u16, 0xd800])).is_err()); - // TODO: Unpaired surrogate path that actually IS valid - } - - // ---- filename_valid_portable ---- - - const VALID_FILENAMES: &[&str] = &[ - // regular, space, and leading period - "test1", "te st", ".test", - // Stuff which would break if the DOS reserved names check is doing dumb pattern matching - "lpt", "lpt0", "lpt10", - ]; - - // Paths which should pass because std::path::Path will recognize the separators - // TODO: Actually run the tests on Windows to make sure they work - #[cfg(windows)] - const PATHS_WITH_NATIVE_SEPARATORS: &[&str] = &[ - "re/lative", "/ab/solute", "re\\lative", "\\ab\\solute"]; - #[cfg(unix)] - const PATHS_WITH_NATIVE_SEPARATORS: &[&str] = &["re/lative", "/ab/solute"]; - - // Paths which should fail because std::path::Path won't recognize the separators and we don't - // want them showing up in the components. - #[cfg(windows)] - const PATHS_WITH_FOREIGN_SEPARATORS: &[&str] = &["Classic Mac HD:Folder Name:File"]; - #[cfg(unix)] - const PATHS_WITH_FOREIGN_SEPARATORS: &[&str] = &[ - "relative\\win32", - "C:\\absolute\\win32", - "\\drive\\relative\\win32", - "\\\\unc\\path\\for\\win32", - "Classic Mac HD:Folder Name:File", - ]; - - // Source: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file - const INVALID_PORTABLE_FILENAMES: &[&str] = &[ - "test\x03", "test\x07", "test\x08", "test\x0B", "test\x7f", // Control characters (VFAT) - "\"test\"", "", "testsss|", "testsss*", "testsss?", "?estsss", // VFAT - "ends with space ", "ends_with_period.", // DOS/Win32 - "CON", "Con", "coN", "cOn", "CoN", "con", "lpt1", "com9", // Reserved names (DOS/Win32) - "con.txt", "lpt1.dat", // DOS/Win32 API (Reserved names are extension agnostic) - "", "\0"]; // POSIX - - #[test] - fn filename_valid_portable_accepts_valid_names() { - for path in VALID_FILENAMES { - assert!(filename_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path); - } - } - - #[test] - fn filename_valid_portable_refuses_path_separators() { - for path in PATHS_WITH_NATIVE_SEPARATORS { - assert!(filename_valid_portable(OsStr::new(path)).is_err(), "{:?}", path); - } - for path in PATHS_WITH_FOREIGN_SEPARATORS { - assert!(filename_valid_portable(OsStr::new(path)).is_err(), "{:?}", path); - } - } - - #[test] - fn filename_valid_portable_refuses_invalid_characters() { - for fname in INVALID_PORTABLE_FILENAMES { - assert!(filename_valid_portable(OsStr::new(fname)).is_err(), "{:?}", fname); - } - } - - #[test] - fn filename_valid_portable_refuses_empty_strings() { - assert!(filename_valid_portable(OsStr::new("")).is_err()); - } - - #[test] - fn filename_valid_portable_enforces_length_limits() { - // 256 characters - let mut test_str = std::str::from_utf8(&[b'X'; 256]).expect("parsing constant"); - assert!(filename_valid_portable(OsStr::new(test_str)).is_err()); - - // 255 characters (maximum for NTFS, ext2/3/4, and a lot of others) - test_str = std::str::from_utf8(&[b'X'; 255]).expect("parsing constant"); - assert!(filename_valid_portable(OsStr::new(test_str)).is_ok()); - } - - #[cfg(unix)] - #[test] - fn filename_valid_portable_accepts_non_utf8_bytes() { - // Ensure that we don't refuse invalid UTF-8 that "bag of bytes" POSIX allows - assert!(filename_valid_portable(OsStr::from_bytes(b"\xff")).is_ok()); - } - #[cfg(windows)] - #[test] - fn filename_valid_portable_accepts_unpaired_surrogates() { - assert!(path_valid_portable(&OsString::from_wide(&[0xd800])).is_ok()); - } - - // ---- path_valid_portable ---- - - #[test] - fn path_valid_portable_accepts_valid_names() { - for path in VALID_FILENAMES { - assert!(path_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path); - } - - // No filename (.file_stem() returns None) - assert!(path_valid_portable(OsStr::new("foo/..")).is_ok()); - } - - #[test] - fn path_valid_portable_accepts_native_path_separators() { - for path in PATHS_WITH_NATIVE_SEPARATORS { - assert!(path_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path); - } - - // Verify that repeated separators are getting collapsed before filename_valid_portable - // sees them. - // TODO: Make this conditional on platform and also test repeated backslashes on Windows - assert!(path_valid_portable(OsStr::new("/path//with/repeated//separators")).is_ok()); - } - - #[test] - fn path_valid_portable_refuses_foreign_path_separators() { - for path in PATHS_WITH_FOREIGN_SEPARATORS { - assert!(path_valid_portable(OsStr::new(path)).is_err(), "{:?}", path); - } - } - - #[test] - fn path_valid_portable_refuses_invalid_characters() { - for fname in INVALID_PORTABLE_FILENAMES { - assert!(path_valid_portable(OsStr::new(fname)).is_err(), "{:?}", fname); - } - } - - #[test] - fn path_valid_portable_enforces_length_limits() { - let mut test_string = String::with_capacity(255 * 130); - #[allow(clippy::decimal_literal_representation)] - while test_string.len() < 32761 { - test_string.push_str(std::str::from_utf8(&[b'X'; 255]).expect("utf8 from literal")); - test_string.push('/'); - } - - // >32760 characters - assert!(path_valid_portable(OsStr::new(&test_string)).is_err()); - - // 32760 characters (maximum for FAT32/VFAT/exFAT) - #[allow(clippy::decimal_literal_representation)] - test_string.truncate(32760); - assert!(path_valid_portable(OsStr::new(&test_string)).is_ok()); - - // 256 characters with no path separators - test_string.truncate(255); - test_string.push('X'); - assert!(path_valid_portable(OsStr::new(&test_string)).is_err()); - - // 255 characters with no path separators - test_string.truncate(255); - assert!(path_valid_portable(OsStr::new(&test_string)).is_ok()); - } - - #[cfg(unix)] - #[test] - fn path_valid_portable_accepts_non_utf8_bytes() { - // Ensure that we don't refuse invalid UTF-8 that "bag of bytes" POSIX allows - assert!(path_valid_portable(OsStr::from_bytes(b"/\xff/foo")).is_ok()); - } - #[cfg(windows)] - #[test] - fn path_valid_portable_accepts_unpaired_surrogates() { - assert!(path_valid_portable(&OsString::from_wide( - &['C' as u16, ':' as u16, '\\' as u16, 0xd800])).is_ok()); - } - -}