From 2213d332782764ab19e20c867bafa75a8aab0b65 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 21 May 2026 19:22:27 +0800 Subject: Add path picker and PathChecker for CLI argument validation --- mingling/src/parser.rs | 1 + mingling/src/parser/picker.rs | 3 + mingling/src/parser/picker/path.rs | 145 +++++++++++++++++++++ mingling/src/parser/picker/path/rule.rs | 218 ++++++++++++++++++++++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 mingling/src/parser/picker/path.rs create mode 100644 mingling/src/parser/picker/path/rule.rs (limited to 'mingling/src') diff --git a/mingling/src/parser.rs b/mingling/src/parser.rs index e25322e..b3d9059 100644 --- a/mingling/src/parser.rs +++ b/mingling/src/parser.rs @@ -5,6 +5,7 @@ mod picker; pub use crate::parser::picker::*; pub use crate::parser::picker::bools::*; +pub use crate::parser::picker::path::*; #[cfg(test)] mod test; diff --git a/mingling/src/parser/picker.rs b/mingling/src/parser/picker.rs index 91c7c83..725d4e6 100644 --- a/mingling/src/parser/picker.rs +++ b/mingling/src/parser/picker.rs @@ -7,6 +7,9 @@ pub mod builtin; #[doc(hidden)] pub mod bools; +#[doc(hidden)] +pub mod path; + /// A builder for extracting values from command-line arguments. /// /// The `Picker` struct holds parsed arguments and provides a fluent interface diff --git a/mingling/src/parser/picker/path.rs b/mingling/src/parser/picker/path.rs new file mode 100644 index 0000000..589531f --- /dev/null +++ b/mingling/src/parser/picker/path.rs @@ -0,0 +1,145 @@ +use std::path::PathBuf; + +use crate::parser::Pickable; + +mod rule; +pub use rule::*; + +impl Pickable for Vec { + type Output = Vec; + + fn pick(args: &mut crate::parser::Argument, flag: mingling_core::Flag) -> Option { + let raw: Vec = args.pick_arguments(flag); + let paths: Vec = raw.into_iter().map(|s| PathBuf::from(s)).collect(); + Some(paths) + } +} + +impl Pickable for PathBuf { + type Output = PathBuf; + + fn pick(args: &mut crate::parser::Argument, flag: mingling_core::Flag) -> Option { + let raw: String = args.pick_argument(flag)?; + let path: PathBuf = PathBuf::from(raw); + Some(path) + } +} + +/// Provides path checking methods for [`Vec`] +/// +/// This trait automatically provides implementations for `Into>` +pub trait PathsChecker { + /// Check if all paths in the list satisfy the rule + fn is_all_passed(&self, rule: &PathCheckRule) -> bool + where + Self: Into> + Clone, + { + check_paths(self.clone(), rule).is_ok() + } + + /// Classify paths into (Passed, Stripped) + /// + /// Passed means paths that satisfy the rule, Stripped means paths that do not. + fn classify(self, rule: &PathCheckRule) -> (Vec, Vec) + where + Self: Into>, + { + let paths = self.into(); + let mut passed = Vec::new(); + let mut stripped = Vec::new(); + for path in paths { + if check_path(&path, rule).is_ok() { + passed.push(path); + } else { + stripped.push(path); + } + } + (passed, stripped) + } + + /// Return paths that satisfy the rule + fn passed(self, rule: &PathCheckRule) -> Vec + where + Self: Into>, + { + self.classify(rule).0 + } + + /// Return paths that do not satisfy the rule + fn stripped(self, rule: &PathCheckRule) -> Vec + where + Self: Into>, + { + self.classify(rule).1 + } +} + +/// Provides path checking methods for [`PathBuf`] +/// +/// This trait automatically provides implementations for `Into` +pub trait PathChecker { + fn is_passed(&self, rule: &PathCheckRule) -> bool + where + Self: Into + Clone, + { + check_path(self.clone(), rule).is_ok() + } +} + +impl>> PathsChecker for T where T: Into> {} +impl> PathChecker for T where T: Into {} + +fn check_paths(path: impl Into>, rule: &PathCheckRule) -> Result<(), ()> { + let paths = path.into(); + for p in paths.iter() { + check_exist(p, rule)?; + check_type(p, rule)?; + } + + Ok(()) +} + +fn check_path(path: impl Into, rule: &PathCheckRule) -> Result<(), ()> { + let p = path.into(); + check_exist(&p, rule)?; + check_type(&p, rule)?; + + Ok(()) +} + +fn check_exist(path: &PathBuf, rule: &PathCheckRule) -> Result<(), ()> { + let Some(exist_check) = &rule.exist_check else { + return Ok(()); + }; + + match exist_check { + PathExistCheck::Exists => bool_to_result(path.exists()), + PathExistCheck::NotExists => bool_to_result(!path.exists()), + } +} + +fn check_type(path: &PathBuf, rule: &PathCheckRule) -> Result<(), ()> { + let Some(type_check) = &rule.type_check else { + return Ok(()); + }; + + let is_dir = path.is_dir(); + let is_file = path.is_file(); + let is_symlink = path.is_symlink(); + + if type_check.allow_dir && is_dir { + return Ok(()); + } + if type_check.allow_file && is_file { + return Ok(()); + } + if type_check.allow_symlink && is_symlink { + return Ok(()); + } + + Err(()) +} + +fn bool_to_result(b: bool) -> Result<(), ()> { + if b { Ok(()) } else { Err(()) } +} diff --git a/mingling/src/parser/picker/path/rule.rs b/mingling/src/parser/picker/path/rule.rs new file mode 100644 index 0000000..07df705 --- /dev/null +++ b/mingling/src/parser/picker/path/rule.rs @@ -0,0 +1,218 @@ +/// Path check rule +#[derive(Default)] +pub struct PathCheckRule { + pub exist_check: Option, + pub type_check: Option, +} + +/// Path existence check +pub enum PathExistCheck { + Exists, + NotExists, +} + +/// Path type check +pub struct PathTypeCheck { + /// Whether the path is allowed to be a file + pub allow_file: bool, + + /// Whether the path is allowed to be a directory + pub allow_dir: bool, + + /// Whether the path is allowed to be a symlink + pub allow_symlink: bool, +} + +impl PathCheckRule { + /// Creates a new `PathCheckRule` with default values + pub fn new() -> Self { + Self { + exist_check: None, + type_check: None, + } + } + + /// Allows the path to be a file + pub fn allow_file(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: type_check.allow_dir, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: false, + allow_symlink: false, + }), + ..self + }, + } + } + + /// Allows the path to be a directory + pub fn allow_dir(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: true, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: true, + allow_symlink: false, + }), + ..self + }, + } + } + + /// Allows the path to be a symlink + pub fn allow_symlink(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: type_check.allow_dir, + allow_symlink: true, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: false, + allow_symlink: true, + }), + ..self + }, + } + } + + /// Denies the path from being a file + pub fn deny_file(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: type_check.allow_dir, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: true, + allow_symlink: true, + }), + ..self + }, + } + } + + /// Denies the path from being a directory + pub fn deny_dir(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: false, + allow_symlink: type_check.allow_symlink, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: false, + allow_symlink: true, + }), + ..self + }, + } + } + + /// Denies the path from being a symlink + pub fn deny_symlink(self) -> Self { + match self.type_check { + Some(type_check) => Self { + type_check: Some(PathTypeCheck { + allow_file: type_check.allow_file, + allow_dir: type_check.allow_dir, + allow_symlink: false, + }), + ..self + }, + None => Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: true, + allow_symlink: false, + }), + ..self + }, + } + } + + /// Requires the path to be a file (overrides type checks) + pub fn must_file(self) -> Self { + Self { + type_check: Some(PathTypeCheck { + allow_file: true, + allow_dir: false, + allow_symlink: false, + }), + ..self + } + } + + /// Requires the path to be a directory (overrides type checks) + pub fn must_dir(self) -> Self { + Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: true, + allow_symlink: false, + }), + ..self + } + } + + /// Requires the path to be a symlink (overrides type checks) + pub fn must_symlink(self) -> Self { + Self { + type_check: Some(PathTypeCheck { + allow_file: false, + allow_dir: false, + allow_symlink: true, + }), + ..self + } + } + + /// Requires the path to exist + pub fn must_exist(self) -> Self { + Self { + exist_check: Some(PathExistCheck::Exists), + ..self + } + } + + /// Requires the path to not exist + pub fn must_not_exist(self) -> Self { + Self { + exist_check: Some(PathExistCheck::NotExists), + ..self + } + } +} -- cgit