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 framed = frame_common_header(input)?;
122    Ok(ParsedHeader {
123        header: framed.header?,
124        raw_bytes: framed.raw_bytes,
125    })
126}
127
128/// A common header as read from the wire, with its contents validated separately
129/// from its framing.
130pub(crate) struct FramedHeader {
131    /// The parsed header, or why its contents were rejected.
132    pub(crate) header: Result<CommonHeader, ParserError>,
133    /// The header bytes as read from the wire.
134    pub(crate) raw_bytes: Bytes,
135    /// Length of the record body following `raw_bytes`.
136    pub(crate) body_length: u32,
137}
138
139/// Read a common header, keeping its body length even when the contents are
140/// rejected. Only I/O failures are returned as `Err`.
141pub(crate) fn frame_common_header<T: Read>(input: &mut T) -> std::io::Result<FramedHeader> {
142    let mut base_bytes = [0u8; 12];
143    input.read_exact(&mut base_bytes)?;
144
145    // Single bounds check via zerocopy instead of four sequential cursor reads.
146    let raw = RawMrtCommonHeader::ref_from_bytes(&base_bytes)
147        .expect("base_bytes is exactly 12 bytes with no alignment requirement");
148
149    let timestamp = raw.timestamp.get();
150    let entry_subtype = raw.entry_subtype.get();
151    // the length field does not include the length of the common header
152    let length = raw.length.get();
153
154    // The on-wire length counts every byte after the 12-byte base header, so it
155    // frames the record even when the header contents are invalid.
156    let rejected = |error: ParserError| FramedHeader {
157        header: Err(error),
158        raw_bytes: Bytes::copy_from_slice(&base_bytes),
159        body_length: length,
160    };
161
162    let entry_type = match EntryType::try_from(raw.entry_type.get()) {
163        Ok(entry_type) => entry_type,
164        Err(error) => return Ok(rejected(error.into())),
165    };
166
167    let (microsecond_timestamp, raw_bytes, body_length) = match &entry_type {
168        EntryType::BGP4MP_ET => {
169            // For ET records, the on-wire length includes the extra 4-byte microsecond timestamp
170            // that lives in the header. Internally we store `length` as the message length only,
171            // so subtract 4 after validating to avoid underflow.
172            if length < 4 {
173                return Ok(rejected(ParserError::ParseError(
174                    "invalid MRT header length for ET record: length < 4".into(),
175                )));
176            }
177            let mut combined = [0u8; 16];
178            combined[..12].copy_from_slice(&base_bytes);
179            input.read_exact(&mut combined[12..])?;
180            let microseconds = u32::from_be_bytes(combined[12..16].try_into().unwrap());
181            (
182                Some(microseconds),
183                Bytes::copy_from_slice(&combined),
184                length - 4,
185            )
186        }
187        _ => (None, Bytes::copy_from_slice(&base_bytes), length),
188    };
189
190    Ok(FramedHeader {
191        header: Ok(CommonHeader {
192            timestamp,
193            microsecond_timestamp,
194            entry_type,
195            entry_subtype,
196            length: body_length,
197        }),
198        raw_bytes,
199        body_length,
200    })
201}
202
203impl CommonHeader {
204    pub fn encode(&self) -> Bytes {
205        let raw = RawMrtHeader::from(self);
206        Bytes::copy_from_slice(raw.as_bytes())
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::models::EntryType;
214    use bytes::Buf;
215
216    #[test]
217    fn test_parse_common_header_with_bytes() {
218        let input = Bytes::from_static(&[
219            0, 0, 0, 1, // timestamp
220            0, 16, // entry type
221            0, 4, // entry subtype
222            0, 0, 0, 5, // length
223        ]);
224
225        let mut reader = input.clone().reader();
226        let result = parse_common_header_with_bytes(&mut reader).unwrap();
227
228        assert_eq!(result.header.timestamp, 1);
229        assert_eq!(result.header.entry_type, EntryType::BGP4MP);
230        assert_eq!(result.header.entry_subtype, 4);
231        assert_eq!(result.header.length, 5);
232        assert_eq!(result.raw_bytes, input);
233    }
234
235    #[test]
236    fn test_parse_common_header_with_bytes_et() {
237        let input = Bytes::from_static(&[
238            0, 0, 0, 1, // timestamp
239            0, 17, // entry type = BGP4MP_ET
240            0, 4, // entry subtype
241            0, 0, 0, 9, // length (includes 4 bytes for microsecond)
242            0, 3, 130, 112, // microsecond timestamp
243        ]);
244
245        let mut reader = input.clone().reader();
246        let result = parse_common_header_with_bytes(&mut reader).unwrap();
247
248        assert_eq!(result.header.timestamp, 1);
249        assert_eq!(result.header.entry_type, EntryType::BGP4MP_ET);
250        assert_eq!(result.header.entry_subtype, 4);
251        assert_eq!(result.header.length, 5); // adjusted length
252        assert_eq!(result.header.microsecond_timestamp, Some(230_000));
253        assert_eq!(result.raw_bytes, input);
254    }
255
256    /// Test that the length is not adjusted when the microsecond timestamp is not present.
257    #[test]
258    fn test_encode_common_header() {
259        let header = CommonHeader {
260            timestamp: 1,
261            microsecond_timestamp: None,
262            entry_type: EntryType::BGP4MP,
263            entry_subtype: 4,
264            length: 5,
265        };
266
267        let expected = Bytes::from_static(&[
268            0, 0, 0, 1, // timestamp
269            0, 16, // entry type
270            0, 4, // entry subtype
271            0, 0, 0, 5, // length
272        ]);
273
274        let encoded = header.encode();
275        assert_eq!(encoded, expected);
276
277        let mut reader = expected.reader();
278        let parsed = parse_common_header(&mut reader).unwrap();
279        assert_eq!(parsed, header);
280    }
281
282    /// Test that the length is adjusted when the microsecond timestamp is present.
283    #[test]
284    fn test_encode_common_header_et() {
285        let header = CommonHeader {
286            timestamp: 1,
287            microsecond_timestamp: Some(230_000),
288            entry_type: EntryType::BGP4MP_ET,
289            entry_subtype: 4,
290            length: 5,
291        };
292
293        let expected = Bytes::from_static(&[
294            0, 0, 0, 1, // timestamp
295            0, 17, // entry type
296            0, 4, // entry subtype
297            0, 0, 0, 9, // length
298            0, 3, 130, 112, // microsecond timestamp
299        ]);
300
301        let encoded = header.encode();
302        assert_eq!(encoded, expected);
303
304        let mut reader = expected.reader();
305        let parsed = parse_common_header(&mut reader).unwrap();
306        assert_eq!(parsed, header);
307    }
308
309    /// Ensure ET header with invalid on-wire length (< 4) returns error instead of panicking.
310    #[test]
311    fn test_parse_common_header_et_invalid_length() {
312        // Construct a header with length=3 for ET (which is invalid since it must include 4 bytes of microsecond field)
313        let bytes = Bytes::from_static(&[
314            0, 0, 0, 0, // timestamp
315            0, 17, // entry type = BGP4MP_ET
316            0, 0, // subtype
317            0, 0, 0, 3, // length (invalid for ET)
318        ]);
319        let mut reader = bytes.reader();
320        let res = parse_common_header(&mut reader);
321        assert!(res.is_err());
322    }
323}