Skip to main content

codecs/decoding/framing/
chunked_gelf.rs

1use std::{
2    any::Any,
3    collections::HashMap,
4    sync::{Arc, Mutex},
5    time::Duration,
6};
7
8use bytes::{Buf, Bytes, BytesMut};
9use derivative::Derivative;
10use snafu::{ResultExt, Snafu, ensure};
11use tokio::{self, task::JoinHandle};
12use tokio_util::codec::Decoder;
13use tracing::{debug, trace, warn};
14use vector_common::constants::{GZIP_MAGIC, ZLIB_MAGIC};
15use vector_common::decompression::CappedDecoder;
16use vector_config::configurable_component;
17
18use super::{BoxedFramingError, FramingError};
19use crate::{BytesDecoder, StreamDecodingError};
20
21const GELF_MAGIC: &[u8] = &[0x1e, 0x0f];
22const GELF_MAX_TOTAL_CHUNKS: u8 = 128;
23const DEFAULT_TIMEOUT_SECS: f64 = 5.0;
24
25const fn default_timeout_secs() -> f64 {
26    DEFAULT_TIMEOUT_SECS
27}
28
29/// Config used to build a `ChunkedGelfDecoder`.
30#[configurable_component]
31#[derive(Debug, Clone, Default)]
32pub struct ChunkedGelfDecoderConfig {
33    /// Options for the chunked GELF decoder.
34    #[serde(default)]
35    pub chunked_gelf: ChunkedGelfDecoderOptions,
36}
37
38impl ChunkedGelfDecoderConfig {
39    /// Build the `ChunkedGelfDecoder` from this configuration.
40    pub fn build(&self) -> ChunkedGelfDecoder {
41        ChunkedGelfDecoder::new(
42            self.chunked_gelf.timeout_secs,
43            self.chunked_gelf.pending_messages_limit,
44            self.chunked_gelf.max_length,
45            self.chunked_gelf.decompression,
46        )
47    }
48}
49
50/// Options for building a `ChunkedGelfDecoder`.
51#[configurable_component]
52#[derive(Clone, Debug, Derivative)]
53#[derivative(Default)]
54pub struct ChunkedGelfDecoderOptions {
55    /// The timeout, in seconds, for a message to be fully received. If the timeout is reached, the
56    /// decoder drops all received chunks for the timed-out message.
57    #[serde(default = "default_timeout_secs")]
58    #[derivative(Default(value = "default_timeout_secs()"))]
59    pub timeout_secs: f64,
60
61    /// The maximum number of pending incomplete messages. If this limit is reached, the decoder starts
62    /// dropping chunks of new messages, ensuring the memory usage of the decoder's state is bounded.
63    /// If this option is not set, the decoder does not limit the number of pending messages and the memory usage
64    /// of its messages buffer can grow unbounded. This matches Graylog Server's behavior.
65    #[serde(default, skip_serializing_if = "vector_core::serde::is_default")]
66    pub pending_messages_limit: Option<usize>,
67
68    /// The maximum length of a single GELF message, in bytes. Messages longer than this length are
69    /// dropped. If this option is not set, the decoder does not limit the length of messages and
70    /// the per-message memory is unbounded.
71    ///
72    /// **Note**: A message can be composed of multiple chunks, and this limit applies to the whole
73    /// message, not to individual chunks.
74    ///
75    /// This limit takes into account only the message payload. GELF header bytes are excluded from the calculation.
76    /// The message payload is the concatenation of all chunk payloads.
77    #[serde(default, skip_serializing_if = "vector_core::serde::is_default")]
78    pub max_length: Option<usize>,
79
80    /// Decompression configuration for GELF messages.
81    #[serde(default, skip_serializing_if = "vector_core::serde::is_default")]
82    pub decompression: ChunkedGelfDecompressionConfig,
83}
84
85/// Decompression options for ChunkedGelfDecoder.
86#[configurable_component]
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
88pub enum ChunkedGelfDecompressionConfig {
89    /// Automatically detect the decompression method based on the magic bytes of the message.
90    #[default]
91    Auto,
92    /// Use Gzip decompression.
93    Gzip,
94    /// Use Zlib decompression.
95    Zlib,
96    /// Do not decompress the message.
97    None,
98}
99
100impl ChunkedGelfDecompressionConfig {
101    pub fn get_decompression(&self, data: &Bytes) -> ChunkedGelfDecompression {
102        match self {
103            Self::Auto => ChunkedGelfDecompression::from_magic(data),
104            Self::Gzip => ChunkedGelfDecompression::Gzip,
105            Self::Zlib => ChunkedGelfDecompression::Zlib,
106            Self::None => ChunkedGelfDecompression::None,
107        }
108    }
109}
110
111#[derive(Debug)]
112struct MessageState {
113    total_chunks: u8,
114    chunks: [Bytes; GELF_MAX_TOTAL_CHUNKS as usize],
115    chunks_bitmap: u128,
116    current_length: usize,
117    timeout_task: JoinHandle<()>,
118}
119
120impl MessageState {
121    pub const fn new(total_chunks: u8, timeout_task: JoinHandle<()>) -> Self {
122        Self {
123            total_chunks,
124            chunks: [const { Bytes::new() }; GELF_MAX_TOTAL_CHUNKS as usize],
125            chunks_bitmap: 0,
126            current_length: 0,
127            timeout_task,
128        }
129    }
130
131    fn is_chunk_present(&self, sequence_number: u8) -> bool {
132        let chunk_bitmap_id = 1 << sequence_number;
133        self.chunks_bitmap & chunk_bitmap_id != 0
134    }
135
136    fn add_chunk(&mut self, sequence_number: u8, chunk: Bytes) {
137        let chunk_bitmap_id = 1 << sequence_number;
138        self.chunks_bitmap |= chunk_bitmap_id;
139        self.current_length += chunk.remaining();
140        self.chunks[sequence_number as usize] = Bytes::copy_from_slice(&chunk);
141    }
142
143    fn is_complete(&self) -> bool {
144        self.chunks_bitmap.count_ones() == self.total_chunks as u32
145    }
146
147    fn current_length(&self) -> usize {
148        self.current_length
149    }
150
151    /// Peak is ~2x the message: a contiguous destination coexists with the chunks it copies
152    /// from. Reserving exactly keeps a growing buffer from adding slack on top of that.
153    fn retrieve_message(&mut self) -> Option<Bytes> {
154        if !self.is_complete() {
155            return None;
156        }
157
158        self.timeout_task.abort();
159        let mut message = BytesMut::with_capacity(self.current_length);
160        for chunk in &mut self.chunks[0..self.total_chunks as usize] {
161            message.extend_from_slice(chunk);
162            *chunk = Bytes::new();
163        }
164        Some(message.freeze())
165    }
166}
167
168#[derive(Debug, PartialEq, Eq)]
169pub enum ChunkedGelfDecompression {
170    Gzip,
171    Zlib,
172    None,
173}
174
175impl ChunkedGelfDecompression {
176    pub fn from_magic(data: &Bytes) -> Self {
177        if data.starts_with(GZIP_MAGIC) {
178            trace!("Detected Gzip compression");
179            return Self::Gzip;
180        }
181
182        if data.starts_with(ZLIB_MAGIC) {
183            // Based on https://datatracker.ietf.org/doc/html/rfc1950#section-2.2
184            if let Some([first_byte, second_byte]) = data.get(0..2)
185                && (*first_byte as u16 * 256 + *second_byte as u16).is_multiple_of(31)
186            {
187                trace!("Detected Zlib compression");
188                return Self::Zlib;
189            };
190
191            warn!(
192                "Detected Zlib magic bytes but the header is invalid: {:?}",
193                data.get(0..2)
194            );
195        };
196
197        trace!("No compression detected",);
198        Self::None
199    }
200
201    pub fn decompress(&self, data: Bytes) -> Result<Bytes, ChunkedGelfDecompressionError> {
202        let decompressed = match self {
203            Self::Gzip => CappedDecoder::gzip(data.reader())
204                .decompress()
205                .map(Bytes::from)
206                .context(GzipDecompressionSnafu)?,
207            Self::Zlib => CappedDecoder::zlib(data.reader())
208                .decompress()
209                .map(Bytes::from)
210                .context(ZlibDecompressionSnafu)?,
211            Self::None => data,
212        };
213        Ok(decompressed)
214    }
215}
216
217#[derive(Debug, Snafu)]
218pub enum ChunkedGelfDecompressionError {
219    #[snafu(display("Gzip decompression error: {source}"))]
220    GzipDecompression { source: std::io::Error },
221    #[snafu(display("Zlib decompression error: {source}"))]
222    ZlibDecompression { source: std::io::Error },
223}
224
225#[derive(Debug, Snafu)]
226pub enum ChunkedGelfDecoderError {
227    #[snafu(display("Invalid chunk header with less than 10 bytes: 0x{header:0x}"))]
228    InvalidChunkHeader { header: Bytes },
229    #[snafu(display(
230        "Received chunk with message id {message_id} and sequence number {sequence_number} has an invalid total chunks value of {total_chunks}. It must be between 1 and {GELF_MAX_TOTAL_CHUNKS}."
231    ))]
232    InvalidTotalChunks {
233        message_id: u64,
234        sequence_number: u8,
235        total_chunks: u8,
236    },
237    #[snafu(display(
238        "Received chunk with message id {message_id} and sequence number {sequence_number} has a sequence number greater than its total chunks value of {total_chunks}"
239    ))]
240    InvalidSequenceNumber {
241        message_id: u64,
242        sequence_number: u8,
243        total_chunks: u8,
244    },
245    #[snafu(display(
246        "Pending messages limit of {pending_messages_limit} reached while processing chunk with message id {message_id} and sequence number {sequence_number}"
247    ))]
248    PendingMessagesLimitReached {
249        message_id: u64,
250        sequence_number: u8,
251        pending_messages_limit: usize,
252    },
253    #[snafu(display(
254        "Received chunk with message id {message_id} and sequence number {sequence_number} has different total chunks values: original total chunks value is {original_total_chunks} and received total chunks value is {received_total_chunks}"
255    ))]
256    TotalChunksMismatch {
257        message_id: u64,
258        sequence_number: u8,
259        original_total_chunks: u8,
260        received_total_chunks: u8,
261    },
262    #[snafu(display(
263        "Message with id {message_id} has exceeded the maximum message length and it will be dropped: got {length} bytes and max message length is {max_length} bytes. Discarding all buffered chunks of that message"
264    ))]
265    MaxLengthExceed {
266        message_id: u64,
267        sequence_number: u8,
268        length: usize,
269        max_length: usize,
270    },
271    #[snafu(display("Error while decompressing message. {source}"))]
272    Decompression {
273        source: ChunkedGelfDecompressionError,
274    },
275}
276
277impl StreamDecodingError for ChunkedGelfDecoderError {
278    fn can_continue(&self) -> bool {
279        true
280    }
281}
282
283impl FramingError for ChunkedGelfDecoderError {
284    fn as_any(&self) -> &dyn Any {
285        self as &dyn Any
286    }
287}
288
289/// A codec for handling GELF messages that may be chunked. The implementation is based on [Graylog's GELF documentation](https://go2docs.graylog.org/5-0/getting_in_log_data/gelf.html#GELFviaUDP)
290/// and [Graylog's go-gelf library](https://github.com/Graylog2/go-gelf/blob/v1/gelf/reader.go).
291#[derive(Debug, Clone)]
292pub struct ChunkedGelfDecoder {
293    // We have to use this decoder to read all the bytes from the buffer first and don't let tokio
294    // read it buffered, as tokio FramedRead will not always call the decode method with the
295    // whole message. (see https://docs.rs/tokio-util/latest/src/tokio_util/codec/framed_impl.rs.html#26).
296    // This limitation is due to the fact that the GELF format does not specify the length of the
297    // message, so we have to read all the bytes from the message (datagram)
298    bytes_decoder: BytesDecoder,
299    decompression_config: ChunkedGelfDecompressionConfig,
300    state: Arc<Mutex<HashMap<u64, Box<MessageState>>>>,
301    timeout: Duration,
302    pending_messages_limit: Option<usize>,
303    max_length: Option<usize>,
304}
305
306impl ChunkedGelfDecoder {
307    /// Creates a new `ChunkedGelfDecoder`.
308    pub fn new(
309        timeout_secs: f64,
310        pending_messages_limit: Option<usize>,
311        max_length: Option<usize>,
312        decompression_config: ChunkedGelfDecompressionConfig,
313    ) -> Self {
314        Self {
315            bytes_decoder: BytesDecoder::new(),
316            decompression_config,
317            state: Arc::new(Mutex::new(HashMap::new())),
318            timeout: Duration::from_secs_f64(timeout_secs),
319            pending_messages_limit,
320            max_length,
321        }
322    }
323
324    /// Decode a GELF chunk
325    pub fn decode_chunk(
326        &mut self,
327        mut chunk: Bytes,
328    ) -> Result<Option<Bytes>, ChunkedGelfDecoderError> {
329        // Encoding scheme:
330        //
331        // +------------+-----------------+--------------+----------------------+
332        // | Message id | Sequence number | Total chunks |    Chunk payload     |
333        // +------------+-----------------+--------------+----------------------+
334        // | 64 bits    | 8 bits          | 8 bits       | remaining bits       |
335        // +------------+-----------------+--------------+----------------------+
336        //
337        // As this codec is oriented for UDP, the chunks (datagrams) are not guaranteed to be received in order,
338        // nor to be received at all. So, we have to store the chunks in a buffer (state field) until we receive
339        // all the chunks of a message. When we receive all the chunks of a message, we can concatenate them
340        // and return the complete payload.
341
342        // We need 10 bytes to read the message id, sequence number and total chunks
343        ensure!(
344            chunk.remaining() >= 10,
345            InvalidChunkHeaderSnafu { header: chunk }
346        );
347
348        let message_id = chunk.get_u64();
349        let sequence_number = chunk.get_u8();
350        let total_chunks = chunk.get_u8();
351
352        ensure!(
353            total_chunks > 0 && total_chunks <= GELF_MAX_TOTAL_CHUNKS,
354            InvalidTotalChunksSnafu {
355                message_id,
356                sequence_number,
357                total_chunks
358            }
359        );
360
361        ensure!(
362            sequence_number < total_chunks,
363            InvalidSequenceNumberSnafu {
364                message_id,
365                sequence_number,
366                total_chunks
367            }
368        );
369
370        let mut state_lock = self.state.lock().expect("poisoned lock");
371
372        // Only a new message grows the table, so the limit applies on insert. Checking it
373        // before the lookup rejected chunks of messages already pending, which could then
374        // never complete and expired instead.
375        if !state_lock.contains_key(&message_id)
376            && let Some(pending_messages_limit) = self.pending_messages_limit
377        {
378            ensure!(
379                state_lock.len() < pending_messages_limit,
380                PendingMessagesLimitReachedSnafu {
381                    message_id,
382                    sequence_number,
383                    pending_messages_limit
384                }
385            );
386        }
387
388        let message_state = state_lock.entry(message_id).or_insert_with(|| {
389            // We need to spawn a task that will clear the message state after a certain time
390            // otherwise we will have a memory leak due to messages that never complete
391            let state = Arc::clone(&self.state);
392            let timeout = self.timeout;
393            let timeout_handle = tokio::spawn(async move {
394                tokio::time::sleep(timeout).await;
395                let mut state_lock = state.lock().expect("poisoned lock");
396                if state_lock.remove(&message_id).is_some() {
397                    warn!(
398                        message_id = message_id,
399                        timeout_secs = timeout.as_secs_f64(),
400                        "Message was not fully received within the timeout window. Discarding it."
401                    );
402                }
403            });
404            Box::new(MessageState::new(total_chunks, timeout_handle))
405        });
406
407        ensure!(
408            message_state.total_chunks == total_chunks,
409            TotalChunksMismatchSnafu {
410                message_id,
411                sequence_number,
412                original_total_chunks: message_state.total_chunks,
413                received_total_chunks: total_chunks
414            }
415        );
416
417        if message_state.is_chunk_present(sequence_number) {
418            debug!(
419                message_id = message_id,
420                sequence_number = sequence_number,
421                "Received a duplicate chunk. Ignoring it."
422            );
423            return Ok(None);
424        }
425
426        message_state.add_chunk(sequence_number, chunk);
427
428        if let Some(max_length) = self.max_length {
429            let length = message_state.current_length();
430            if length > max_length {
431                // Abort on removal, or the task outlives its entry and the live-task count is
432                // no longer bounded by `pending_messages_limit`.
433                if let Some(dropped) = state_lock.remove(&message_id) {
434                    dropped.timeout_task.abort();
435                }
436                return Err(ChunkedGelfDecoderError::MaxLengthExceed {
437                    message_id,
438                    sequence_number,
439                    length,
440                    max_length,
441                });
442            }
443        }
444
445        if let Some(message) = message_state.retrieve_message() {
446            state_lock.remove(&message_id);
447            Ok(Some(message))
448        } else {
449            Ok(None)
450        }
451    }
452
453    /// Decode a GELF message that may be chunked or not. The source bytes are expected to be
454    /// datagram-based (or message-based), so it must not contain multiple GELF messages
455    /// delimited by '\0', such as it would be in a stream-based protocol.
456    pub fn decode_message(
457        &mut self,
458        mut src: Bytes,
459    ) -> Result<Option<Bytes>, ChunkedGelfDecoderError> {
460        let message = if src.starts_with(GELF_MAGIC) {
461            trace!("Received a chunked GELF message based on the magic bytes");
462            src.advance(2);
463            self.decode_chunk(src)?
464        } else {
465            // Slice defensively: a frame here is only known to be non-empty, and one shorter
466            // than the magic is reachable from an unauthenticated sender.
467            trace!(
468                "Received an unchunked GELF message. First bytes of message: {:?}",
469                &src[..src.len().min(GELF_MAGIC.len())]
470            );
471            Some(src)
472        };
473
474        // We can have both chunked and unchunked messages that are compressed
475        message
476            .map(|message| {
477                self.decompression_config
478                    .get_decompression(&message)
479                    .decompress(message)
480                    .context(DecompressionSnafu)
481            })
482            .transpose()
483    }
484}
485
486impl Default for ChunkedGelfDecoder {
487    fn default() -> Self {
488        Self::new(
489            DEFAULT_TIMEOUT_SECS,
490            None,
491            None,
492            ChunkedGelfDecompressionConfig::Auto,
493        )
494    }
495}
496
497impl Decoder for ChunkedGelfDecoder {
498    type Item = Bytes;
499
500    type Error = BoxedFramingError;
501
502    fn decode(&mut self, src: &mut bytes::BytesMut) -> Result<Option<Self::Item>, Self::Error> {
503        if src.is_empty() {
504            return Ok(None);
505        }
506
507        Ok(self
508            .bytes_decoder
509            .decode(src)?
510            .and_then(|frame| self.decode_message(frame).transpose())
511            .transpose()?)
512    }
513    fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
514        if buf.is_empty() {
515            return Ok(None);
516        }
517
518        Ok(self
519            .bytes_decoder
520            .decode_eof(buf)?
521            .and_then(|frame| self.decode_message(frame).transpose())
522            .transpose()?)
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use std::{fmt::Write as FmtWrite, io::Write as IoWrite};
529
530    use bytes::{BufMut, BytesMut};
531    use flate2::write::{GzEncoder, ZlibEncoder};
532    use rand::{SeedableRng, rngs::SmallRng, seq::SliceRandom};
533    use rstest::{fixture, rstest};
534    use tracing_test::traced_test;
535
536    use super::*;
537
538    pub enum Compression {
539        Gzip,
540        Zlib,
541    }
542
543    impl Compression {
544        pub fn compress(&self, payload: &impl AsRef<[u8]>) -> Bytes {
545            self.compress_with_level(payload, flate2::Compression::default())
546        }
547
548        pub fn compress_with_level(
549            &self,
550            payload: &impl AsRef<[u8]>,
551            level: flate2::Compression,
552        ) -> Bytes {
553            match self {
554                Compression::Gzip => {
555                    let mut encoder = GzEncoder::new(Vec::new(), level);
556                    encoder
557                        .write_all(payload.as_ref())
558                        .expect("failed to write to encoder");
559                    encoder.finish().expect("failed to finish encoder").into()
560                }
561                Compression::Zlib => {
562                    let mut encoder = ZlibEncoder::new(Vec::new(), level);
563                    encoder
564                        .write_all(payload.as_ref())
565                        .expect("failed to write to encoder");
566                    encoder.finish().expect("failed to finish encoder").into()
567                }
568            }
569        }
570    }
571
572    fn create_chunk(
573        message_id: u64,
574        sequence_number: u8,
575        total_chunks: u8,
576        payload: &impl AsRef<[u8]>,
577    ) -> BytesMut {
578        let mut chunk = BytesMut::new();
579        chunk.put_slice(GELF_MAGIC);
580        chunk.put_u64(message_id);
581        chunk.put_u8(sequence_number);
582        chunk.put_u8(total_chunks);
583        chunk.extend_from_slice(payload.as_ref());
584        chunk
585    }
586
587    #[fixture]
588    fn unchunked_message() -> (BytesMut, String) {
589        let payload = "foo";
590        (BytesMut::from(payload), payload.to_string())
591    }
592
593    #[fixture]
594    fn two_chunks_message() -> ([BytesMut; 2], String) {
595        let message_id = 1u64;
596        let total_chunks = 2u8;
597
598        let first_sequence_number = 0u8;
599        let first_payload = "foo";
600        let first_chunk = create_chunk(
601            message_id,
602            first_sequence_number,
603            total_chunks,
604            &first_payload,
605        );
606
607        let second_sequence_number = 1u8;
608        let second_payload = "bar";
609        let second_chunk = create_chunk(
610            message_id,
611            second_sequence_number,
612            total_chunks,
613            &second_payload,
614        );
615
616        (
617            [first_chunk, second_chunk],
618            format!("{first_payload}{second_payload}"),
619        )
620    }
621
622    #[fixture]
623    fn three_chunks_message() -> ([BytesMut; 3], String) {
624        let message_id = 2u64;
625        let total_chunks = 3u8;
626
627        let first_sequence_number = 0u8;
628        let first_payload = "foo";
629        let first_chunk = create_chunk(
630            message_id,
631            first_sequence_number,
632            total_chunks,
633            &first_payload,
634        );
635
636        let second_sequence_number = 1u8;
637        let second_payload = "bar";
638        let second_chunk = create_chunk(
639            message_id,
640            second_sequence_number,
641            total_chunks,
642            &second_payload,
643        );
644
645        let third_sequence_number = 2u8;
646        let third_payload = "baz";
647        let third_chunk = create_chunk(
648            message_id,
649            third_sequence_number,
650            total_chunks,
651            &third_payload,
652        );
653
654        (
655            [first_chunk, second_chunk, third_chunk],
656            format!("{first_payload}{second_payload}{third_payload}"),
657        )
658    }
659
660    fn downcast_framing_error(error: &BoxedFramingError) -> &ChunkedGelfDecoderError {
661        error
662            .as_any()
663            .downcast_ref::<ChunkedGelfDecoderError>()
664            .expect("Expected ChunkedGelfDecoderError to be downcasted")
665    }
666
667    #[rstest]
668    #[tokio::test]
669    async fn decode_chunked(two_chunks_message: ([BytesMut; 2], String)) {
670        let (mut chunks, expected_message) = two_chunks_message;
671        let mut decoder = ChunkedGelfDecoder::default();
672
673        let frame = decoder.decode_eof(&mut chunks[0]).unwrap();
674        assert!(frame.is_none());
675
676        let frame = decoder.decode_eof(&mut chunks[1]).unwrap();
677        assert_eq!(frame, Some(Bytes::from(expected_message)));
678    }
679
680    #[rstest]
681    #[tokio::test]
682    async fn decode_unchunked(unchunked_message: (BytesMut, String)) {
683        let (mut message, expected_message) = unchunked_message;
684        let mut decoder = ChunkedGelfDecoder::default();
685
686        let frame = decoder.decode_eof(&mut message).unwrap();
687        assert_eq!(frame, Some(Bytes::from(expected_message)));
688    }
689
690    #[rstest]
691    #[tokio::test]
692    async fn decode_unordered_chunks(two_chunks_message: ([BytesMut; 2], String)) {
693        let (mut chunks, expected_message) = two_chunks_message;
694        let mut decoder = ChunkedGelfDecoder::default();
695
696        let frame = decoder.decode_eof(&mut chunks[1]).unwrap();
697        assert!(frame.is_none());
698
699        let frame = decoder.decode_eof(&mut chunks[0]).unwrap();
700        assert_eq!(frame, Some(Bytes::from(expected_message)));
701    }
702
703    #[rstest]
704    #[tokio::test]
705    async fn decode_unordered_messages(
706        two_chunks_message: ([BytesMut; 2], String),
707        three_chunks_message: ([BytesMut; 3], String),
708    ) {
709        let (mut two_chunks, two_chunks_expected) = two_chunks_message;
710        let (mut three_chunks, three_chunks_expected) = three_chunks_message;
711        let mut decoder = ChunkedGelfDecoder::default();
712
713        let frame = decoder.decode_eof(&mut three_chunks[2]).unwrap();
714        assert!(frame.is_none());
715
716        let frame = decoder.decode_eof(&mut two_chunks[0]).unwrap();
717        assert!(frame.is_none());
718
719        let frame = decoder.decode_eof(&mut three_chunks[0]).unwrap();
720        assert!(frame.is_none());
721
722        let frame = decoder.decode_eof(&mut two_chunks[1]).unwrap();
723        assert_eq!(frame, Some(Bytes::from(two_chunks_expected)));
724
725        let frame = decoder.decode_eof(&mut three_chunks[1]).unwrap();
726        assert_eq!(frame, Some(Bytes::from(three_chunks_expected)));
727    }
728
729    #[rstest]
730    #[tokio::test]
731    async fn decode_mixed_chunked_and_unchunked_messages(
732        unchunked_message: (BytesMut, String),
733        two_chunks_message: ([BytesMut; 2], String),
734    ) {
735        let (mut unchunked_message, expected_unchunked_message) = unchunked_message;
736        let (mut chunks, expected_chunked_message) = two_chunks_message;
737        let mut decoder = ChunkedGelfDecoder::default();
738
739        let frame = decoder.decode_eof(&mut chunks[1]).unwrap();
740        assert!(frame.is_none());
741
742        let frame = decoder.decode_eof(&mut unchunked_message).unwrap();
743        assert_eq!(frame, Some(Bytes::from(expected_unchunked_message)));
744
745        let frame = decoder.decode_eof(&mut chunks[0]).unwrap();
746        assert_eq!(frame, Some(Bytes::from(expected_chunked_message)));
747    }
748
749    #[tokio::test]
750    async fn decode_shuffled_messages() {
751        let mut rng = SmallRng::seed_from_u64(42);
752        let total_chunks = 100u8;
753        let first_message_id = 1u64;
754        let first_payload = "first payload";
755        let second_message_id = 2u64;
756        let second_payload = "second payload";
757        let first_message_chunks = (0..total_chunks).map(|sequence_number| {
758            create_chunk(
759                first_message_id,
760                sequence_number,
761                total_chunks,
762                &first_payload,
763            )
764        });
765        let second_message_chunks = (0..total_chunks).map(|sequence_number| {
766            create_chunk(
767                second_message_id,
768                sequence_number,
769                total_chunks,
770                &second_payload,
771            )
772        });
773        let expected_first_message = first_payload.repeat(total_chunks as usize);
774        let expected_second_message = second_payload.repeat(total_chunks as usize);
775        let mut merged_chunks = first_message_chunks
776            .chain(second_message_chunks)
777            .collect::<Vec<_>>();
778        merged_chunks.shuffle(&mut rng);
779        let mut decoder = ChunkedGelfDecoder::default();
780
781        let mut count = 0;
782        let first_retrieved_message = loop {
783            assert!(count < 2 * total_chunks as usize);
784            if let Some(message) = decoder.decode_eof(&mut merged_chunks[count]).unwrap() {
785                break message;
786            } else {
787                count += 1;
788            }
789        };
790        let second_retrieved_message = loop {
791            assert!(count < 2 * total_chunks as usize);
792            if let Some(message) = decoder.decode_eof(&mut merged_chunks[count]).unwrap() {
793                break message;
794            } else {
795                count += 1
796            }
797        };
798
799        assert_eq!(second_retrieved_message, expected_first_message);
800        assert_eq!(first_retrieved_message, expected_second_message);
801    }
802
803    #[rstest]
804    #[tokio::test(start_paused = true)]
805    #[traced_test]
806    async fn decode_timeout(two_chunks_message: ([BytesMut; 2], String)) {
807        let (mut chunks, _) = two_chunks_message;
808        let mut decoder = ChunkedGelfDecoder::default();
809
810        let frame = decoder.decode_eof(&mut chunks[0]).unwrap();
811        assert!(frame.is_none());
812        assert!(!decoder.state.lock().unwrap().is_empty());
813
814        // The message state should be cleared after a certain time
815        tokio::time::sleep(Duration::from_secs_f64(DEFAULT_TIMEOUT_SECS + 1.0)).await;
816        assert!(decoder.state.lock().unwrap().is_empty());
817        assert!(logs_contain(
818            "Message was not fully received within the timeout window. Discarding it."
819        ));
820
821        let frame = decoder.decode_eof(&mut chunks[1]).unwrap();
822        assert!(frame.is_none());
823
824        tokio::time::sleep(Duration::from_secs_f64(DEFAULT_TIMEOUT_SECS + 1.0)).await;
825        assert!(decoder.state.lock().unwrap().is_empty());
826        assert!(logs_contain(
827            "Message was not fully received within the timeout window. Discarding it"
828        ));
829    }
830
831    #[rstest]
832    #[case::one_byte(&b"x"[..])]
833    #[case::two_bytes(&b"xy"[..])]
834    #[tokio::test]
835    #[traced_test]
836    async fn decode_short_unchunked_frame_does_not_panic(#[case] payload: &[u8]) {
837        // The trace log on that branch formats two bytes. `traced_test` enables the level.
838        let mut src = BytesMut::from(payload);
839        let mut decoder = ChunkedGelfDecoder::default();
840
841        let frame = decoder.decode_eof(&mut src).expect("must not fail");
842
843        assert_eq!(frame, Some(Bytes::copy_from_slice(payload)));
844    }
845
846    #[tokio::test]
847    async fn decode_empty_input() {
848        let mut src = BytesMut::new();
849        let mut decoder = ChunkedGelfDecoder::default();
850
851        let frame = decoder.decode_eof(&mut src).unwrap();
852        assert!(frame.is_none());
853    }
854
855    #[tokio::test]
856    async fn decode_chunk_with_invalid_header() {
857        let mut src = BytesMut::new();
858        src.extend_from_slice(GELF_MAGIC);
859        // Invalid chunk header with less than 10 bytes
860        let invalid_chunk = [0x12, 0x34];
861        src.extend_from_slice(&invalid_chunk);
862        let mut decoder = ChunkedGelfDecoder::default();
863        let frame = decoder.decode_eof(&mut src);
864
865        let error = frame.unwrap_err();
866        let downcasted_error = downcast_framing_error(&error);
867        assert!(matches!(
868            downcasted_error,
869            ChunkedGelfDecoderError::InvalidChunkHeader { .. }
870        ));
871    }
872
873    #[tokio::test]
874    async fn decode_chunk_with_invalid_total_chunks() {
875        let message_id = 1u64;
876        let sequence_number = 1u8;
877        let invalid_total_chunks = GELF_MAX_TOTAL_CHUNKS + 1;
878        let payload = "foo";
879        let mut chunk = create_chunk(message_id, sequence_number, invalid_total_chunks, &payload);
880        let mut decoder = ChunkedGelfDecoder::default();
881
882        let frame = decoder.decode_eof(&mut chunk);
883        let error = frame.unwrap_err();
884        let downcasted_error = downcast_framing_error(&error);
885        assert!(matches!(
886            downcasted_error,
887            ChunkedGelfDecoderError::InvalidTotalChunks {
888                message_id: 1,
889                sequence_number: 1,
890                total_chunks: 129,
891            }
892        ));
893    }
894
895    #[tokio::test]
896    async fn decode_chunk_with_invalid_sequence_number() {
897        let message_id = 1u64;
898        let total_chunks = 2u8;
899        let invalid_sequence_number = total_chunks + 1;
900        let payload = "foo";
901        let mut chunk = create_chunk(message_id, invalid_sequence_number, total_chunks, &payload);
902        let mut decoder = ChunkedGelfDecoder::default();
903
904        let frame = decoder.decode_eof(&mut chunk);
905        let error = frame.unwrap_err();
906        let downcasted_error = downcast_framing_error(&error);
907        assert!(matches!(
908            downcasted_error,
909            ChunkedGelfDecoderError::InvalidSequenceNumber {
910                message_id: 1,
911                sequence_number: 3,
912                total_chunks: 2,
913            }
914        ));
915    }
916
917    #[rstest]
918    #[tokio::test]
919    async fn decode_reached_pending_messages_limit(
920        two_chunks_message: ([BytesMut; 2], String),
921        three_chunks_message: ([BytesMut; 3], String),
922    ) {
923        let (mut two_chunks, _) = two_chunks_message;
924        let (mut three_chunks, _) = three_chunks_message;
925        let mut decoder = ChunkedGelfDecoder {
926            pending_messages_limit: Some(1),
927            ..Default::default()
928        };
929
930        let frame = decoder.decode_eof(&mut two_chunks[0]).unwrap();
931        assert!(frame.is_none());
932        assert!(decoder.state.lock().unwrap().len() == 1);
933
934        let frame = decoder.decode_eof(&mut three_chunks[0]);
935        let error = frame.unwrap_err();
936        let downcasted_error = downcast_framing_error(&error);
937        assert!(matches!(
938            downcasted_error,
939            ChunkedGelfDecoderError::PendingMessagesLimitReached {
940                message_id: 2u64,
941                sequence_number: 0u8,
942                pending_messages_limit: 1,
943            }
944        ));
945        assert!(decoder.state.lock().unwrap().len() == 1);
946    }
947
948    #[rstest]
949    #[tokio::test]
950    async fn decode_accepts_chunks_of_pending_messages_at_the_limit(
951        two_chunks_message: ([BytesMut; 2], String),
952        three_chunks_message: ([BytesMut; 3], String),
953    ) {
954        // The limit bounds how many messages may be pending, not which chunks are accepted.
955        let (mut two_chunks, two_chunks_expected) = two_chunks_message;
956        let (mut three_chunks, _) = three_chunks_message;
957        let mut decoder = ChunkedGelfDecoder {
958            pending_messages_limit: Some(1),
959            ..Default::default()
960        };
961
962        let frame = decoder.decode_eof(&mut two_chunks[0]).unwrap();
963        assert!(frame.is_none());
964        assert_eq!(decoder.state.lock().unwrap().len(), 1);
965
966        // The table is full, so a new message id is rejected.
967        assert!(decoder.decode_eof(&mut three_chunks[0]).is_err());
968
969        // ...but the pending message still completes.
970        let frame = decoder.decode_eof(&mut two_chunks[1]).unwrap();
971        assert_eq!(frame, Some(Bytes::from(two_chunks_expected)));
972        assert_eq!(decoder.state.lock().unwrap().len(), 0);
973    }
974
975    #[rstest]
976    #[tokio::test(start_paused = true)]
977    async fn decode_max_length_exceeded_does_not_leak_timeout_task(
978        two_chunks_message: ([BytesMut; 2], String),
979    ) {
980        // An unaborted task outlives its entry, unbounding the live-task count.
981        let (mut chunks, _) = two_chunks_message;
982        let mut decoder = ChunkedGelfDecoder {
983            max_length: Some(5),
984            ..Default::default()
985        };
986
987        assert!(decoder.decode_eof(&mut chunks[0]).unwrap().is_none());
988        let timeout_task = {
989            let state = decoder.state.lock().unwrap();
990            state
991                .values()
992                .next()
993                .map(|message_state| message_state.timeout_task.abort_handle())
994                .expect("a message should be pending")
995        };
996        assert!(!timeout_task.is_finished());
997
998        assert!(decoder.decode_eof(&mut chunks[1]).is_err());
999        assert_eq!(decoder.state.lock().unwrap().len(), 0);
1000
1001        tokio::task::yield_now().await;
1002        assert!(
1003            timeout_task.is_finished(),
1004            "the timeout task must be aborted when the message is dropped",
1005        );
1006    }
1007
1008    #[tokio::test]
1009    async fn add_chunk_does_not_retain_the_source_buffer() {
1010        // `BytesDecoder` slices the read buffer without copying, so retaining it would pin
1011        // the whole buffer (>= 8 KiB) per chunk.
1012        let mut chunk = create_chunk(1u64, 0u8, 2u8, &"foo");
1013        let source_range = chunk.as_ptr_range();
1014        let mut decoder = ChunkedGelfDecoder::default();
1015
1016        assert!(decoder.decode_eof(&mut chunk).unwrap().is_none());
1017
1018        let state = decoder.state.lock().unwrap();
1019        let message_state = state.values().next().expect("message pending");
1020        let stored = message_state.chunks[0].as_ptr();
1021        assert!(
1022            !source_range.contains(&stored),
1023            "the stored chunk must not alias the decoder's input buffer"
1024        );
1025    }
1026
1027    #[tokio::test]
1028    async fn retrieve_message_releases_chunks_while_assembling() {
1029        // Does not lower the 2x peak, just shortens how long the source side is held.
1030        let mut state = MessageState::new(2, tokio::spawn(async {}));
1031        state.add_chunk(0, Bytes::from_static(b"foo"));
1032        state.add_chunk(1, Bytes::from_static(b"bar"));
1033
1034        let message = state
1035            .retrieve_message()
1036            .expect("message should be complete");
1037
1038        assert_eq!(message, Bytes::from_static(b"foobar"));
1039        assert!(
1040            state.chunks[..2].iter().all(Bytes::is_empty),
1041            "each chunk must be released as it is copied"
1042        );
1043        assert_eq!(state.current_length(), 6);
1044    }
1045
1046    #[rstest]
1047    #[tokio::test]
1048    async fn decode_chunk_with_different_total_chunks() {
1049        let message_id = 1u64;
1050        let sequence_number = 0u8;
1051        let total_chunks = 2u8;
1052        let payload = "foo";
1053        let mut first_chunk = create_chunk(message_id, sequence_number, total_chunks, &payload);
1054        let mut second_chunk =
1055            create_chunk(message_id, sequence_number + 1, total_chunks + 1, &payload);
1056        let mut decoder = ChunkedGelfDecoder::default();
1057
1058        let frame = decoder.decode_eof(&mut first_chunk).unwrap();
1059        assert!(frame.is_none());
1060
1061        let frame = decoder.decode_eof(&mut second_chunk);
1062        let error = frame.unwrap_err();
1063        let downcasted_error = downcast_framing_error(&error);
1064        assert!(matches!(
1065            downcasted_error,
1066            ChunkedGelfDecoderError::TotalChunksMismatch {
1067                message_id: 1,
1068                sequence_number: 1,
1069                original_total_chunks: 2,
1070                received_total_chunks: 3,
1071            }
1072        ));
1073    }
1074
1075    #[rstest]
1076    #[tokio::test]
1077    async fn decode_message_greater_than_max_length(two_chunks_message: ([BytesMut; 2], String)) {
1078        let (mut chunks, _) = two_chunks_message;
1079        let mut decoder = ChunkedGelfDecoder {
1080            max_length: Some(5),
1081            ..Default::default()
1082        };
1083
1084        let frame = decoder.decode_eof(&mut chunks[0]).unwrap();
1085        assert!(frame.is_none());
1086        let frame = decoder.decode_eof(&mut chunks[1]);
1087        let error = frame.unwrap_err();
1088        let downcasted_error = downcast_framing_error(&error);
1089        assert!(matches!(
1090            downcasted_error,
1091            ChunkedGelfDecoderError::MaxLengthExceed {
1092                message_id: 1,
1093                sequence_number: 1,
1094                length: 6,
1095                max_length: 5,
1096            }
1097        ));
1098        assert_eq!(decoder.state.lock().unwrap().len(), 0);
1099    }
1100
1101    #[rstest]
1102    #[tokio::test]
1103    #[traced_test]
1104    async fn decode_duplicated_chunk(two_chunks_message: ([BytesMut; 2], String)) {
1105        let (mut chunks, _) = two_chunks_message;
1106        let mut decoder = ChunkedGelfDecoder::default();
1107
1108        let frame = decoder.decode_eof(&mut chunks[0].clone()).unwrap();
1109        assert!(frame.is_none());
1110
1111        let frame = decoder.decode_eof(&mut chunks[0]).unwrap();
1112        assert!(frame.is_none());
1113        assert!(logs_contain("Received a duplicate chunk. Ignoring it."));
1114    }
1115
1116    #[tokio::test]
1117    #[rstest]
1118    #[case::gzip(Compression::Gzip)]
1119    #[case::zlib(Compression::Zlib)]
1120    async fn decode_compressed_unchunked_message(#[case] compression: Compression) {
1121        let payload = (0..100).fold(String::new(), |mut payload, n| {
1122            write!(payload, "foo{n}").unwrap();
1123            payload
1124        });
1125        let compressed_payload = compression.compress(&payload);
1126        let mut decoder = ChunkedGelfDecoder::default();
1127
1128        let frame = decoder
1129            .decode_eof(&mut compressed_payload.into())
1130            .expect("decoding should not fail")
1131            .expect("decoding should return a frame");
1132
1133        assert_eq!(frame, payload);
1134    }
1135
1136    #[tokio::test]
1137    #[rstest]
1138    #[case::gzip(Compression::Gzip)]
1139    #[case::zlib(Compression::Zlib)]
1140    async fn decode_compressed_chunked_message(#[case] compression: Compression) {
1141        let message_id = 1u64;
1142        let max_chunk_size = 5;
1143        let payload = (0..100).fold(String::new(), |mut payload, n| {
1144            write!(payload, "foo{n}").unwrap();
1145            payload
1146        });
1147        let compressed_payload = compression.compress(&payload);
1148        let total_chunks = compressed_payload.len().div_ceil(max_chunk_size) as u8;
1149        assert!(total_chunks < GELF_MAX_TOTAL_CHUNKS);
1150        let mut chunks = compressed_payload
1151            .chunks(max_chunk_size)
1152            .enumerate()
1153            .map(|(i, chunk)| create_chunk(message_id, i as u8, total_chunks, &chunk))
1154            .collect::<Vec<_>>();
1155        let (last_chunk, first_chunks) =
1156            chunks.split_last_mut().expect("chunks should not be empty");
1157        let mut decoder = ChunkedGelfDecoder::default();
1158
1159        for chunk in first_chunks {
1160            let frame = decoder.decode_eof(chunk).expect("decoding should not fail");
1161            assert!(frame.is_none());
1162        }
1163        let frame = decoder
1164            .decode_eof(last_chunk)
1165            .expect("decoding should not fail")
1166            .expect("decoding should return a frame");
1167
1168        assert_eq!(frame, payload);
1169    }
1170
1171    #[tokio::test]
1172    async fn decode_malformed_gzip_message() {
1173        let mut compressed_payload = BytesMut::new();
1174        compressed_payload.extend(GZIP_MAGIC);
1175        compressed_payload.extend(&[0x12, 0x34, 0x56, 0x78]);
1176        let mut decoder = ChunkedGelfDecoder::default();
1177
1178        let error = decoder
1179            .decode_eof(&mut compressed_payload)
1180            .expect_err("decoding should fail");
1181
1182        let downcasted_error = downcast_framing_error(&error);
1183        assert!(matches!(
1184            downcasted_error,
1185            ChunkedGelfDecoderError::Decompression {
1186                source: ChunkedGelfDecompressionError::GzipDecompression { .. }
1187            }
1188        ));
1189    }
1190
1191    #[tokio::test]
1192    async fn decode_malformed_zlib_message() {
1193        let mut compressed_payload = BytesMut::new();
1194        compressed_payload.extend(ZLIB_MAGIC);
1195        compressed_payload.extend(&[0x9c, 0x12, 0x00, 0xFF]);
1196        let mut decoder = ChunkedGelfDecoder::default();
1197
1198        let error = decoder
1199            .decode_eof(&mut compressed_payload)
1200            .expect_err("decoding should fail");
1201
1202        let downcasted_error = downcast_framing_error(&error);
1203        assert!(matches!(
1204            downcasted_error,
1205            ChunkedGelfDecoderError::Decompression {
1206                source: ChunkedGelfDecompressionError::ZlibDecompression { .. }
1207            }
1208        ));
1209    }
1210
1211    #[tokio::test]
1212    async fn decode_zlib_payload_with_zlib_decoder() {
1213        let payload = "foo";
1214        let compressed_payload = Compression::Zlib.compress(&payload);
1215        let mut decoder = ChunkedGelfDecoder {
1216            decompression_config: ChunkedGelfDecompressionConfig::Zlib,
1217            ..Default::default()
1218        };
1219
1220        let frame = decoder
1221            .decode_eof(&mut compressed_payload.into())
1222            .expect("decoding should not fail")
1223            .expect("decoding should return a frame");
1224
1225        assert_eq!(frame, payload);
1226    }
1227
1228    #[tokio::test]
1229    async fn decode_gzip_payload_with_zlib_decoder() {
1230        let payload = "foo";
1231        let compressed_payload = Compression::Gzip.compress(&payload);
1232        let mut decoder = ChunkedGelfDecoder {
1233            decompression_config: ChunkedGelfDecompressionConfig::Zlib,
1234            ..Default::default()
1235        };
1236
1237        let error = decoder
1238            .decode_eof(&mut compressed_payload.into())
1239            .expect_err("decoding should fail");
1240
1241        let downcasted_error = downcast_framing_error(&error);
1242        assert!(matches!(
1243            downcasted_error,
1244            ChunkedGelfDecoderError::Decompression {
1245                source: ChunkedGelfDecompressionError::ZlibDecompression { .. }
1246            }
1247        ));
1248    }
1249
1250    #[tokio::test]
1251    async fn decode_uncompressed_payload_with_zlib_decoder() {
1252        let payload = "foo";
1253        let mut decoder = ChunkedGelfDecoder {
1254            decompression_config: ChunkedGelfDecompressionConfig::Zlib,
1255            ..Default::default()
1256        };
1257
1258        let error = decoder
1259            .decode_eof(&mut payload.into())
1260            .expect_err("decoding should fail");
1261
1262        let downcasted_error = downcast_framing_error(&error);
1263        assert!(matches!(
1264            downcasted_error,
1265            ChunkedGelfDecoderError::Decompression {
1266                source: ChunkedGelfDecompressionError::ZlibDecompression { .. }
1267            }
1268        ));
1269    }
1270
1271    #[tokio::test]
1272    async fn decode_gzip_payload_with_gzip_decoder() {
1273        let payload = "foo";
1274        let compressed_payload = Compression::Gzip.compress(&payload);
1275        let mut decoder = ChunkedGelfDecoder {
1276            decompression_config: ChunkedGelfDecompressionConfig::Gzip,
1277            ..Default::default()
1278        };
1279
1280        let frame = decoder
1281            .decode_eof(&mut compressed_payload.into())
1282            .expect("decoding should not fail")
1283            .expect("decoding should return a frame");
1284
1285        assert_eq!(frame, payload);
1286    }
1287
1288    #[tokio::test]
1289    async fn decode_zlib_payload_with_gzip_decoder() {
1290        let payload = "foo";
1291        let compressed_payload = Compression::Zlib.compress(&payload);
1292        let mut decoder = ChunkedGelfDecoder {
1293            decompression_config: ChunkedGelfDecompressionConfig::Gzip,
1294            ..Default::default()
1295        };
1296
1297        let error = decoder
1298            .decode_eof(&mut compressed_payload.into())
1299            .expect_err("decoding should fail");
1300
1301        let downcasted_error = downcast_framing_error(&error);
1302        assert!(matches!(
1303            downcasted_error,
1304            ChunkedGelfDecoderError::Decompression {
1305                source: ChunkedGelfDecompressionError::GzipDecompression { .. }
1306            }
1307        ));
1308    }
1309
1310    #[tokio::test]
1311    async fn decode_uncompressed_payload_with_gzip_decoder() {
1312        let payload = "foo";
1313        let mut decoder = ChunkedGelfDecoder {
1314            decompression_config: ChunkedGelfDecompressionConfig::Gzip,
1315            ..Default::default()
1316        };
1317
1318        let error = decoder
1319            .decode_eof(&mut payload.into())
1320            .expect_err("decoding should fail");
1321
1322        let downcasted_error = downcast_framing_error(&error);
1323        assert!(matches!(
1324            downcasted_error,
1325            ChunkedGelfDecoderError::Decompression {
1326                source: ChunkedGelfDecompressionError::GzipDecompression { .. }
1327            }
1328        ));
1329    }
1330
1331    #[tokio::test]
1332    #[rstest]
1333    #[case::gzip(Compression::Gzip)]
1334    #[case::zlib(Compression::Zlib)]
1335    async fn decode_compressed_payload_with_no_decompression_decoder(
1336        #[case] compression: Compression,
1337    ) {
1338        let payload = "foo";
1339        let compressed_payload = compression.compress(&payload);
1340        let mut decoder = ChunkedGelfDecoder {
1341            decompression_config: ChunkedGelfDecompressionConfig::None,
1342            ..Default::default()
1343        };
1344
1345        let frame = decoder
1346            .decode_eof(&mut compressed_payload.clone().into())
1347            .expect("decoding should not fail")
1348            .expect("decoding should return a frame");
1349
1350        assert_eq!(frame, compressed_payload);
1351    }
1352
1353    #[test]
1354    fn detect_gzip_compression() {
1355        let payload = "foo";
1356
1357        for level in 0..=9 {
1358            let level = flate2::Compression::new(level);
1359            let compressed_payload = Compression::Gzip.compress_with_level(&payload, level);
1360            let actual = ChunkedGelfDecompression::from_magic(&compressed_payload);
1361            assert_eq!(
1362                actual,
1363                ChunkedGelfDecompression::Gzip,
1364                "Failed for level {}",
1365                level.level()
1366            );
1367        }
1368    }
1369
1370    #[test]
1371    fn detect_zlib_compression() {
1372        let payload = "foo";
1373
1374        for level in 0..=9 {
1375            let level = flate2::Compression::new(level);
1376            let compressed_payload = Compression::Zlib.compress_with_level(&payload, level);
1377            let actual = ChunkedGelfDecompression::from_magic(&compressed_payload);
1378            assert_eq!(
1379                actual,
1380                ChunkedGelfDecompression::Zlib,
1381                "Failed for level {}",
1382                level.level()
1383            );
1384        }
1385    }
1386
1387    #[test]
1388    fn detect_no_compression() {
1389        let payload = "foo";
1390
1391        let detected_compression = ChunkedGelfDecompression::from_magic(&payload.into());
1392
1393        assert_eq!(detected_compression, ChunkedGelfDecompression::None);
1394    }
1395}