diff --git a/Cargo.lock b/Cargo.lock index e4bc56e4..8417fb46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,7 +457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -472,6 +472,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -594,6 +604,29 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "parse-zoneinfo" version = "0.3.1" @@ -784,6 +817,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.11.1" @@ -881,6 +923,38 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selinux" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37f432dfe840521abd9a72fefdf88ed7ad0f43bbea7d9d1d3d80383e9f4ad13" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "parking_lot", + "selinux-sys", + "thiserror", +] + +[[package]] +name = "selinux-sys" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280da3df1236da180be5ac50a893b26a1d3c49e3a44acb2d10d1f082523ff916" +dependencies = [ + "bindgen", + "cc", + "dunce", + "walkdir", +] + [[package]] name = "serde" version = "1.0.219" @@ -926,6 +1000,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + [[package]] name = "smartcols-sys" version = "0.1.2" @@ -1142,6 +1222,7 @@ dependencies = [ "uu_mcookie", "uu_mesg", "uu_mountpoint", + "uu_namei", "uu_renice", "uu_rev", "uu_setsid", @@ -1283,6 +1364,15 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_namei" +version = "0.0.1" +dependencies = [ + "clap", + "selinux", + "uucore", +] + [[package]] name = "uu_renice" version = "0.0.1" @@ -1461,7 +1551,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3798c367..cba88077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ feat_common_core = [ "renice", "rev", "setsid", + "namei", ] [workspace.dependencies] @@ -67,7 +68,7 @@ sysinfo = "0.34" tempfile = "3.9.0" textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "2.0" -uucore = "0.0.30" +uucore = { version = "0.0.30", features = ["entries"] } xattr = "1.3.1" [dependencies] @@ -98,6 +99,7 @@ mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", pa renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } +namei = { optional = true, version = "0.0.1", package = "uu_namei", path ="src/uu/namei" } [dev-dependencies] # dmesg test require fixed-boot-time feature turned on. diff --git a/src/uu/namei/Cargo.toml b/src/uu/namei/Cargo.toml new file mode 100644 index 00000000..b9a4b764 --- /dev/null +++ b/src/uu/namei/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "uu_namei" +version = "0.0.1" +edition = "2021" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +selinux = { version = "0.5.1", optional = true } + +[features] +selinux = ["dep:selinux"] + +[lib] +path = "src/namei.rs" + +[[bin]] +name = "namei" +path = "src/main.rs" diff --git a/src/uu/namei/namei.md b/src/uu/namei/namei.md new file mode 100644 index 00000000..44e8dd3c --- /dev/null +++ b/src/uu/namei/namei.md @@ -0,0 +1,7 @@ +# namei + +``` +namei [options] ... +``` + +Follow a pathname until a terminal point is found. \ No newline at end of file diff --git a/src/uu/namei/src/main.rs b/src/uu/namei/src/main.rs new file mode 100644 index 00000000..df3fa7f9 --- /dev/null +++ b/src/uu/namei/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_namei); diff --git a/src/uu/namei/src/namei.rs b/src/uu/namei/src/namei.rs new file mode 100644 index 00000000..2ed6d119 --- /dev/null +++ b/src/uu/namei/src/namei.rs @@ -0,0 +1,644 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Arg, ArgAction, Command}; +#[cfg(feature = "selinux")] +use selinux::SecurityContext; +#[cfg(not(target_os = "windows"))] +use std::cmp::max; +use std::env; +// #[cfg(not(target_os = "windows"))] +// use std::fs::Metadata; +#[cfg(not(target_os = "windows"))] +use std::os::unix::fs::MetadataExt; +use std::process; +use std::str::FromStr; +use std::{fs, path::Path}; +#[cfg(not(target_os = "windows"))] +use uucore::entries::{gid2grp, uid2usr}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("namei.md"); +const USAGE: &str = help_usage!("namei.md"); + +const MAXSYMLINKS: usize = 256; + +mod options { + pub const LONG: &str = "long"; + pub const MODES: &str = "modes"; + pub const NOSYMLINKS: &str = "nosymlinks"; + pub const VERTICAL: &str = "vertical"; + pub const PATHNAMES: &str = "pathnames"; + + #[cfg(not(target_os = "windows"))] + pub const OWNERS: &str = "owners"; + + #[cfg(not(target_os = "windows"))] + pub const MOUNTPOINTS: &str = "mountpoints"; + + #[cfg(feature = "selinux")] + pub const CONTEXT: &str = "context"; +} + +struct OutputOptions { + long: bool, + modes: bool, + nosymlinks: bool, + vertical: bool, + + #[cfg(not(target_os = "windows"))] + mountpoints: bool, + + #[cfg(not(target_os = "windows"))] + owners: bool, + + #[cfg(feature = "selinux")] + context: bool, +} + +pub fn uu_app() -> Command { + let cmd = Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .arg( + Arg::new(options::PATHNAMES) + .value_name("PATH") + .help("Paths to follow") + .hide(true) + .action(ArgAction::Append) + .required(true) + .num_args(1..), + ) + .arg( + Arg::new(options::LONG) + .short('l') + .long("long") + .help("use a long listing format (-m -o -v)") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::MODES) + .short('m') + .long("modes") + .help("show the mode bits of each file") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::NOSYMLINKS) + .short('n') + .long("nosymlinks") + .help("don't follow symlinks") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::VERTICAL) + .short('v') + .long("vertical") + .help("vertical align of modes and owners") + .action(ArgAction::SetTrue), + ); + + #[cfg(not(target_os = "windows"))] + let cmd = cmd.arg( + Arg::new(options::MOUNTPOINTS) + .short('x') + .long("mountpoints") + .help("show mount point directories with a 'D'") + .action(ArgAction::SetTrue), + ); + + #[cfg(not(target_os = "windows"))] + let cmd = cmd.arg( + Arg::new(options::OWNERS) + .short('o') + .long("owners") + .help("show owner and group name of each file") + .action(ArgAction::SetTrue), + ); + + #[cfg(feature = "selinux")] + let cmd = cmd.arg( + Arg::new(options::CONTEXT) + .short('Z') + .long("context") + .help("print any security context of each file") + .action(ArgAction::SetTrue), + ); + + cmd +} + +#[cfg(not(target_os = "windows"))] +fn max_owner_length(path: &Path) -> usize { + let mut max_length = 0; + + for entry in path.ancestors() { + if let Err(_e) = entry.metadata() { + continue; + } + let metadata = entry.metadata().unwrap(); + let uid = metadata.uid(); + + let owner = uid2usr(uid).unwrap(); + let max_entry_length = owner.len(); + max_length = max(max_entry_length, max_length); + } + + max_length +} +#[cfg(not(target_os = "windows"))] +fn max_group_length(path: &Path) -> usize { + let mut max_length = 0; + + for entry in path.ancestors() { + if let Err(_e) = entry.metadata() { + continue; + } + let metadata = entry.metadata().unwrap(); + let gid = metadata.gid(); + + let group = gid2grp(gid).unwrap(); + let max_entry_length = group.len(); + max_length = max(max_entry_length, max_length); + } + + max_length +} + +#[cfg(not(target_os = "windows"))] +fn get_file_name(input: &str) -> &str { + let stripped = input.trim_end_matches('/'); + if stripped.is_empty() { + return "/"; + } + stripped.rsplit('/').next().unwrap_or("/") +} + +#[cfg(target_os = "windows")] +fn get_file_name(input: &str) -> &str { + if !input.contains('\\') { + input + } else { + let stripped = input.trim_end_matches('\\'); + if stripped.is_empty() { + return ":"; + } + if !stripped.contains('\\') { + return stripped; + } + stripped.rsplit('\\').next().unwrap_or("\\") + } +} + +#[cfg(not(target_os = "windows"))] +fn get_file_type(path: &Path, outputmountpoints: bool) -> char { + if path.is_symlink() { + return 'l'; + } + if outputmountpoints && is_mount_point(path) { + return 'D'; + } + + let metadata = fs::metadata(path).unwrap(); + + let mode = metadata.mode(); + + match mode & 0o170000 { + 0o100000 => '-', // Regular file + 0o040000 => 'd', // Directory + 0o020000 => 'c', // Character device + 0o060000 => 'b', // Block device + 0o010000 => 'p', // FIFO + 0o140000 => 's', // Socket + _ => '?', // Unknown + } +} + +#[cfg(not(unix))] +fn get_file_type(path: &Path, _outputmountpoints: bool) -> char { + let filetype = fs::metadata(path).unwrap().file_type(); + + if filetype.is_dir() { + 'd' + } else if filetype.is_file() { + '-' + } else if filetype.is_symlink() { + 'l' + } else { + '?' + } +} + +#[cfg(not(target_os = "windows"))] +fn is_mount_point(input_path: &Path) -> bool { + let canonical_path = input_path.canonicalize().unwrap().into_os_string(); + let path = Path::new(&canonical_path); + if let Some(parent) = path.parent() { + let metadata = fs::metadata(path).unwrap(); + if let Err(_e) = fs::metadata(parent) { + return false; + } + let parent_metadata = fs::metadata(parent).unwrap(); + metadata.dev() != parent_metadata.dev() + } else { + true + } +} + +#[cfg(not(target_os = "windows"))] +fn get_permissions(path: &Path) -> String { + let metadata = fs::metadata(path).unwrap(); + let mode = metadata.mode(); + + let permissions = [ + (mode & 0o400, 'r'), + (mode & 0o200, 'w'), + (mode & 0o100, 'x'), // Owner + (mode & 0o040, 'r'), + (mode & 0o020, 'w'), + (mode & 0o010, 'x'), // Group + (mode & 0o004, 'r'), + (mode & 0o002, 'w'), + (mode & 0o001, 'x'), // Others + ]; + let mut perm_string = String::new(); + for &(bit, ch) in &permissions { + perm_string.push(if bit != 0 { ch } else { '-' }); + } + if mode & 0o4000 != 0 { + // Set UID + perm_string.replace_range( + 2..3, + if perm_string.chars().nth(3) == Some('x') { + "s" + } else { + "S" + }, + ); + } + if mode & 0o2000 != 0 { + // Set GID + perm_string.replace_range( + 5..6, + if perm_string.chars().nth(6) == Some('x') { + "s" + } else { + "S" + }, + ); + } + if mode & 0o1000 != 0 { + // Sticky Bit + perm_string.replace_range( + 8..9, + if perm_string.chars().nth(9) == Some('x') { + "t" + } else { + "T" + }, + ); + } + + perm_string +} + +#[cfg(not(unix))] +fn get_permissions(path: &Path) -> String { + let metadata = fs::metadata(path).unwrap(); + let file_type = metadata.file_type(); + + // NOTE: Windows doesn't give full Unix-style permissions + // We're faking it: check readonly bit to infer write access + let readonly = metadata.permissions().readonly(); + + // Use PATHEXT environment variable to figure out if file is executable + let executable = if file_type.is_file() { + match path.extension().and_then(|e| e.to_str()) { + Some(e) => { + let ext = e.to_ascii_lowercase(); + let pathext = env::var("PATHEXT").unwrap_or_default(); + pathext + .split(';') + .filter_map(|e| e.strip_prefix('.')) // ".EXE" -> "EXE" + .any(|e| e.eq_ignore_ascii_case(&ext)) + } + None => false, + } + } else { + false + }; + + let mut perm_string = String::new(); + + let mut rwx = if readonly { + "r-".to_string() + } else { + "rw".to_string() + }; + if executable { + rwx.push('x'); + } else { + rwx.push('-'); + } + + perm_string.push_str(rwx.as_str()); + perm_string.push_str(rwx.as_str()); + perm_string.push_str(rwx.as_str()); + + perm_string +} + +fn get_prefix( + level: usize, + path: &Path, + output_opts: &OutputOptions, + maximum_owner_length: usize, + maximum_group_length: usize, +) -> String { + // Avoid unused variable warnings on non-Unix platforms + #[cfg(not(unix))] + { + let _ = (maximum_owner_length, maximum_group_length); + } + + let mut prefix = String::new(); + + if !output_opts.vertical { + let mut st = String::from(" "); + st.push_str(&" ".repeat(level * 2)); + prefix.push_str(&st); + } + + if let Err(_e) = fs::metadata(path) { + let mut blanks = 1 + level * 2; + if output_opts.modes { + blanks += 9; + } + #[cfg(not(target_os = "windows"))] + if output_opts.owners { + blanks += maximum_owner_length + maximum_group_length + 2; + } + if output_opts.vertical { + blanks += 1; + } + + #[cfg(feature = "selinux")] + if !output_opts.context { + blanks += 1; + } + + prefix = " ".repeat(blanks); + return prefix; + } + + #[cfg(target_os = "windows")] + let mountpoints = false; + #[cfg(not(target_os = "windows"))] + let mountpoints = output_opts.mountpoints; + + prefix.push(get_file_type(path, mountpoints)); + + if output_opts.modes || output_opts.long { + let perm_string = get_permissions(path); + prefix.push_str(&perm_string); + } + + prefix.push(' '); + + #[cfg(not(target_os = "windows"))] + if output_opts.owners { + let metadata = fs::metadata(path).unwrap(); + let uid = metadata.uid(); + let gid = metadata.gid(); + let mut owner = uid2usr(uid).unwrap(); + let str1 = " ".repeat(maximum_owner_length - owner.len() + 1); + owner = format!("{}{}", owner, str1); + let mut group = gid2grp(gid).unwrap(); + let str2 = " ".repeat(maximum_group_length - group.len() + 1); + group = format!("{}{}", group, str2); + + prefix = format!("{}{}{}", prefix, owner, group); + } + + #[cfg(feature = "selinux")] + if output_opts.context { + let context_not_available_string: String = '?'.to_string(); + match SecurityContext::of_path(path, !output_opts.nosymlinks, false) { + Err(_r) => prefix.push_str(context_not_available_string.as_str()), + Ok(None) => prefix.push_str(context_not_available_string.as_str()), + Ok(Some(cntxt)) => { + let context = cntxt.as_bytes(); + let context = context.strip_suffix(&[0]).unwrap_or(context); + prefix.push_str( + String::from_utf8(context.to_vec()) + .unwrap_or_else(|_e| String::from_utf8_lossy(context).into_owned()) + .as_str(), + ) + } + } + prefix.push_str(" "); + } + + if output_opts.vertical { + let mut st = String::new(); + st.push_str(&" ".repeat(level * 2)); + prefix.push_str(&st); + } + + prefix +} + +fn tokenize_relative_path(path: &str, cwd: &str) -> Vec { + let mut result = Vec::new(); + let mut current = String::from(cwd); + + #[cfg(not(target_os = "windows"))] + let sep = '/'; + #[cfg(target_os = "windows")] + let sep = '\\'; + + for part in path.split(sep) { + if !part.is_empty() { + current.push(sep); + current.push_str(part); + result.push(current.clone()); + } + } + + result +} + +fn print_file( + level: usize, + path: &Path, + output_opts: &OutputOptions, + maximum_owner_length: usize, + maximum_group_length: usize, +) { + let prefix = get_prefix( + level, + path, + output_opts, + maximum_owner_length, + maximum_group_length, + ); + + let symlinksuffix = if path.is_symlink() { + let mut suffix = String::from_str(" -> ").unwrap(); + let target = fs::read_link(path).unwrap(); + suffix.push_str(target.to_str().unwrap()); + suffix + } else { + String::new() + }; + + match fs::metadata(path) { + Err(e) => { + eprintln!( + "{}{} - {}", + prefix, + get_file_name(path.to_str().unwrap()), + e + ); + process::exit(1); + } + _ => println!( + "{}{}{}", + prefix, + get_file_name(path.to_str().unwrap()), + symlinksuffix + ), + } + + if !output_opts.nosymlinks && path.is_symlink() && level < MAXSYMLINKS - 1 { + let target_pathbuf = fs::read_link(path).unwrap(); + if target_pathbuf.is_relative() { + let target_pathrel = Path::new(target_pathbuf.to_str().unwrap()); + let symlink_dir = path.parent().unwrap(); + let joindir = symlink_dir.join(target_pathrel); + let target_path = joindir.as_path(); + print_files( + level + 1, + target_path, + output_opts, + maximum_owner_length, + maximum_group_length, + ); + } else { + let osstr = fs::read_link(path).unwrap().into_os_string(); + print_files( + level + 1, + Path::new(&osstr), + output_opts, + maximum_owner_length, + maximum_group_length, + ); + } + } +} + +fn print_files( + level: usize, + path: &Path, + output_opts: &OutputOptions, + maximum_owner_length: usize, + maximum_group_length: usize, +) { + if path.is_absolute() { + if let Some(pt) = path.parent() { + print_files( + level, + pt, + output_opts, + maximum_owner_length, + maximum_group_length, + ); + } + print_file( + level, + path, + output_opts, + maximum_owner_length, + maximum_group_length, + ); + } + + if path.is_relative() { + match env::current_dir() { + Ok(pt) => { + if let Some(cwd) = pt.to_str() { + if path.to_str().is_none() { + eprintln!("Invalid Path (Non-unicode)"); + } else { + let paths = tokenize_relative_path(path.to_str().unwrap(), cwd); + for path_string in &paths { + let p = Path::new(path_string); + + print_file( + level, + p, + output_opts, + maximum_owner_length, + maximum_group_length, + ); + } + } + } + } + Err(e) => eprintln!("{}", e), + } + } +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + let pathlist = matches.get_many::(options::PATHNAMES); + + let output_opts = OutputOptions { + long: matches.get_flag(options::LONG), + modes: matches.get_flag(options::MODES) || matches.get_flag(options::LONG), + nosymlinks: matches.get_flag(options::NOSYMLINKS), + vertical: matches.get_flag(options::VERTICAL) || matches.get_flag(options::LONG), + + #[cfg(not(target_os = "windows"))] + mountpoints: matches.get_flag(options::MOUNTPOINTS), + + #[cfg(not(target_os = "windows"))] + owners: matches.get_flag(options::OWNERS) || matches.get_flag(options::LONG), + + #[cfg(feature = "selinux")] + context: matches.get_flag(options::CONTEXT), + }; + + if let Some(paths) = pathlist { + for path_str in paths { + let path = Path::new(path_str); + println!("f: {}", path.to_str().unwrap()); + + #[cfg(target_os = "windows")] + let maximum_owner_length = 0; + #[cfg(target_os = "windows")] + let maximum_group_length = 0; + + #[cfg(not(target_os = "windows"))] + let maximum_owner_length = max_owner_length(path); + #[cfg(not(target_os = "windows"))] + let maximum_group_length = max_group_length(path); + + print_files( + 0, + path, + &output_opts, + maximum_owner_length, + maximum_group_length, + ); + } + } + + Ok(()) +} diff --git a/tests/by-util/test_namei.rs b/tests/by-util/test_namei.rs new file mode 100644 index 00000000..dcd56d2f --- /dev/null +++ b/tests/by-util/test_namei.rs @@ -0,0 +1,114 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use regex::Regex; + +use crate::common::util::TestScenario; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +fn test_fails_on_non_existing_path() { + let (at, mut ucmd) = at_and_ucmd!(); + let argmnt = at.plus_as_string("nonexisting"); + + #[cfg(target_os = "windows")] + let err = "The system cannot find the file specified"; + #[cfg(not(target_os = "windows"))] + let err = "No such file or directory"; + + ucmd.arg(argmnt).fails().code_is(1).stderr_contains(err); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_fails_on_no_permission() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("noperms"); + at.make_file("noperms/testfile"); + at.set_mode("noperms", 0); + let argmnt = at.plus_as_string("noperms/testfile"); + ucmd.arg(argmnt) + .fails() + .code_is(1) + .stderr_contains("Permission denied"); +} + +#[test] +fn test_long_arg() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(at.plus_as_string("test-long")); + + #[cfg(not(target_os = "windows"))] + let regex = r" *[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3} [a-z0-9_\.][a-z0-9_\-\.]*[$]? [a-z0-9_\.][a-z0-9_\-\.]*[$]? .*"; + #[cfg(target_os = "windows")] + let regex = r"[-dl]([r-][w-][x-]){3} .*"; + + let re = &Regex::new(regex).unwrap(); + + let args = vec!["-l", "--long"]; + for arg in args { + let result = scene.ucmd().arg(arg).arg(at.as_string()).succeeds(); + result.stdout_matches(re); + } +} + +#[test] +fn test_modes_arg() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(at.plus_as_string("test-modes")); + + let regex = r" +[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3} .*"; + + let re = &Regex::new(regex).unwrap(); + + let args = vec!["-m", "--modes"]; + for arg in args { + let result = scene.ucmd().arg(arg).arg(at.as_string()).succeeds(); + result.stdout_matches(re); + } +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_owners_arg() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(at.plus_as_string("test-owners")); + + #[cfg(not(windows))] + let regex = + r" +[-bcCdDlMnpPsStTx?] [a-z0-9_\.][a-z0-9_\-\.]*[$]? [a-z0-9_\.][a-z0-9_\-\.]*[$]? .*"; + + let re = &Regex::new(regex).unwrap(); + + let args = vec!["-o", "--owners"]; + for arg in args { + let result = scene.ucmd().arg(arg).arg(at.as_string()).succeeds(); + result.stdout_matches(re); + } +} + +#[test] +fn test_vertical_arg() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(at.plus_as_string("test-vertical")); + + let regex = r"[-bcCdDlMnpPsStTx?] +.*"; + + let re = &Regex::new(regex).unwrap(); + + let args = vec!["-v", "--vertical"]; + for arg in args { + let result = scene.ucmd().arg(arg).arg(at.as_string()).succeeds(); + result.stdout_matches(re); + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 0fb3ed14..ce74cb10 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -60,3 +60,7 @@ mod test_fsfreeze; #[cfg(feature = "mcookie")] #[path = "by-util/test_mcookie.rs"] mod test_mcookie; + +#[cfg(feature = "namei")] +#[path = "by-util/test_namei.rs"] +mod test_namei;