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}