bgpkit_parser/parser/mrt/
mrt_header.rs

1use crate::models::{CommonHeader, EntryType};
2use crate::ParserError;
3use bytes::{Buf, BufMut, Bytes, BytesMut};
4use std::io::Read;
5
6/// Result of parsing a common header, including the raw bytes.
7pub struct ParsedHeader {
8    pub header: CommonHeader,
9    pub raw_bytes: Bytes,
10}
11
12/// MRT common header [RFC6396][header].
13///
14/// [header]: https://tools.ietf.org/html/rfc6396#section-4.1
15///
16/// A MRT record is constructed as the following:
17/// ```text
18///  0                   1                   2                   3
19///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
20/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21/// |                           Timestamp                           |
22/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23/// |             Type              |            Subtype            |
24/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25/// |                             Length                            |
26/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27/// |                      Message... (variable)
28/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
29///
30/// ```
31///
32/// Or with extended timestamp:
33/// ```text
34///  0                   1                   2                   3
35///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
36/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37/// |                           Timestamp                           |
38/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
39/// |             Type              |            Subtype            |
40/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
41/// |                             Length                            |
42/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
43/// |                      Microsecond Timestamp                    |
44/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
45/// |                      Message... (variable)
46/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
47/// ```
48pub fn parse_common_header<T: Read>(input: &mut T) -> Result<CommonHeader, ParserError> {
49    Ok(parse_common_header_with_bytes(input)?.header)
50}
51
52/// Parse the MRT common header and return both the parsed header and raw bytes.
53///
54/// This is useful when you need to preserve the original bytes for debugging
55/// or exporting problematic records without re-encoding.
56pub fn parse_common_header_with_bytes<T: Read>(input: &mut T) -> Result<ParsedHeader, ParserError> {
57    let mut base_bytes = [0u8; 12];
58    input.read_exact(&mut base_bytes)?;
59    let mut data = &base_bytes[..];
60
61    let timestamp = data.get_u32();
62    let entry_type_raw = data.get_u16();
63    let entry_type = EntryType::try_from(entry_type_raw)?;
64    let entry_subtype = data.get_u16();
65    // the length field does not include the length of the common header
66    let mut length = data.get_u32();
67
68    let (microsecond_timestamp, raw_bytes) = match &entry_type {
69        EntryType::BGP4MP_ET => {
70            // For ET records, the on-wire length includes the extra 4-byte microsecond timestamp
71            // that lives in the header. Internally we store `length` as the message length only,
72            // so subtract 4 after validating to avoid underflow.
73            if length < 4 {
74                return Err(ParserError::ParseError(
75                    "invalid MRT header length for ET record: length < 4".into(),
76                ));
77            }
78            length -= 4;
79            let mut et_bytes = [0u8; 4];
80            input.read_exact(&mut et_bytes)?;
81            let microseconds = (&et_bytes[..]).get_u32();
82
83            // Combine base header bytes + ET bytes
84            let mut combined = BytesMut::with_capacity(16);
85            combined.put_slice(&base_bytes);
86            combined.put_slice(&et_bytes);
87            (Some(microseconds), combined.freeze())
88        }
89        _ => (None, Bytes::copy_from_slice(&base_bytes)),
90    };
91
92    Ok(ParsedHeader {
93        header: CommonHeader {
94            timestamp,
95            microsecond_timestamp,
96            entry_type,
97            entry_subtype,
98            length,
99        },
100        raw_bytes,
101    })
102}
103
104impl CommonHeader {
105    pub fn encode(&self) -> Bytes {
106        let mut bytes = BytesMut::new();
107        bytes.put_slice(&self.timestamp.to_be_bytes());
108        bytes.put_u16(self.entry_type as u16);
109        bytes.put_u16(self.entry_subtype);
110
111        match self.microsecond_timestamp {
112            None => bytes.put_u32(self.length),
113            Some(microseconds) => {
114                // When the microsecond timestamp is present, the length must be adjusted to account
115                // for the stace used by the extra timestamp data.
116                bytes.put_u32(self.length + 4);
117                bytes.put_u32(microseconds);
118            }
119        };
120        bytes.freeze()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::models::EntryType;
128    use bytes::Buf;
129
130    #[test]
131    fn test_parse_common_header_with_bytes() {
132        let input = Bytes::from_static(&[
133            0, 0, 0, 1, // timestamp
134            0, 16, // entry type
135            0, 4, // entry subtype
136            0, 0, 0, 5, // length
137        ]);
138
139        let mut reader = input.clone().reader();
140        let result = parse_common_header_with_bytes(&mut reader).unwrap();
141
142        assert_eq!(result.header.timestamp, 1);
143        assert_eq!(result.header.entry_type, EntryType::BGP4MP);
144        assert_eq!(result.header.entry_subtype, 4);
145        assert_eq!(result.header.length, 5);
146        assert_eq!(result.raw_bytes, input);
147    }
148
149    #[test]
150    fn test_parse_common_header_with_bytes_et() {
151        let input = Bytes::from_static(&[
152            0, 0, 0, 1, // timestamp
153            0, 17, // entry type = BGP4MP_ET
154            0, 4, // entry subtype
155            0, 0, 0, 9, // length (includes 4 bytes for microsecond)
156            0, 3, 130, 112, // microsecond timestamp
157        ]);
158
159        let mut reader = input.clone().reader();
160        let result = parse_common_header_with_bytes(&mut reader).unwrap();
161
162        assert_eq!(result.header.timestamp, 1);
163        assert_eq!(result.header.entry_type, EntryType::BGP4MP_ET);
164        assert_eq!(result.header.entry_subtype, 4);
165        assert_eq!(result.header.length, 5); // adjusted length
166        assert_eq!(result.header.microsecond_timestamp, Some(230_000));
167        assert_eq!(result.raw_bytes, input);
168    }
169
170    /// Test that the length is not adjusted when the microsecond timestamp is not present.
171    #[test]
172    fn test_encode_common_header() {
173        let header = CommonHeader {
174            timestamp: 1,
175            microsecond_timestamp: None,
176            entry_type: EntryType::BGP4MP,
177            entry_subtype: 4,
178            length: 5,
179        };
180
181        let expected = Bytes::from_static(&[
182            0, 0, 0, 1, // timestamp
183            0, 16, // entry type
184            0, 4, // entry subtype
185            0, 0, 0, 5, // length
186        ]);
187
188        let encoded = header.encode();
189        assert_eq!(encoded, expected);
190
191        let mut reader = expected.reader();
192        let parsed = parse_common_header(&mut reader).unwrap();
193        assert_eq!(parsed, header);
194    }
195
196    /// Test that the length is adjusted when the microsecond timestamp is present.
197    #[test]
198    fn test_encode_common_header_et() {
199        let header = CommonHeader {
200            timestamp: 1,
201            microsecond_timestamp: Some(230_000),
202            entry_type: EntryType::BGP4MP_ET,
203            entry_subtype: 4,
204            length: 5,
205        };
206
207        let expected = Bytes::from_static(&[
208            0, 0, 0, 1, // timestamp
209            0, 17, // entry type
210            0, 4, // entry subtype
211            0, 0, 0, 9, // length
212            0, 3, 130, 112, // microsecond timestamp
213        ]);
214
215        let encoded = header.encode();
216        assert_eq!(encoded, expected);
217
218        let mut reader = expected.reader();
219        let parsed = parse_common_header(&mut reader).unwrap();
220        assert_eq!(parsed, header);
221    }
222
223    /// Ensure ET header with invalid on-wire length (< 4) returns error instead of panicking.
224    #[test]
225    fn test_parse_common_header_et_invalid_length() {
226        // Construct a header with length=3 for ET (which is invalid since it must include 4 bytes of microsecond field)
227        let bytes = Bytes::from_static(&[
228            0, 0, 0, 0, // timestamp
229            0, 17, // entry type = BGP4MP_ET
230            0, 0, // subtype
231            0, 0, 0, 3, // length (invalid for ET)
232        ]);
233        let mut reader = bytes.reader();
234        let res = parse_common_header(&mut reader);
235        assert!(res.is_err());
236    }
237}