Repository: duesee/imap-codec (language: python)

Issue:
bug/regression: `response` can panic.
```
Running: fuzz/artifacts/response/crash-8891eb2620da6b2c513b8e3e096abb5af24f1fa1
[!] Input: + Nose[CAY a\r\n
thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: Ambiguity', src/imap4rev1/response.rs:216:45
```


Discussion / hints:

Fix patch:
diff --git a/src/imap4rev1/response.rs b/src/imap4rev1/response.rs
index b16114a5..0330cce2 100644
--- a/src/imap4rev1/response.rs
+++ b/src/imap4rev1/response.rs
@@ -205,21 +205,34 @@ pub fn response(input: &[u8]) -> IResult<&[u8], Response> {
 
 /// `continue-req = "+" SP (resp-text / base64) CRLF`
 pub fn continue_req(input: &[u8]) -> IResult<&[u8], Continue> {
+    // We can't map the output of `resp_text` directly to `Continue::basic()` because we might end
+    // up with a subset of `Text` that is valid base64 and will panic on `unwrap()`. Thus, we first
+    // let the parsing finish and only later map to `Continue`.
+
+    // A helper struct to postpone the unification to `Continue` in the `alt` combinator below.
+    enum Either<A, B> {
+        Base64(A),
+        Basic(B),
+    }
+
     let mut parser = tuple((
         tag(b"+ "),
         alt((
             map(
                 map_res(take_until("\r\n"), |input| _base64.decode(input)),
-                Continue::base64,
+                Either::Base64,
             ),
-            map(resp_text, |(code, text)| {
-                Continue::basic(code, text).unwrap()
-            }),
+            map(resp_text, Either::Basic),
         )),
         CRLF,
     ));
 
-    let (remaining, (_, continue_request, _)) = parser(input)?;
+    let (remaining, (_, either, _)) = parser(input)?;
+
+    let continue_request = match either {
+        Either::Base64(data) => Continue::base64(data),
+        Either::Basic((code, text)) => Continue::basic(code, text).unwrap(),
+    };
 
     Ok((remaining, continue_request))
 }
@@ -361,6 +374,16 @@ pub fn message_data(input: &[u8]) -> IResult<&[u8], Data> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::testing::known_answer_test_parse;
+
+    #[test]
+    fn test_parse_response_negative() {
+        let tests = [b"+ Nose[CAY a\r\n".as_ref()];
+
+        for test in tests {
+            assert!(response(test).is_err());
+        }
+    }
 
     #[test]
     fn test_resp_text_code() {
@@ -382,24 +405,21 @@ mod tests {
             ),
         ];
 
-        for (input, expected_remainder, expected_code) in tests.iter() {
-            let (got_remainder, got_code) = resp_text_code(input).unwrap();
-
-            assert_eq!(*expected_remainder, got_remainder);
-            assert_eq!(*expected_code, got_code);
+        for test in tests {
+            known_answer_test_parse(test, resp_text_code);
         }
     }
 
     #[test]
     fn test_continue_req() {
-        let tests = vec![(
-            "+ \x01\r\n".as_bytes(),
-            Ok((b"".as_slice(), Continue::basic(None, "\x01").unwrap())),
+        let tests = [(
+            b"+ \x01\r\n".as_ref(),
+            b"".as_ref(),
+            Continue::basic(None, "\x01").unwrap(),
         )];
 
-        for (test, expected) in tests.into_iter() {
-            let got = continue_req(test);
-            assert_eq!(expected, got);
+        for test in tests {
+            known_answer_test_parse(test, continue_req);
         }
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 55004d53..e7dda02f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -99,8 +99,10 @@ pub mod codec;
 ))]
 mod extensions;
 mod imap4rev1;
+#[cfg(test)]
+mod testing;
 
-pub use imap_types::{command, core, message, response, security, state, testing, utils};
+pub use imap_types::{command, core, message, response, security, state, utils};
 
 // ----------- Compatibility modules -----------
 


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
new file mode 100644
index 00000000..71709d2f
--- /dev/null
+++ b/src/testing.rs
@@ -0,0 +1,16 @@
+use std::fmt::Debug;
+
+pub use imap_types::testing::known_answer_test_encode;
+use nom::IResult;
+
+pub fn known_answer_test_parse<'a, O, P>(
+    (test, expected_remainder, expected_object): (&'a [u8], &[u8], O),
+    parser: P,
+) where
+    O: Debug + Eq + 'a,
+    P: Fn(&'a [u8]) -> IResult<&'a [u8], O>,
+{
+    let (got_remainder, got_object) = parser(test).unwrap();
+    assert_eq!(expected_remainder.as_ref(), got_remainder);
+    assert_eq!(expected_object, got_object);
+}