use nom_derive::{Nom, Parse};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use thiserror::Error;
#[derive(Nom, Debug)]
pub struct File<'a> {
pub header: Header<'a>,
pub packets: Vec<Packet<'a>>,
}
#[derive(Nom, Debug)]
#[repr(u32)]
pub enum DatalinkType {
UnencapsulatedHci = 1001,
HciUart = 1002,
HciBscp = 1003,
HciSerial = 1004,
}
#[derive(Nom, Debug)]
pub struct Header<'a> {
#[nom(Tag(b"btsnoop\0"))]
pub identification_pattern: &'a [u8],
#[nom(Verify = "*version == 1")]
pub version: u32,
pub datalink_type: DatalinkType,
}
#[derive(Debug, FromPrimitive)]
pub enum DirectionFlag {
Sent = 0,
Received = 1,
}
#[derive(Debug, FromPrimitive)]
pub enum CommandFlag {
Data = 0,
CommandOrEvent = 1,
}
fn parse_single_bit_enum<Enum: FromPrimitive>(
input: (&[u8], usize),
) -> nom::IResult<(&[u8], usize), Enum> {
nom::combinator::map_opt(nom::bits::complete::take(1_usize), Enum::from_u8)(input)
}
#[derive(Debug)]
pub struct PacketFlags {
pub direction: DirectionFlag,
pub command: CommandFlag,
pub reserved: u32,
}
impl<'a> Parse<&'a [u8]> for PacketFlags {
fn parse(input: &'a [u8]) -> nom::IResult<&'a [u8], Self> {
nom::combinator::map(
nom::bits::bits(nom::sequence::tuple((
parse_single_bit_enum::<DirectionFlag>,
parse_single_bit_enum::<CommandFlag>,
nom::bits::complete::take(30_usize),
))),
|(direction, command, reserved)| PacketFlags {
direction,
command,
reserved,
},
)(input)
}
}
#[derive(Nom, Debug)]
pub struct Packet<'a> {
pub original_length: u32,
pub included_length: u32,
pub packet_flags: PacketFlags,
pub culmulative_drops: u32,
pub timestamp_microseconds: i64,
#[nom(Take(included_length))]
pub packet_data: &'a [u8],
}
#[derive(Error, Debug)]
pub enum Error<'a> {
#[error(transparent)]
ParseError(#[from] nom::Err<nom::error::Error<Vec<u8>>>),
#[error("unexpected data remaining")]
UnexpectedData(&'a [u8]),
}
impl<'a> From<nom::Err<nom::error::Error<&[u8]>>> for Error<'a> {
fn from(nom_error: nom::Err<nom::error::Error<&[u8]>>) -> Self {
Self::ParseError(match nom_error {
nom::Err::Incomplete(n) => nom::Err::Incomplete(n),
nom::Err::Error(nom::error::Error { input, code }) => {
nom::Err::Error(nom::error::Error {
input: input.to_vec(),
code,
})
}
nom::Err::Failure(nom::error::Error { input, code }) => {
nom::Err::Failure(nom::error::Error {
input: input.to_vec(),
code,
})
}
})
}
}
pub fn parse_btsnoop_file(input: &[u8]) -> Result<File, Error> {
let (rem, file) = File::parse(input)?;
if rem.is_empty() {
Ok(file)
} else {
Err(Error::UnexpectedData(rem))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_file_parsing_works() {
let hci_bytes = include_bytes!("testdata/btsnoop_hci.log");
let (rem, file) = File::parse(hci_bytes).unwrap();
assert!(rem.is_empty(), "Unexpected remaining bytes: {rem:?}");
assert_eq!(file.packets.len(), 222);
assert_eq!(file.packets[0].packet_data, &[0x01, 0x03, 0x0c, 0x00]);
}
#[test]
fn truncated() {
let hci_bytes = include_bytes!("testdata/btsnoop_hci.log");
let hci_bytes = &hci_bytes[..hci_bytes.len() - 50];
let (rem, file) = File::parse(hci_bytes).unwrap();
assert!(!rem.is_empty());
assert_eq!(file.header.identification_pattern, b"btsnoop\0");
}
}