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

Issue:
refactor: Use `Fragmentizer` from imap-next



Discussion / hints:

Fix patch:
diff --git a/imap-codec/examples/fragmentizer_client.rs b/imap-codec/examples/fragmentizer_client.rs
new file mode 100644
index 00000000..0c131e29
--- /dev/null
+++ b/imap-codec/examples/fragmentizer_client.rs
@@ -0,0 +1,64 @@
+use std::{io::Read, net::TcpStream};
+
+use imap_codec::{
+    fragmentizer::{Fragmentizer, MaxMessageSize},
+    GreetingCodec, ResponseCodec,
+};
+
+enum State {
+    Greeting,
+    Response,
+}
+
+fn main() {
+    let mut stream = TcpStream::connect("127.0.0.1:12345").unwrap();
+    let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Limited(1024));
+
+    let mut state = State::Greeting;
+
+    loop {
+        match fragmentizer.progress() {
+            Some(fragment_info) => {
+                dbg!(fragment_info);
+                dbg!(fragmentizer.fragment_bytes(fragment_info));
+
+                if fragmentizer.is_message_complete() {
+                    match state {
+                        State::Greeting => {
+                            match fragmentizer.decode_message(&GreetingCodec::new()) {
+                                Ok(greeting) => {
+                                    dbg!(greeting);
+                                    state = State::Response;
+                                }
+                                Err(error) => {
+                                    dbg!(error);
+                                }
+                            }
+                        }
+                        State::Response => {
+                            match fragmentizer.decode_message(&ResponseCodec::new()) {
+                                Ok(response) => {
+                                    dbg!(response);
+                                }
+                                Err(error) => {
+                                    dbg!(error);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            None => {
+                println!("Reading bytes...");
+                let mut buffer = [0; 64];
+                let count = dbg!(stream.read(&mut buffer).unwrap());
+                if count == 0 {
+                    println!("<Connection closed>");
+                    break;
+                }
+
+                fragmentizer.enqueue_bytes(&buffer[..count]);
+            }
+        }
+    }
+}
diff --git a/imap-codec/examples/fragmentizer_server.rs b/imap-codec/examples/fragmentizer_server.rs
new file mode 100644
index 00000000..c6ee5a23
--- /dev/null
+++ b/imap-codec/examples/fragmentizer_server.rs
@@ -0,0 +1,83 @@
+use std::{io::Read, net::TcpListener};
+
+use imap_codec::{
+    fragmentizer::{Fragmentizer, MaxMessageSize},
+    AuthenticateDataCodec, CommandCodec, IdleDoneCodec,
+};
+use imap_types::command::CommandBody;
+
+enum State {
+    Command,
+    AuthenticateData,
+    Idle,
+}
+
+fn main() {
+    let mut stream = {
+        let listener = TcpListener::bind("127.0.0.1:12345").unwrap();
+        listener.accept().unwrap().0
+    };
+
+    let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Limited(1024));
+
+    let mut state = State::Command;
+
+    loop {
+        match fragmentizer.progress() {
+            Some(fragment_info) => {
+                dbg!(fragment_info);
+                dbg!(fragmentizer.fragment_bytes(fragment_info));
+
+                if fragmentizer.is_message_complete() {
+                    match state {
+                        State::Command => match fragmentizer.decode_message(&CommandCodec::new()) {
+                            Ok(command) => {
+                                dbg!(&command);
+                                state = match command.body {
+                                    CommandBody::Authenticate { .. } => State::AuthenticateData,
+                                    CommandBody::Idle => State::Idle,
+                                    _ => State::Command,
+                                };
+                            }
+                            Err(error) => {
+                                dbg!(error);
+                            }
+                        },
+                        State::AuthenticateData => {
+                            match fragmentizer.decode_message(&AuthenticateDataCodec::new()) {
+                                Ok(authenticate_data) => {
+                                    dbg!(authenticate_data);
+                                    // Pretend we are done after one SASL round.
+                                    state = State::Command;
+                                }
+                                Err(error) => {
+                                    dbg!(error);
+                                }
+                            }
+                        }
+                        State::Idle => match fragmentizer.decode_message(&IdleDoneCodec::new()) {
+                            Ok(idle_done) => {
+                                dbg!(idle_done);
+                                state = State::Command;
+                            }
+                            Err(error) => {
+                                dbg!(error);
+                            }
+                        },
+                    }
+                }
+            }
+            None => {
+                println!("Reading bytes...");
+                let mut buffer = [0; 64];
+                let count = dbg!(stream.read(&mut buffer).unwrap());
+                if count == 0 {
+                    println!("<Connection closed>");
+                    break;
+                }
+
+                fragmentizer.enqueue_bytes(&buffer[..count]);
+            }
+        }
+    }
+}
diff --git a/imap-codec/src/fragmentizer.rs b/imap-codec/src/fragmentizer.rs
new file mode 100644
index 00000000..d2065831
--- /dev/null
+++ b/imap-codec/src/fragmentizer.rs
@@ -0,0 +1,1401 @@
+//! Parser for IMAP message fragments.
+
+use std::{collections::VecDeque, ops::Range};
+
+use imap_types::{
+    core::{LiteralMode, Tag},
+    secret::Secret,
+};
+
+use crate::decode::Decoder;
+
+/// Limits the size of messages that can be decoded by [`Fragmentizer`].
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum MaxMessageSize {
+    /// Message size is not limited.
+    ///
+    /// Using this might be dangerous because a buffer with the size of the message is
+    /// necessary. This allows an attacker to allocate an arbitrary amount of memory by
+    /// sending a large message.
+    Unlimited,
+    /// Message size is limited by the given value.
+    ///
+    /// If the size limit is exceeded then any following bytes of the current message will be
+    /// dropped. However, the fragments will still be parsed so that the end of the message can
+    /// be detected. Decoding the message with the exceeding size will fail.
+    Limited(u32),
+}
+
+/// The character sequence used for ending a line.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum LineEnding {
+    /// The line ends with the character `\n`.
+    Lf,
+    /// The line ends with the character sequence `\r\n`.
+    CrLf,
+}
+
+/// Used by a line to announce a literal following the line.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct LiteralAnnouncement {
+    /// The mode of the announced literal.
+    pub mode: LiteralMode,
+    /// The length of the announced literal in bytes.
+    pub length: u32,
+}
+
+/// Describes a fragment of the current message found by [`Fragmentizer::progress`].
+///
+/// The corresponding bytes can be retrieved via [`Fragmentizer::fragment_bytes`]
+/// until [`Fragmentizer::is_message_complete`] returns true. After that the
+/// next call of [`Fragmentizer::progress`] will start the next message.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum FragmentInfo {
+    /// The fragment is a line.
+    Line {
+        /// Inclusive start index relative to the current message.
+        start: usize,
+        /// Exclusive end index relative to the current message.
+        end: usize,
+        /// Whether the next fragment will be a literal.
+        announcement: Option<LiteralAnnouncement>,
+        /// The detected ending sequence for this line.
+        ending: LineEnding,
+    },
+    /// The fragment is a literal.
+    Literal {
+        /// Inclusive start index relative to the current message.
+        start: usize,
+        /// Exclusive end index relative to the current message.
+        end: usize,
+    },
+}
+
+impl FragmentInfo {
+    /// The index range relative to the current message.
+    pub fn range(self) -> Range<usize> {
+        match self {
+            FragmentInfo::Line { start, end, .. } => start..end,
+            FragmentInfo::Literal { start, end } => start..end,
+        }
+    }
+}
+
+/// An error returned by [`Fragmentizer::decode_message`].
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DecodeMessageError<'a, C: Decoder> {
+    /// The decoder failed decoding the message.
+    DecodingFailure(C::Error<'a>),
+    /// Not all bytes of the message were used when decoding the message.
+    DecodingRemainder {
+        /// The decoded message.
+        message: C::Message<'a>,
+        /// The unused bytes.
+        remainder: Secret<&'a [u8]>,
+    },
+    /// Max message size was exceeded and bytes were dropped.
+    MessageTooLong { initial: Secret<&'a [u8]> },
+}
+
+/// Parser for IMAP message fragments.
+///
+/// A fragment is either a line or a literal. A single IMAP message may consist of multiple
+/// fragments.
+///
+/// Using this fragment parser has the following benefits:
+/// - It can reliable detect when all fragments of a message were received. Only then it makes
+/// sense to decode the actual message. This allows us to decode large messages efficiently.
+/// - Even if the message is malformed it allows us to drop the right number of bytes. This
+/// prevents us from treating untrusted bytes (e.g. literal bytes) as IMAP messages.
+/// - It has support for max message length. Any exceeding bytes will be dropped. This
+/// prevents an attacker from allocating indefinite amount of memory.
+/// - It allows us to handle literals before the message is complete. This is important for IMAP
+/// servers because they need to accept or reject literals from the client.
+#[derive(Clone, Debug)]
+pub struct Fragmentizer {
+    /// Enqueued bytes that are not parsed by [`Fragmentizer::progress`] yet.
+    unparsed_buffer: VecDeque<u8>,
+    /// Upper limit for the size of parsed messages.
+    max_message_size: MaxMessageSize,
+    /// Whether the size limit is exceeded for the current message.
+    max_message_size_exceeded: bool,
+    /// Parsed bytes of the current messages. The length is limited by
+    /// [`Fragmentizer::max_message_size`].
+    message_buffer: Vec<u8>,
+    /// Parser for the next fragment of the current message. Is `None` if no fragment is expected
+    /// because the message is complete.
+    parser: Option<Parser>,
+}
+
+impl Fragmentizer {
+    pub fn new(max_message_size: MaxMessageSize) -> Self {
+        Self {
+            unparsed_buffer: VecDeque::new(),
+            max_message_size,
+            max_message_size_exceeded: false,
+            message_buffer: Vec::new(),
+            parser: Some(Parser::Line(LineParser::new(0))),
+        }
+    }
+
+    /// Enqueues more byte that can be parsed by [`Fragmentizer::progress`].
+    ///
+    /// Note that the message size limit is not enforced on the enqueued bytes. You can control
+    /// the size of the enqueued bytes by only calling this function if more bytes are necessary.
+    /// More bytes are necessary if [`Fragmentizer::progress`] returns `None`.
+    pub fn enqueue_bytes(&mut self, bytes: &[u8]) {
+        self.unparsed_buffer.extend(bytes);
+    }
+
+    /// Returns whether the size limit is exceeded for the current message.
+    pub fn is_max_message_size_exceeded(&self) -> bool {
+        self.max_message_size_exceeded
+    }
+
+    /// Returns whether the current message was fully parsed.
+    ///
+    /// If it returns true then it makes sense to call [`Fragmentizer::decode_message`]
+    /// to decode the message. Alternatively, you can access all bytes of the message via
+    /// [`Fragmentizer::message_bytes`].
+    pub fn is_message_complete(&self) -> bool {
+        self.parser.is_none()
+    }
+
+    /// Returns the bytes of the current message.
+    ///
+    /// Note that the bytes might be incomplete:
+    /// - The message might not be fully parsed yet and [`Fragmentizer::progress`] need to be
+    ///   called. You can check whether the message is complete via
+    ///   [`Fragmentizer::is_message_complete`].
+    /// - The size limit might be exceeded and bytes might be dropped. You can check this
+    ///   via [`Fragmentizer::is_max_message_size_exceeded`]
+    pub fn message_bytes(&self) -> &[u8] {
+        &self.message_buffer
+    }
+
+    /// Returns the bytes for a fragment of the current message.
+    pub fn fragment_bytes(&self, fragment_info: FragmentInfo) -> &[u8] {
+        let (start, end) = match fragment_info {
+            FragmentInfo::Line { start, end, .. } => (start, end),
+            FragmentInfo::Literal { start, end } => (start, end),
+        };
+        let start = start.min(self.message_buffer.len());
+        let end = end.min(self.message_buffer.len());
+        &self.message_buffer[start..end]
+    }
+
+    /// Skips the current message and starts the next message immediately.
+    ///
+    /// Warning: Using this method might be dangerous. If client and server don't
+    /// agree at which point a message is skipped, then the client or server might
+    /// treat untrusted bytes (e.g. literal bytes) as IMAP messages. Currently the
+    /// only valid use-case is a server that rejects synchronizing literals from the
+    /// client.
+    pub fn skip_message(&mut self) {
+        self.max_message_size_exceeded = false;
+        self.message_buffer.clear();
+        self.parser = Some(Parser::Line(LineParser::new(0)));
+    }
+
+    /// Continue parsing the current message until the next fragment is detected.
+    ///
+    /// Returns `None` if more bytes need to be enqueued via [`Fragmentizer::enqueue_bytes`].
+    /// If [`Fragmentizer::is_message_complete`] returns true after this function was called,
+    /// then the message was fully parsed. The following call of this function will then start
+    /// the next message.
+    pub fn progress(&mut self) -> Option<FragmentInfo> {
+        let parser = match &mut self.parser {
+            Some(parser) => {
+                // Continue current message
+                parser
+            }
+            None => {
+                // Start next message
+                self.max_message_size_exceeded = false;
+                self.message_buffer.clear();
+                self.parser.insert(Parser::Line(LineParser::new(0)))
+            }
+        };
+
+        // Progress fragment
+        let (parsed_byte_count, fragment) = match parser {
+            Parser::Line(parser) => parser.parse(&self.unparsed_buffer),
+            Parser::Literal(parser) => parser.parse(&self.unparsed_buffer),
+        };
+        self.dequeue_parsed_bytes(parsed_byte_count);
+
+        if let Some(fragment) = fragment {
+            self.parser = match fragment {
+                // Finish current message
+                FragmentInfo::Line {
+                    announcement: None, ..
+                } => None,
+                // Next fragment will be a literal
+                FragmentInfo::Line {
+                    end,
+                    announcement: Some(LiteralAnnouncement { length, .. }),
+                    ..
+                } => Some(Parser::Literal(LiteralParser::new(end, length))),
+                // Next fragment will be a line
+                FragmentInfo::Literal { end, .. } => Some(Parser::Line(LineParser::new(end))),
+            }
+        }
+
+        fragment
+    }
+
+    /// Tries to decode the [`Tag`] for the current message.
+    ///
+    /// Note that decoding the [`Tag`] is on best effort basis. Some message types don't have
+    /// a [`Tag`] and without context you can't know whether this function will succeed. However,
+    /// this function is useful if the message is incomplete or malformed and you want to decode
+    /// the [`Tag`] in order to send a response.
+    pub fn decode_tag(&self) -> Option<Tag> {
+        parse_tag(&self.message_buffer)
+    }
+
+    /// Tries to decode the current message with the given decoder.
+    ///
+    /// You usually want to call this method once [`Fragmentizer::is_message_complete`] returns
+    /// true. Which decoder should be used depends on the state of the IMAP conversation. The
+    /// caller is responsible for tracking this state and choosing the decoder.
+    pub fn decode_message<'a, C: Decoder>(
+        &'a self,
+        codec: &C,
+    ) -> Result<C::Message<'a>, DecodeMessageError<'a, C>> {
+        if self.max_message_size_exceeded {
+            return Err(DecodeMessageError::MessageTooLong {
+                initial: Secret::new(&self.message_buffer),
+            });
+        }
+
+        let (remainder, message) = match codec.decode(&self.message_buffer) {
+            Ok(res) => res,
+            Err(err) => return Err(DecodeMessageError::DecodingFailure(err)),
+        };
+
+        if !remainder.is_empty() {
+            return Err(DecodeMessageError::DecodingRemainder {
+                message,
+                remainder: Secret::new(remainder),
+            });
+        }
+
+        Ok(message)
+    }
+
+    fn dequeue_parsed_bytes(&mut self, parsed_byte_count: usize) {
+        // This will remove the parsed bytes even if we don't add them to the message buffer
+        let parsed_bytes = self.unparsed_buffer.drain(..parsed_byte_count);
+        // How many bytes can we add to the message buffer?
+        let remaining_size = match self.max_message_size {
+            MaxMessageSize::Unlimited => None,
+            MaxMessageSize::Limited(size) => Some(size as usize - self.message_buffer.len()),
+        };
+
+        // Add bytes to the message buffer
+        match remaining_size {
+            Some(remaining_size) if remaining_size < parsed_byte_count => {
+                let remaining_bytes = parsed_bytes.take(remaining_size);
+                self.message_buffer.extend(remaining_bytes);
+                self.max_message_size_exceeded = true;
+            }
+            _ => {
+                self.message_buffer.extend(parsed_bytes);
+            }
+        }
+    }
+}
+
+/// Stateful parser for the next fragment.
+#[derive(Clone, Debug)]
+enum Parser {
+    Line(LineParser),
+    Literal(LiteralParser),
+}
+
+/// Stateful parser for the next line fragment.
+#[derive(Clone, Debug)]
+struct LineParser {
+    /// Where we started parsing the line.
+    start: usize,
+    /// Until where we parsed the line.
+    end: usize,
+    /// Accumulated state based on the parsed bytes.
+    latest_byte: LatestByte,
+}
+
+impl LineParser {
+    fn new(start: usize) -> Self {
+        Self {
+            start,
+            end: start,
+            latest_byte: LatestByte::Other,
+        }
+    }
+
+    fn parse(&mut self, unprocessed_bytes: &VecDeque<u8>) -> (usize, Option<FragmentInfo>) {
+        let mut parsed_byte_count = 0;
+        let mut parsed_line = None;
+
+        // Parse next byte
+        for &next_byte in unprocessed_bytes {
+            parsed_byte_count += 1;
+            self.end += 1;
+
+            self.latest_byte = match self.latest_byte {
+                LatestByte::Other => match next_byte {
+                    b'\r' => LatestByte::Cr { announcement: None },
+                    b'\n' => {
+                        parsed_line = Some(FragmentInfo::Line {
+                            start: self.start,
+                            end: self.end,
+                            announcement: None,
+                            ending: LineEnding::Lf,
+                        });
+                        LatestByte::Other
+                    }
+                    b'{' => LatestByte::OpeningBracket,
+                    _ => LatestByte::Other,
+                },
+                LatestByte::OpeningBracket => match next_byte {
+                    b'\r' => LatestByte::Cr { announcement: None },
+                    b'\n' => {
+                        parsed_line = Some(FragmentInfo::Line {
+                            start: self.start,
+                            end: self.end,
+                            announcement: None,
+                            ending: LineEnding::Lf,
+                        });
+                        LatestByte::Other
+                    }
+                    b'{' => LatestByte::OpeningBracket,
+                    b'0'..=b'9' => {
+                        let digit = (next_byte - b'0') as u32;
+                        LatestByte::Digit { length: digit }
+                    }
+                    _ => LatestByte::Other,
+                },
+                LatestByte::Plus { length } => match next_byte {
+                    b'\r' => LatestByte::Cr { announcement: None },
+                    b'\n' => {
+                        parsed_line = Some(FragmentInfo::Line {
+                            start: self.start,
+                            end: self.end,
+                            announcement: None,
+                            ending: LineEnding::Lf,
+                        });
+                        LatestByte::Other
+                    }
+                    b'{' => LatestByte::OpeningBracket,
+                    b'}' => LatestByte::ClosingBracket {
+                        announcement: LiteralAnnouncement {
+                            mode: LiteralMode::NonSync,
+                            length,
+                        },
+                    },
+                    _ => LatestByte::Other,
+                },
+                LatestByte::Digit { length } => match next_byte {
+                    b'\r' => LatestByte::Cr { announcement: None },
+                    b'\n' => {
+                        parsed_line = Some(FragmentInfo::Line {
+                            start: self.start,
+                            end: self.end,
+                            announcement: None,
+                            ending: LineEnding::Lf,
+                        });
+                        LatestByte::Other
+                    }
+                    b'{' => LatestByte::OpeningBracket,
+                    b'0'..=b'9' => {
+                        let digit = (next_byte - b'0') as u32;
+                        let new_length = length.checked_mul(10).and_then(|x| x.checked_add(digit));
+                        match new_length {
+                            None => LatestByte::Other,
+                            Some(length) => LatestByte::Digit { length },
+                        }
+                    }
+                    b'+' => LatestByte::Plus { length },
+                    b'}' => LatestByte::ClosingBracket {
+                        announcement: LiteralAnnouncement {
+                            mode: LiteralMode::Sync,
+                            length,
+                        },
+                    },
+                    _ => LatestByte::Other,
+                },
+                LatestByte::ClosingBracket { announcement } => match next_byte {
+                    b'\r' => LatestByte::Cr {
+                        announcement: Some(announcement),
+                    },
+                    b'\n' => {
+                        parsed_line = Some(FragmentInfo::Line {
+                            start: self.start,
+                            end: self.end,
+                            announcement: Some(announcement),
+                            ending: LineEnding::Lf,
+                        });
+                        LatestByte::Other
+                    }
+                    b'{' => LatestByte::OpeningBracket,
+                    _ => LatestByte::Other,
+                },
+                LatestByte::Cr { announcement } => match next_byte {
+                    b'\r' => LatestByte::Cr { announcement: None },
+                    b'\n' => {
+                        parsed_line = Some(FragmentInfo::Line {
+                            start: self.start,
+                            end: self.end,
+                            announcement,
+                            ending: LineEnding::CrLf,
+                        });
+                        LatestByte::Other
+                    }
+                    b'{' => LatestByte::OpeningBracket,
+                    _ => LatestByte::Other,
+                },
+            };
+
+            if parsed_line.is_some() {
+                // We parsed a complete line
+                break;
+            }
+        }
+
+        (parsed_byte_count, parsed_line)
+    }
+}
+
+/// The latest byte seen by the [`LineParser`] with additional accumulated state.
+#[derive(Clone, Debug)]
+enum LatestByte {
+    Other,
+    OpeningBracket,
+    Digit {
+        length: u32,
+    },
+    Plus {
+        length: u32,
+    },
+    ClosingBracket {
+        announcement: LiteralAnnouncement,
+    },
+    Cr {
+        announcement: Option<LiteralAnnouncement>,
+    },
+}
+
+/// Stateful parser for the next literal fragment.
+#[derive(Clone, Debug)]
+struct LiteralParser {
+    /// Where we started parsing the literal.
+    start: usize,
+    /// Until where we parsed the literal.
+    end: usize,
+    /// Remaining bytes we need to parse.
+    remaining: u32,
+}
+
+impl LiteralParser {
+    fn new(start: usize, length: u32) -> Self {
+        Self {
+            start,
+            end: start,
+            remaining: length,
+        }
+    }
+
+    fn parse(&mut self, unprocessed_bytes: &VecDeque<u8>) -> (usize, Option<FragmentInfo>) {
+        if unprocessed_bytes.len() < self.remaining as usize {
+            // Not enough bytes yet
+            let parsed_byte_count = unprocessed_bytes.len();
+            self.end += parsed_byte_count;
+            self.remaining -= parsed_byte_count as u32;
+            (parsed_byte_count, None)
+        } else {
+            // There are enough bytes
+            let parsed_byte_count = self.remaining as usize;
+            self.end += parsed_byte_count;
+            self.remaining = 0;
+            let parsed_literal = FragmentInfo::Literal {
+                start: self.start,
+                end: self.end,
+            };
+            (parsed_byte_count, Some(parsed_literal))
+        }
+    }
+}
+
+fn parse_tag(message_bytes: &[u8]) -> Option<Tag> {
+    let mut bytes = message_bytes.iter().enumerate();
+    let sp = loop {
+        let (i, byte) = bytes.next()?;
+        match byte {
+            // A tag is always delimited by SP
+            b' ' => break i,
+            // End of line reached
+            b'\n' => return None,
+            // Parse more bytes
+            _ => continue,
+        }
+    };
+
+    Tag::try_from(&message_bytes[..sp]).ok()
+}
+
+#[cfg(test)]
+mod tests {
+    use core::panic;
+    use std::collections::VecDeque;
+
+    use imap_types::{
+        command::{Command, CommandBody},
+        core::{LiteralMode, Tag},
+        secret::Secret,
+    };
+
+    use super::{
+        parse_tag, FragmentInfo, Fragmentizer, LineEnding, LineParser, LiteralAnnouncement,
+        MaxMessageSize,
+    };
+    use crate::{
+        decode::ResponseDecodeError, fragmentizer::DecodeMessageError, CommandCodec, ResponseCodec,
+    };
+
+    #[test]
+    fn fragmentizer_progress_nothing() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+
+        fragmentizer.enqueue_bytes(&[]);
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[test]
+    fn fragmentizer_progress_single_message() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+        fragmentizer.enqueue_bytes(b"* OK ...\r\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 10,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"* OK ...\r\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[test]
+    fn fragmentizer_progress_multiple_messages() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+        fragmentizer.enqueue_bytes(b"A1 OK ...\r\n");
+        fragmentizer.enqueue_bytes(b"A2 BAD ...\r\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 11,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"A1 OK ...\r\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 12,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A2 BAD ...\r\n"
+        );
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[test]
+    fn fragmentizer_progress_multiple_messages_with_lf() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+        fragmentizer.enqueue_bytes(b"A1 NOOP\n");
+        fragmentizer.enqueue_bytes(b"A2 LOGIN {5}\n");
+        fragmentizer.enqueue_bytes(b"ABCDE");
+        fragmentizer.enqueue_bytes(b" EFGIJ\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 8,
+                announcement: None,
+                ending: LineEnding::Lf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"A1 NOOP\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 13,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5
+                }),
+                ending: LineEnding::Lf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A2 LOGIN {5}\n"
+        );
+        assert!(!fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(fragment_info, FragmentInfo::Literal { start: 13, end: 18 });
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"ABCDE");
+        assert!(!fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 18,
+                end: 25,
+                announcement: None,
+                ending: LineEnding::Lf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b" EFGIJ\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[test]
+    fn fragmentizer_progress_message_with_multiple_literals() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+        fragmentizer.enqueue_bytes(b"A1 LOGIN {5}\r\n");
+        fragmentizer.enqueue_bytes(b"ABCDE");
+        fragmentizer.enqueue_bytes(b" {5}\r\n");
+        fragmentizer.enqueue_bytes(b"FGHIJ");
+        fragmentizer.enqueue_bytes(b"\r\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 14,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5,
+                }),
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A1 LOGIN {5}\r\n"
+        );
+        assert!(!fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(fragment_info, FragmentInfo::Literal { start: 14, end: 19 });
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"ABCDE");
+        assert!(!fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 19,
+                end: 25,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5,
+                }),
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b" {5}\r\n");
+        assert!(!fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(fragment_info, FragmentInfo::Literal { start: 25, end: 30 });
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"FGHIJ");
+        assert!(!fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 30,
+                end: 32,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"\r\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[test]
+    fn fragmentizer_progress_message_and_skip_after_literal_announcement() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+        fragmentizer.enqueue_bytes(b"A1 LOGIN {5}\r\n");
+        fragmentizer.enqueue_bytes(b"A2 NOOP\r\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 14,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5,
+                }),
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A1 LOGIN {5}\r\n"
+        );
+        assert!(!fragmentizer.is_message_complete());
+
+        fragmentizer.skip_message();
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 9,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"A2 NOOP\r\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[test]
+    fn fragmentizer_progress_message_byte_by_byte() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Unlimited);
+        let mut bytes = VecDeque::new();
+        bytes.extend(b"A1 LOGIN {5}\r\n");
+        bytes.extend(b"ABCDE");
+        bytes.extend(b" FGHIJ\r\n");
+
+        for _ in 0..14 {
+            let fragment_info = fragmentizer.progress();
+
+            assert_eq!(fragment_info, None);
+            assert!(!fragmentizer.is_message_complete());
+
+            fragmentizer.enqueue_bytes(&[bytes.pop_front().unwrap()]);
+        }
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 14,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5,
+                }),
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A1 LOGIN {5}\r\n"
+        );
+        assert!(!fragmentizer.is_message_complete());
+
+        for _ in 0..5 {
+            let fragment_info = fragmentizer.progress();
+
+            assert_eq!(fragment_info, None);
+            assert!(!fragmentizer.is_message_complete());
+
+            fragmentizer.enqueue_bytes(&[bytes.pop_front().unwrap()]);
+        }
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(fragment_info, FragmentInfo::Literal { start: 14, end: 19 });
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"ABCDE");
+        assert!(!fragmentizer.is_message_complete());
+
+        for _ in 0..8 {
+            let fragment_info = fragmentizer.progress();
+
+            assert_eq!(fragment_info, None);
+            assert!(!fragmentizer.is_message_complete());
+
+            fragmentizer.enqueue_bytes(&[bytes.pop_front().unwrap()]);
+        }
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 19,
+                end: 27,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b" FGHIJ\r\n");
+        assert!(fragmentizer.is_message_complete());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+    }
+
+    #[track_caller]
+    fn assert_is_line(
+        unprocessed_bytes: &[u8],
+        line_byte_count: usize,
+        expected_announcement: Option<LiteralAnnouncement>,
+        expected_ending: LineEnding,
+    ) {
+        let mut line_parser = LineParser::new(0);
+        let unprocessed_bytes = unprocessed_bytes.iter().copied().collect();
+
+        let (parsed_byte_count, fragment_info) = line_parser.parse(&unprocessed_bytes);
+
+        assert_eq!(parsed_byte_count, line_byte_count);
+
+        let Some(FragmentInfo::Line {
+            start,
+            end,
+            announcement,
+            ending,
+        }) = fragment_info
+        else {
+            panic!("Unexpected fragment: {fragment_info:?}");
+        };
+
+        assert_eq!(start, 0);
+        assert_eq!(end, line_byte_count);
+        assert_eq!(announcement, expected_announcement);
+        assert_eq!(ending, expected_ending);
+    }
+
+    #[test]
+    fn fragmentizer_progress_multiple_messages_longer_than_max_size() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Limited(17));
+        fragmentizer.enqueue_bytes(b"A1 NOOP\r\n");
+        fragmentizer.enqueue_bytes(b"A2 LOGIN ABCDE EFGIJ\r\n");
+        fragmentizer.enqueue_bytes(b"A3 LOGIN {5}\r\n");
+        fragmentizer.enqueue_bytes(b"ABCDE");
+        fragmentizer.enqueue_bytes(b" EFGIJ\r\n");
+        fragmentizer.enqueue_bytes(b"A4 LOGIN A B\r\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 9,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"A1 NOOP\r\n");
+        assert_eq!(fragmentizer.message_bytes(), b"A1 NOOP\r\n");
+        assert!(fragmentizer.is_message_complete());
+        assert!(!fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 22,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A2 LOGIN ABCDE EF"
+        );
+        assert_eq!(fragmentizer.message_bytes(), b"A2 LOGIN ABCDE EF");
+        assert!(fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 14,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5
+                }),
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A3 LOGIN {5}\r\n"
+        );
+        assert_eq!(fragmentizer.message_bytes(), b"A3 LOGIN {5}\r\n");
+        assert!(!fragmentizer.is_message_complete());
+        assert!(!fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(fragment_info, FragmentInfo::Literal { start: 14, end: 19 });
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"ABC");
+        assert_eq!(fragmentizer.message_bytes(), b"A3 LOGIN {5}\r\nABC");
+        assert!(!fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 19,
+                end: 27,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"A3 LOGIN {5}\r\nABC");
+        assert!(fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 14,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(
+            fragmentizer.fragment_bytes(fragment_info),
+            b"A4 LOGIN A B\r\n"
+        );
+        assert_eq!(fragmentizer.message_bytes(), b"A4 LOGIN A B\r\n");
+        assert!(fragmentizer.is_message_complete());
+        assert!(!fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+        assert!(!fragmentizer.is_max_message_size_exceeded());
+    }
+
+    #[test]
+    fn fragmentizer_progress_messages_with_zero_max_size() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Limited(0));
+        fragmentizer.enqueue_bytes(b"A1 NOOP\r\n");
+        fragmentizer.enqueue_bytes(b"A2 LOGIN ABCDE EFGIJ\r\n");
+        fragmentizer.enqueue_bytes(b"A3 LOGIN {5}\r\n");
+        fragmentizer.enqueue_bytes(b"ABCDE");
+        fragmentizer.enqueue_bytes(b" EFGIJ\r\n");
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 9,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 22,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 0,
+                end: 14,
+                announcement: Some(LiteralAnnouncement {
+                    mode: LiteralMode::Sync,
+                    length: 5
+                }),
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(fragment_info, FragmentInfo::Literal { start: 14, end: 19 });
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress().unwrap();
+
+        assert_eq!(
+            fragment_info,
+            FragmentInfo::Line {
+                start: 19,
+                end: 27,
+                announcement: None,
+                ending: LineEnding::CrLf,
+            }
+        );
+        assert_eq!(fragmentizer.fragment_bytes(fragment_info), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(fragmentizer.is_message_complete());
+        assert!(fragmentizer.is_max_message_size_exceeded());
+
+        let fragment_info = fragmentizer.progress();
+
+        assert_eq!(fragment_info, None);
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert_eq!(fragmentizer.message_bytes(), b"");
+        assert!(!fragmentizer.is_message_complete());
+        assert!(!fragmentizer.is_max_message_size_exceeded());
+    }
+
+    #[test]
+    fn fragmentizer_decode_message() {
+        let mut fragmentizer = Fragmentizer::new(MaxMessageSize::Limited(10));
+        fragmentizer.enqueue_bytes(b"A1 NOOP\r\n");
+        fragmentizer.enqueue_bytes(b"A2 LOGIN ABCDE EFGIJ\r\n");
+
+        let command_codec = CommandCodec::new();
+        let response_codec = ResponseCodec::new();
+
+        fragmentizer.progress();
+        assert_eq!(
+            fragmentizer.decode_message(&command_codec),
+            Ok(Command::new("A1", CommandBody::Noop).unwrap()),
+        );
+        assert_eq!(
+            fragmentizer.decode_message(&response_codec),
+            Err(DecodeMessageError::DecodingFailure(
+                ResponseDecodeError::Failed
+            )),
+        );
+
+        fragmentizer.progress();
+        assert_eq!(
+            fragmentizer.decode_message(&response_codec),
+            Err(DecodeMessageError::MessageTooLong {
+                initial: Secret::new(b"A2 LOGIN A"),
+            }),
+        );
+    }
+
+    #[track_caller]
+    fn assert_not_line(not_a_line_bytes: &[u8]) {
+        let mut line_parser = LineParser::new(0);
+        let not_a_line_bytes = not_a_line_bytes.iter().copied().collect();
+
+        let (parsed_byte_count, fragment_info) = line_parser.parse(&not_a_line_bytes);
+
+        assert_eq!(parsed_byte_count, not_a_line_bytes.len());
+        assert_eq!(fragment_info, None);
+    }
+
+    #[test]
+    fn parse_line_examples() {
+        assert_not_line(b"");
+
+        assert_not_line(b"foo");
+
+        assert_is_line(b"\n", 1, None, LineEnding::Lf);
+
+        assert_is_line(b"\r\n", 2, None, LineEnding::CrLf);
+
+        assert_is_line(b"\n\r", 1, None, LineEnding::Lf);
+
+        assert_is_line(b"foo\n", 4, None, LineEnding::Lf);
+
+        assert_is_line(b"foo\r\n", 5, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo\n\r", 4, None, LineEnding::Lf);
+
+        assert_is_line(b"foo\nbar\n", 4, None, LineEnding::Lf);
+
+        assert_is_line(b"foo\r\nbar\r\n", 5, None, LineEnding::CrLf);
+
+        assert_is_line(b"\r\nfoo\r\n", 2, None, LineEnding::CrLf);
+
+        assert_is_line(
+            b"{1}\r\n",
+            5,
+            Some(LiteralAnnouncement {
+                length: 1,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(
+            b"{1}\n",
+            4,
+            Some(LiteralAnnouncement {
+                length: 1,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::Lf,
+        );
+
+        assert_is_line(
+            b"foo {1}\r\n",
+            9,
+            Some(LiteralAnnouncement {
+                length: 1,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(
+            b"foo {2} {1}\r\n",
+            13,
+            Some(LiteralAnnouncement {
+                length: 1,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(b"foo {1} \r\n", 10, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo \n {1}\r\n", 5, None, LineEnding::Lf);
+
+        assert_is_line(b"foo {1} foo\r\n", 13, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {1\r\n", 8, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo 1}\r\n", 8, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo { 1}\r\n", 10, None, LineEnding::CrLf);
+
+        assert_is_line(
+            b"foo {{1}\r\n",
+            10,
+            Some(LiteralAnnouncement {
+                length: 1,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(
+            b"foo {42}\r\n",
+            10,
+            Some(LiteralAnnouncement {
+                length: 42,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(
+            b"foo {42+}\r\n",
+            11,
+            Some(LiteralAnnouncement {
+                length: 42,
+                mode: LiteralMode::NonSync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(
+            b"foo +{42}\r\n",
+            11,
+            Some(LiteralAnnouncement {
+                length: 42,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(b"foo {+}\r\n", 9, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {42++}\r\n", 12, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {+42+}\r\n", 12, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {+42}\r\n", 11, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {42}+\r\n", 11, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {-42}\r\n", 11, None, LineEnding::CrLf);
+
+        assert_is_line(b"foo {42-}\r\n", 11, None, LineEnding::CrLf);
+
+        assert_is_line(
+            b"foo {4294967295}\r\n",
+            18,
+            Some(LiteralAnnouncement {
+                length: 4294967295,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+
+        assert_is_line(b"foo {4294967296}\r\n", 18, None, LineEnding::CrLf);
+    }
+
+    #[test]
+    fn parse_line_corner_case() {
+        // According to the IMAP RFC, this line does not announce a literal.
+        // We thought intensively about this corner case and asked different people.
+        // Our conclusion: This corner case is an oversight of the RFC authors and
+        // doesn't appear in the wild. We ignore it for now. If this becomes an issue
+        // in practice then we should implement a detection for "* OK", "* NO" and
+        // "* BAD".
+        // See https://github.com/duesee/imap-codec/issues/432#issuecomment-1962427538
+        assert_is_line(
+            b"* OK {1}\r\n",
+            10,
+            Some(LiteralAnnouncement {
+                length: 1,
+                mode: LiteralMode::Sync,
+            }),
+            LineEnding::CrLf,
+        );
+    }
+
+    #[test]
+    fn parse_tag_examples() {
+        assert_eq!(parse_tag(b"1 NOOP\r\n"), Tag::try_from("1").ok());
+        assert_eq!(parse_tag(b"12 NOOP\r\n"), Tag::try_from("12").ok());
+        assert_eq!(parse_tag(b"123 NOOP\r\n"), Tag::try_from("123").ok());
+        assert_eq!(parse_tag(b"1234 NOOP\r\n"), Tag::try_from("1234").ok());
+        assert_eq!(parse_tag(b"12345 NOOP\r\n"), Tag::try_from("12345").ok());
+
+        assert_eq!(parse_tag(b"A1 NOOP\r\n"), Tag::try_from("A1").ok());
+        assert_eq!(parse_tag(b"A1 NOOP"), Tag::try_from("A1").ok());
+        assert_eq!(parse_tag(b"A1 "), Tag::try_from("A1").ok());
+        assert_eq!(parse_tag(b"A1  "), Tag::try_from("A1").ok());
+        assert_eq!(parse_tag(b"A1 \r\n"), Tag::try_from("A1").ok());
+        assert_eq!(parse_tag(b"A1 \n"), Tag::try_from("A1").ok());
+        assert_eq!(parse_tag(b"A1"), None);
+        assert_eq!(parse_tag(b"A1\r\n"), None);
+        assert_eq!(parse_tag(b"A1\n"), None);
+        assert_eq!(parse_tag(b" \r\n"), None);
+        assert_eq!(parse_tag(b"\r\n"), None);
+        assert_eq!(parse_tag(b""), None);
+        assert_eq!(parse_tag(b" A1 NOOP\r\n"), None);
+    }
+}
diff --git a/imap-codec/src/lib.rs b/imap-codec/src/lib.rs
index 7ad97a71..45f9f892 100644
--- a/imap-codec/src/lib.rs
+++ b/imap-codec/src/lib.rs
@@ -130,6 +130,7 @@ mod status;
 #[cfg(test)]
 mod testing;
 
+pub mod fragmentizer;
 #[cfg(feature = "fuzz")]
 pub mod fuzz {
     pub use crate::core::fuzz_tag_imap;


Test patch: