Repository: trifectatechfoundation/sudo-rs (Rust)

Issue:
Escaping characters in command arguments gives "illegal escape sequence"
**Describe the bug**
ogsudo allows escaping '=' and other characters in command line arguments using '\\'. Currently for sudo-rs, this will throw an error for "illegal escape sequence".

**To Reproduce**
Steps to reproduce the behavior:
1. Create a file called `test` with the following contents:
```shell
Cmd_Alias FOO_CMD = /bin/foo --bar=1
Cmd_Alias BAR_CMD = /bin/foo --bar\=1
```
2. Run visudo on the file:
```shell
visudo -c -f test
```
3. A syntax error is given:
```shell
test:2:36: syntax error: illegal escape sequence
Cmd_Alias BAR_CMD = /bin/foo --bar\=1
                                   ^
visudo: invalid sudoers file
```

**Expected behavior**
The sudoers file `test` should parse correctly, giving a warning for unused command aliases:
```shell
Warning: test:2:11: unused Cmnd_Alias "BAR_CMD"
Warning: test:1:11: unused Cmnd_Alias "FOO_CMD"
test: parsed OK
```

**Environment:**
 - Linux distribution: Ubuntu 25.10
 - `sudo-rs` commit hash: bbe2bf6



Discussion:
I have found a fix for this bug. We may want to discuss its nature, but it currently means adding back '=' to the escaped match for `SimpleCommand`'s implementation of `Token`. 
```rust
    const ALLOW_ESCAPE: bool = true;
    fn escaped(c: char) -> bool {
        matches!(c, '\\' | ',' | ':' | '=' | '#' | ' ')
    }
```

Because '=' is allowed both in its escaped and un-escaped form, this also means adding an explicit check for '=' in the parser. I find this a bit hacky, but it is the most apparent solution I can see right now.
```rust
const ESCAPE: char = '\\';
if let Some(c) = stream.next_if(|c| c == '=') {
    Ok(c)
}
else if T::ALLOW_ESCAPE && stream.eat_char(ESCAPE) {
...
```
It seems that the previously proposed fix introduces other side effects. Currently looking at having a special parsing function only for `SimpleCommand`. This is a use case where the specialization feature would be useful if it weren't unstable.
One fix that passes tests is to add an additional `else if` before giving an unrecoverable error in the `Parse` implementation for `Token` in `src/sudoers/basic_parser.rs`:
```rust
                ...
                } else if let Some(_) = stream.next_if(|c| c == '=') {
                    Ok('=')
                } else {
                    unrecoverable!(stream, "illegal escape sequence")
                }
               ...
```
The placement of this is considerably better than my previous suggestion. In this case, '=' is **not** added back as an escaped character for `SimpleCommand`.
Maybe `SimpleCommand::escaped` could list `=`, but `SimpleCommand::accept` could also accept unescaped `=`? I'm not sure if globally allowing `\=` wouldn't have any unintended side-effects.
That might be a solution! I will investigate. I agree that globally allowing an escape sequence could lead to unpredictable behaviors.
Can confirm that listing `=` in `SimpleCommand::escaped` and `SimpleCommand::accept` solves the problem. Will change my fix to use that instead.



Patch:
diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs
index e6666cb51..f3a148d75 100644
--- a/src/sudoers/tokens.rs
+++ b/src/sudoers/tokens.rs
@@ -259,12 +259,13 @@ impl Token for SimpleCommand {
     }
 
     fn accept(c: char) -> bool {
-        !Self::escaped(c) && !c.is_control()
+        // '=' is allowed both escaped and un-escaped
+        (!Self::escaped(c) && !c.is_control()) || c == '='
     }
 
     const ALLOW_ESCAPE: bool = true;
     fn escaped(c: char) -> bool {
-        matches!(c, '\\' | ',' | ':' | '#' | ' ')
+        matches!(c, '\\' | ',' | ':' | '=' | '#' | ' ')
     }
 }
 


Test patch:
diff --git a/src/sudoers/test/mod.rs b/src/sudoers/test/mod.rs
index 4fe96bc9d..10f9b3792 100644
--- a/src/sudoers/test/mod.rs
+++ b/src/sudoers/test/mod.rs
@@ -470,6 +470,12 @@ fn gh676_percent_h_escape_unsupported() {
     );
 }
 
+#[test]
+fn gh1295_escaped_equal_argument_ok() {
+    assert!(try_parse_line("Cmd_Alias FOO_CMD = /bin/foo --bar=1").is_some());
+    assert!(try_parse_line(r"Cmd_Alias FOO_CMD = /bin/foo --bar\=1").is_some());
+}
+
 #[test]
 fn hashsign_error() {
     assert!(parse_line("#include foo bar").is_line_comment());