use anstyle::AnsiColor;
use anyhow::{Error, anyhow};
use clap::ValueHint;
use clap::builder::{ValueParser, styling::Styles};
use clap::{Parser, ValueEnum};
use normpath::PathExt;
use std::path::{Path, PathBuf};
use std::thread;
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Yellow.on_default())
.usage(AnsiColor::Green.on_default())
.literal(AnsiColor::Green.on_default())
.placeholder(AnsiColor::Green.on_default());
#[derive(Parser, Default, Debug, Clone)]
#[clap(author, version, about, long_about = None, styles=STYLES)]
pub struct Args {
#[clap(short = 'f', long, action = clap::ArgAction::Set, default_value_t = false, visible_short_alias = 'L')]
pub follow_symlinks: bool,
#[clap(short = 'o', long, action = clap::ArgAction::Set, default_value_t = true, visible_alias = "xdev")]
pub one_filesystem: bool,
#[clap(short = 'x', long, value_parser = ValueParser::new(parse_threads), default_value_t = thread::available_parallelism().map(| n | n.get()).unwrap_or(2))]
pub threads: usize,
#[clap(short = 'd', long, value_parser)]
pub max_depth: Option<usize>,
#[clap(short = 'n', long, value_parser, conflicts_with = "regex")]
pub name: Option<Vec<String>>,
#[clap(short = 'r', long, value_parser, conflicts_with = "name")]
pub regex: Option<Vec<String>>,
#[clap(short = 'i', long, action = clap::ArgAction::Set, default_value_t = false)]
pub case_insensitive: bool,
#[clap(short = 't', long, value_enum, default_values_t = [FileType::Directory, FileType::File, FileType::Symlink])]
pub file_type: Vec<FileType>,
#[clap(required = true, value_parser = ValueParser::new(parse_paths), value_hint = ValueHint::AnyPath)]
pub path: Vec<PathBuf>,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)]
pub enum FileType {
#[value(alias = "e")]
Empty,
#[value(alias = "b")]
BlockDevice,
#[value(alias = "c")]
CharDevice,
#[value(alias = "d")]
Directory,
#[value(alias = "p")]
Pipe,
#[value(alias = "f")]
File,
#[value(alias = "l")]
Symlink,
#[value(alias = "s")]
Socket,
}
fn parse_threads(x: &str) -> Result<usize, Error> {
let v = x.parse::<usize>()?;
if (2..=65535).contains(&v) {
Ok(v)
} else {
Err(anyhow!("threads should be in [2..=65535] range"))
}
}
fn parse_paths(x: &str) -> Result<PathBuf, Error> {
let p = Path::new(x);
if p.is_dir() {
Ok(p.normalize()?.into_path_buf())
} else {
Err(anyhow!("'{x}' is not an existing directory"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_threads_min_valid() {
assert_eq!(parse_threads("2").unwrap(), 2);
}
#[test]
fn test_parse_threads_max_valid() {
assert_eq!(parse_threads("65535").unwrap(), 65535);
}
#[test]
fn test_parse_threads_mid_valid() {
assert_eq!(parse_threads("100").unwrap(), 100);
}
#[test]
fn test_parse_threads_zero_invalid() {
assert!(parse_threads("0").is_err());
}
#[test]
fn test_parse_threads_one_invalid() {
assert!(parse_threads("1").is_err());
}
#[test]
fn test_parse_threads_too_large() {
assert!(parse_threads("65536").is_err());
}
#[test]
fn test_parse_threads_non_numeric() {
assert!(parse_threads("abc").is_err());
assert!(parse_threads("").is_err());
}
#[test]
fn test_parse_threads_negative() {
assert!(parse_threads("-1").is_err());
}
#[test]
fn test_parse_paths_valid_dir() {
let tmp = std::env::temp_dir();
let result = parse_paths(tmp.to_str().unwrap());
assert!(result.is_ok());
}
#[test]
fn test_parse_paths_normalizes() {
let tmp = std::env::temp_dir();
let result = parse_paths(tmp.to_str().unwrap()).unwrap();
assert!(result.is_dir());
}
#[test]
fn test_parse_paths_nonexistent() {
assert!(parse_paths("/nonexistent/xyz/abc123").is_err());
}
#[test]
fn test_parse_paths_file_not_dir() {
assert!(parse_paths("/etc/hosts").is_err());
}
#[test]
fn test_parse_paths_normalizes_dotdot() {
use std::path::Component;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let parent = tmp.path().parent().unwrap();
let name = tmp.path().file_name().unwrap();
let with_dotdot = parent.join(name).join("..").join(name);
let result = parse_paths(with_dotdot.to_str().unwrap()).unwrap();
assert!(
!result.components().any(|c| matches!(c, Component::ParentDir)),
"normalized path must not contain .. components"
);
}
}