use super::ast_names::UserFriendly;
use super::basic_parser::*;
use super::tokens::*;
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub enum Qualified<T> {
Allow(T),
Forbid(T),
}
pub type Spec<T> = Qualified<Meta<T>>;
pub type SpecList<T> = Vec<Spec<T>>;
#[cfg_attr(test, derive(Clone, Debug, PartialEq, Eq))]
pub enum Identifier {
Name(String),
ID(u32),
}
#[cfg_attr(test, derive(Clone, Debug, PartialEq, Eq))]
pub enum UserSpecifier {
User(Identifier),
Group(Identifier),
NonunixGroup(Identifier),
}
pub struct RunAs {
pub users: SpecList<UserSpecifier>,
pub groups: SpecList<Identifier>,
}
#[derive(Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct Tag {
pub passwd: bool,
pub cwd: Option<ChDir>,
}
impl Default for Tag {
fn default() -> Tag {
Tag {
passwd: true,
cwd: None,
}
}
}
pub struct CommandSpec(pub Vec<Modifier>, pub Spec<Command>);
type PairVec<A, B> = Vec<(A, Vec<B>)>;
pub struct PermissionSpec {
pub users: SpecList<UserSpecifier>,
pub permissions: PairVec<SpecList<Hostname>, (Option<RunAs>, CommandSpec)>,
}
pub type Defs<T> = Vec<Def<T>>;
pub struct Def<T>(pub String, pub SpecList<T>);
pub enum Directive {
UserAlias(Defs<UserSpecifier>),
HostAlias(Defs<Hostname>),
CmndAlias(Defs<Command>),
RunasAlias(Defs<UserSpecifier>),
Defaults(Vec<(String, ConfigValue)>),
}
pub type TextEnum = crate::defaults::StrEnum<'static>;
pub enum ConfigValue {
Flag(bool),
Text(Option<Box<str>>),
Num(i128),
List(Mode, Vec<String>),
Enum(TextEnum),
}
pub enum Mode {
Add,
Set,
Del,
}
pub enum Sudo {
Spec(PermissionSpec),
Decl(Directive),
Include(String),
IncludeDir(String),
LineComment,
}
impl Parse for Identifier {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
if accept_if(|c| c == '#', stream).is_ok() {
let Digits(guid) = expect_nonterminal(stream)?;
make(Identifier::ID(guid))
} else {
let Username(name) = try_nonterminal(stream)?;
make(Identifier::Name(name))
}
}
}
impl Many for Identifier {}
impl<T: Parse + UserFriendly> Parse for Qualified<T> {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
if is_syntax('!', stream)? {
let mut neg = true;
while is_syntax('!', stream)? {
neg = !neg;
}
let ident = expect_nonterminal(stream)?;
if neg {
make(Qualified::Forbid(ident))
} else {
make(Qualified::Allow(ident))
}
} else {
let ident = try_nonterminal(stream)?;
make(Qualified::Allow(ident))
}
}
}
impl<T: Many> Many for Qualified<T> {
const SEP: char = T::SEP;
const LIMIT: usize = T::LIMIT;
}
fn parse_meta<T: Parse>(
stream: &mut impl CharStream,
embed: impl FnOnce(String) -> T,
) -> Parsed<Meta<T>> {
if let Some(meta) = try_nonterminal(stream)? {
make(match meta {
Meta::All => Meta::All,
Meta::Alias(alias) => Meta::Alias(alias),
Meta::Only(Username(name)) => Meta::Only(embed(name)),
})
} else {
make(Meta::Only(T::parse(stream)?))
}
}
impl Parse for Meta<Identifier> {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
parse_meta(stream, Identifier::Name)
}
}
impl Parse for UserSpecifier {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let userspec = if accept_if(|c| c == '%', stream).is_ok() {
let ctor = if accept_if(|c| c == ':', stream).is_ok() {
UserSpecifier::NonunixGroup
} else {
UserSpecifier::Group
};
ctor(expect_nonterminal(stream)?)
} else if accept_if(|c| c == '+', stream).is_ok() {
unrecoverable!(stream, "netgroups are not supported yet");
} else {
UserSpecifier::User(try_nonterminal(stream)?)
};
make(userspec)
}
}
impl Many for UserSpecifier {}
impl Parse for Meta<UserSpecifier> {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
parse_meta(stream, |name| UserSpecifier::User(Identifier::Name(name)))
}
}
impl Parse for RunAs {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
try_syntax('(', stream)?;
let users = try_nonterminal(stream).unwrap_or_default();
let groups = maybe(try_syntax(':', stream).and_then(|_| try_nonterminal(stream)))?
.unwrap_or_default();
expect_syntax(')', stream)?;
make(RunAs { users, groups })
}
}
struct MetaOrTag(Meta<Modifier>);
pub type Modifier = Box<dyn Fn(&mut Tag)>;
impl Parse for MetaOrTag {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
use Meta::*;
let AliasName(keyword) = try_nonterminal(stream)?;
let mut switch = |modifier: fn(&mut Tag)| {
expect_syntax(':', stream)?;
make(Box::new(modifier))
};
let result: Modifier = match keyword.as_str() {
"PASSWD" => switch(|tag| tag.passwd = true)?,
"NOPASSWD" => switch(|tag| tag.passwd = false)?,
"CWD" => {
expect_syntax('=', stream)?;
let path: ChDir = expect_nonterminal(stream)?;
Box::new(move |tag| tag.cwd = Some(path.clone()))
}
"ALL" => return make(MetaOrTag(All)),
alias => return make(MetaOrTag(Alias(alias.to_string()))),
};
make(MetaOrTag(Only(result)))
}
}
impl Parse for CommandSpec {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let mut tags = vec![];
while let Some(MetaOrTag(keyword)) = try_nonterminal(stream)? {
use Qualified::Allow;
match keyword {
Meta::Only(modifier) => tags.push(modifier),
Meta::All => return make(CommandSpec(tags, Allow(Meta::All))),
Meta::Alias(name) => return make(CommandSpec(tags, Allow(Meta::Alias(name)))),
}
if tags.len() > Identifier::LIMIT {
unrecoverable!(stream, "too many tags for command specifier")
}
}
let start_pos = stream.get_pos();
if let Some(Username(keyword)) = try_nonterminal(stream)? {
if keyword == "sudoedit" {
unrecoverable!(pos = start_pos, stream, "sudoedit is not yet supported");
} else if keyword.starts_with("sha") {
unrecoverable!(
pos = start_pos,
stream,
"digest specifications are not supported"
)
} else {
unrecoverable!(
pos = start_pos,
stream,
"expected command but found {keyword}"
)
};
}
let cmd: Spec<Command> = expect_nonterminal(stream)?;
make(CommandSpec(tags, cmd))
}
}
impl Parse for (SpecList<Hostname>, Vec<(Option<RunAs>, CommandSpec)>) {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let hosts = try_nonterminal(stream)?;
expect_syntax('=', stream)?;
let runas_cmds = expect_nonterminal(stream)?;
make((hosts, runas_cmds))
}
}
impl Many for (SpecList<Hostname>, Vec<(Option<RunAs>, CommandSpec)>) {
const SEP: char = ':';
}
impl Parse for (Option<RunAs>, CommandSpec) {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let runas: Option<RunAs> = try_nonterminal(stream)?;
let cmd = if runas.is_some() {
expect_nonterminal(stream)?
} else {
try_nonterminal(stream)?
};
make((runas, cmd))
}
}
impl Many for (Option<RunAs>, CommandSpec) {}
impl Parse for Sudo {
fn parse(stream: &mut impl CharStream) -> Parsed<Sudo> {
if accept_if(|c| c == '@', stream).is_ok() {
return parse_include(stream);
}
if stream.peek() == Some('#') {
return if let Ok(ident) = try_nonterminal::<Identifier>(stream) {
let first_user = Qualified::Allow(Meta::Only(UserSpecifier::User(ident)));
let users = if is_syntax(',', stream)? {
let mut rest = expect_nonterminal::<SpecList<_>>(stream)?;
rest.insert(0, first_user);
rest
} else {
vec![first_user]
};
let permissions = expect_nonterminal(stream)?;
make(Sudo::Spec(PermissionSpec { users, permissions }))
} else {
parse_include(stream).or_else(|_| {
while accept_if(|c| c != '\n', stream).is_ok() {}
make(Sudo::LineComment)
})
};
}
let start_pos = stream.get_pos();
if let Some(users) = maybe(try_nonterminal::<SpecList<_>>(stream))? {
let key = &users[0];
if let Some(directive) = maybe(get_directive(key, stream))? {
if users.len() != 1 {
unrecoverable!(pos = start_pos, stream, "invalid user name list");
}
make(Sudo::Decl(directive))
} else {
let permissions = expect_nonterminal(stream)?;
make(Sudo::Spec(PermissionSpec { users, permissions }))
}
} else {
make(Sudo::LineComment)
}
}
}
fn parse_include(stream: &mut impl CharStream) -> Parsed<Sudo> {
fn get_path(stream: &mut impl CharStream) -> Parsed<String> {
if accept_if(|c| c == '"', stream).is_ok() {
let QuotedText(path) = expect_nonterminal(stream)?;
expect_syntax('"', stream)?;
make(path)
} else {
let value_pos = stream.get_pos();
let IncludePath(path) = expect_nonterminal(stream)?;
if stream.peek() != Some('\n') {
unrecoverable!(
pos = value_pos,
stream,
"use quotes around filenames or escape whitespace"
)
}
make(path)
}
}
let key_pos = stream.get_pos();
let result = match try_nonterminal(stream)? {
Some(Username(key)) if key == "include" => Sudo::Include(get_path(stream)?),
Some(Username(key)) if key == "includedir" => Sudo::IncludeDir(get_path(stream)?),
_ => unrecoverable!(pos = key_pos, stream, "unknown directive"),
};
make(result)
}
use crate::defaults::sudo_default;
use crate::defaults::SudoDefault as Setting;
impl<T> Parse for Def<T>
where
T: UserFriendly,
Meta<T>: Parse + Many,
{
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let begin_pos = stream.get_pos();
let AliasName(name) = try_nonterminal(stream)?;
if name == "ALL" {
unrecoverable!(
pos = begin_pos,
stream,
"the reserved alias ALL cannot be redefined"
);
}
expect_syntax('=', stream)?;
make(Def(name, expect_nonterminal(stream)?))
}
}
impl<T> Many for Def<T> {
const SEP: char = ':';
}
fn get_directive(
perhaps_keyword: &Spec<UserSpecifier>,
stream: &mut impl CharStream,
) -> Parsed<Directive> {
use super::ast::Directive::*;
use super::ast::Meta::*;
use super::ast::Qualified::*;
use super::ast::UserSpecifier::*;
let Allow(Only(User(Identifier::Name(keyword)))) = perhaps_keyword else { return reject() };
match keyword.as_str() {
"User_Alias" => make(UserAlias(expect_nonterminal(stream)?)),
"Host_Alias" => make(HostAlias(expect_nonterminal(stream)?)),
"Cmnd_Alias" | "Cmd_Alias" => make(CmndAlias(expect_nonterminal(stream)?)),
"Runas_Alias" => make(RunasAlias(expect_nonterminal(stream)?)),
"Defaults" => make(Defaults(expect_nonterminal(stream)?)),
_ => reject(),
}
}
impl Parse for (String, ConfigValue) {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let id_pos = stream.get_pos();
let parse_vars = |stream: &mut _| -> Parsed<Vec<String>> {
if accept_if(|c| c == '"', stream).is_ok() {
let mut result = Vec::new();
while let Some(EnvVar(name)) = try_nonterminal(stream)? {
result.push(name);
if is_syntax('=', stream)? {
let EnvVar(_) = expect_nonterminal(stream)?;
unrecoverable!(stream, "values in environment variables not yet supported")
}
if result.len() > Identifier::LIMIT {
unrecoverable!(stream, "environment variable list too long")
}
}
expect_syntax('"', stream)?;
if result.is_empty() {
unrecoverable!(stream, "empty string not allowed");
}
make(result)
} else {
let EnvVar(name) = expect_nonterminal(stream)?;
make(vec![name])
}
};
let list_items = |mode: Mode, name: String, cfg: Setting, stream: &mut _| {
expect_syntax('=', stream)?;
if !matches!(cfg, Setting::List(_)) {
unrecoverable!(pos = id_pos, stream, "{name} is not a list parameter");
}
make((name, ConfigValue::List(mode, parse_vars(stream)?)))
};
let text_item = |stream: &mut _| {
if accept_if(|c| c == '"', stream).is_ok() {
let QuotedText(text) = expect_nonterminal(stream)?;
expect_syntax('"', stream)?;
make(text)
} else {
let StringParameter(name) = expect_nonterminal(stream)?;
make(name)
}
};
use crate::defaults::OptTuple;
if is_syntax('!', stream)? {
let value_pos = stream.get_pos();
let DefaultName(name) = expect_nonterminal(stream)?;
let value = match sudo_default(&name) {
Some(Setting::Flag(_)) => ConfigValue::Flag(false),
Some(Setting::List(_)) => ConfigValue::List(Mode::Set, vec![]),
Some(Setting::Text(OptTuple {
negated: Some(val), ..
})) => ConfigValue::Text(val.map(|x| x.into())),
Some(Setting::Enum(OptTuple {
negated: Some(val), ..
})) => ConfigValue::Enum(val),
Some(Setting::Integer(
OptTuple {
negated: Some(val), ..
},
_checker,
)) => ConfigValue::Num(val),
None => unrecoverable!(pos = value_pos, stream, "unknown setting: '{name}'"),
_ => unrecoverable!(
pos = value_pos,
stream,
"'{name}' cannot be used in a boolean context"
),
};
make((name, value))
} else {
let DefaultName(name) = try_nonterminal(stream)?;
let Some(cfg) = sudo_default(&name) else {
unrecoverable!(pos = id_pos, stream, "unknown setting: '{name}'");
};
if is_syntax('+', stream)? {
list_items(Mode::Add, name, cfg, stream)
} else if is_syntax('-', stream)? {
list_items(Mode::Del, name, cfg, stream)
} else if is_syntax('=', stream)? {
let value_pos = stream.get_pos();
match cfg {
Setting::Flag(_) => {
unrecoverable!(stream, "can't assign to boolean setting '{name}'")
}
Setting::Integer(_, checker) => {
let Numeric(denotation) = expect_nonterminal(stream)?;
if let Some(value) = checker(&denotation) {
make((name, ConfigValue::Num(value)))
} else {
unrecoverable!(
pos = value_pos,
stream,
"'{denotation}' is not a valid value for {name}"
);
}
}
Setting::List(_) => {
let items = parse_vars(stream)?;
make((name, ConfigValue::List(Mode::Set, items)))
}
Setting::Text(_) => {
let text = text_item(stream)?;
make((name, ConfigValue::Text(Some(text.into_boxed_str()))))
}
Setting::Enum(OptTuple { default: key, .. }) => {
let text = text_item(stream)?;
let Some(value) = key.alt(&text) else {
unrecoverable!(
pos = value_pos,
stream,
"'{text}' is not a valid value for {name}"
);
};
make((name, ConfigValue::Enum(value)))
}
}
} else {
if !matches!(cfg, Setting::Flag(_)) {
unrecoverable!(pos = id_pos, stream, "'{name}' is not a boolean setting");
}
make((name, ConfigValue::Flag(true)))
}
}
}
}
impl Many for (String, ConfigValue) {}