1#![warn(missing_docs)]
19#![allow(clippy::module_name_repetitions)]
20
21pub const PCAP_MAGIC_LE: u32 = 0xA1B2_C3D4;
23pub const PCAP_MAGIC_BE: u32 = 0xD4C3_B2A1;
25pub const PCAP_MAGIC_NS_LE: u32 = 0xA1B2_3C4D;
27pub const RTPS_MAGIC: &[u8; 4] = b"RTPS";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct PcapFileHeader {
33 pub magic: u32,
35 pub version_major: u16,
37 pub version_minor: u16,
39 pub snaplen: u32,
41 pub linktype: u32,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct PcapRecordHeader {
48 pub ts_sec: u32,
50 pub ts_usec: u32,
52 pub incl_len: u32,
54 pub orig_len: u32,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum PcapError {
61 TooShort,
63 BadMagic(u32),
65 TruncatedRecord,
67}
68
69impl std::fmt::Display for PcapError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::TooShort => write!(f, "pcap file too short for header"),
73 Self::BadMagic(m) => write!(f, "unknown pcap magic 0x{m:08x}"),
74 Self::TruncatedRecord => write!(f, "pcap record exceeds remaining bytes"),
75 }
76 }
77}
78
79impl std::error::Error for PcapError {}
80
81pub fn parse_file_header(bytes: &[u8]) -> Result<(PcapFileHeader, bool), PcapError> {
86 if bytes.len() < 24 {
87 return Err(PcapError::TooShort);
88 }
89 let magic = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
90 let big_endian = match magic {
91 PCAP_MAGIC_LE | PCAP_MAGIC_NS_LE => false,
92 PCAP_MAGIC_BE => true,
93 m => return Err(PcapError::BadMagic(m)),
94 };
95 let read_u16 = |off: usize| -> u16 {
96 if big_endian {
97 u16::from_be_bytes([bytes[off], bytes[off + 1]])
98 } else {
99 u16::from_le_bytes([bytes[off], bytes[off + 1]])
100 }
101 };
102 let read_u32 = |off: usize| -> u32 {
103 if big_endian {
104 u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
105 } else {
106 u32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
107 }
108 };
109 Ok((
110 PcapFileHeader {
111 magic,
112 version_major: read_u16(4),
113 version_minor: read_u16(6),
114 snaplen: read_u32(16),
115 linktype: read_u32(20),
116 },
117 big_endian,
118 ))
119}
120
121pub struct PcapIter<'a> {
124 bytes: &'a [u8],
125 pos: usize,
126 big_endian: bool,
127}
128
129impl<'a> PcapIter<'a> {
130 #[must_use]
133 pub fn new(bytes: &'a [u8], big_endian: bool) -> Self {
134 Self {
135 bytes,
136 pos: 24,
137 big_endian,
138 }
139 }
140
141 pub fn next_record(&mut self) -> Result<Option<(PcapRecordHeader, &'a [u8])>, PcapError> {
146 if self.pos + 16 > self.bytes.len() {
147 return Ok(None);
148 }
149 let read_u32 = |b: &[u8], off: usize, be: bool| -> u32 {
150 if be {
151 u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
152 } else {
153 u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
154 }
155 };
156 let h = PcapRecordHeader {
157 ts_sec: read_u32(self.bytes, self.pos, self.big_endian),
158 ts_usec: read_u32(self.bytes, self.pos + 4, self.big_endian),
159 incl_len: read_u32(self.bytes, self.pos + 8, self.big_endian),
160 orig_len: read_u32(self.bytes, self.pos + 12, self.big_endian),
161 };
162 let data_start = self.pos + 16;
163 let data_end = data_start
164 .checked_add(h.incl_len as usize)
165 .ok_or(PcapError::TruncatedRecord)?;
166 if data_end > self.bytes.len() {
167 return Err(PcapError::TruncatedRecord);
168 }
169 self.pos = data_end;
170 Ok(Some((h, &self.bytes[data_start..data_end])))
171 }
172}
173
174#[must_use]
180pub fn find_rtps_offset(payload: &[u8]) -> Option<usize> {
181 payload.windows(4).position(|w| w == RTPS_MAGIC)
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub enum Command {
187 Parse(FileArgs),
189 Stats(FileArgs),
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct FileArgs {
196 pub file: String,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ParseError {
203 Missing,
205 Unknown(String),
207 MissingFile,
209}
210
211impl std::fmt::Display for ParseError {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 match self {
214 Self::Missing => write!(f, "no sub-command given"),
215 Self::Unknown(s) => write!(f, "unknown sub-command: {s}"),
216 Self::MissingFile => write!(f, "missing FILE argument"),
217 }
218 }
219}
220
221impl std::error::Error for ParseError {}
222
223pub fn parse_args(args: &[String]) -> Result<Command, ParseError> {
228 let sub = args.first().ok_or(ParseError::Missing)?;
229 match sub.as_str() {
230 "parse" => {
231 let file = args.get(1).ok_or(ParseError::MissingFile)?.clone();
232 Ok(Command::Parse(FileArgs { file }))
233 }
234 "stats" => {
235 let file = args.get(1).ok_or(ParseError::MissingFile)?.clone();
236 Ok(Command::Stats(FileArgs { file }))
237 }
238 other => Err(ParseError::Unknown(other.to_string())),
239 }
240}
241
242#[cfg(test)]
243#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
244mod tests {
245 use super::*;
246
247 fn s(args: &[&str]) -> Vec<String> {
248 args.iter().map(|s| (*s).to_string()).collect()
249 }
250
251 fn make_pcap_le(records: &[&[u8]]) -> Vec<u8> {
252 let mut out = Vec::new();
253 out.extend_from_slice(&PCAP_MAGIC_LE.to_le_bytes());
255 out.extend_from_slice(&2u16.to_le_bytes());
256 out.extend_from_slice(&4u16.to_le_bytes());
257 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&65535u32.to_le_bytes());
260 out.extend_from_slice(&1u32.to_le_bytes());
261 for rec in records {
262 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(rec.len() as u32).to_le_bytes()); out.extend_from_slice(&(rec.len() as u32).to_le_bytes()); out.extend_from_slice(rec);
267 }
268 out
269 }
270
271 #[test]
272 fn parse_file_header_le() {
273 let pcap = make_pcap_le(&[]);
274 let (h, be) = parse_file_header(&pcap).unwrap();
275 assert_eq!(h.magic, PCAP_MAGIC_LE);
276 assert_eq!(h.version_major, 2);
277 assert_eq!(h.linktype, 1);
278 assert!(!be);
279 }
280
281 #[test]
282 fn parse_file_header_too_short() {
283 assert!(matches!(
284 parse_file_header(&[1, 2, 3]),
285 Err(PcapError::TooShort)
286 ));
287 }
288
289 #[test]
290 fn parse_file_header_bad_magic() {
291 let mut bytes = vec![0u8; 24];
292 bytes[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
293 assert!(matches!(
294 parse_file_header(&bytes),
295 Err(PcapError::BadMagic(_))
296 ));
297 }
298
299 #[test]
300 fn iter_returns_records() {
301 let pcap = make_pcap_le(&[b"hello-record-1", b"hello-record-2"]);
302 let (_h, be) = parse_file_header(&pcap).unwrap();
303 let mut iter = PcapIter::new(&pcap, be);
304 let (_h1, payload1) = iter.next_record().unwrap().unwrap();
305 assert_eq!(payload1, b"hello-record-1");
306 let (_h2, payload2) = iter.next_record().unwrap().unwrap();
307 assert_eq!(payload2, b"hello-record-2");
308 assert!(iter.next_record().unwrap().is_none());
309 }
310
311 #[test]
312 fn find_rtps_offset_no_match() {
313 assert!(find_rtps_offset(b"plain text without magic").is_none());
314 }
315
316 #[test]
317 fn find_rtps_offset_finds_after_header() {
318 let mut payload = vec![0u8; 42]; payload.extend_from_slice(b"RTPS\x02\x05\x01\x10");
320 let off = find_rtps_offset(&payload).unwrap();
321 assert_eq!(off, 42);
322 }
323
324 #[test]
325 fn parse_args_parse_subcommand() {
326 let cmd = parse_args(&s(&["parse", "x.pcap"])).unwrap();
327 assert_eq!(
328 cmd,
329 Command::Parse(FileArgs {
330 file: "x.pcap".into()
331 })
332 );
333 }
334
335 #[test]
336 fn parse_args_stats_subcommand() {
337 let cmd = parse_args(&s(&["stats", "y.pcap"])).unwrap();
338 assert_eq!(
339 cmd,
340 Command::Stats(FileArgs {
341 file: "y.pcap".into()
342 })
343 );
344 }
345
346 #[test]
347 fn parse_args_missing_file() {
348 assert!(matches!(
349 parse_args(&s(&["parse"])),
350 Err(ParseError::MissingFile)
351 ));
352 }
353
354 #[test]
355 fn parse_args_unknown() {
356 assert!(matches!(
357 parse_args(&s(&["foo"])),
358 Err(ParseError::Unknown(_))
359 ));
360 }
361}