Repository: memorysafety/sudo-rs
Pull request #37 (2023-02-08 11:11:41)

Higher level interface for sudoers parsing
Closing #15 

Right now sudoers::compile() just takes a filename as argument and returns an opaque Sudoers object and a list of syntax errors (the latter maybe can be handled differently later).

Squash and merge only, and merge #34 first.


Patch:
diff --git a/lib/sudoers/src/ast.rs b/lib/sudoers/src/ast.rs
index f494bd5a..47d6ef10 100644
--- a/lib/sudoers/src/ast.rs
+++ b/lib/sudoers/src/ast.rs
@@ -65,6 +65,7 @@ pub enum Directive {
 
 /// The Sudoers file can contain permissions and directives
 // TODO: Defaults
+#[derive(Debug)]
 pub enum Sudo {
     Spec(PermissionSpec),
     Decl(Directive),
diff --git a/lib/sudoers/src/basic_parser.rs b/lib/sudoers/src/basic_parser.rs
index d3b3584a..c0f0923d 100644
--- a/lib/sudoers/src/basic_parser.rs
+++ b/lib/sudoers/src/basic_parser.rs
@@ -31,6 +31,7 @@ use std::iter::Peekable;
 pub type Parsed<T> = Result<T, Status>;
 
 #[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
 pub enum Status {
     Fatal(String), // not recoverable; stream in inconsistent state
     Reject,        // parsing failed by no input consumed
@@ -101,44 +102,75 @@ pub fn accept_if(
     }
 }
 
+/// Structures representing whitespace (trailing whitespace can contain comments)
 #[derive(Debug)]
 #[cfg_attr(test, derive(PartialEq, Eq))]
+struct LeadingWhitespace;
 
-/// A structure representing whitespace
-struct Whitespace;
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq, Eq))]
+struct TrailingWhitespace;
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq, Eq))]
+struct Comment;
 
-/// Accept one or more whitespace characters; fails if no whitespace is found (to parse zero or
-/// more whitespace characters, parse `Option<Whitespace>`
-impl Parse for Whitespace {
+/// Accept zero or more whitespace characters; fails if the whitespace is not "leading" to something
+/// (which can be used to detect end-of-input).
+impl Parse for LeadingWhitespace {
     fn parse(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<Self> {
         let eat_space = |stream: &mut _| accept_if(|c| "\t ".contains(c), stream);
+        while eat_space(stream).is_ok() {}
 
-        let comment = |stream: &mut _| {
-            accept_if(|c| c == '#', stream)?;
-            while accept_if(|c| c != '\n', stream).is_ok() {}
-            make(())
-        };
-
-        if eat_space(stream).is_ok() {
-            while eat_space(stream).is_ok() {}
-            maybe(comment(stream))?;
+        if stream.peek().is_some() {
+            make(LeadingWhitespace {})
         } else {
-            comment(stream)?;
+            unrecoverable!("superfluous whitespace")
         }
+    }
+}
 
-        make(Whitespace {})
+/// Accept zero or more whitespace characters; since this accepts zero characters, it
+/// always succeeds (unless some serious error occurs). This parser also accepts comments,
+/// since those can form part of trailing white space.
+impl Parse for TrailingWhitespace {
+    fn parse(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<Self> {
+        loop {
+            let _ = LeadingWhitespace::parse(stream); // don't propagate any errors
+
+            // line continuations
+            if accept_if(|c| c == '\\', stream).is_ok() {
+                // do the equivalent of expect_syntax('\n', stream)?, without recursion
+                if accept_if(|c| c == '\n', stream).is_err() {
+                    unrecoverable!("stray escape sequence")
+                }
+            } else {
+                break;
+            }
+        }
+
+        make(TrailingWhitespace {})
     }
 }
 
-pub fn skip_whitespace(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<()> {
-    maybe(Whitespace::parse(stream))?;
+/// Parses a comment
+impl Parse for Comment {
+    fn parse(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<Self> {
+        accept_if(|c| c == '#', stream)?;
+        while accept_if(|c| c != '\n', stream).is_ok() {}
+        make(Comment {})
+    }
+}
+
+fn skip_trailing_whitespace(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<()> {
+    TrailingWhitespace::parse(stream)?;
     make(())
 }
 
 /// Adheres to the contract of the [Parse] trait, accepts one character and consumes trailing whitespace.
 pub fn try_syntax(syntax: char, stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<()> {
     accept_if(|c| c == syntax, stream)?;
-    skip_whitespace(stream)?;
+    skip_trailing_whitespace(stream)?;
     make(())
 }
 
@@ -151,7 +183,7 @@ pub fn expect_syntax(
         let str = if let Some(c) = stream.peek() {
             c.to_string()
         } else {
-            "EOL".to_string()
+            "EOF".to_string()
         };
         unrecoverable!("parse error: expecting `{syntax}' but found `{str}'")
     }
@@ -162,7 +194,7 @@ pub fn expect_syntax(
 /// type inference. Use this instead of calling [Parse::parse] directly.
 pub fn try_nonterminal<T: Parse>(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<T> {
     let result = T::parse(stream)?;
-    skip_whitespace(stream)?;
+    skip_trailing_whitespace(stream)?;
     make(result)
 }
 
@@ -222,15 +254,10 @@ impl<T: Token> Parse for T {
     }
 }
 
-/// Example parser for something that has two alternatives (don't use)
-impl<T1: Token, T2: Parse> Parse for Result<T1, T2> {
+/// Parser for Option<T> (this can be used to make the code more readable)
+impl<T: Parse> Parse for Option<T> {
     fn parse(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<Self> {
-        let &c = stream.peek().ok_or(Status::Reject)?;
-        if T1::accept(c) {
-            T1::parse(stream).map(Ok)
-        } else {
-            T2::parse(stream).map(Err)
-        }
+        maybe(T::parse(stream))
     }
 }
 
@@ -267,21 +294,45 @@ impl<T: Parse + Many> Parse for Vec<T> {
     }
 }
 
-fn expect_end_of_parse(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<()> {
-    if stream.peek().is_some() {
-        unrecoverable!("parse error: trailing garbage")
+/// Entry point utility function; parse a Vec<T> but with fatal error recovery per line
+pub fn parse_lines<T: Parse>(stream: &mut Peekable<impl Iterator<Item = char>>) -> Vec<Parsed<T>> {
+    let mut result = Vec::new();
+
+    // this will terminate; if the inner accept_if is an error, either a character will be consumed
+    // by the second accept_if (making progress), or the end of the stream will have been reacherd
+    // (which will cause the next iteration to fall through)
+
+    while LeadingWhitespace::parse(stream).is_ok() {
+        result.push(expect_nonterminal(stream));
+        let _ = maybe(Comment::parse(stream));
+        if accept_if(|c| c == '\n', stream).is_err() {
+            result.push(Err(Status::Fatal(
+                if stream.peek().is_none() {
+                    "parse error: missing line terminator at end of file"
+                } else {
+                    "parse error: garbage at end of line"
+                }
+                .to_string(),
+            )));
+            while accept_if(|c| c != '\n', stream).is_ok() {}
+        }
     }
-    make(())
+
+    result
 }
 
-pub fn expect_complete<T: Parse>(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<T> {
+#[cfg(test)]
+fn expect_complete<T: Parse>(stream: &mut Peekable<impl Iterator<Item = char>>) -> Parsed<T> {
     let result = expect_nonterminal(stream)?;
-    expect_end_of_parse(stream)?;
+    if let Some(c) = stream.peek() {
+        unrecoverable!("parse error: garbage at end of line: {c}")
+    }
     make(result)
 }
 
 /// Convenience function (especially useful for writing test cases, to avoid having to write out the
 /// AST constructors by hand.
+#[cfg(test)]
 pub fn parse_string<T: Parse>(text: &str) -> Parsed<T> {
     expect_complete(&mut text.chars().peekable())
 }
@@ -295,13 +346,63 @@ pub fn parse_eval<T: Parse>(text: &str) -> T {
 mod test {
     use super::*;
 
+    impl Token for String {
+        fn construct(val: String) -> Parsed<Self> {
+            make(val)
+        }
+
+        fn accept(c: char) -> bool {
+            c.is_ascii_alphanumeric()
+        }
+    }
+
+    impl Many for String {}
+
     #[test]
     fn comment_test() {
-        assert_eq!(parse_eval::<Whitespace>(" # hello"), Whitespace);
+        assert_eq!(parse_eval::<Comment>("# hello"), Comment);
     }
     #[test]
     #[should_panic]
     fn comment_test_fail() {
-        assert_eq!(parse_eval::<Whitespace>(" # hello\nsomething"), Whitespace);
+        assert_eq!(parse_eval::<Comment>("# hello\nsomething"), Comment);
+    }
+
+    #[test]
+    fn lines_test() {
+        let input = |text: &str| parse_lines(&mut text.chars().peekable());
+
+        let s = |text: &str| Ok(text.to_string());
+        assert_eq!(input("hello\nworld\n"), vec![s("hello"), s("world")]);
+        assert_eq!(input("   hello\nworld\n"), vec![s("hello"), s("world")]);
+        assert_eq!(input("hello  \nworld\n"), vec![s("hello"), s("world")]);
+        assert_eq!(input("hello\n   world\n"), vec![s("hello"), s("world")]);
+        assert_eq!(input("hello\nworld  \n"), vec![s("hello"), s("world")]);
+        assert_eq!(input("hello\nworld")[0..2], vec![s("hello"), s("world")]);
+        let Err(_) = input("hello\nworld")[2] else { panic!() };
+        let Err(_) = input("hello\nworld:\n")[2] else { panic!() };
+    }
+    #[test]
+    fn whitespace_test() {
+        assert_eq!(
+            parse_eval::<Vec<String>>("hello,something"),
+            vec!["hello", "something"]
+        );
+        assert_eq!(
+            parse_eval::<Vec<String>>("hello , something"),
+            vec!["hello", "something"]
+        );
+        assert_eq!(
+            parse_eval::<Vec<String>>("hello, something"),
+            vec!["hello", "something"]
+        );
+        assert_eq!(
+            parse_eval::<Vec<String>>("hello ,something"),
+            vec!["hello", "something"]
+        );
+        assert_eq!(
+            parse_eval::<Vec<String>>("hello\\\n,something"),
+            vec!["hello", "something"]
+        );
     }
 }
diff --git a/lib/sudoers/src/lib.rs b/lib/sudoers/src/lib.rs
index ed0bc7bc..f3343c11 100644
--- a/lib/sudoers/src/lib.rs
+++ b/lib/sudoers/src/lib.rs
@@ -7,26 +7,54 @@ mod basic_parser;
 mod tokens;
 
 use std::collections::HashSet;
+use std::path::Path;
 
 use ast::*;
 use tokens::*;
 
 /// Export some necessary symbols from modules
-pub use ast::Sudo;
 pub use ast::Tag;
-pub use basic_parser::parse_string;
+pub type Error = basic_parser::Status;
 
 mod sysuser;
 pub use sysuser::*;
 
+pub struct Sudoers {
+    rules: Vec<PermissionSpec>,
+    aliases: AliasTable,
+}
+
 pub struct Request<'a, User: Identifiable, Group: UnixGroup> {
     pub user: &'a User,
     pub group: &'a Group,
 }
 
-// TODO: combine this with Vec<PermissionSpec> into a single data structure?
+/// This function takes a file argument for a sudoers file and processes it.
+
+pub fn compile(path: impl AsRef<Path>) -> Result<(Sudoers, Vec<Error>), std::io::Error> {
+    let sudoers: Vec<basic_parser::Parsed<Sudo>> = {
+        use std::fs::File;
+        use std::io::Read;
+        let mut source = std::io::BufReader::new(File::open(path)?);
+
+        // it's a bit frustrating that BufReader.chars() does not exist
+        let mut buffer = String::new();
+        source.read_to_string(&mut buffer)?;
+
+        basic_parser::parse_lines(&mut buffer.chars().peekable())
+    };
+
+    let diagnostics = sudoers
+        .iter()
+        .filter_map(|line| line.as_ref().err().cloned())
+        .collect();
+
+    let (rules, aliases) = analyze(sudoers.into_iter().flatten());
+    Ok((Sudoers { rules, aliases }, diagnostics))
+}
+
 #[derive(Default)]
-pub struct AliasTable {
+pub(crate) struct AliasTable {
     user: VecOrd<Def<UserSpecifier>>,
     host: VecOrd<Def<Hostname>>,
     cmnd: VecOrd<Def<Command>>,
@@ -48,22 +76,21 @@ fn elems<T>(vec: &VecOrd<T>) -> impl Iterator<Item = &T> {
 
 // This code is structure to allow easily reading the 'happy path'; i.e. as soon as something
 // doesn't match, we escape using the '?' mechanism.
-pub fn check_permission<'a, User: Identifiable + PartialEq<User>, Group: UnixGroup>(
-    sudoers: impl IntoIterator<Item = &'a PermissionSpec>,
-    alias_table: &AliasTable,
+pub fn check_permission<User: Identifiable + PartialEq<User>, Group: UnixGroup>(
+    Sudoers { rules, aliases }: &Sudoers,
     am_user: &User,
     request: Request<User, Group>,
     on_host: &str,
     cmdline: &str,
 ) -> Option<Vec<Tag>> {
-    let user_aliases = get_aliases(&alias_table.user, &match_user(am_user));
-    let host_aliases = get_aliases(&alias_table.host, &match_token(on_host));
-    let cmnd_aliases = get_aliases(&alias_table.cmnd, &match_command(cmdline));
-    let runas_user_aliases = get_aliases(&alias_table.runas, &match_user(request.user));
-    let runas_group_aliases = get_aliases(&alias_table.runas, &match_group_alias(request.group));
-
-    let allowed_commands = sudoers
-        .into_iter()
+    let user_aliases = get_aliases(&aliases.user, &match_user(am_user));
+    let host_aliases = get_aliases(&aliases.host, &match_token(on_host));
+    let cmnd_aliases = get_aliases(&aliases.cmnd, &match_command(cmdline));
+    let runas_user_aliases = get_aliases(&aliases.runas, &match_user(request.user));
+    let runas_group_aliases = get_aliases(&aliases.runas, &match_group_alias(request.group));
+
+    let allowed_commands = rules
+        .iter()
         .filter_map(|sudo| {
             find_item(&sudo.users, &match_user(am_user), &user_aliases)?;
 
@@ -205,7 +232,7 @@ fn match_identifier(user: &impl Identifiable, ident: &ast::Identifier) -> bool {
 
 /// Process a sudoers-parsing file into a workable AST
 
-pub fn analyze(sudoers: impl IntoIterator<Item = Sudo>) -> (Vec<PermissionSpec>, AliasTable) {
+fn analyze(sudoers: impl IntoIterator<Item = Sudo>) -> (Vec<PermissionSpec>, AliasTable) {
     use Directive::*;
     let mut permits = Vec::new();
     let mut alias: AliasTable = Default::default();
@@ -286,17 +313,24 @@ fn sanitize_alias_table<T>(table: &Vec<Def<T>>) -> Vec<usize> {
 mod test {
     use super::*;
     use crate::ast;
-    use basic_parser::parse_eval;
+    use basic_parser::{parse_eval, parse_lines, parse_string};
     use std::iter;
 
     macro_rules! sudoer {
         ($h:expr $(,$e:expr)*) => {
-            (
-                iter::once($h)
-                $(
-                    .chain(iter::once($e))
-                )*
-            ).map(parse_eval)
+	    parse_lines(&mut
+		(
+		    iter::once($h)
+		    $(
+			.chain(iter::once($e))
+		    )*
+		)
+		.map(|s|s.chars().chain(iter::once('\n')))
+		.flatten()
+		.peekable()
+	    )
+	    .into_iter()
+	    .map(|x| x.unwrap())
         }
     }
 
@@ -310,27 +344,10 @@ mod test {
     }
 
     #[test]
-    #[should_panic]
-    fn invalid_spec() {
-        parse_eval::<ast::Sudo>("ALL ALL = (;) ALL");
-    }
-
-    #[test]
-    fn ambiguous_spec1() {
+    fn ambiguous_spec() {
         let Sudo::Spec(_) = parse_eval::<ast::Sudo>("marc, User_Alias ALL = ALL") else { todo!() };
     }
 
-    #[test]
-    fn ambiguous_spec2() {
-        let Sudo::Decl(_) = parse_eval::<ast::Sudo>("User_Alias ALIAS = ALL") else { todo!() };
-    }
-
-    #[test]
-    #[should_panic]
-    fn ambiguous_spec3() {
-        parse_eval::<ast::Sudo>("User_Alias, marc ALL = ALL");
-    }
-
     #[test]
     fn permission_test() {
         let root = || Request::<&str, _> {
@@ -340,21 +357,28 @@ mod test {
 
         macro_rules! FAIL {
             ([$($sudo:expr),*], $user:expr => $req:expr, $server:expr; $command:expr) => {
-                let (input,alias) = analyze(sudoer![$($sudo),*]);
-                assert_eq!(check_permission(&input, &alias, &$user, $req, $server, $command), None);
+                let (rules,aliases) = analyze(sudoer![$($sudo),*]);
+                assert_eq!(check_permission(&Sudoers { rules, aliases }, &$user, $req, $server, $command), None);
             }
         }
 
         macro_rules! pass {
             ([$($sudo:expr),*], $user:expr => $req:expr, $server:expr; $command:expr $(=> [$($list:expr),*])?) => {
-                let (input,alias) = analyze(sudoer![$($sudo),*]);
-                let result = check_permission(&input, &alias, &$user, $req, $server, $command);
+                let (rules,aliases) = analyze(sudoer![$($sudo),*]);
+                let result = check_permission(&Sudoers { rules, aliases}, &$user, $req, $server, $command);
                 $(assert_eq!(result, Some(vec![$($list),*]));)?
                 assert!(!result.is_none());
             }
         }
+        macro_rules! SYNTAX {
+            ([$sudo:expr]) => {
+                assert!(parse_string::<Sudo>($sudo).is_err())
+            };
+        }
+
         use crate::ast::Tag::*;
 
+        SYNTAX!(["ALL ALL = (;) ALL"]);
         FAIL!(["user ALL=(ALL:ALL) ALL"], "nobody"    => root(), "server"; "/bin/hello");
         pass!(["user ALL=(ALL:ALL) ALL"], "user"      => root(), "server"; "/bin/hello");
         pass!(["user ALL=(ALL:ALL) /bin/foo"], "user" => root(), "server"; "/bin/foo");
@@ -362,6 +386,9 @@ mod test {
         pass!(["user ALL=(ALL:ALL) /bin/foo, NOPASSWD: /bin/bar"], "user" => root(), "server"; "/bin/foo");
         pass!(["user ALL=(ALL:ALL) /bin/foo, NOPASSWD: /bin/bar"], "user" => root(), "server"; "/bin/bar" => [NoPasswd]);
 
+        pass!(["user ALL=/bin/e##o"], "user" => root(), "vm"; "/bin/e");
+        SYNTAX!(["ALL ALL=(ALL) /bin/\n/echo"]);
+
         pass!(["user server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
         FAIL!(["user laptop=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
 
@@ -393,6 +420,8 @@ mod test {
         pass!(["user ALL=/bin/hel* me"], "user" => root(), "server"; "/bin/help me");
         FAIL!(["user ALL=/bin/hel* me"], "user" => root(), "server"; "/bin/help me please");
 
+        SYNTAX!(["User_Alias, marc ALL = ALL"]);
+
         pass!(["User_Alias FULLTIME=ALL,!marc","FULLTIME ALL=ALL"], "user" => root(), "server"; "/bin/bash");
         FAIL!(["User_Alias FULLTIME=ALL,!marc","FULLTIME ALL=ALL"], "marc" => root(), "server"; "/bin/bash");
         FAIL!(["User_Alias FULLTIME=ALL,!marc","ALL,!FULLTIME ALL=ALL"], "user" => root(), "server"; "/bin/bash");
@@ -413,6 +442,8 @@ mod test {
         FAIL!(["Runas_Alias TIME=%wheel,sudo","user ALL=(:TIME) ALL"], "user" => Request{ user: &"sudo", group: &(42,"sudo") }, "vm"; "/bin/ls");
         pass!(["Runas_Alias TIME=%wheel,sudo","user ALL=(:TIME) ALL"], "user" => Request{ user: &"user", group: &(42,"sudo") }, "vm"; "/bin/ls");
         pass!(["Runas_Alias TIME=%wheel,sudo","user ALL=(TIME) ALL"], "user" => Request{ user: &"wheel", group: &(37,"wheel") }, "vm"; "/bin/ls");
+
+        pass!(["Runas_Alias \\"," TIME=%wheel\\",",sudo # hallo","user ALL\\","=(TIME) ALL"], "user" => Request{ user: &"wheel", group: &(37,"wheel") }, "vm"; "/bin/ls");
     }
 
     #[test]
diff --git a/lib/sudoers/src/tokens.rs b/lib/sudoers/src/tokens.rs
index ff13eeb0..be0c1de7 100644
--- a/lib/sudoers/src/tokens.rs
+++ b/lib/sudoers/src/tokens.rs
@@ -176,12 +176,12 @@ impl Token for Command {
     }
 
     fn accept(c: char) -> bool {
-        !Self::escaped(c)
+        !Self::escaped(c) && !c.is_control()
     }
 
     const ESCAPE: char = '\\';
     fn escaped(c: char) -> bool {
-        "\\,:=".contains(c)
+        "\\,:=#".contains(c)
     }
 }
 
diff --git a/sudo/src/main.rs b/sudo/src/main.rs
index e1daf82c..836889c5 100644
--- a/sudo/src/main.rs
+++ b/sudo/src/main.rs
@@ -1,8 +1,3 @@
-use std::{
-    fs::File,
-    io::{self, BufRead},
-};
-
 use sudo_cli::SudoOptions;
 use sudo_common::{
     context::{CommandAndArguments, Context},
@@ -58,23 +53,15 @@ fn check_sudoers(context: &Context, sudo_options: &SudoOptions) -> Result<Option
     // TODO: move to global configuration
     let sudoers_path = "/etc/sudoers.test";
 
-    let file = File::open(sudoers_path)
+    let (sudoers, syntax_errors) = sudoers::compile(sudoers_path)
         .map_err(|e| Error::Configuration(format!("no sudoers file {e}")))?;
 
-    let sudoers_lines = io::BufReader::new(file).lines().map(|x| x.unwrap());
-    let parsed_file = sudoers_lines.filter_map(|text| match sudoers::parse_string(&text) {
-        Ok(x) => Some(x),
-        Err(error) => {
-            eprintln!("Parse error: {error:?}");
-            None
-        }
-    });
-
-    let (input, aliases) = sudoers::analyze(parsed_file);
+    for error in syntax_errors {
+        eprintln!("Parse error: {error:?}");
+    }
 
     Ok(sudoers::check_permission(
-        &input,
-        &aliases,
+        &sudoers,
         &context.current_user,
         sudoers::Request {
             user: &context.target_user,


Test patch:
diff --git a/test-binaries/checkpermit.rs b/test-binaries/checkpermit.rs
index 3faa243a..7a038599 100644
--- a/test-binaries/checkpermit.rs
+++ b/test-binaries/checkpermit.rs
@@ -1,16 +1,5 @@
-// TODO: this should give parse error messages etc.
-fn sudoers_parse(lines: impl Iterator<Item = String>) -> impl Iterator<Item = sudoers::Sudo> {
-    lines.filter_map(|text| match sudoers::parse_string(&text) {
-        Ok(x) => Some(x),
-        Err(error) => {
-            eprintln!("PARSE ERROR: {error:?}");
-            None
-        }
-    })
-}
-
 fn chatty_check_permission(
-    sudoers: impl Iterator<Item = String>,
+    sudoers: sudoers::Sudoers,
     am_user: &str,
     request: sudoers::Request<&str, sudoers::GroupID>,
     on_host: &str,
@@ -20,21 +9,19 @@ fn chatty_check_permission(
         "Is '{}' allowed on '{}' to run: '{}' (as {}:{:?})?",
         am_user, on_host, chosen_poison, request.user, request.group
     );
-    let (input, aliases) = sudoers::analyze(sudoers_parse(sudoers));
-    let result =
-        sudoers::check_permission(&input, &aliases, &am_user, request, on_host, chosen_poison);
+    let result = sudoers::check_permission(&sudoers, &am_user, request, on_host, chosen_poison);
     println!("OUTCOME: {result:?}");
 }
 
 use std::env;
-use std::fs::File;
-use std::io::{self, BufRead};
 
 fn main() {
     use sudoers::GroupID;
     let args: Vec<String> = env::args().collect();
-    if let Ok(file) = File::open("./sudoers") {
-        let cfg = io::BufReader::new(file).lines().map(|x| x.unwrap());
+    if let Ok((cfg, warn)) = sudoers::compile("./sudoers") {
+        for foobar in warn {
+            println!("ERROR: {foobar:?}")
+        }
         println!(
             "{:?}",
             chatty_check_permission(