From d504ae18c90a663717d2e4fa0683ca1b3214b247 Mon Sep 17 00:00:00 2001 From: ibabushkin Date: Sun, 22 May 2016 13:59:57 +0200 Subject: [PATCH] pathchk implemented (see #841) (#860) * Added pathchk --- Cargo.lock | 10 ++ Cargo.toml | 2 + Makefile | 2 + src/pathchk/Cargo.toml | 17 +++ src/pathchk/main.rs | 5 + src/pathchk/pathchk.rs | 229 +++++++++++++++++++++++++++++++++++++++++ tests/common/util.rs | 17 +++ tests/pathchk.rs | 25 +++++ 8 files changed, 307 insertions(+) create mode 100644 src/pathchk/Cargo.toml create mode 100644 src/pathchk/main.rs create mode 100644 src/pathchk/pathchk.rs create mode 100644 tests/pathchk.rs diff --git a/Cargo.lock b/Cargo.lock index 873b1adf7..02b1168d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,7 @@ dependencies = [ "nproc 0.0.1", "od 0.0.1", "paste 0.0.1", + "pathchk 0.0.1", "primal 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "printenv 0.0.1", "printf 0.0.1", @@ -578,6 +579,15 @@ dependencies = [ "uucore 0.0.1", ] +[[package]] +name = "pathchk" +version = "0.0.1" +dependencies = [ + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "uucore 0.0.1", +] + [[package]] name = "pretty-bytes" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 19ec44f2f..ad3e8b78c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ unix = [ "mv", "nice", "nohup", + "pathchk", "stdbuf", "timeout", "touch", @@ -136,6 +137,7 @@ nohup = { optional=true, path="src/nohup" } nproc = { optional=true, path="src/nproc" } od = { optional=true, path="src/od" } paste = { optional=true, path="src/paste" } +pathchk = { optional=true, path="src/pathchk" } printenv = { optional=true, path="src/printenv" } printf = { optional=true, path="src/printf" } ptx = { optional=true, path="src/ptx" } diff --git a/Makefile b/Makefile index 2af905cea..20dd47815 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,7 @@ UNIX_PROGS := \ mv \ nice \ nohup \ + pathchk \ stdbuf \ timeout \ touch \ @@ -156,6 +157,7 @@ TEST_PROGS := \ nl \ od \ paste \ + pathchk \ printf \ ptx \ pwd \ diff --git a/src/pathchk/Cargo.toml b/src/pathchk/Cargo.toml new file mode 100644 index 000000000..7afe4d511 --- /dev/null +++ b/src/pathchk/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pathchk" +version = "0.0.1" +authors = [] + +[lib] +name = "uu_pathchk" +path = "pathchk.rs" + +[dependencies] +getopts = "*" +libc = "*" +uucore = { path="../uucore" } + +[[bin]] +name = "pathchk" +path = "main.rs" diff --git a/src/pathchk/main.rs b/src/pathchk/main.rs new file mode 100644 index 000000000..71e51454c --- /dev/null +++ b/src/pathchk/main.rs @@ -0,0 +1,5 @@ +extern crate uu_pathchk; + +fn main() { + std::process::exit(uu_pathchk::uumain(std::env::args().collect())); +} diff --git a/src/pathchk/pathchk.rs b/src/pathchk/pathchk.rs new file mode 100644 index 000000000..7f320cfe5 --- /dev/null +++ b/src/pathchk/pathchk.rs @@ -0,0 +1,229 @@ +#![allow(unused_must_use)] // because we of writeln! +#![crate_name = "uu_pathchk"] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Inokentiy Babushkin + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +extern crate getopts; +extern crate libc; + +#[macro_use] +extern crate uucore; + +use getopts::Options; +use std::fs; +use std::io::{Write, ErrorKind}; + +// operating mode +enum Mode { + Default, // use filesystem to determine information and limits + Basic, // check basic compatibility with POSIX + Extra, // check for leading dashes and empty names + Both, // a combination of `Basic` and `Extra` + Help, // show help + Version // show version information +} + +static NAME: &'static str = "pathchk"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +// a few global constants as used in the GNU implememntation +static POSIX_PATH_MAX: usize = 256; +static POSIX_NAME_MAX: usize = 14; + +pub fn uumain(args: Vec) -> i32 { + // add options + let mut opts = Options::new(); + opts.optflag("p", "posix", "check for (most) POSIX systems"); + opts.optflag("P", + "posix-special", "check for empty names and leading \"-\""); + opts.optflag("", + "portability", "check for all POSIX systems (equivalent to -p -P)"); + opts.optflag("h", "help", "display this help text and exit"); + opts.optflag("V", "version", "output version information and exit"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(e) => { crash!(1, "{}", e) } + }; + + // set working mode + let mode = if matches.opt_present("version") { + Mode::Version + } else if matches.opt_present("help") { + Mode::Help + } else if (matches.opt_present("posix") && + matches.opt_present("posix-special")) || + matches.opt_present("portability") { + Mode::Both + } else if matches.opt_present("posix") { + Mode::Basic + } else if matches.opt_present("posix-special") { + Mode::Extra + } else { + Mode::Default + }; + + // take necessary actions + match mode { + Mode::Help => { help(opts); 0 } + Mode::Version => { version(); 0 } + _ => { + let mut res = true; + if matches.free.len() == 0 { + show_error!( + "missing operand\nTry {} --help for more information", NAME + ); + res = false; + } + // free strings are path operands + // FIXME: TCS, seems inefficient and overly verbose (?) + for p in matches.free { + let mut path = Vec::new(); + for path_segment in p.split('/') { + path.push(path_segment.to_string()); + } + res &= check_path(&mode, &path); + } + // determine error code + if res { 0 } else { 1 } + } + } +} + +// print help +fn help(opts: Options) { + let msg = format!("Usage: {} [OPTION]... NAME...\n\n\ + Diagnose invalid or unportable file names.", NAME); + + print!("{}", opts.usage(&msg)); +} + +// print version information +fn version() { + println!("{} {}", NAME, VERSION); +} + +// check a path, given as a slice of it's components and an operating mode +fn check_path(mode: &Mode, path: &[String]) -> bool { + match *mode { + Mode::Basic => check_basic(&path), + Mode::Extra => check_default(&path) && check_extra(&path), + Mode::Both => check_basic(&path) && check_extra(&path), + _ => check_default(&path) + } +} + +// check a path in basic compatibility mode +fn check_basic(path: &[String]) -> bool { + let joined_path = path.join("/"); + let total_len = joined_path.len(); + // path length + if total_len > POSIX_PATH_MAX { + writeln!(&mut std::io::stderr(), + "limit {} exceeded by length {} of file name {}", + POSIX_PATH_MAX, total_len, joined_path); + return false; + } else if total_len == 0 { + writeln!(&mut std::io::stderr(), "empty file name"); + return false; + } + // components: character portability and length + for p in path { + let component_len = p.len(); + if component_len > POSIX_NAME_MAX { + writeln!(&mut std::io::stderr(), + "limit {} exceeded by length {} of file name component '{}'", + POSIX_NAME_MAX, component_len, p); + return false; + } + if !check_portable_chars(&p) { + return false; + } + } + // permission checks + check_searchable(&joined_path) +} + +// check a path in extra compatibility mode +fn check_extra(path: &[String]) -> bool { + // components: leading hyphens + for p in path { + if !no_leading_hyphen(&p) { + writeln!(&mut std::io::stderr(), + "leading hyphen in file name component '{}'", p); + return false; + } + } + // path length + if path.join("/").len() == 0 { + writeln!(&mut std::io::stderr(), "empty file name"); + return false; + } + true +} + +// check a path in default mode (using the file system) +fn check_default(path: &[String]) -> bool { + let joined_path = path.join("/"); + let total_len = joined_path.len(); + // path length + if total_len > libc::PATH_MAX as usize { + writeln!(&mut std::io::stderr(), + "limit {} exceeded by length {} of file name '{}'", + libc::PATH_MAX, total_len, joined_path); + return false; + } + // components: length + for p in path { + let component_len = p.len(); + if component_len > libc::FILENAME_MAX as usize { + writeln!(&mut std::io::stderr(), + "limit {} exceeded by length {} of file name component '{}'", + libc::FILENAME_MAX, component_len, p); + return false; + } + } + // permission checks + check_searchable(&joined_path) +} + +// check whether a path is or if other problems arise +fn check_searchable(path: &String) -> bool { + // we use lstat, just like the original implementation + match fs::symlink_metadata(path) { + Ok(_) => true, + Err(e) => if e.kind() == ErrorKind::NotFound { + true + } else { + writeln!(&mut std::io::stderr(), "{}", e); + false + } + } +} + +// check for a hypthen at the beginning of a path segment +fn no_leading_hyphen(path_segment: &String) -> bool { + !path_segment.starts_with('-') +} + +// check whether a path segment contains only valid (read: portable) characters +fn check_portable_chars(path_segment: &String) -> bool { + let valid_str = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-" + .to_string(); + for ch in path_segment.chars() { + if !valid_str.contains(ch) { + writeln!(&mut std::io::stderr(), + "nonportable character '{}' in file name component '{}'", + ch, path_segment); + return false; + } + } + true +} diff --git a/tests/common/util.rs b/tests/common/util.rs index 540cf5b5f..c233635dc 100755 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -41,6 +41,15 @@ macro_rules! assert_empty_stderr( ); ); +#[macro_export] +macro_rules! assert_empty_stdout( + ($cond:expr) => ( + if $cond.stdout.len() > 0 { + panic!(format!("stdout: {}", $cond.stdout)) + } + ); +); + #[macro_export] macro_rules! assert_no_error( ($cond:expr) => ( @@ -51,6 +60,14 @@ macro_rules! assert_no_error( ); ); +pub fn repeat_str(s: &str, n: u32) -> String { + let mut repeated = String::new(); + for _ in 0..n { + repeated.push_str(s); + } + repeated +} + #[macro_export] macro_rules! path_concat { ($e:expr, ..$n:expr) => {{ diff --git a/tests/pathchk.rs b/tests/pathchk.rs new file mode 100644 index 000000000..0c2857fd8 --- /dev/null +++ b/tests/pathchk.rs @@ -0,0 +1,25 @@ +#[macro_use] +mod common; + +use common::util::*; + +static UTIL_NAME: &'static str = "pathchk"; + +#[test] +fn test_default_mode() { + // test the default mode + { + // accept some reasonable default + let (_, mut ucmd) = testing(UTIL_NAME); + let result = ucmd.args(&["abc/def"]).run(); + assert_eq!(result.stdout, ""); + assert!(result.success); + } + { + // fail on long inputs + let (_, mut ucmd) = testing(UTIL_NAME); + let result = ucmd.args(&[repeat_str("test", 20000)]).run(); + assert_eq!(result.stdout, ""); + assert!(!result.success); + } +}