11use super :: ast_names:: UserFriendly ;
22use super :: basic_parser:: * ;
3- use super :: char_stream:: advance;
43use super :: tokens:: * ;
54use crate :: common:: SudoString ;
65use crate :: common:: {
@@ -465,6 +464,25 @@ impl Parse for (Option<RunAs>, CommandSpec) {
465464/// "runas" specifier (which has to be placed correctly during the AST analysis phase)
466465impl Many for ( Option < RunAs > , CommandSpec ) { }
467466
467+ /// Directives such as "Defaults" and "HostAlias" also could be parsed as part of a
468+ /// UserSpecifier list. We need to detect to give a parse error in cases where the syntax
469+ /// seems to suggest they are used in that manner.
470+ /// Technically, "Defaults@host" is also a valid username, but the logic can disambiguate
471+ /// those cases clearly. E.g. "Defaults@host1,host2 secure_path=/bin/sh" is technically
472+ /// ambiguous but it can be parsed. Time flies like an arrow, fruit flies like a banana.
473+ impl Directive {
474+ fn could_be_username ( & self ) -> bool {
475+ matches ! (
476+ self ,
477+ Self :: Defaults ( _, ConfigScope :: Generic )
478+ | Self :: UserAlias ( _)
479+ | Self :: HostAlias ( _)
480+ | Self :: CmndAlias ( _)
481+ | Self :: RunasAlias ( _)
482+ )
483+ }
484+ }
485+
468486/// grammar:
469487/// ```text
470488/// sudo = permissionspec
@@ -508,7 +526,7 @@ impl Parse for Sudo {
508526 } ;
509527 }
510528
511- let start_pos = stream. get_pos ( ) ;
529+ let start_state = stream. clone ( ) ;
512530 if stream. peek ( ) == Some ( '"' ) {
513531 // a quoted userlist follows; this forces us to read a userlist
514532 let users = expect_nonterminal ( stream) ?;
@@ -518,9 +536,11 @@ impl Parse for Sudo {
518536 // this could be the start of a Defaults or Alias definition, so distinguish.
519537 // element 1 always exists (parse_list fails on an empty list)
520538 let key = & users[ 0 ] ;
521- if let Some ( directive) = maybe ( get_directive ( key, stream, start_pos) ) ? {
522- if users. len ( ) != 1 {
523- unrecoverable ! ( pos = start_pos, stream, "invalid user name list" ) ;
539+ let start_pos = start_state. get_pos ( ) ;
540+ if let Some ( directive) = maybe ( get_directive ( key, stream, start_state) ) ? {
541+ if users. len ( ) != 1 && directive. could_be_username ( ) {
542+ // this is here to guard against User_Alias,foo ALIAS=def being accepted
543+ unrecoverable ! ( pos = start_pos, stream, "ambiguous syntax" ) ;
524544 }
525545 make ( Sudo :: Decl ( directive) )
526546 } else {
@@ -613,10 +633,10 @@ impl<T> Many for Def<T> {
613633// I.e. after a valid username has been parsed, we check if it isn't actually a valid start of a
614634// directive. A more robust solution would be to use the approach taken by the `MetaOrTag` above.
615635
616- fn get_directive (
636+ fn get_directive < ' a > (
617637 perhaps_keyword : & Spec < UserSpecifier > ,
618- stream : & mut CharStream ,
619- begin_pos : ( usize , usize ) ,
638+ stream : & mut CharStream < ' a > ,
639+ begin_state : CharStream < ' a > ,
620640) -> Parsed < Directive > {
621641 use super :: ast:: Directive :: * ;
622642 use super :: ast:: Meta :: * ;
@@ -626,31 +646,32 @@ fn get_directive(
626646 return reject ( ) ;
627647 } ;
628648
649+ let begin_pos = begin_state. get_pos ( ) ;
650+
629651 match keyword. as_str ( ) {
630652 "User_Alias" => make ( UserAlias ( expect_nonterminal ( stream) ?) ) ,
631653 "Host_Alias" => make ( HostAlias ( expect_nonterminal ( stream) ?) ) ,
632654 "Cmnd_Alias" | "Cmd_Alias" => make ( CmndAlias ( expect_nonterminal ( stream) ?) ) ,
633655 "Runas_Alias" => make ( RunasAlias ( expect_nonterminal ( stream) ?) ) ,
634656 _ if keyword. starts_with ( "Defaults" ) => {
635- //HACK #1: no space is allowed between "Defaults" and '!>@:'. The below avoids having to
636- //add "Defaults!" etc as separate tokens; but relying on positional information during
637- //parsing is of course, cheating.
638- //HACK #2: '@' can be part of a username, so it will already have been parsed;
639- //an acceptable hostname is subset of an acceptable username, so that's actually OK.
640- //This resolves an ambiguity in the grammar similarly to how MetaOrTag does that.
657+ // HACK #1: no space is allowed between "Defaults" and '!>@:'. The below avoids having to
658+ // add "Defaults!" etc as separate tokens; but relying on positional information during
659+ // parsing is of course, cheating.
660+ // HACK #2: '@' can be part of a username, so it will already have been parsed;
661+ // an acceptable hostname is subset of an acceptable username, so that's actually OK.
662+ // This resolves an ambiguity in the grammar similarly to how MetaOrTag does that.
641663 const DEFAULTS_LEN : usize = "Defaults" . len ( ) ;
642664 let allow_scope_modifier = stream. get_pos ( ) . 0 == begin_pos. 0
643665 && ( stream. get_pos ( ) . 1 - begin_pos. 1 == DEFAULTS_LEN
644666 || keyword. len ( ) > DEFAULTS_LEN ) ;
645667
646668 let scope = if allow_scope_modifier {
647669 if keyword[ DEFAULTS_LEN ..] . starts_with ( '@' ) {
648- let inner_stream = & mut CharStream :: new_with_pos (
649- & keyword[ DEFAULTS_LEN + 1 ..] ,
650- advance ( begin_pos, DEFAULTS_LEN + 1 ) ,
651- ) ;
670+ // Backtrack and start parsing following the '@' sign
671+ * stream = begin_state;
672+ stream. advance ( DEFAULTS_LEN + 1 ) ;
652673
653- ConfigScope :: Host ( expect_nonterminal ( inner_stream ) ?)
674+ ConfigScope :: Host ( expect_nonterminal ( stream ) ?)
654675 } else if is_syntax ( ':' , stream) ? {
655676 ConfigScope :: User ( expect_nonterminal ( stream) ?)
656677 } else if is_syntax ( '!' , stream) ? {
0 commit comments