#![allow(unused_must_use)] use clap::{crate_version, Arg, ArgAction, Command};
use std::fs;
use std::io::{ErrorKind, Write};
use uucore::display::Quotable;
use uucore::error::{set_exit_code, UResult, UUsageError};
use uucore::{format_usage, help_about, help_usage};
enum Mode {
Default, Basic, Extra, Both, }
const ABOUT: &str = help_about!("pathchk.md");
const USAGE: &str = help_usage!("pathchk.md");
mod options {
pub const POSIX: &str = "posix";
pub const POSIX_SPECIAL: &str = "posix-special";
pub const PORTABILITY: &str = "portability";
pub const PATH: &str = "path";
}
const POSIX_PATH_MAX: usize = 256;
const POSIX_NAME_MAX: usize = 14;
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let is_posix = matches.get_flag(options::POSIX);
let is_posix_special = matches.get_flag(options::POSIX_SPECIAL);
let is_portability = matches.get_flag(options::PORTABILITY);
let mode = if (is_posix && is_posix_special) || is_portability {
Mode::Both
} else if is_posix {
Mode::Basic
} else if is_posix_special {
Mode::Extra
} else {
Mode::Default
};
let paths = matches.get_many::<String>(options::PATH);
if paths.is_none() {
return Err(UUsageError::new(1, "missing operand"));
}
let mut res = true;
for p in paths.unwrap() {
let mut path = Vec::new();
for path_segment in p.split('/') {
path.push(path_segment.to_string());
}
res &= check_path(&mode, &path);
}
if !res {
set_exit_code(1);
}
Ok(())
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(
Arg::new(options::POSIX)
.short('p')
.help("check for most POSIX systems")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::POSIX_SPECIAL)
.short('P')
.help(r#"check for empty names and leading "-""#)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::PORTABILITY)
.long(options::PORTABILITY)
.help("check for all POSIX systems (equivalent to -p -P)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::PATH)
.hide(true)
.action(ArgAction::Append)
.value_hint(clap::ValueHint::AnyPath),
)
}
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),
}
}
fn check_basic(path: &[String]) -> bool {
let joined_path = path.join("/");
let total_len = joined_path.len();
if total_len > POSIX_PATH_MAX {
writeln!(
std::io::stderr(),
"limit {POSIX_PATH_MAX} exceeded by length {total_len} of file name {joined_path}"
);
return false;
} else if total_len == 0 {
writeln!(std::io::stderr(), "empty file name");
return false;
}
for p in path {
let component_len = p.len();
if component_len > POSIX_NAME_MAX {
writeln!(
std::io::stderr(),
"limit {} exceeded by length {} of file name component {}",
POSIX_NAME_MAX,
component_len,
p.quote()
);
return false;
}
if !check_portable_chars(p) {
return false;
}
}
check_searchable(&joined_path)
}
fn check_extra(path: &[String]) -> bool {
for p in path {
if p.starts_with('-') {
writeln!(
std::io::stderr(),
"leading hyphen in file name component {}",
p.quote()
);
return false;
}
}
if path.join("/").is_empty() {
writeln!(std::io::stderr(), "empty file name");
return false;
}
true
}
fn check_default(path: &[String]) -> bool {
let joined_path = path.join("/");
let total_len = joined_path.len();
if total_len > libc::PATH_MAX as usize {
writeln!(
std::io::stderr(),
"limit {} exceeded by length {} of file name {}",
libc::PATH_MAX,
total_len,
joined_path.quote()
);
return false;
}
if total_len == 0 {
if fs::symlink_metadata(&joined_path).is_err() {
writeln!(std::io::stderr(), "pathchk: '': No such file or directory",);
return false;
}
}
for p in path {
let component_len = p.len();
if component_len > libc::FILENAME_MAX as usize {
writeln!(
std::io::stderr(),
"limit {} exceeded by length {} of file name component {}",
libc::FILENAME_MAX,
component_len,
p.quote()
);
return false;
}
}
check_searchable(&joined_path)
}
fn check_searchable(path: &str) -> bool {
match fs::symlink_metadata(path) {
Ok(_) => true,
Err(e) => {
if e.kind() == ErrorKind::NotFound {
true
} else {
writeln!(std::io::stderr(), "{e}");
false
}
}
}
}
fn check_portable_chars(path_segment: &str) -> bool {
const VALID_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
for (i, ch) in path_segment.as_bytes().iter().enumerate() {
if !VALID_CHARS.contains(ch) {
let invalid = path_segment[i..].chars().next().unwrap();
writeln!(
std::io::stderr(),
"nonportable character '{}' in file name component {}",
invalid,
path_segment.quote()
);
return false;
}
}
true
}