sudo_rs/visudo/
cli.rs

1#[derive(Debug, PartialEq)]
2pub(crate) struct VisudoOptions {
3    pub(crate) file: Option<String>,
4    pub(crate) owner: bool,
5    pub(crate) perms: bool,
6    pub(crate) action: VisudoAction,
7}
8
9impl Default for VisudoOptions {
10    fn default() -> Self {
11        Self {
12            file: None,
13            owner: false,
14            perms: false,
15            action: VisudoAction::Run,
16        }
17    }
18}
19
20#[derive(Debug, PartialEq)]
21pub(crate) enum VisudoAction {
22    Help,
23    Version,
24    Check,
25    Run,
26}
27
28type OptionSetter = fn(&mut VisudoOptions, Option<String>) -> Result<(), String>;
29
30struct VisudoOption {
31    short: char,
32    long: &'static str,
33    takes_argument: bool,
34    set: OptionSetter,
35}
36
37impl VisudoOptions {
38    const VISUDO_OPTIONS: &'static [VisudoOption] = &[
39        VisudoOption {
40            short: 'c',
41            long: "check",
42            takes_argument: false,
43            set: |options, _| {
44                options.action = VisudoAction::Check;
45                Ok(())
46            },
47        },
48        VisudoOption {
49            short: 'f',
50            long: "file",
51            takes_argument: true,
52            set: |options, argument| {
53                options.file = Some(argument.ok_or("option requires an argument -- 'f'")?);
54                Ok(())
55            },
56        },
57        VisudoOption {
58            short: 'h',
59            long: "help",
60            takes_argument: false,
61            set: |options, _| {
62                options.action = VisudoAction::Help;
63                Ok(())
64            },
65        },
66        VisudoOption {
67            short: 'I',
68            long: "no-includes",
69            takes_argument: false,
70            set: |_, _| Ok(()),
71            /* ignored for compatibility sake */
72        },
73        VisudoOption {
74            short: 'q',
75            long: "quiet",
76            takes_argument: false,
77            set: |_, _| Ok(()),
78            /* ignored for compatibility sake */
79        },
80        VisudoOption {
81            short: 's',
82            long: "strict",
83            takes_argument: false,
84            set: |_, _| Ok(()),
85            /* ignored for compatibility sake */
86        },
87        VisudoOption {
88            short: 'V',
89            long: "version",
90            takes_argument: false,
91            set: |options, _| {
92                options.action = VisudoAction::Version;
93                Ok(())
94            },
95        },
96        VisudoOption {
97            short: 'O',
98            long: "owner",
99            takes_argument: false,
100            set: |options, _| {
101                options.owner = true;
102                Ok(())
103            },
104        },
105        VisudoOption {
106            short: 'P',
107            long: "perms",
108            takes_argument: false,
109            set: |options, _| {
110                options.perms = true;
111                Ok(())
112            },
113        },
114    ];
115
116    pub(crate) fn from_env() -> Result<VisudoOptions, String> {
117        let args = std::env::args().collect();
118
119        Self::parse_arguments(args)
120    }
121
122    /// parse su arguments into VisudoOptions struct
123    pub(crate) fn parse_arguments(arguments: Vec<String>) -> Result<VisudoOptions, String> {
124        let mut options: VisudoOptions = VisudoOptions::default();
125        let mut arg_iter = arguments.into_iter().skip(1);
126
127        while let Some(arg) = arg_iter.next() {
128            // if the argument starts with -- it must be a full length option name
129            if arg.starts_with("--") {
130                // parse assignments like '--file=/etc/sudoers'
131                if let Some((key, value)) = arg.split_once('=') {
132                    // lookup the option by name
133                    if let Some(option) = Self::VISUDO_OPTIONS.iter().find(|o| o.long == &key[2..])
134                    {
135                        // the value is already present, when the option does not take any arguments this results in an error
136                        if option.takes_argument {
137                            (option.set)(&mut options, Some(value.to_string()))?;
138                        } else {
139                            Err(format!("'--{}' does not take any arguments", option.long))?;
140                        }
141                    } else {
142                        Err(format!("unrecognized option '{arg}'"))?;
143                    }
144                // lookup the option
145                } else if let Some(option) =
146                    Self::VISUDO_OPTIONS.iter().find(|o| o.long == &arg[2..])
147                {
148                    // try to parse an argument when the option needs an argument
149                    if option.takes_argument {
150                        let next_arg = arg_iter.next();
151                        (option.set)(&mut options, next_arg)?;
152                    } else {
153                        (option.set)(&mut options, None)?;
154                    }
155                } else {
156                    Err(format!("unrecognized option '{arg}'"))?;
157                }
158            } else if arg.starts_with('-') && arg != "-" {
159                // flags can be grouped, so we loop over the characters
160                for (n, char) in arg.trim_start_matches('-').chars().enumerate() {
161                    // lookup the option
162                    if let Some(option) = Self::VISUDO_OPTIONS.iter().find(|o| o.short == char) {
163                        // try to parse an argument when one is necessary, either the rest of the current flag group or the next argument
164                        if option.takes_argument {
165                            let rest = arg[(n + 2)..].trim().to_string();
166                            let next_arg = if rest.is_empty() {
167                                arg_iter.next()
168                            } else {
169                                Some(rest)
170                            };
171                            (option.set)(&mut options, next_arg)?;
172                            // stop looping over flags if the current flag takes an argument
173                            break;
174                        } else {
175                            // parse flag without argument
176                            (option.set)(&mut options, None)?;
177                        }
178                    } else {
179                        Err(format!("unrecognized option '{char}'"))?;
180                    }
181                }
182            } else {
183                // If the arg doesn't start with a `-` it must be a file argument. However `-f`
184                // must take precedence
185                if options.file.is_none() {
186                    options.file = Some(arg);
187                }
188            }
189        }
190
191        Ok(options)
192    }
193}