Skip to main content

bgpkit_parser/parser/mrt/
mrt_header.rs

1use crate::models::{CommonHeader, EntryType};
2use crate::ParserError;
3use bytes::Bytes;
4use std::io::Read;
5use zerocopy::big_endian::{U16, U32};
6use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
7
8/// On-wire MRT common header layout (12 bytes, network byte order).
9#[derive(IntoBytes, FromBytes, KnownLayout, Immutable)]
10#[repr(C)]
11struct RawMrtCommonHeader {
12    timestamp: U32,
13    entry_type: U16,
14    entry_subtype: U16,
15    length: U32,
16}
17
18const _: () = assert!(size_of::<RawMrtCommonHeader>() == 12);
19
20/// On-wire MRT header with microseconds included (16 bytes, network byte order)
21#[derive(IntoBytes, FromBytes, KnownLayout, Immutable)]
22#[repr(C)]
23struct RawMrtEtCommonHeader {
24    timestamp: U32,
25    entry_type: U16,
26    entry_subtype: U16,
27    length: U32,
28    microseconds: U32,
29}
30
31const _: () = assert!(size_of::<RawMrtEtCommonHeader>() == 16);
32
33enum RawMrtHeader {
34    Standard(RawMrtCommonHeader),
35    Et(RawMrtEtCommonHeader),
36}
37
38impl From<&CommonHeader> for RawMrtHeader {
39    fn from(header: &CommonHeader) -> Self {
40        match header.microsecond_timestamp {
41            None => RawMrtHeader::Standard(RawMrtCommonHeader {
42                timestamp: U32::new(header.timestamp),
43                entry_type: U16::new(header.entry_type as u16),
44                entry_subtype: U16::new(header.entry_subtype),
45                length: U32::new(header.length),
46            }),
47            Some(microseconds) => RawMrtHeader::Et(RawMrtEtCommonHeader {
48                timestamp: U32::new(header.timestamp),
49                entry_type: U16::new(header.entry_type as u16),
50                entry_subtype: U16::new(header.entry_subtype),
51                // Internally, we use the length of the MRT payload.
52                // However in the header, the length includes the space used by the extra timestamp
53                // data.
54                length: U32::new(header.length + 4),
55                microseconds: U32::new(microseconds),
56            }),
57        }
58    }
59}
60
61impl RawMrtHeader {
62    fn as_bytes(&self) -> &[u8] {
63        match self {
64            RawMrtHeader::Standard(raw) => raw.as_bytes(),
65            RawMrtHeader::Et(raw) => raw.as_bytes(),
66        }
67    }
68}
69
70/// Result of parsing a common header, including the raw bytes.
71pub struct ParsedHeader {
72    pub header: CommonHeader,
73    pub raw_bytes: Bytes,
74}
75
76/// MRT common header [RFC6396][header].
77///
78/// [header]: https://tools.ietf.org/html/rfc6396#section-4.1
79///
80/// A MRT record is constructed as the following:
81/// ```text
82///  0                   1                   2                   3
83///  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
84/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
85/// |                           Timestamp                           |
86/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
87/// |             Type              |            Subtype            |
88/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
89/// |                             Length                            |
90/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
91/// |                      Message... (variable)
92/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
93///
94/// ```
95///
96/// Or with extended timestamp:
97/// ```text
98///  0                   1                   2                   3
99///  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
100/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
101/// |                           Timestamp                           |
102/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
103/// |             Type              |            Subtype            |
104/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
105/// |                             Length                            |
106/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
107/// |                      Microsecond Timestamp                    |
108/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
109/// |                      Message... (variable)
110/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
111/// ```
112pub fn parse_common_header<T: Read>(input: &mut T) -> Result<CommonHeader, ParserError> {
113    Ok(parse_common_header_with_bytes(input)?.header)
114}
115
116/// Parse the MRT common header and return both the parsed header and raw bytes.
117///
118/// This is useful when you need to preserve the original bytes for debugging
119/// or exporting problematic records without re-encoding.
120pub fn parse_common_header_with_bytes<T: Read>(input: &mut T) -> Result<ParsedHeader, ParserError> {
121    let mut base_bytes = [0u8; 12];
122    input.read_exact(&mut base_bytes)?;
123
124    // Single bounds check via zerocopy instead of four sequential cursor reads.
125    let raw = RawMrtCommonHeader::ref_from_bytes(&base_bytes)
126        .expect("base_bytes is exactly 12 bytes with no alignment requirement");
127
128    let timestamp = raw.timestamp.get();
129    let entry_type = EntryType::try_from(raw.entry_type.get())?;
130    let entry_subtype = raw.entry_subtype.get();
131    // the length field does not include the length of the common header
132    let mut length = raw.length.get();
133
134    let (microsecond_timestamp, raw_bytes) = match &entry_type {
135        EntryType::BGP4MP_ET => {
136            // For ET records, the on-wire length includes the extra 4-byte microsecond timestamp
137            // that lives in the header. Internally we store `length` as the message length only,
138            // so subtract 4 after validating to avoid underflow.
139            if length < 4 {
140                return Err(ParserError::ParseError(
141                    "invalid MRT header length for ET record: length < 4".into(),
142                ));
143            }
144            length -= 4;
145            let mut combined = [0u8; 16];
146            combined[..12].copy_from_slice(&base_bytes);
147            input.read_exact(&mut combined[12..])?;
148            let microseconds = u32::from_be_bytes(combined[12..16].try_into().unwrap());
149            (Some(microseconds), Bytes::copy_from_slice(&combined))
150        }
151        _ => (None, Bytes::copy_from_slice(&base_bytes)),
152    };
153
154    Ok(ParsedHeader {
155        header: CommonHeader {
156            timestamp,
157            microsecond_timestamp,
158            entry_type,
159            entry_subtype,
160            length,
161        },
162        raw_bytes,
163    })
164}
165
166impl CommonHeader {
167    pub fn encode(&self) -> Bytes {
168        let raw = RawMrtHeader::from(self);
169        Bytes::copy_from_slice(raw.as_bytes())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::models::EntryType;
177    use bytes::Buf;
178
179    #[test]
180    fn test_parse_common_header_with_bytes() {
181        let input = 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 mut reader = input.clone().reader();
189        let result = parse_common_header_with_bytes(&mut reader).unwrap();
190
191        assert_eq!(result.header.timestamp, 1);
192        assert_eq!(result.header.entry_type, EntryType::BGP4MP);
193        assert_eq!(result.header.entry_subtype, 4);
194        assert_eq!(result.header.length, 5);
195        assert_eq!(result.raw_bytes, input);
196    }
197
198    #[test]
199    fn test_parse_common_header_with_bytes_et() {
200        let input = Bytes::from_static(&[
201            0, 0, 0, 1, // timestamp
202            0, 17, // entry type = BGP4MP_ET
203            0, 4, // entry subtype
204            0, 0, 0, 9, // length (includes 4 bytes for microsecond)
205            0, 3, 130, 112, // microsecond timestamp
206        ]);
207
208        let mut reader = input.clone().reader();
209        let result = parse_common_header_with_bytes(&mut reader).unwrap();
210
211        assert_eq!(result.header.timestamp, 1);
212        assert_eq!(result.header.entry_type, EntryType::BGP4MP_ET);
213        assert_eq!(result.header.entry_subtype, 4);
214        assert_eq!(result.header.length, 5); // adjusted length
215        assert_eq!(result.header.microsecond_timestamp, Some(230_000));
216        assert_eq!(result.raw_bytes, input);
217    }
218
219    /// Test that the length is not adjusted when the microsecond timestamp is not present.
220    #[test]
221    fn test_encode_common_header() {
222        let header = CommonHeader {
223            timestamp: 1,
224            microsecond_timestamp: None,
225            entry_type: EntryType::BGP4MP,
226            entry_subtype: 4,
227            length: 5,
228        };
229
230        let expected = Bytes::from_static(&[
231            0, 0, 0, 1, // timestamp
232            0, 16, // entry type
233            0, 4, // entry subtype
234            0, 0, 0, 5, // length
235        ]);
236
237        let encoded = header.encode();
238        assert_eq!(encoded, expected);
239
240        let mut reader = expected.reader();
241        let parsed = parse_common_header(&mut reader).unwrap();
242        assert_eq!(parsed, header);
243    }
244
245    /// Test that the length is adjusted when the microsecond timestamp is present.
246    #[test]
247    fn test_encode_common_header_et() {
248        let header = CommonHeader {
249            timestamp: 1,
250            microsecond_timestamp: Some(230_000),
251            entry_type: EntryType::BGP4MP_ET,
252            entry_subtype: 4,
253            length: 5,
254        };
255
256        let expected = Bytes::from_static(&[
257            0, 0, 0, 1, // timestamp
258            0, 17, // entry type
259            0, 4, // entry subtype
260            0, 0, 0, 9, // length
261            0, 3, 130, 112, // microsecond timestamp
262        ]);
263
264        let encoded = header.encode();
265        assert_eq!(encoded, expected);
266
267        let mut reader = expected.reader();
268        let parsed = parse_common_header(&mut reader).unwrap();
269        assert_eq!(parsed, header);
270    }
271
272    /// Ensure ET header with invalid on-wire length (< 4) returns error instead of panicking.
273    #[test]
274    fn test_parse_common_header_et_invalid_length() {
275        // Construct a header with length=3 for ET (which is invalid since it must include 4 bytes of microsecond field)
276        let bytes = Bytes::from_static(&[
277            0, 0, 0, 0, // timestamp
278            0, 17, // entry type = BGP4MP_ET
279            0, 0, // subtype
280            0, 0, 0, 3, // length (invalid for ET)
281        ]);
282        let mut reader = bytes.reader();
283        let res = parse_common_header(&mut reader);
284        assert!(res.is_err());
285    }
286}