Skip to main content

bgpkit_parser/parser/mrt/
mrt_record.rs

1use super::mrt_header::frame_common_header;
2use crate::bmp::messages::{BmpMessage, BmpMessageBody};
3use crate::error::{check_max, EncodingError, ParserError};
4use crate::models::*;
5use crate::parser::{
6    parse_bgp4mp, parse_legacy_bgp, parse_table_dump_messages, parse_table_dump_v2_message,
7    ParserErrorWithBytes,
8};
9use crate::utils::convert_timestamp;
10use bytes::{BufMut, Bytes, BytesMut};
11use log::{debug, warn};
12use std::convert::TryFrom;
13use std::fs::File;
14use std::io::{Read, Write};
15use std::net::IpAddr;
16use std::path::Path;
17use std::str::FromStr;
18
19/// Upper bound on the MRT body length the parser will read into memory.
20const MAX_MRT_MESSAGE_LEN: u32 = 16 * 1024 * 1024; // 16 MiB
21
22/// Raw MRT record containing the common header and unparsed message bytes.
23/// This allows for lazy parsing of the MRT message body, and provides
24/// utilities for debugging and exporting problematic records.
25#[derive(Debug, Clone)]
26pub struct RawMrtRecord {
27    pub common_header: CommonHeader,
28    /// The raw bytes of the MRT common header (as read from the wire).
29    pub header_bytes: Bytes,
30    /// The raw bytes of the MRT message body (excluding the common header).
31    pub message_bytes: Bytes,
32}
33
34/// Internal framing error that retains MRT header context when it was parsed.
35pub(crate) struct RawMrtRecordError {
36    pub(crate) error: ParserError,
37    pub(crate) common_header: Option<CommonHeader>,
38    pub(crate) bytes: Option<Vec<u8>>,
39    /// A rejected header's declared body was read in full, so the next read starts
40    /// after this record, though not necessarily on a true boundary.
41    pub(crate) record_consumed: bool,
42}
43
44impl RawMrtRecord {
45    /// Parse the raw MRT record into a fully parsed MrtRecord.
46    /// This consumes the RawMrtRecord and returns a MrtRecord.
47    pub fn parse(self) -> Result<MrtRecord, ParserError> {
48        let message = parse_mrt_body(
49            self.common_header.entry_type as u16,
50            self.common_header.entry_subtype,
51            self.message_bytes,
52        )?;
53
54        Ok(MrtRecord {
55            common_header: self.common_header,
56            message,
57        })
58    }
59
60    /// Returns the complete MRT record as raw bytes (header + message body).
61    ///
62    /// This returns the exact bytes as they were read from the wire,
63    /// without any re-encoding. This is useful for debugging problematic
64    /// MRT records by exporting them as-is to a file for further analysis.
65    ///
66    /// # Example
67    /// ```ignore
68    /// let raw_record = parser.into_raw_record_iter().next().unwrap();
69    /// let bytes = raw_record.raw_bytes();
70    /// std::fs::write("record.mrt", &bytes).unwrap();
71    /// ```
72    pub fn raw_bytes(&self) -> Bytes {
73        let mut bytes = BytesMut::with_capacity(self.header_bytes.len() + self.message_bytes.len());
74        bytes.put_slice(&self.header_bytes);
75        bytes.put_slice(&self.message_bytes);
76        bytes.freeze()
77    }
78
79    /// Writes the raw MRT record (header + message body) to a file.
80    ///
81    /// This is useful for extracting problematic MRT records for debugging
82    /// or further analysis with other tools.
83    ///
84    /// # Arguments
85    /// * `path` - The path to write the raw bytes to.
86    ///
87    /// # Example
88    /// ```ignore
89    /// let raw_record = parser.into_raw_record_iter().next().unwrap();
90    /// raw_record.write_raw_bytes("problematic_record.mrt").unwrap();
91    /// ```
92    pub fn write_raw_bytes<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
93        let mut file = File::create(path)?;
94        file.write_all(&self.header_bytes)?;
95        file.write_all(&self.message_bytes)?;
96        Ok(())
97    }
98
99    /// Appends the raw MRT record (header + message body) to a file.
100    ///
101    /// This is useful for collecting multiple problematic records into a single file.
102    ///
103    /// # Arguments
104    /// * `path` - The path to append the raw bytes to.
105    ///
106    /// # Example
107    /// ```ignore
108    /// for raw_record in parser.into_raw_record_iter() {
109    ///     if is_problematic(&raw_record) {
110    ///         raw_record.append_raw_bytes("problematic_records.mrt").unwrap();
111    ///     }
112    /// }
113    /// ```
114    pub fn append_raw_bytes<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
115        let mut file = std::fs::OpenOptions::new()
116            .create(true)
117            .append(true)
118            .open(path)?;
119        file.write_all(&self.header_bytes)?;
120        file.write_all(&self.message_bytes)?;
121        Ok(())
122    }
123
124    /// Returns the total length of the complete MRT record in bytes (header + body).
125    pub fn total_bytes_len(&self) -> usize {
126        self.header_bytes.len() + self.message_bytes.len()
127    }
128}
129
130pub fn chunk_mrt_record(input: &mut impl Read) -> Result<RawMrtRecord, ParserErrorWithBytes> {
131    chunk_mrt_record_with_context(input).map_err(|error| ParserErrorWithBytes {
132        error: error.error,
133        bytes: error.bytes,
134    })
135}
136
137/// Read one raw MRT record while retaining parsed header context on framing errors.
138pub(crate) fn chunk_mrt_record_with_context(
139    input: &mut impl Read,
140) -> Result<RawMrtRecord, RawMrtRecordError> {
141    // read the common header and capture raw bytes
142    let mut consumed_header = Vec::with_capacity(16);
143    let framed = match frame_common_header(&mut CapturingReader {
144        inner: input,
145        captured: &mut consumed_header,
146    }) {
147        Ok(framed) => framed,
148        Err(error) => {
149            if error.kind() == std::io::ErrorKind::UnexpectedEof && consumed_header.is_empty() {
150                return Err(RawMrtRecordError {
151                    error: ParserError::EofExpected,
152                    common_header: None,
153                    bytes: None,
154                    record_consumed: false,
155                });
156            }
157            return Err(RawMrtRecordError {
158                error: error.into(),
159                common_header: None,
160                bytes: Some(consumed_header),
161                record_consumed: false,
162            });
163        }
164    };
165
166    let header_bytes = framed.raw_bytes;
167    let parsed_header = framed.header.as_ref().ok().copied();
168
169    // Protect against unreasonable allocations from corrupt headers
170    if framed.body_length > MAX_MRT_MESSAGE_LEN {
171        return Err(RawMrtRecordError {
172            error: ParserError::OversizedRecord {
173                length: framed.body_length,
174            },
175            common_header: parsed_header,
176            bytes: Some(header_bytes.to_vec()),
177            record_consumed: false,
178        });
179    }
180
181    // Read the declared body while retaining partial bytes on failure.
182    let mut buffer = Vec::with_capacity(framed.body_length as usize + 4);
183    if let Err(error) = input
184        .take(framed.body_length as u64)
185        .read_to_end(&mut buffer)
186    {
187        return Err(record_io_error(
188            error,
189            parsed_header,
190            &header_bytes,
191            &buffer,
192        ));
193    }
194    if buffer.len() != framed.body_length as usize {
195        return Err(record_io_error(
196            std::io::Error::new(
197                std::io::ErrorKind::UnexpectedEof,
198                format!(
199                    "truncated MRT body: expected {} bytes, read {}",
200                    framed.body_length,
201                    buffer.len()
202                ),
203            ),
204            parsed_header,
205            &header_bytes,
206            &buffer,
207        ));
208    }
209
210    // A rejected header is reported only now, so its body is not left in the stream.
211    let common_header = match framed.header {
212        Ok(common_header) => common_header,
213        Err(error) => {
214            return Err(RawMrtRecordError {
215                error,
216                common_header: None,
217                bytes: Some([&header_bytes[..], &buffer].concat()),
218                record_consumed: true,
219            })
220        }
221    };
222
223    if common_header.entry_type == EntryType::TABLE_DUMP
224        && super::messages::table_dump::needs_legacy_length_correction(
225            common_header.entry_subtype,
226            &buffer,
227        )
228    {
229        let mut correction = Vec::with_capacity(4);
230        if let Err(error) = input.take(4).read_to_end(&mut correction) {
231            buffer.extend_from_slice(&correction);
232            return Err(record_io_error(
233                error,
234                Some(common_header),
235                &header_bytes,
236                &buffer,
237            ));
238        }
239        buffer.extend_from_slice(&correction);
240        if correction.len() != 4 {
241            return Err(record_io_error(
242                std::io::Error::new(
243                    std::io::ErrorKind::UnexpectedEof,
244                    "truncated historical TABLE_DUMP length correction",
245                ),
246                Some(common_header),
247                &header_bytes,
248                &buffer,
249            ));
250        }
251        debug!(
252            "recovered historical TABLE_DUMP record whose declared length was four bytes short (timestamp={}, subtype={}, declared_length={})",
253            common_header.timestamp,
254            common_header.entry_subtype,
255            common_header.length
256        );
257    }
258
259    Ok(RawMrtRecord {
260        common_header,
261        header_bytes,
262        message_bytes: Bytes::from(buffer),
263    })
264}
265
266struct CapturingReader<'a, R: ?Sized> {
267    inner: &'a mut R,
268    captured: &'a mut Vec<u8>,
269}
270
271impl<R: Read + ?Sized> Read for CapturingReader<'_, R> {
272    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
273        let read = self.inner.read(buffer)?;
274        self.captured.extend_from_slice(&buffer[..read]);
275        Ok(read)
276    }
277}
278
279fn record_io_error(
280    error: std::io::Error,
281    common_header: Option<CommonHeader>,
282    header: &[u8],
283    body: &[u8],
284) -> RawMrtRecordError {
285    let mut bytes = Vec::with_capacity(header.len() + body.len());
286    bytes.extend_from_slice(header);
287    bytes.extend_from_slice(body);
288    RawMrtRecordError {
289        error: ParserError::IoError(error),
290        common_header,
291        bytes: Some(bytes),
292        record_consumed: false,
293    }
294}
295
296pub fn parse_mrt_record(input: &mut impl Read) -> Result<MrtRecord, ParserErrorWithBytes> {
297    parse_mrt_record_with_zebra_compat(input).map(|(record, _)| record)
298}
299
300pub(crate) fn raw_record_uses_zebra_compat(raw_record: &RawMrtRecord) -> bool {
301    matches!(
302        raw_record.common_header.entry_type,
303        EntryType::BGP4MP | EntryType::BGP4MP_ET
304    ) && crate::parser::mrt::messages::bgp4mp::uses_zebra_compat(
305        raw_record.common_header.entry_subtype,
306        &raw_record.message_bytes,
307    )
308}
309
310pub(crate) fn parse_mrt_record_with_zebra_compat(
311    input: &mut impl Read,
312) -> Result<(MrtRecord, bool), ParserErrorWithBytes> {
313    let raw_record = chunk_mrt_record(input)?;
314    parse_chunked_record_with_zebra_compat(raw_record)
315}
316
317/// Parse an already-chunked raw record instead of reading the next one.
318pub(crate) fn parse_chunked_record_with_zebra_compat(
319    raw_record: RawMrtRecord,
320) -> Result<(MrtRecord, bool), ParserErrorWithBytes> {
321    let used_zebra_compat = raw_record_uses_zebra_compat(&raw_record);
322    // Parse from a clone so the original is available for raw_bytes() on error,
323    // avoiding manual reassembly that could diverge from RawMrtRecord::raw_bytes().
324    match raw_record.clone().parse() {
325        Ok(record) => Ok((record, used_zebra_compat)),
326        Err(e) => Err(ParserErrorWithBytes {
327            error: e,
328            bytes: Some(raw_record.raw_bytes().to_vec()),
329        }),
330    }
331}
332
333/// Parse MRT message body with given entry type and subtype.
334///
335/// The entry type and subtype are parsed from the common header. The message body is parsed
336/// according to the entry type and subtype. The message body is the remaining bytes after the
337/// common header. The length of the message body is also parsed from the common header.
338pub fn parse_mrt_body(
339    entry_type: u16,
340    entry_subtype: u16,
341    data: Bytes,
342) -> Result<MrtMessage, ParserError> {
343    let etype = EntryType::try_from(entry_type)?;
344
345    let message: MrtMessage = match &etype {
346        EntryType::TABLE_DUMP => {
347            let mut messages = parse_table_dump_messages(entry_subtype, data)?;
348            if messages.len() == 1 {
349                MrtMessage::TableDumpMessage(messages.remove(0))
350            } else {
351                MrtMessage::TableDumpMessageBatch(messages)
352            }
353        }
354        EntryType::TABLE_DUMP_V2 => {
355            let msg = parse_table_dump_v2_message(entry_subtype, data);
356            match msg {
357                Ok(msg) => MrtMessage::TableDumpV2Message(msg),
358                Err(e) => {
359                    return Err(e);
360                }
361            }
362        }
363        EntryType::BGP4MP | EntryType::BGP4MP_ET => {
364            let msg = parse_bgp4mp(entry_subtype, data);
365            match msg {
366                Ok(msg) => MrtMessage::Bgp4Mp(msg),
367                Err(e) => {
368                    return Err(e);
369                }
370            }
371        }
372        EntryType::BGP => MrtMessage::LegacyBgp(parse_legacy_bgp(entry_subtype, data)?),
373        v => {
374            // deprecated
375            return Err(ParserError::Unsupported(format!(
376                "unsupported MRT type: {v:?}"
377            )));
378        }
379    };
380    Ok(message)
381}
382
383impl MrtRecord {
384    pub fn encode(&self) -> Result<Bytes, EncodingError> {
385        let message_bytes = self.message.encode(self.common_header.entry_subtype)?;
386        let mut new_header = self.common_header;
387        if message_bytes.len() != new_header.length as usize {
388            warn!(
389                "message length {} does not match the length in the header {} (encoding MRT record)",
390                message_bytes.len(),
391                new_header.length
392            );
393        }
394        check_max(
395            "MRT record message length",
396            message_bytes.len(),
397            u32::MAX as usize,
398        )?;
399        new_header.length = message_bytes.len() as u32;
400        let header_bytes = new_header.encode();
401
402        // // debug begins
403        // let parsed_body = parse_mrt_body(
404        //     self.common_header.entry_type as u16,
405        //     self.common_header.entry_subtype,
406        //     message_bytes.clone(),
407        // )
408        // .unwrap();
409        // assert!(self.message == parsed_body);
410        // // debug ends
411
412        let mut bytes = BytesMut::with_capacity(header_bytes.len() + message_bytes.len());
413        bytes.put_slice(&header_bytes);
414        bytes.put_slice(&message_bytes);
415        Ok(bytes.freeze())
416    }
417}
418
419impl TryFrom<&BmpMessage> for MrtRecord {
420    type Error = String;
421
422    fn try_from(bmp_message: &BmpMessage) -> Result<Self, Self::Error> {
423        let bgp_message = match &bmp_message.message_body {
424            BmpMessageBody::RouteMonitoring(m) => &m.bgp_message,
425            _ => return Err("unsupported bmp message type".to_string()),
426        };
427        let bmp_header = match &bmp_message.per_peer_header {
428            Some(h) => h,
429            None => return Err("missing per peer header".to_string()),
430        };
431
432        let local_ip = match bmp_header.peer_ip {
433            IpAddr::V4(_) => IpAddr::from_str("0.0.0.0").unwrap(),
434            IpAddr::V6(_) => IpAddr::from_str("::").unwrap(),
435        };
436        let local_asn = match bmp_header.peer_asn.is_four_byte() {
437            true => Asn::new_32bit(0),
438            false => Asn::new_16bit(0),
439        };
440
441        let bgp4mp_message = Bgp4MpMessage {
442            msg_type: Bgp4MpType::MessageAs4, // TODO: check Message or MessageAs4
443            peer_asn: bmp_header.peer_asn,
444            local_asn,
445            interface_index: 0,
446            peer_ip: bmp_header.peer_ip,
447            local_ip,
448            bgp_message: bgp_message.clone(),
449        };
450
451        let mrt_message = MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(bgp4mp_message));
452
453        let (seconds, microseconds) = convert_timestamp(bmp_header.timestamp);
454
455        let subtype = Bgp4MpType::MessageAs4 as u16;
456        let encoded_message = mrt_message
457            .encode(subtype)
458            .map_err(|e| format!("cannot encode MRT message: {e}"))?;
459        let mrt_header = CommonHeader {
460            timestamp: seconds,
461            microsecond_timestamp: Some(microseconds),
462            entry_type: EntryType::BGP4MP_ET,
463            entry_subtype: Bgp4MpType::MessageAs4 as u16,
464            length: encoded_message.len() as u32,
465        };
466
467        Ok(MrtRecord {
468            common_header: mrt_header,
469            message: mrt_message,
470        })
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use crate::bmp::messages::headers::{BmpPeerType, PeerFlags, PerPeerFlags};
478    use crate::bmp::messages::{BmpCommonHeader, BmpMsgType, BmpPerPeerHeader, RouteMonitoring};
479    use crate::models::{AttributeValue, Origin};
480    use crate::parser::mrt::messages::table_dump::encode_table_dump_batch;
481    use std::io::Cursor;
482    use std::net::Ipv4Addr;
483    use tempfile::tempdir;
484
485    fn table_dump_message(prefix: &str) -> TableDumpMessage {
486        let mut attributes = Attributes::default();
487        attributes.add_attr(AttributeValue::Origin(Origin::IGP).into());
488        TableDumpMessage {
489            view_number: 0,
490            sequence_number: 1,
491            prefix: prefix.parse().unwrap(),
492            status: 1,
493            originated_time: 946_684_800,
494            peer_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
495            peer_asn: Asn::new_16bit(64512),
496            attributes,
497        }
498    }
499
500    #[test]
501    fn test_raw_mrt_record_raw_bytes() {
502        let header = CommonHeader {
503            timestamp: 1609459200,
504            microsecond_timestamp: None,
505            entry_type: EntryType::BGP4MP,
506            entry_subtype: 4,
507            length: 10,
508        };
509        let header_bytes = Bytes::from_static(&[
510            0x5f, 0xee, 0x6a, 0x80, // timestamp
511            0x00, 0x10, // entry type
512            0x00, 0x04, // entry subtype
513            0x00, 0x00, 0x00, 0x0a, // length
514        ]);
515        let message_bytes = Bytes::from_static(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
516
517        let raw_record = RawMrtRecord {
518            common_header: header,
519            header_bytes,
520            message_bytes,
521        };
522
523        let mrt_bytes = raw_record.raw_bytes();
524        // Header is 12 bytes + 10 bytes body = 22 bytes total
525        assert_eq!(mrt_bytes.len(), 22);
526        assert_eq!(raw_record.total_bytes_len(), 22);
527    }
528
529    #[test]
530    fn test_raw_mrt_record_raw_bytes_with_et() {
531        let header = CommonHeader {
532            timestamp: 1609459200,
533            microsecond_timestamp: Some(500000),
534            entry_type: EntryType::BGP4MP_ET,
535            entry_subtype: 4,
536            length: 10,
537        };
538        let header_bytes = Bytes::from_static(&[
539            0x5f, 0xee, 0x6a, 0x80, // timestamp
540            0x00, 0x11, // entry type (BGP4MP_ET = 17)
541            0x00, 0x04, // entry subtype
542            0x00, 0x00, 0x00, 0x0e, // length (10 + 4 for microseconds)
543            0x00, 0x07, 0xa1, 0x20, // microsecond timestamp (500000)
544        ]);
545        let message_bytes = Bytes::from_static(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
546
547        let raw_record = RawMrtRecord {
548            common_header: header,
549            header_bytes,
550            message_bytes,
551        };
552
553        let mrt_bytes = raw_record.raw_bytes();
554        // ET Header is 16 bytes + 10 bytes body = 26 bytes total
555        assert_eq!(mrt_bytes.len(), 26);
556        assert_eq!(raw_record.total_bytes_len(), 26);
557    }
558
559    #[test]
560    fn test_raw_mrt_record_write_to_file() {
561        let dir = tempdir().unwrap();
562        let file_path = dir.path().join("test_record.mrt");
563
564        let header = CommonHeader {
565            timestamp: 1609459200,
566            microsecond_timestamp: None,
567            entry_type: EntryType::BGP4MP,
568            entry_subtype: 4,
569            length: 5,
570        };
571        let header_bytes = Bytes::from_static(&[
572            0x5f, 0xee, 0x6a, 0x80, // timestamp
573            0x00, 0x10, // entry type
574            0x00, 0x04, // entry subtype
575            0x00, 0x00, 0x00, 0x05, // length
576        ]);
577        let message_bytes = Bytes::from_static(&[1, 2, 3, 4, 5]);
578
579        let raw_record = RawMrtRecord {
580            common_header: header,
581            header_bytes,
582            message_bytes,
583        };
584
585        raw_record.write_raw_bytes(&file_path).unwrap();
586
587        let written_bytes = std::fs::read(&file_path).unwrap();
588        assert_eq!(written_bytes.len(), 17); // 12 header + 5 body
589    }
590
591    #[test]
592    fn test_raw_mrt_record_append_to_file() {
593        let dir = tempdir().unwrap();
594        let file_path = dir.path().join("test_records.mrt");
595
596        let header = CommonHeader {
597            timestamp: 1609459200,
598            microsecond_timestamp: None,
599            entry_type: EntryType::BGP4MP,
600            entry_subtype: 4,
601            length: 3,
602        };
603        let header_bytes = Bytes::from_static(&[
604            0x5f, 0xee, 0x6a, 0x80, // timestamp
605            0x00, 0x10, // entry type
606            0x00, 0x04, // entry subtype
607            0x00, 0x00, 0x00, 0x03, // length
608        ]);
609        let message_bytes = Bytes::from_static(&[1, 2, 3]);
610
611        let raw_record = RawMrtRecord {
612            common_header: header,
613            header_bytes,
614            message_bytes,
615        };
616
617        raw_record.append_raw_bytes(&file_path).unwrap();
618        raw_record.append_raw_bytes(&file_path).unwrap();
619
620        let written_bytes = std::fs::read(&file_path).unwrap();
621        assert_eq!(written_bytes.len(), 30); // (12 header + 3 body) * 2
622    }
623
624    #[test]
625    fn test_try_from_bmp_message() {
626        let bmp_message = BmpMessage {
627            common_header: BmpCommonHeader {
628                version: 0,
629                msg_len: 0,
630                msg_type: BmpMsgType::RouteMonitoring,
631            },
632            per_peer_header: Some(BmpPerPeerHeader {
633                peer_asn: Asn::new_32bit(0),
634                peer_ip: IpAddr::from_str("10.0.0.1").unwrap(),
635                peer_bgp_id: Ipv4Addr::from_str("10.0.0.2").unwrap(),
636                timestamp: 0.0,
637                peer_type: BmpPeerType::Global,
638                peer_flags: PerPeerFlags::PeerFlags(PeerFlags::empty()),
639                peer_distinguisher: 0,
640            }),
641            message_body: BmpMessageBody::RouteMonitoring(RouteMonitoring {
642                bgp_message: BgpMessage::KeepAlive,
643            }),
644        };
645
646        let mrt_record = MrtRecord::try_from(&bmp_message).unwrap();
647        assert_eq!(mrt_record.common_header.entry_type, EntryType::BGP4MP_ET);
648    }
649
650    #[test]
651    fn test_parse_mrt_body() {
652        let mut data = BytesMut::new();
653        data.put_u16(0);
654        data.put_u16(0);
655        data.put_u32(0);
656        data.put_u16(0);
657
658        let result = parse_mrt_body(0, 0, data.freeze());
659        assert!(result.is_err());
660    }
661
662    #[test]
663    fn test_mrt_record_encode_updates_header_length() {
664        let record = MrtRecord {
665            common_header: CommonHeader {
666                timestamp: 1609459200,
667                microsecond_timestamp: None,
668                entry_type: EntryType::BGP4MP,
669                entry_subtype: Bgp4MpType::MessageAs4 as u16,
670                length: 0,
671            },
672            message: MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(Bgp4MpMessage {
673                msg_type: Bgp4MpType::MessageAs4,
674                peer_asn: Asn::new_32bit(65000),
675                local_asn: Asn::new_32bit(65001),
676                interface_index: 1,
677                peer_ip: IpAddr::from_str("10.0.0.1").unwrap(),
678                local_ip: IpAddr::from_str("10.0.0.2").unwrap(),
679                bgp_message: BgpMessage::KeepAlive,
680            })),
681        };
682
683        let encoded = record.encode().unwrap();
684        let mut cursor = Cursor::new(encoded);
685        let parsed = parse_mrt_record(&mut cursor).unwrap();
686        let expected_len = parsed
687            .message
688            .encode(parsed.common_header.entry_subtype)
689            .unwrap()
690            .len() as u32;
691
692        assert_eq!(parsed.common_header.length, expected_len);
693    }
694
695    #[test]
696    fn chunk_recovers_four_byte_short_table_dump_and_preserves_alignment() {
697        let body = encode_table_dump_batch(
698            &[
699                table_dump_message("192.0.2.0/24"),
700                table_dump_message("198.51.100.0/24"),
701            ],
702            1,
703        )
704        .unwrap();
705        let header = CommonHeader {
706            timestamp: 946_684_800,
707            microsecond_timestamp: None,
708            entry_type: EntryType::TABLE_DUMP,
709            entry_subtype: 1,
710            length: (body.len() - 4) as u32,
711        };
712        let next_body = Bytes::from_static(&[0, 1, 192, 0, 2, 1, 0, 2, 192, 0, 2, 2]);
713        let next_header = CommonHeader {
714            timestamp: 946_684_801,
715            microsecond_timestamp: None,
716            entry_type: EntryType::BGP,
717            entry_subtype: 7,
718            length: next_body.len() as u32,
719        };
720
721        let mut wire = BytesMut::new();
722        wire.put_slice(&header.encode());
723        wire.put_slice(&body);
724        wire.put_slice(&next_header.encode());
725        wire.put_slice(&next_body);
726        let mut cursor = Cursor::new(wire.freeze());
727
728        let first = chunk_mrt_record(&mut cursor).unwrap();
729        assert_eq!(first.message_bytes, body);
730        assert!(matches!(
731            first.parse().unwrap().message,
732            MrtMessage::TableDumpMessageBatch(messages) if messages.len() == 2
733        ));
734        let second = chunk_mrt_record(&mut cursor).unwrap();
735        assert_eq!(second.common_header, next_header);
736        assert!(matches!(
737            second.parse().unwrap().message,
738            MrtMessage::LegacyBgp(LegacyBgp::Message(LegacyBgpMessage {
739                bgp_message: BgpMessage::KeepAlive,
740                ..
741            }))
742        ));
743    }
744
745    #[test]
746    fn chunk_does_not_overread_near_match_table_dump() {
747        let body = encode_table_dump_batch(&[table_dump_message("192.0.2.0/24")], 1).unwrap();
748        let declared_length = body.len() - 3;
749        let header = CommonHeader {
750            timestamp: 946_684_800,
751            microsecond_timestamp: None,
752            entry_type: EntryType::TABLE_DUMP,
753            entry_subtype: 1,
754            length: declared_length as u32,
755        };
756        let mut wire = BytesMut::new();
757        wire.put_slice(&header.encode());
758        wire.put_slice(&body);
759        wire.put_slice(&[0xaa; 12]);
760        let mut cursor = Cursor::new(wire.freeze());
761
762        let raw = chunk_mrt_record(&mut cursor).unwrap();
763        assert_eq!(raw.message_bytes.len(), declared_length);
764        assert_eq!(cursor.position(), (12 + declared_length) as u64);
765        let error = raw.parse().unwrap_err();
766        assert!(matches!(error, ParserError::TruncatedMsg(_)));
767    }
768
769    #[test]
770    fn chunk_errors_include_invalid_header_and_partial_body_bytes() {
771        let invalid_header = Bytes::from_static(&[0, 0, 0, 1, 0xff, 0xff, 0, 0, 0, 0, 0, 0]);
772        let error = chunk_mrt_record(&mut Cursor::new(invalid_header.clone())).unwrap_err();
773        assert_eq!(error.bytes.as_deref(), Some(invalid_header.as_ref()));
774
775        let header = CommonHeader {
776            timestamp: 1,
777            microsecond_timestamp: None,
778            entry_type: EntryType::BGP,
779            entry_subtype: 7,
780            length: 5,
781        };
782        let mut wire = BytesMut::new();
783        wire.put_slice(&header.encode());
784        wire.put_slice(&[1, 2]);
785        let error = chunk_mrt_record(&mut Cursor::new(wire.clone().freeze())).unwrap_err();
786        assert_eq!(error.bytes.as_deref(), Some(wire.as_ref()));
787    }
788
789    /// A record of type 0x7777 whose 20-byte body reads as a plausible common
790    /// header (timestamp 1, type BGP, subtype 7, length 32), so a reader that
791    /// skips only the 12 header bytes would treat the body as the next record.
792    fn unknown_type_record() -> Vec<u8> {
793        let mut wire = BytesMut::new();
794        wire.put_u32(946_684_800);
795        wire.put_u16(0x7777);
796        wire.put_u16(0);
797        wire.put_u32(20);
798        wire.put_slice(&[0, 0, 0, 1, 0, 5, 0, 7, 0, 0, 0, 32]);
799        wire.put_slice(&[0xaa; 8]);
800        wire.to_vec()
801    }
802
803    fn keepalive_record() -> (CommonHeader, Vec<u8>) {
804        let body = vec![0, 1, 192, 0, 2, 1, 0, 2, 192, 0, 2, 2];
805        let header = CommonHeader {
806            timestamp: 946_684_801,
807            microsecond_timestamp: None,
808            entry_type: EntryType::BGP,
809            entry_subtype: 7,
810            length: body.len() as u32,
811        };
812        let mut wire = header.encode().to_vec();
813        wire.extend_from_slice(&body);
814        (header, wire)
815    }
816
817    #[test]
818    fn chunk_skips_body_of_unknown_entry_type() {
819        let unknown = unknown_type_record();
820        let (next_header, next_wire) = keepalive_record();
821        let mut wire = unknown.clone();
822        wire.extend_from_slice(&next_wire);
823        let mut cursor = Cursor::new(wire);
824
825        let error = chunk_mrt_record(&mut cursor).unwrap_err();
826        assert!(
827            matches!(&error.error, ParserError::ParseError(msg) if msg.contains("entry type")),
828            "unexpected error: {}",
829            error.error
830        );
831        assert_eq!(error.bytes.as_deref(), Some(unknown.as_slice()));
832        assert_eq!(cursor.position(), unknown.len() as u64);
833
834        let next = chunk_mrt_record(&mut cursor).unwrap();
835        assert_eq!(next.common_header, next_header);
836        assert!(matches!(
837            next.parse().unwrap().message,
838            MrtMessage::LegacyBgp(LegacyBgp::Message(LegacyBgpMessage {
839                bgp_message: BgpMessage::KeepAlive,
840                ..
841            }))
842        ));
843        assert!(matches!(
844            chunk_mrt_record(&mut cursor).unwrap_err().error,
845            ParserError::EofExpected
846        ));
847    }
848
849    #[test]
850    fn chunk_skips_body_of_et_header_with_short_length() {
851        let mut wire = BytesMut::new();
852        wire.put_u32(1);
853        wire.put_u16(EntryType::BGP4MP_ET as u16);
854        wire.put_u16(4);
855        wire.put_u32(2);
856        wire.put_slice(&[0xaa; 2]);
857        let rejected = wire.to_vec();
858        let (next_header, next_wire) = keepalive_record();
859        wire.put_slice(&next_wire);
860        let mut cursor = Cursor::new(wire.to_vec());
861
862        let error = chunk_mrt_record_with_context(&mut cursor).unwrap_err();
863        assert!(matches!(error.error, ParserError::ParseError(_)));
864        assert!(error.record_consumed);
865        assert_eq!(error.bytes.as_deref(), Some(rejected.as_slice()));
866        assert_eq!(
867            chunk_mrt_record(&mut cursor).unwrap().common_header,
868            next_header
869        );
870    }
871
872    #[test]
873    fn chunk_reports_truncated_body_of_unknown_entry_type() {
874        let unknown = unknown_type_record();
875        let truncated = &unknown[..unknown.len() - 5];
876        let mut cursor = Cursor::new(truncated.to_vec());
877
878        let error = chunk_mrt_record_with_context(&mut cursor).unwrap_err();
879        assert!(matches!(
880            &error.error,
881            ParserError::IoError(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof
882        ));
883        assert!(error.common_header.is_none());
884        assert!(!error.record_consumed);
885        assert_eq!(error.bytes.as_deref(), Some(truncated));
886        assert!(matches!(
887            chunk_mrt_record(&mut cursor).unwrap_err().error,
888            ParserError::EofExpected
889        ));
890    }
891
892    #[test]
893    fn chunk_reports_oversized_length_without_reading_body() {
894        let mut wire = BytesMut::new();
895        wire.put_u32(1);
896        wire.put_u16(EntryType::BGP as u16);
897        wire.put_u16(7);
898        wire.put_u32(MAX_MRT_MESSAGE_LEN + 1);
899        wire.put_slice(&[0xaa; 32]);
900        let mut cursor = Cursor::new(wire.to_vec());
901
902        let error = chunk_mrt_record(&mut cursor).unwrap_err();
903        assert!(matches!(
904            error.error,
905            ParserError::OversizedRecord { length } if length == MAX_MRT_MESSAGE_LEN + 1
906        ));
907        assert_eq!(error.bytes.as_deref(), Some(&wire[..12]));
908        assert_eq!(cursor.position(), 12);
909    }
910
911    #[test]
912    fn chunk_reports_oversized_length_for_unknown_entry_type() {
913        let mut wire = BytesMut::new();
914        wire.put_u32(1);
915        wire.put_u16(0x7777);
916        wire.put_u16(0);
917        wire.put_u32(u32::MAX);
918        wire.put_slice(&[0xaa; 32]);
919        let mut cursor = Cursor::new(wire.to_vec());
920
921        let error = chunk_mrt_record(&mut cursor).unwrap_err();
922        assert!(matches!(
923            error.error,
924            ParserError::OversizedRecord { length: u32::MAX }
925        ));
926        assert_eq!(error.bytes.as_deref(), Some(&wire[..12]));
927        assert_eq!(cursor.position(), 12);
928    }
929}