#![warn(missing_docs)]
#![allow(clippy::module_name_repetitions)]
pub const PCAP_MAGIC_LE: u32 = 0xA1B2_C3D4;
pub const PCAP_MAGIC_BE: u32 = 0xD4C3_B2A1;
pub const PCAP_MAGIC_NS_LE: u32 = 0xA1B2_3C4D;
pub const RTPS_MAGIC: &[u8; 4] = b"RTPS";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PcapFileHeader {
pub magic: u32,
pub version_major: u16,
pub version_minor: u16,
pub snaplen: u32,
pub linktype: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PcapRecordHeader {
pub ts_sec: u32,
pub ts_usec: u32,
pub incl_len: u32,
pub orig_len: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PcapError {
TooShort,
BadMagic(u32),
TruncatedRecord,
}
impl std::fmt::Display for PcapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooShort => write!(f, "pcap file too short for header"),
Self::BadMagic(m) => write!(f, "unknown pcap magic 0x{m:08x}"),
Self::TruncatedRecord => write!(f, "pcap record exceeds remaining bytes"),
}
}
}
impl std::error::Error for PcapError {}
pub fn parse_file_header(bytes: &[u8]) -> Result<(PcapFileHeader, bool), PcapError> {
if bytes.len() < 24 {
return Err(PcapError::TooShort);
}
let magic = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let big_endian = match magic {
PCAP_MAGIC_LE | PCAP_MAGIC_NS_LE => false,
PCAP_MAGIC_BE => true,
m => return Err(PcapError::BadMagic(m)),
};
let read_u16 = |off: usize| -> u16 {
if big_endian {
u16::from_be_bytes([bytes[off], bytes[off + 1]])
} else {
u16::from_le_bytes([bytes[off], bytes[off + 1]])
}
};
let read_u32 = |off: usize| -> u32 {
if big_endian {
u32::from_be_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
} else {
u32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
}
};
Ok((
PcapFileHeader {
magic,
version_major: read_u16(4),
version_minor: read_u16(6),
snaplen: read_u32(16),
linktype: read_u32(20),
},
big_endian,
))
}
pub struct PcapIter<'a> {
bytes: &'a [u8],
pos: usize,
big_endian: bool,
}
impl<'a> PcapIter<'a> {
#[must_use]
pub fn new(bytes: &'a [u8], big_endian: bool) -> Self {
Self {
bytes,
pos: 24,
big_endian,
}
}
pub fn next_record(&mut self) -> Result<Option<(PcapRecordHeader, &'a [u8])>, PcapError> {
if self.pos + 16 > self.bytes.len() {
return Ok(None);
}
let read_u32 = |b: &[u8], off: usize, be: bool| -> u32 {
if be {
u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
} else {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
};
let h = PcapRecordHeader {
ts_sec: read_u32(self.bytes, self.pos, self.big_endian),
ts_usec: read_u32(self.bytes, self.pos + 4, self.big_endian),
incl_len: read_u32(self.bytes, self.pos + 8, self.big_endian),
orig_len: read_u32(self.bytes, self.pos + 12, self.big_endian),
};
let data_start = self.pos + 16;
let data_end = data_start
.checked_add(h.incl_len as usize)
.ok_or(PcapError::TruncatedRecord)?;
if data_end > self.bytes.len() {
return Err(PcapError::TruncatedRecord);
}
self.pos = data_end;
Ok(Some((h, &self.bytes[data_start..data_end])))
}
}
#[must_use]
pub fn find_rtps_offset(payload: &[u8]) -> Option<usize> {
payload.windows(4).position(|w| w == RTPS_MAGIC)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Parse(FileArgs),
Stats(FileArgs),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileArgs {
pub file: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
Missing,
Unknown(String),
MissingFile,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Missing => write!(f, "no sub-command given"),
Self::Unknown(s) => write!(f, "unknown sub-command: {s}"),
Self::MissingFile => write!(f, "missing FILE argument"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse_args(args: &[String]) -> Result<Command, ParseError> {
let sub = args.first().ok_or(ParseError::Missing)?;
match sub.as_str() {
"parse" => {
let file = args.get(1).ok_or(ParseError::MissingFile)?.clone();
Ok(Command::Parse(FileArgs { file }))
}
"stats" => {
let file = args.get(1).ok_or(ParseError::MissingFile)?.clone();
Ok(Command::Stats(FileArgs { file }))
}
other => Err(ParseError::Unknown(other.to_string())),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|s| (*s).to_string()).collect()
}
fn make_pcap_le(records: &[&[u8]]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&PCAP_MAGIC_LE.to_le_bytes());
out.extend_from_slice(&2u16.to_le_bytes());
out.extend_from_slice(&4u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&65535u32.to_le_bytes());
out.extend_from_slice(&1u32.to_le_bytes());
for rec in records {
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);
}
out
}
#[test]
fn parse_file_header_le() {
let pcap = make_pcap_le(&[]);
let (h, be) = parse_file_header(&pcap).unwrap();
assert_eq!(h.magic, PCAP_MAGIC_LE);
assert_eq!(h.version_major, 2);
assert_eq!(h.linktype, 1);
assert!(!be);
}
#[test]
fn parse_file_header_too_short() {
assert!(matches!(
parse_file_header(&[1, 2, 3]),
Err(PcapError::TooShort)
));
}
#[test]
fn parse_file_header_bad_magic() {
let mut bytes = vec![0u8; 24];
bytes[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
assert!(matches!(
parse_file_header(&bytes),
Err(PcapError::BadMagic(_))
));
}
#[test]
fn iter_returns_records() {
let pcap = make_pcap_le(&[b"hello-record-1", b"hello-record-2"]);
let (_h, be) = parse_file_header(&pcap).unwrap();
let mut iter = PcapIter::new(&pcap, be);
let (_h1, payload1) = iter.next_record().unwrap().unwrap();
assert_eq!(payload1, b"hello-record-1");
let (_h2, payload2) = iter.next_record().unwrap().unwrap();
assert_eq!(payload2, b"hello-record-2");
assert!(iter.next_record().unwrap().is_none());
}
#[test]
fn find_rtps_offset_no_match() {
assert!(find_rtps_offset(b"plain text without magic").is_none());
}
#[test]
fn find_rtps_offset_finds_after_header() {
let mut payload = vec![0u8; 42]; payload.extend_from_slice(b"RTPS\x02\x05\x01\x10");
let off = find_rtps_offset(&payload).unwrap();
assert_eq!(off, 42);
}
#[test]
fn parse_args_parse_subcommand() {
let cmd = parse_args(&s(&["parse", "x.pcap"])).unwrap();
assert_eq!(
cmd,
Command::Parse(FileArgs {
file: "x.pcap".into()
})
);
}
#[test]
fn parse_args_stats_subcommand() {
let cmd = parse_args(&s(&["stats", "y.pcap"])).unwrap();
assert_eq!(
cmd,
Command::Stats(FileArgs {
file: "y.pcap".into()
})
);
}
#[test]
fn parse_args_missing_file() {
assert!(matches!(
parse_args(&s(&["parse"])),
Err(ParseError::MissingFile)
));
}
#[test]
fn parse_args_unknown() {
assert!(matches!(
parse_args(&s(&["foo"])),
Err(ParseError::Unknown(_))
));
}
}