• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

duesee / imap-codec / 18634705484

19 Oct 2025 06:56PM UTC coverage: 91.776% (+0.008%) from 91.768%
18634705484

push

github

duesee
feat: Implement `UTF8={ACCEPT,ONLY}`

44 of 66 new or added lines in 11 files covered. (66.67%)

219 existing lines in 3 files now uncovered.

10311 of 11235 relevant lines covered (91.78%)

941.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.03
/imap-codec/src/codec/decode.rs
1
//! # Decoding of messages.
2
//!
3
//! You can use [`Decoder`]s to parse messages.
4
//!
5
//! IMAP literals make separating the parsing logic from the application logic difficult.
6
//! When a server recognizes a literal (e.g. `{42}\r\n`) in a command, it first needs to agree to receive more data by sending a so-called "command continuation request" (`+ ...`).
7
//! Without a command continuation request, a client won't send more data, and the command parser on the server would always return `LiteralFound { length: 42, .. }`.
8
//! This makes real-world decoding of IMAP more elaborate.
9
//!
10
//! Have a look at the [parse_command](https://github.com/duesee/imap-codec/blob/main/imap-codec/examples/parse_command.rs) example to see how a real-world application could decode IMAP.
11

12
use std::{
13
    num::{ParseIntError, TryFromIntError},
14
    str::Utf8Error,
15
};
16

17
use imap_types::{
18
    IntoStatic,
19
    auth::AuthenticateData,
20
    command::Command,
21
    core::{LiteralMode, Tag},
22
    extensions::idle::IdleDone,
23
    response::{Greeting, Response},
24
};
25
use nom::error::{ErrorKind, FromExternalError, ParseError};
26

27
use crate::{
28
    AuthenticateDataCodec, CommandCodec, GreetingCodec, IdleDoneCodec, ResponseCodec,
29
    auth::authenticate_data,
30
    command::command,
31
    extensions::idle::idle_done,
32
    response::{greeting, response},
33
};
34

35
/// An extended version of [`nom::IResult`].
36
pub(crate) type IMAPResult<'a, I, O> = Result<(I, O), nom::Err<IMAPParseError<'a, I>>>;
37

38
/// An extended version of [`nom::error::Error`].
39
#[derive(Debug)]
40
pub(crate) struct IMAPParseError<'a, I> {
41
    #[allow(unused)]
42
    pub input: I,
43
    pub kind: IMAPErrorKind<'a>,
44
}
45

46
/// An extended version of [`nom::error::ErrorKind`].
47
#[derive(Debug)]
48
pub(crate) enum IMAPErrorKind<'a> {
49
    Literal {
50
        tag: Option<Tag<'a>>,
51
        length: u32,
52
        mode: LiteralMode,
53
    },
54
    BadNumber,
55
    BadBase64,
56
    BadUtf8,
57
    BadDateTime,
58
    LiteralContainsNull,
59
    RecursionLimitExceeded,
60
    Nom(#[allow(dead_code)] ErrorKind),
61
}
62

63
impl<I> ParseError<I> for IMAPParseError<'_, I> {
64
    fn from_error_kind(input: I, kind: ErrorKind) -> Self {
68,092✔
65
        Self {
68,092✔
66
            input,
68,092✔
67
            kind: IMAPErrorKind::Nom(kind),
68,092✔
68
        }
68,092✔
69
    }
68,092✔
70

71
    fn append(input: I, kind: ErrorKind, _: Self) -> Self {
5,126✔
72
        Self {
5,126✔
73
            input,
5,126✔
74
            kind: IMAPErrorKind::Nom(kind),
5,126✔
75
        }
5,126✔
76
    }
5,126✔
77
}
78

79
impl<I> FromExternalError<I, ParseIntError> for IMAPParseError<'_, I> {
80
    fn from_external_error(input: I, _: ErrorKind, _: ParseIntError) -> Self {
×
81
        Self {
×
82
            input,
×
83
            kind: IMAPErrorKind::BadNumber,
×
84
        }
×
85
    }
×
86
}
87

88
impl<I> FromExternalError<I, TryFromIntError> for IMAPParseError<'_, I> {
89
    fn from_external_error(input: I, _: ErrorKind, _: TryFromIntError) -> Self {
6✔
90
        Self {
6✔
91
            input,
6✔
92
            kind: IMAPErrorKind::BadNumber,
6✔
93
        }
6✔
94
    }
6✔
95
}
96

97
impl<I> FromExternalError<I, base64::DecodeError> for IMAPParseError<'_, I> {
98
    fn from_external_error(input: I, _: ErrorKind, _: base64::DecodeError) -> Self {
16✔
99
        Self {
16✔
100
            input,
16✔
101
            kind: IMAPErrorKind::BadBase64,
16✔
102
        }
16✔
103
    }
16✔
104
}
105

106
impl<I> FromExternalError<I, Utf8Error> for IMAPParseError<'_, I> {
NEW
107
    fn from_external_error(input: I, _: ErrorKind, _: Utf8Error) -> Self {
×
NEW
108
        Self {
×
NEW
109
            input,
×
NEW
110
            kind: IMAPErrorKind::BadUtf8,
×
NEW
111
        }
×
NEW
112
    }
×
113
}
114

115
/// Decoder.
116
///
117
/// Implemented for types that know how to decode a specific IMAP message. See [implementors](trait.Decoder.html#implementors).
118
pub trait Decoder {
119
    type Message<'a>: Sized;
120
    type Error<'a>;
121

122
    fn decode<'a>(&self, input: &'a [u8])
123
    -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'a>>;
124

125
    fn decode_static<'a>(
222✔
126
        &self,
222✔
127
        input: &'a [u8],
222✔
128
    ) -> Result<(&'a [u8], Self::Message<'static>), Self::Error<'static>>
222✔
129
    where
222✔
130
        Self::Message<'a>: IntoStatic<Static = Self::Message<'static>>,
222✔
131
        Self::Error<'a>: IntoStatic<Static = Self::Error<'static>>,
222✔
132
    {
133
        let (remaining, value) = self.decode(input).map_err(IntoStatic::into_static)?;
222✔
134
        Ok((remaining, value.into_static()))
36✔
135
    }
222✔
136
}
137

138
/// Error during greeting decoding.
139
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140
pub enum GreetingDecodeError {
141
    /// More data is needed.
142
    Incomplete,
143

144
    /// Decoding failed.
145
    Failed,
146
}
147

148
impl IntoStatic for GreetingDecodeError {
149
    type Static = Self;
150

151
    fn into_static(self) -> Self::Static {
36✔
152
        self
36✔
153
    }
36✔
154
}
155

156
/// Error during command decoding.
157
#[derive(Clone, Debug, Eq, PartialEq)]
158
pub enum CommandDecodeError<'a> {
159
    /// More data is needed.
160
    Incomplete,
161

162
    /// More data is needed (and further action may be necessary).
163
    ///
164
    /// The decoder stopped at the beginning of literal data. Typically, a server MUST send a
165
    /// command continuation request to agree to the receival of the remaining data. This behaviour
166
    /// is different when `LITERAL+/LITERAL-` is used.
167
    ///
168
    /// # With `LITERAL+/LITERAL-`
169
    ///
170
    /// When the `mode` is sync, everything is the same as above.
171
    ///
172
    /// When the `mode` is non-sync, *and* the server advertised the LITERAL+ capability,
173
    /// it MUST NOT send a command continuation request and accept the data right away.
174
    ///
175
    /// When the `mode` is non-sync, *and* the server advertised the LITERAL- capability,
176
    /// *and* the literal length is smaller or equal than 4096,
177
    /// it MUST NOT send a command continuation request and accept the data right away.
178
    ///
179
    /// When the `mode` is non-sync, *and* the server advertised the LITERAL- capability,
180
    /// *and* the literal length is greater than 4096,
181
    /// it MUST be handled as sync.
182
    ///
183
    /// ```rust,ignore
184
    /// match mode {
185
    ///     LiteralMode::Sync => /* Same as sync. */
186
    ///     LiteralMode::Sync => match advertised {
187
    ///         Capability::LiteralPlus => /* Accept data right away. */
188
    ///         Capability::LiteralMinus => {
189
    ///             if literal_length <= 4096 {
190
    ///                 /* Accept data right away. */
191
    ///             } else {
192
    ///                 /* Same as sync. */
193
    ///             }
194
    ///         }
195
    ///     }
196
    /// }
197
    /// ```
198
    LiteralFound {
199
        /// The corresponding command (tag) to which this literal is bound.
200
        ///
201
        /// This is required to reject literals, e.g., when their size exceeds a limit.
202
        tag: Tag<'a>,
203

204
        /// Literal length.
205
        length: u32,
206

207
        /// Literal mode, i.e., sync or non-sync.
208
        mode: LiteralMode,
209
    },
210

211
    /// Decoding failed.
212
    Failed,
213
}
214

215
impl IntoStatic for CommandDecodeError<'_> {
216
    type Static = CommandDecodeError<'static>;
217

218
    fn into_static(self) -> Self::Static {
46✔
219
        match self {
46✔
220
            CommandDecodeError::Incomplete => CommandDecodeError::Incomplete,
32✔
221
            CommandDecodeError::LiteralFound { tag, length, mode } => {
6✔
222
                CommandDecodeError::LiteralFound {
6✔
223
                    tag: tag.into_static(),
6✔
224
                    length,
6✔
225
                    mode,
6✔
226
                }
6✔
227
            }
228
            CommandDecodeError::Failed => CommandDecodeError::Failed,
8✔
229
        }
230
    }
46✔
231
}
232

233
/// Error during authenticate data line decoding.
234
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235
pub enum AuthenticateDataDecodeError {
236
    /// More data is needed.
237
    Incomplete,
238

239
    /// Decoding failed.
240
    Failed,
241
}
242

243
impl IntoStatic for AuthenticateDataDecodeError {
244
    type Static = Self;
245

246
    fn into_static(self) -> Self::Static {
26✔
247
        self
26✔
248
    }
26✔
249
}
250

251
/// Error during response decoding.
252
#[derive(Clone, Debug, Eq, PartialEq)]
253
pub enum ResponseDecodeError {
254
    /// More data is needed.
255
    Incomplete,
256

257
    /// The decoder stopped at the beginning of literal data.
258
    ///
259
    /// The client *MUST* accept the literal and has no option to reject it.
260
    /// However, when the client ultimately does not want to handle the literal, it can do something
261
    /// similar to <https://datatracker.ietf.org/doc/html/rfc7888#section-4>.
262
    ///
263
    /// It can implement a discarding mechanism, basically, consuming the whole literal but not
264
    /// saving the bytes in memory. Or, it can close the connection.
265
    LiteralFound {
266
        /// Literal length.
267
        length: u32,
268
    },
269

270
    /// Decoding failed.
271
    Failed,
272
}
273

274
impl IntoStatic for ResponseDecodeError {
275
    type Static = Self;
276

277
    fn into_static(self) -> Self::Static {
60✔
278
        self
60✔
279
    }
60✔
280
}
281

282
/// Error during idle done decoding.
283
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284
pub enum IdleDoneDecodeError {
285
    /// More data is needed.
286
    Incomplete,
287

288
    /// Decoding failed.
289
    Failed,
290
}
291

292
impl IntoStatic for IdleDoneDecodeError {
293
    type Static = Self;
294

295
    fn into_static(self) -> Self::Static {
18✔
296
        self
18✔
297
    }
18✔
298
}
299

300
// -------------------------------------------------------------------------------------------------
301

302
impl Decoder for GreetingCodec {
303
    type Message<'a> = Greeting<'a>;
304
    type Error<'a> = GreetingDecodeError;
305

306
    fn decode<'a>(
132✔
307
        &self,
132✔
308
        input: &'a [u8],
132✔
309
    ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> {
132✔
310
        match greeting(input) {
132✔
311
            Ok((rem, grt)) => Ok((rem, grt)),
60✔
312
            Err(nom::Err::Incomplete(_)) => Err(GreetingDecodeError::Incomplete),
56✔
313
            Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => Err(GreetingDecodeError::Failed),
16✔
314
        }
315
    }
132✔
316
}
317

318
impl Decoder for CommandCodec {
319
    type Message<'a> = Command<'a>;
320
    type Error<'a> = CommandDecodeError<'a>;
321

322
    fn decode<'a>(
1,184✔
323
        &self,
1,184✔
324
        input: &'a [u8],
1,184✔
325
    ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'a>> {
1,184✔
326
        match command(input) {
1,184✔
327
            Ok((rem, cmd)) => Ok((rem, cmd)),
1,092✔
328
            Err(nom::Err::Incomplete(_)) => Err(CommandDecodeError::Incomplete),
64✔
329
            Err(nom::Err::Failure(error)) => match error {
12✔
330
                IMAPParseError {
331
                    input: _,
332
                    kind: IMAPErrorKind::Literal { tag, length, mode },
12✔
333
                } => Err(CommandDecodeError::LiteralFound {
12✔
334
                    // Unwrap: We *must* receive a `tag` during command parsing.
12✔
335
                    tag: tag.expect("Expected `Some(tag)` in `IMAPErrorKind::Literal`, got `None`"),
12✔
336
                    length,
12✔
337
                    mode,
12✔
338
                }),
12✔
339
                _ => Err(CommandDecodeError::Failed),
×
340
            },
341
            Err(nom::Err::Error(_)) => Err(CommandDecodeError::Failed),
16✔
342
        }
343
    }
1,184✔
344
}
345

346
impl Decoder for ResponseCodec {
347
    type Message<'a> = Response<'a>;
348
    type Error<'a> = ResponseDecodeError;
349

350
    fn decode<'a>(
2,510✔
351
        &self,
2,510✔
352
        input: &'a [u8],
2,510✔
353
    ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> {
2,510✔
354
        match response(input) {
2,510✔
355
            Ok((rem, rsp)) => Ok((rem, rsp)),
2,388✔
356
            Err(nom::Err::Incomplete(_)) => Err(ResponseDecodeError::Incomplete),
96✔
357
            Err(nom::Err::Error(error) | nom::Err::Failure(error)) => match error {
26✔
358
                IMAPParseError {
359
                    kind: IMAPErrorKind::Literal { length, .. },
8✔
360
                    ..
361
                } => Err(ResponseDecodeError::LiteralFound { length }),
8✔
362
                _ => Err(ResponseDecodeError::Failed),
18✔
363
            },
364
        }
365
    }
2,510✔
366
}
367

368
impl Decoder for AuthenticateDataCodec {
369
    type Message<'a> = AuthenticateData<'a>;
370
    type Error<'a> = AuthenticateDataDecodeError;
371

372
    fn decode<'a>(
88✔
373
        &self,
88✔
374
        input: &'a [u8],
88✔
375
    ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> {
88✔
376
        match authenticate_data(input) {
88✔
377
            Ok((rem, rsp)) => Ok((rem, rsp)),
36✔
378
            Err(nom::Err::Incomplete(_)) => Err(AuthenticateDataDecodeError::Incomplete),
36✔
379
            Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => {
380
                Err(AuthenticateDataDecodeError::Failed)
16✔
381
            }
382
        }
383
    }
88✔
384
}
385

386
impl Decoder for IdleDoneCodec {
387
    type Message<'a> = IdleDone;
388
    type Error<'a> = IdleDoneDecodeError;
389

390
    fn decode<'a>(
74✔
391
        &self,
74✔
392
        input: &'a [u8],
74✔
393
    ) -> Result<(&'a [u8], Self::Message<'a>), Self::Error<'static>> {
74✔
394
        match idle_done(input) {
74✔
395
            Ok((rem, rsp)) => Ok((rem, rsp)),
20✔
396
            Err(nom::Err::Incomplete(_)) => Err(IdleDoneDecodeError::Incomplete),
30✔
397
            Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => Err(IdleDoneDecodeError::Failed),
24✔
398
        }
399
    }
74✔
400
}
401

402
#[cfg(test)]
403
mod tests {
404
    use std::num::NonZeroU32;
405

406
    use imap_types::{
407
        command::{Command, CommandBody},
408
        core::{IString, Literal, NString, Vec1},
409
        extensions::idle::IdleDone,
410
        fetch::MessageDataItem,
411
        mailbox::Mailbox,
412
        response::{Data, Greeting, GreetingKind, Response},
413
    };
414

415
    use super::*;
416

417
    #[test]
418
    fn test_decode_greeting() {
2✔
419
        let tests = [
2✔
420
            // Ok
2✔
421
            (
2✔
422
                b"* OK ...\r\n".as_ref(),
2✔
423
                Ok((
2✔
424
                    b"".as_ref(),
2✔
425
                    Greeting::new(GreetingKind::Ok, None, "...").unwrap(),
2✔
426
                )),
2✔
427
            ),
2✔
428
            (
2✔
429
                b"* ByE .\r\n???".as_ref(),
2✔
430
                Ok((
2✔
431
                    b"???".as_ref(),
2✔
432
                    Greeting::new(GreetingKind::Bye, None, ".").unwrap(),
2✔
433
                )),
2✔
434
            ),
2✔
435
            (
2✔
436
                b"* preaUth x\r\n?".as_ref(),
2✔
437
                Ok((
2✔
438
                    b"?".as_ref(),
2✔
439
                    Greeting::new(GreetingKind::PreAuth, None, "x").unwrap(),
2✔
440
                )),
2✔
441
            ),
2✔
442
            // Incomplete
2✔
443
            (b"*".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
444
            (b"* ".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
445
            (b"* O".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
446
            (b"* OK".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
447
            (b"* OK ".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
448
            (b"* OK .".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
449
            (b"* OK .\r".as_ref(), Err(GreetingDecodeError::Incomplete)),
2✔
450
            // Failed
2✔
451
            (b"**".as_ref(), Err(GreetingDecodeError::Failed)),
2✔
452
            (b"* NO x\r\n".as_ref(), Err(GreetingDecodeError::Failed)),
2✔
453
        ];
2✔
454

455
        for (test, expected) in tests {
26✔
456
            let got = GreetingCodec::default().decode(test);
24✔
457
            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
24✔
458
            assert_eq!(expected, got);
24✔
459

460
            {
461
                let got = GreetingCodec::default().decode_static(test);
24✔
462
                assert_eq!(expected, got);
24✔
463
            }
464
        }
465
    }
2✔
466

467
    #[test]
468
    fn test_decode_command() {
2✔
469
        let tests = [
2✔
470
            // Ok
2✔
471
            (
2✔
472
                b"a noop\r\n".as_ref(),
2✔
473
                Ok((b"".as_ref(), Command::new("a", CommandBody::Noop).unwrap())),
2✔
474
            ),
2✔
475
            (
2✔
476
                b"a noop\r\n???".as_ref(),
2✔
477
                Ok((
2✔
478
                    b"???".as_ref(),
2✔
479
                    Command::new("a", CommandBody::Noop).unwrap(),
2✔
480
                )),
2✔
481
            ),
2✔
482
            (
2✔
483
                b"a select {5}\r\ninbox\r\n".as_ref(),
2✔
484
                Ok((
2✔
485
                    b"".as_ref(),
2✔
486
                    Command::new(
2✔
487
                        "a",
2✔
488
                        CommandBody::Select {
2✔
489
                            mailbox: Mailbox::Inbox,
2✔
490
                            #[cfg(feature = "ext_condstore_qresync")]
2✔
491
                            parameters: Vec::default(),
2✔
492
                        },
2✔
493
                    )
2✔
494
                    .unwrap(),
2✔
495
                )),
2✔
496
            ),
2✔
497
            (
2✔
498
                b"a select {5}\r\ninbox\r\nxxx".as_ref(),
2✔
499
                Ok((
2✔
500
                    b"xxx".as_ref(),
2✔
501
                    Command::new(
2✔
502
                        "a",
2✔
503
                        CommandBody::Select {
2✔
504
                            mailbox: Mailbox::Inbox,
2✔
505
                            #[cfg(feature = "ext_condstore_qresync")]
2✔
506
                            parameters: Vec::default(),
2✔
507
                        },
2✔
508
                    )
2✔
509
                    .unwrap(),
2✔
510
                )),
2✔
511
            ),
2✔
512
            // Incomplete
2✔
513
            (b"a".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
514
            (b"a ".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
515
            (b"a n".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
516
            (b"a no".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
517
            (b"a noo".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
518
            (b"a noop".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
519
            (b"a noop\r".as_ref(), Err(CommandDecodeError::Incomplete)),
2✔
520
            // LiteralAckRequired
2✔
521
            (
2✔
522
                b"a select {5}\r\n".as_ref(),
2✔
523
                Err(CommandDecodeError::LiteralFound {
2✔
524
                    tag: Tag::try_from("a").unwrap(),
2✔
525
                    length: 5,
2✔
526
                    mode: LiteralMode::Sync,
2✔
527
                }),
2✔
528
            ),
2✔
529
            // Incomplete (after literal)
2✔
530
            (
2✔
531
                b"a select {5}\r\nxxx".as_ref(),
2✔
532
                Err(CommandDecodeError::Incomplete),
2✔
533
            ),
2✔
534
            // Failed
2✔
535
            (b"* noop\r\n".as_ref(), Err(CommandDecodeError::Failed)),
2✔
536
            (b"A  noop\r\n".as_ref(), Err(CommandDecodeError::Failed)),
2✔
537
        ];
2✔
538

539
        for (test, expected) in tests {
32✔
540
            let got = CommandCodec::default().decode(test);
30✔
541
            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
30✔
542
            assert_eq!(expected, got);
30✔
543

544
            {
545
                let got = CommandCodec::default().decode_static(test);
30✔
546
                assert_eq!(expected, got);
30✔
547
            }
548
        }
549
    }
2✔
550

551
    #[test]
552
    fn test_decode_authenticate_data() {
2✔
553
        let tests = [
2✔
554
            // Ok
2✔
555
            (
2✔
556
                b"VGVzdA==\r\n".as_ref(),
2✔
557
                Ok((b"".as_ref(), AuthenticateData::r#continue(b"Test".to_vec()))),
2✔
558
            ),
2✔
559
            (
2✔
560
                b"VGVzdA==\r\nx".as_ref(),
2✔
561
                Ok((
2✔
562
                    b"x".as_ref(),
2✔
563
                    AuthenticateData::r#continue(b"Test".to_vec()),
2✔
564
                )),
2✔
565
            ),
2✔
566
            (
2✔
567
                b"*\r\n".as_ref(),
2✔
568
                Ok((b"".as_ref(), AuthenticateData::Cancel)),
2✔
569
            ),
2✔
570
            (
2✔
571
                b"*\r\nx".as_ref(),
2✔
572
                Ok((b"x".as_ref(), AuthenticateData::Cancel)),
2✔
573
            ),
2✔
574
            // Incomplete
2✔
575
            (b"V".as_ref(), Err(AuthenticateDataDecodeError::Incomplete)),
2✔
576
            (b"VG".as_ref(), Err(AuthenticateDataDecodeError::Incomplete)),
2✔
577
            (
2✔
578
                b"VGV".as_ref(),
2✔
579
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
580
            ),
2✔
581
            (
2✔
582
                b"VGVz".as_ref(),
2✔
583
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
584
            ),
2✔
585
            (
2✔
586
                b"VGVzd".as_ref(),
2✔
587
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
588
            ),
2✔
589
            (
2✔
590
                b"VGVzdA".as_ref(),
2✔
591
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
592
            ),
2✔
593
            (
2✔
594
                b"VGVzdA=".as_ref(),
2✔
595
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
596
            ),
2✔
597
            (
2✔
598
                b"VGVzdA==".as_ref(),
2✔
599
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
600
            ),
2✔
601
            (
2✔
602
                b"VGVzdA==\r".as_ref(),
2✔
603
                Err(AuthenticateDataDecodeError::Incomplete),
2✔
604
            ),
2✔
605
            (
2✔
606
                b"VGVzdA==\r\n".as_ref(),
2✔
607
                Ok((b"".as_ref(), AuthenticateData::r#continue(b"Test".to_vec()))),
2✔
608
            ),
2✔
609
            // Failed
2✔
610
            (
2✔
611
                b"VGVzdA== \r\n".as_ref(),
2✔
612
                Err(AuthenticateDataDecodeError::Failed),
2✔
613
            ),
2✔
614
            (
2✔
615
                b" VGVzdA== \r\n".as_ref(),
2✔
616
                Err(AuthenticateDataDecodeError::Failed),
2✔
617
            ),
2✔
618
            (
2✔
619
                b" V GVzdA== \r\n".as_ref(),
2✔
620
                Err(AuthenticateDataDecodeError::Failed),
2✔
621
            ),
2✔
622
            (
2✔
623
                b" V GVzdA= \r\n".as_ref(),
2✔
624
                Err(AuthenticateDataDecodeError::Failed),
2✔
625
            ),
2✔
626
        ];
2✔
627

628
        for (test, expected) in tests {
38✔
629
            let got = AuthenticateDataCodec::default().decode(test);
36✔
630
            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
36✔
631
            assert_eq!(expected, got);
36✔
632

633
            {
634
                let got = AuthenticateDataCodec::default().decode_static(test);
36✔
635
                assert_eq!(expected, got);
36✔
636
            }
637
        }
638
    }
2✔
639

640
    #[test]
641
    fn test_decode_idle_done() {
2✔
642
        let tests = [
2✔
643
            // Ok
2✔
644
            (b"done\r\n".as_ref(), Ok((b"".as_ref(), IdleDone))),
2✔
645
            (b"done\r\n?".as_ref(), Ok((b"?".as_ref(), IdleDone))),
2✔
646
            // Incomplete
2✔
647
            (b"d".as_ref(), Err(IdleDoneDecodeError::Incomplete)),
2✔
648
            (b"do".as_ref(), Err(IdleDoneDecodeError::Incomplete)),
2✔
649
            (b"don".as_ref(), Err(IdleDoneDecodeError::Incomplete)),
2✔
650
            (b"done".as_ref(), Err(IdleDoneDecodeError::Incomplete)),
2✔
651
            (b"done\r".as_ref(), Err(IdleDoneDecodeError::Incomplete)),
2✔
652
            // Failed
2✔
653
            (b"donee\r\n".as_ref(), Err(IdleDoneDecodeError::Failed)),
2✔
654
            (b" done\r\n".as_ref(), Err(IdleDoneDecodeError::Failed)),
2✔
655
            (b"done \r\n".as_ref(), Err(IdleDoneDecodeError::Failed)),
2✔
656
            (b" done \r\n".as_ref(), Err(IdleDoneDecodeError::Failed)),
2✔
657
        ];
2✔
658

659
        for (test, expected) in tests {
24✔
660
            let got = IdleDoneCodec::default().decode(test);
22✔
661
            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
22✔
662
            assert_eq!(expected, got);
22✔
663

664
            {
665
                let got = IdleDoneCodec::default().decode_static(test);
22✔
666
                assert_eq!(expected, got);
22✔
667
            }
668
        }
669
    }
2✔
670

671
    #[test]
672
    fn test_decode_response() {
2✔
673
        let tests = [
2✔
674
            // Incomplete
2✔
675
            (b"".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
676
            (b"*".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
677
            (b"* ".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
678
            (b"* S".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
679
            (b"* SE".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
680
            (b"* SEA".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
681
            (b"* SEAR".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
682
            (b"* SEARC".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
683
            (b"* SEARCH".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
684
            (b"* SEARCH ".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
685
            (b"* SEARCH 1".as_ref(), Err(ResponseDecodeError::Incomplete)),
2✔
686
            (
2✔
687
                b"* SEARCH 1\r".as_ref(),
2✔
688
                Err(ResponseDecodeError::Incomplete),
2✔
689
            ),
2✔
690
            // Ok
2✔
691
            (
2✔
692
                b"* SEARCH 1\r\n".as_ref(),
2✔
693
                Ok((
2✔
694
                    b"".as_ref(),
2✔
695
                    Response::Data(Data::Search(
2✔
696
                        vec![NonZeroU32::new(1).unwrap()],
2✔
697
                        #[cfg(feature = "ext_condstore_qresync")]
2✔
698
                        None,
2✔
699
                    )),
2✔
700
                )),
2✔
701
            ),
2✔
702
            #[cfg(feature = "quirk_trailing_space_search")]
2✔
703
            (
2✔
704
                b"* SEARCH \r\n".as_ref(),
2✔
705
                Ok((
2✔
706
                    b"".as_ref(),
2✔
707
                    Response::Data(Data::Search(
2✔
708
                        vec![],
2✔
709
                        #[cfg(feature = "ext_condstore_qresync")]
2✔
710
                        None,
2✔
711
                    )),
2✔
712
                )),
2✔
713
            ),
2✔
714
            (
2✔
715
                b"* SEARCH 1\r\n???".as_ref(),
2✔
716
                Ok((
2✔
717
                    b"???".as_ref(),
2✔
718
                    Response::Data(Data::Search(
2✔
719
                        vec![NonZeroU32::new(1).unwrap()],
2✔
720
                        #[cfg(feature = "ext_condstore_qresync")]
2✔
721
                        None,
2✔
722
                    )),
2✔
723
                )),
2✔
724
            ),
2✔
725
            (
2✔
726
                b"* 1 FETCH (RFC822 {5}\r\nhello)\r\n".as_ref(),
2✔
727
                Ok((
2✔
728
                    b"".as_ref(),
2✔
729
                    Response::Data(Data::Fetch {
2✔
730
                        seq: NonZeroU32::new(1).unwrap(),
2✔
731
                        items: Vec1::from(MessageDataItem::Rfc822(NString(Some(
2✔
732
                            IString::Literal(Literal::try_from(b"hello".as_ref()).unwrap()),
2✔
733
                        )))),
2✔
734
                    }),
2✔
735
                )),
2✔
736
            ),
2✔
737
            (
2✔
738
                b"* 1 FETCH (RFC822 {5}\r\n".as_ref(),
2✔
739
                Err(ResponseDecodeError::LiteralFound { length: 5 }),
2✔
740
            ),
2✔
741
            // Failed
2✔
742
            (
2✔
743
                b"*  search 1 2 3\r\n".as_ref(),
2✔
744
                Err(ResponseDecodeError::Failed),
2✔
745
            ),
2✔
746
            #[cfg(not(feature = "quirk_trailing_space_search"))]
2✔
747
            (b"* search \r\n".as_ref(), Err(ResponseDecodeError::Failed)),
2✔
748
            (b"A search\r\n".as_ref(), Err(ResponseDecodeError::Failed)),
2✔
749
        ];
2✔
750

751
        for (test, expected) in tests {
40✔
752
            let got = ResponseCodec::default().decode(test);
38✔
753
            dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
38✔
754
            assert_eq!(expected, got);
38✔
755

756
            {
757
                let got = ResponseCodec::default().decode_static(test);
38✔
758
                assert_eq!(expected, got);
38✔
759
            }
760
        }
761
    }
2✔
762
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc