rudo/src/app.rs

309 lines
13 KiB
Rust

use std::{env::args, path::PathBuf};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(Clone, Copy, Debug, EnumIter)]
pub enum AppOption {
Background,
Chdir,
CloseFrom,
GenerateConfig,
Group,
Help,
// Host,
// List,
// Login,
NoUpdate,
// OtherUser,
PreserveEnv,
// PreserveGroups,
RemoveTimestamp,
ResetTimestamp,
// SetHome,
// Shell,
Stdin,
User,
Validate,
Version,
Z,
}
impl AppOption {
pub fn from_str(s: &str, _has_arg: bool) -> Option<Self> {
match s {
"--background" | "-b" => Self::Background,
"--close-from" | "-C" => Self::CloseFrom,
"--chdir" | "-D" => Self::Chdir,
"--group" | "-g" => Self::Group,
"--gen-conf" => Self::GenerateConfig,
"--help" | "-h" => {
// if has_arg {
Self::Help
// } else {
// Self::Host
// }
}
// "host" => Self::Host,
// "list" | "l" => Self::List,
// "--login" | "-i" => Self::Login,
"--no-update" | "-N" => Self::NoUpdate,
// "other-user" | "U" => Self::OtherUser,
"--preserve-env" | "-E" => Self::PreserveEnv,
// "--preserve-groups" | "-P" => Self::PreserveGroups,
"--remove-timestamp" | "-K" => Self::RemoveTimestamp,
"--reset_timestamp" | "-k" => Self::ResetTimestamp,
// "--set-home" | "-H" => Self::SetHome,
// "--shell" | "-s" => Self::Shell,
"--stdin" | "-S" => Self::Stdin,
"--user" | "-u" => Self::User,
"--validate" | "-v" => Self::Validate,
"--version" | "-V" => Self::Version,
"--" => Self::Z,
_ => {
return None;
}
}
.into()
}
pub fn from_flags(s: &str) -> Result<Vec<Self>, String> {
let mut output = vec![];
for ch in s.chars().skip(1) {
if let Some(opt) = Self::from_str(&format!["-{ch}"], false) {
if opt.is_flag() {
output.push(opt)
} else {
return Err(format!["{ch}"]);
}
} else {
return Err(format!["{ch}"]);
}
}
Ok(output)
}
pub fn usage(self) -> (&'static str, &'static str) {
match self {
AppOption::Background => (
"--background | -b",
"Run the given command in the background. Caution: backgrounded processes are not subject to shell job control. Interactive commands may misbehave.",
),
AppOption::Chdir => (
"--chdir dir | -D dir",
"Change to the specified directory before running the command."
),
AppOption::CloseFrom => (
"--close-from n | -C n",
"Close all file descriptors greater than or equal to n.",
),
AppOption::GenerateConfig => ("--gen-config", "Print example config to standard output."),
AppOption::Group => (
"--group | -g",
"Run the command with the primary group set to group.",
),
AppOption::Help => ("--help | -h", "Display program help."),
// AppOption::Host => ("", ""),
// AppOption::List => ("--list (command) | -l (command)", "If command is specified, display the fully-qualified path as dictated by the security policy including command-line arguments. Otherwise, display allowed commands for the invoking user."),
// AppOption::Login => (
// "--login | -i",
// "Runs the target user's login shell and passes command to the shell, if any.",
// ),
AppOption::NoUpdate => ("--no-update | -N", "Do not update user's cache credentials."),
// AppOption::OtherUser => ("--other-user user | -U user", ""),
AppOption::PreserveEnv => (
"--preserve-env var, ... | -E var, ...",
"Preserve environment variables.",
),
// AppOption::PreserveGroups => ("--preserve-groups | -P", "Preserve invoking user's groups."),
AppOption::RemoveTimestamp => ("--remove-timestamp | -K", "Remove all cached credentials for invoking user."),
AppOption::ResetTimestamp => ("--reset-timestamp | -k", "Without command, clears session credentials. With a command, runs the command without updating cached credentials."),
// AppOption::SetHome => ("--set-home | -H", "Set HOME environment variable to target user's home."),
// AppOption::Shell => ("--shell | -s", "Run the shell specified by the SHELL environment variable or as specific to invoking user. Passes command to shell."),
AppOption::Stdin => ("--stdin | -S", "Write the password prompt to standard error and read the password from standard input."),
AppOption::User => ("--user user| -u user", "Run command as user instead of default."),
AppOption::Validate => ("--validate | -v", "Re-authenticate the user and update cached credentials."),
AppOption::Version => ("--version | -V", "Print the version."),
AppOption::Z => ("--", "Indicates end of options. Subsequent options are passed to the command."),
}
}
pub fn is_flag(self) -> bool {
match self {
Self::Background
| Self::Help
// | AppOption::Login
// | AppOption::PreserveGroups
| Self::GenerateConfig
| Self::NoUpdate
| Self::RemoveTimestamp
| Self::ResetTimestamp
// | AppOption::SetHome
// | AppOption::Shell
| Self::Stdin
| Self::Validate
| Self::Version => true,
_ => false,
}
}
pub fn format_all() -> String {
let mut output = String::from("Usage: rudo (options) (--) (command)\n");
let max_len = Self::iter().fold(0, |n, v| n.max(v.usage().0.len()));
for (dashing, details) in Self::iter().map(|v| v.usage()) {
let mut spacing = String::from(" ");
for _ in dashing.len()..max_len {
spacing.push(' ');
}
let s = format!["\n {dashing}{spacing}{details}"];
let chunks = s
.chars()
.collect::<Vec<char>>()
.chunks(80)
.map(|chunk| chunk.into_iter().collect::<String>())
.collect::<Vec<String>>();
let f = chunks.join("\n");
output.push_str(&f);
}
output
}
}
#[derive(Debug, Default)]
pub struct App {
pub background: bool,
pub chdir: Option<PathBuf>,
pub chroot: Option<PathBuf>,
pub close_from: Option<i32>,
pub cmd: Vec<String>,
pub gen_config: bool,
pub group: Option<String>,
pub help: bool,
pub host: Option<String>,
pub list: (bool, Option<String>),
pub login: bool,
pub no_update: bool,
pub preserve_env: Vec<String>,
pub preserve_groups: bool,
pub remove_timestamp: bool,
pub reset_timestamp: bool,
pub set_home: bool,
pub shell: bool,
pub stdin: bool,
pub user: Option<String>,
pub validate: bool,
pub version: bool,
}
impl App {
pub fn parse() -> Result<Self, String> {
let args: Vec<String> = args().collect();
let mut app = App::default();
let mut args_iter = args.iter().skip(1).peekable();
'sm: loop {
if let Some(arg) = args_iter.next() {
let app_opts = if arg.starts_with("--") {
if let Some(o) = AppOption::from_str(arg, args_iter.peek().is_some()) {
vec![o]
} else {
return Err(format!["Error: Unknown option {arg}"]);
}
} else if arg.starts_with("-") {
match AppOption::from_flags(arg) {
Ok(v) => v,
Err(s) => return Err(format!["Unknown flag {s}"]),
}
} else {
app.cmd.push(arg.to_owned());
break;
};
for app_opt in app_opts {
match app_opt {
AppOption::Background => app.background = true,
AppOption::Chdir => {
let next = args_iter.next();
if let Some(s) = next {
app.chdir = Some(s.into());
} else {
return Err(format![
"Error: Missing argument for {arg}\nUsage: {}\n{}",
AppOption::Chdir.usage().0,
AppOption::Chdir.usage().1,
]);
}
}
AppOption::CloseFrom => {
let next = args_iter.next();
if let Some(s) = next {
if let Ok(n) = s.parse() {
app.close_from = Some(n);
} else {
return Err(format![
"Error: Invalid argument for {arg}: Expected number, got {s}\nUsage: {}\n{}",
AppOption::CloseFrom.usage().0,
AppOption::CloseFrom.usage().1,
]);
}
} else {
return Err(format![
"Error: Missing argument for {arg}\nUsage: {}\n{}",
AppOption::CloseFrom.usage().0,
AppOption::CloseFrom.usage().1,
]);
}
}
AppOption::GenerateConfig => app.gen_config = true,
AppOption::Group => {
let next = args_iter.next();
if let Some(s) = next {
app.group = Some(s.to_owned());
} else {
return Err(format![
"Error: Missing argument for {arg}\nUsage: {}\n{}",
AppOption::Group.usage().0,
AppOption::Group.usage().1,
]);
}
}
AppOption::Help => app.help = true,
// AppOption::Login => app.login = true,
AppOption::NoUpdate => app.no_update = true,
AppOption::PreserveEnv => loop {
if let Some(arg) = args_iter.next() {
app.preserve_env.push(arg.to_owned());
if !arg.ends_with(",") {
break;
}
} else {
break 'sm;
}
},
// AppOption::PreserveGroups => app.preserve_groups = true,
AppOption::RemoveTimestamp => app.remove_timestamp = true,
AppOption::ResetTimestamp => app.reset_timestamp = true,
// AppOption::SetHome => app.set_home = true,
// AppOption::Shell => app.shell = true,
AppOption::Stdin => app.stdin = true,
AppOption::User => {
if let Some(arg) = args_iter.next() {
app.user = Some(arg.to_owned());
} else {
return Err(format![
"Error: Missing argument for {arg}\nUsage: {}\n{}",
AppOption::User.usage().0,
AppOption::User.usage().1,
]);
}
}
AppOption::Validate => app.validate = true,
AppOption::Version => app.version = true,
AppOption::Z => break 'sm,
}
}
} else {
break;
}
}
app.cmd.extend(args_iter.map(|s| s.to_owned()));
Ok(app)
}
pub fn usage() -> String {
AppOption::format_all()
}
}