Browse Source

add first steps

master
Schneider 5 years ago
parent
commit
923f85d988
Signed by: schneider GPG Key ID: 3F50B02A50039F3B
  1. 408
      Cargo.lock
  2. 4
      justfile
  3. 58
      src/app.rs
  4. 10
      src/errors.rs
  5. 61
      src/helpers.rs
  6. 36
      src/main.rs
  7. 563
      src/validators.rs

408
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"

4
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 = ""

58
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 `<name>` `<version>` 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 `<name> <version>` line for part of the description.)
/// `help2man` depends on to generate your manpage. (Specifically, it will mistake the `<name>
/// <version>` 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<PathBuf>,
#[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(())
}

10
src/errors.rs

@ -1,7 +1,11 @@
/*! `error-chain` boilerplate and custom `Error` types */
// Copyright 2020, Marcel Schneider <marcel@webschneider.org>
use error_chain::*;
// Create the Error, ErrorKind, ResultExt, and Result types
error_chain! {}
error_chain! {
errors{
NoGitRootFound {
description("no git root found"),
}
}
}

61
src/helpers.rs

@ -5,7 +5,13 @@
// for .expect() instead) in my two Option<T> 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<clap::Shell>,
}
/// 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<bool> {
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<Option<PathBuf>> {
let mut cwd: Option<PathBuf> =
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)
}

36
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);
};

563
src/validators.rs

@ -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
/// <https://doc.rust-lang.org/book/conditional-compilation.html>
/// 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<P: AsRef<Path> + ?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<P: AsRef<Path> + ?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<P: AsRef<Path> + ?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<NULL>`.
/// [[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<P: AsRef<Path> + ?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<P: AsRef<Path> + ?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|", "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());
}
}
Loading…
Cancel
Save