use super::symbol::{parse_clauses, ParseClausesError};
use super::Command;
use crate::common::syntax::{parse_arguments, Mode, OptionSpec, ParseError};
use std::borrow::Cow;
use std::num::ParseIntError;
use thiserror::Error;
use yash_env::semantics::Field;
use yash_env::Env;
use yash_syntax::source::pretty::{Annotation, AnnotationType, MessageBase};
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
CommonError(#[from] ParseError<'static>),
#[error("too many operands")]
TooManyOperands(Vec<Field>),
#[error("invalid mask notation")]
InvalidNumericMode(Field, ParseIntError),
#[error("invalid mask notation")]
InvalidSymbolicMode(Field, ParseClausesError),
}
impl MessageBase for Error {
fn message_title(&self) -> Cow<str> {
self.to_string().into()
}
fn main_annotation(&self) -> Annotation<'_> {
match self {
Self::CommonError(e) => e.main_annotation(),
Self::TooManyOperands(operands) => Annotation::new(
AnnotationType::Error,
format!("{}: redundant operand", operands[1].value).into(),
&operands[1].origin,
),
Self::InvalidNumericMode(operand, e) => Annotation::new(
AnnotationType::Error,
format!("{}: {}", operand.value, e).into(),
&operand.origin,
),
Self::InvalidSymbolicMode(operand, e) => Annotation::new(
AnnotationType::Error,
format!("{}: {}", operand.value, e).into(),
&operand.origin,
),
}
}
}
pub type Result = std::result::Result<Command, Error>;
const OPTION_SPECS: &[OptionSpec] = &[OptionSpec::new().short('S')];
pub fn parse(env: &Env, args: Vec<Field>) -> Result {
let (options, operands) = parse_arguments(OPTION_SPECS, Mode::with_env(env), args)?;
match operands.len() {
0 => {
let symbolic = options.iter().any(|o| o.spec.get_short() == Some('S'));
Ok(Command::Show { symbolic })
}
1 => {
let field = { operands }.pop().unwrap();
if field.value.starts_with(|c: char| c.is_ascii_digit()) {
return match u16::from_str_radix(&field.value, 8) {
Ok(mask) => Ok(Command::set_from_raw_mask(mask)),
Err(e) => Err(Error::InvalidNumericMode(field, e)),
};
}
match parse_clauses(&field.value) {
Ok(clauses) => Ok(Command::Set(clauses)),
Err(e) => Err(Error::InvalidSymbolicMode(field, e)),
}
}
_ => Err(Error::TooManyOperands(operands)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::umask::symbol::{Action, Clause, Operator, Permission, Who};
use assert_matches::assert_matches;
#[test]
fn no_arguments() {
let env = Env::new_virtual();
let result = parse(&env, vec![]);
assert_eq!(result, Ok(Command::Show { symbolic: false }));
}
#[test]
fn symbolic_option() {
let env = Env::new_virtual();
let result = parse(&env, Field::dummies(["-S"]));
assert_eq!(result, Ok(Command::Show { symbolic: true }));
}
#[test]
fn numeric_mask() {
let env = Env::new_virtual();
let args = Field::dummies(["022"]);
let result = parse(&env, args);
assert_eq!(
result,
Ok(Command::Set(vec![Clause {
who: Who { mask: 0o777 },
actions: vec![Action {
operator: Operator::Set,
permission: Permission::Literal {
mask: !0o022,
conditional_executable: false
},
}],
}]))
);
}
#[test]
fn symbolic_mask() {
let env = Env::new_virtual();
let args = Field::dummies(["u=rwx,go+r-w"]);
let result = parse(&env, args);
assert_eq!(
result,
Ok(Command::Set(vec![
Clause {
who: Who { mask: 0o700 },
actions: vec![Action {
operator: Operator::Set,
permission: Permission::Literal {
mask: 0o777,
conditional_executable: false
}
}]
},
Clause {
who: Who { mask: 0o077 },
actions: vec![
Action {
operator: Operator::Add,
permission: Permission::Literal {
mask: 0o444,
conditional_executable: false
}
},
Action {
operator: Operator::Remove,
permission: Permission::Literal {
mask: 0o222,
conditional_executable: false
}
}
]
}
]))
);
}
#[test]
fn too_many_operands() {
let env = Env::new_virtual();
let args = Field::dummies(["022", "002"]);
let result = parse(&env, args.clone());
assert_eq!(result, Err(Error::TooManyOperands(args)));
}
#[test]
fn operand_overrides_option() {
let env = Env::new_virtual();
let args = Field::dummies(["-S", "go=u"]);
let result = parse(&env, args);
assert_eq!(
result,
Ok(Command::Set(vec![Clause {
who: Who { mask: 0o077 },
actions: vec![Action {
operator: Operator::Set,
permission: Permission::CopyUser,
}],
}]))
);
}
#[test]
fn invalid_numeric_mask() {
let env = Env::new_virtual();
let arg = Field::dummy("02x2");
let result = parse(&env, vec![arg.clone()]);
assert_matches!(result, Err(Error::InvalidNumericMode(field, e)) => {
assert_eq!(field, arg);
assert_eq!(e.kind(), &std::num::IntErrorKind::InvalidDigit);
});
}
#[test]
fn numeric_mask_starting_with_plus() {
let env = Env::new_virtual();
let arg = Field::dummy("+022");
let result = parse(&env, vec![arg.clone()]);
assert_eq!(
result,
Err(Error::InvalidSymbolicMode(
arg,
ParseClausesError::InvalidChar('0')
))
);
}
#[test]
fn invalid_option() {
let env = Env::new_virtual();
let arg = Field::dummy("-x");
let result = parse(&env, vec![arg.clone()]);
assert_eq!(
result,
Err(Error::CommonError(ParseError::UnknownShortOption('x', arg)))
);
}
}