Skip to main content

yash_builtin/umask/
syntax.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Parsing command line arguments to the `umask` built-in
18
19use super::Command;
20use super::symbol::{ParseClausesError, parse_clauses};
21use crate::common::syntax::{Mode, OptionSpec, ParseError, parse_arguments};
22use std::num::ParseIntError;
23use thiserror::Error;
24use yash_env::Env;
25use yash_env::semantics::Field;
26use yash_env::source::pretty::{Report, ReportType, Snippet};
27
28#[derive(Clone, Debug, Eq, Error, PartialEq)]
29#[non_exhaustive]
30pub enum Error {
31    /// An error occurred in the common syntax parser.
32    #[error(transparent)]
33    CommonError(#[from] ParseError<'static>),
34
35    /// More than one operand is given.
36    ///
37    /// The vector contains *all* the operands, including the first proper one.
38    #[error("too many operands")]
39    TooManyOperands(Vec<Field>),
40
41    /// An operand starts with a digit and is not a valid mode.
42    #[error("invalid mask notation")]
43    InvalidNumericMode(Field, ParseIntError),
44
45    /// An operand does not start with a digit and is not a valid mode.
46    #[error("invalid mask notation")]
47    InvalidSymbolicMode(Field, ParseClausesError),
48}
49
50impl Error {
51    /// Converts the error to a report.
52    #[must_use]
53    pub fn to_report(&self) -> Report<'_> {
54        let snippets = match self {
55            Self::CommonError(e) => return e.to_report(),
56            Self::TooManyOperands(operands) => Snippet::with_primary_span(
57                &operands[1].origin,
58                format!("{}: unexpected operand", operands[1].value).into(),
59            ),
60            Self::InvalidNumericMode(operand, parse_int_error) => Snippet::with_primary_span(
61                &operand.origin,
62                format!("{operand}: {parse_int_error}").into(),
63            ),
64            Self::InvalidSymbolicMode(operand, parse_clauses_error) => Snippet::with_primary_span(
65                &operand.origin,
66                format!("{operand}: {parse_clauses_error}").into(),
67            ),
68        };
69        let mut report = Report::new();
70        report.r#type = ReportType::Error;
71        report.title = self.to_string().into();
72        report.snippets = snippets;
73        report
74    }
75}
76
77impl<'a> From<&'a Error> for Report<'a> {
78    #[inline]
79    fn from(error: &'a Error) -> Self {
80        error.to_report()
81    }
82}
83
84/// Result of parsing command line arguments
85pub type Result = std::result::Result<Command, Error>;
86
87/// List of all options supported by the `umask` built-in
88const OPTION_SPECS: &[OptionSpec] = &[OptionSpec::new().short('S')];
89
90/// Parses command line arguments.
91pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result {
92    let (options, operands) = parse_arguments(OPTION_SPECS, Mode::with_env(env), args)?;
93
94    match operands.len() {
95        0 => {
96            let symbolic = options.iter().any(|o| o.spec.get_short() == Some('S'));
97            Ok(Command::Show { symbolic })
98        }
99
100        1 => {
101            let field = { operands }.pop().unwrap();
102
103            // TODO Use char::is_ascii_octdigit
104            if field.value.starts_with(|c: char| c.is_ascii_digit()) {
105                return match u16::from_str_radix(&field.value, 8) {
106                    Ok(mask) => Ok(Command::set_from_raw_mask(mask)),
107                    Err(e) => Err(Error::InvalidNumericMode(field, e)),
108                };
109            }
110
111            match parse_clauses(&field.value) {
112                Ok(clauses) => Ok(Command::Set(clauses)),
113                Err(e) => Err(Error::InvalidSymbolicMode(field, e)),
114            }
115        }
116
117        _ => Err(Error::TooManyOperands(operands)),
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::umask::symbol::{Action, Clause, Operator, Permission, Who};
125    use assert_matches::assert_matches;
126
127    #[test]
128    fn no_arguments() {
129        let env = Env::new_virtual();
130        let result = parse(&env, vec![]);
131        assert_eq!(result, Ok(Command::Show { symbolic: false }));
132    }
133
134    #[test]
135    fn symbolic_option() {
136        let env = Env::new_virtual();
137        let result = parse(&env, Field::dummies(["-S"]));
138        assert_eq!(result, Ok(Command::Show { symbolic: true }));
139    }
140
141    #[test]
142    fn numeric_mask() {
143        let env = Env::new_virtual();
144        let args = Field::dummies(["022"]);
145        let result = parse(&env, args);
146        assert_eq!(
147            result,
148            Ok(Command::Set(vec![Clause {
149                who: Who { mask: 0o777 },
150                actions: vec![Action {
151                    operator: Operator::Set,
152                    permission: Permission::Literal {
153                        mask: !0o022,
154                        conditional_executable: false
155                    },
156                }],
157            }]))
158        );
159    }
160
161    #[test]
162    fn symbolic_mask() {
163        let env = Env::new_virtual();
164        let args = Field::dummies(["u=rwx,go+r-w"]);
165        let result = parse(&env, args);
166        assert_eq!(
167            result,
168            Ok(Command::Set(vec![
169                Clause {
170                    who: Who { mask: 0o700 },
171                    actions: vec![Action {
172                        operator: Operator::Set,
173                        permission: Permission::Literal {
174                            mask: 0o777,
175                            conditional_executable: false
176                        }
177                    }]
178                },
179                Clause {
180                    who: Who { mask: 0o077 },
181                    actions: vec![
182                        Action {
183                            operator: Operator::Add,
184                            permission: Permission::Literal {
185                                mask: 0o444,
186                                conditional_executable: false
187                            }
188                        },
189                        Action {
190                            operator: Operator::Remove,
191                            permission: Permission::Literal {
192                                mask: 0o222,
193                                conditional_executable: false
194                            }
195                        }
196                    ]
197                }
198            ]))
199        );
200    }
201
202    #[test]
203    fn too_many_operands() {
204        let env = Env::new_virtual();
205        let args = Field::dummies(["022", "002"]);
206        let result = parse(&env, args.clone());
207        assert_eq!(result, Err(Error::TooManyOperands(args)));
208    }
209
210    #[test]
211    fn operand_overrides_option() {
212        // Currently, the `-S` option is ignored if the mode is given.
213        let env = Env::new_virtual();
214        let args = Field::dummies(["-S", "go=u"]);
215        let result = parse(&env, args);
216        assert_eq!(
217            result,
218            Ok(Command::Set(vec![Clause {
219                who: Who { mask: 0o077 },
220                actions: vec![Action {
221                    operator: Operator::Set,
222                    permission: Permission::CopyUser,
223                }],
224            }]))
225        );
226    }
227
228    #[test]
229    fn invalid_numeric_mask() {
230        let env = Env::new_virtual();
231        let arg = Field::dummy("02x2");
232        let result = parse(&env, vec![arg.clone()]);
233        assert_matches!(result, Err(Error::InvalidNumericMode(field, e)) => {
234            assert_eq!(field, arg);
235            assert_eq!(e.kind(), &std::num::IntErrorKind::InvalidDigit);
236        });
237    }
238
239    #[test]
240    fn numeric_mask_starting_with_plus() {
241        let env = Env::new_virtual();
242        let arg = Field::dummy("+022");
243        let result = parse(&env, vec![arg.clone()]);
244        assert_eq!(
245            result,
246            Err(Error::InvalidSymbolicMode(
247                arg,
248                ParseClausesError::InvalidChar('0')
249            ))
250        );
251    }
252
253    #[test]
254    fn invalid_option() {
255        // Though "-x" may look like a valid symbolic mode,
256        // it is regarded as an invalid option without the "--" separator.
257        let env = Env::new_virtual();
258        let arg = Field::dummy("-x");
259        let result = parse(&env, vec![arg.clone()]);
260        assert_eq!(
261            result,
262            Err(Error::CommonError(ParseError::UnknownShortOption('x', arg)))
263        );
264    }
265}