use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::error::SortError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PacketIndex {
pub timestamp_ns: u64,
pub byte_offset: u64,
pub caplen: u32,
}
impl PacketIndex {
pub(crate) const SIZE: usize = 20;
pub fn to_bytes(self) -> [u8; Self::SIZE] {
let mut buf = [0u8; Self::SIZE];
buf[0..8].copy_from_slice(&self.timestamp_ns.to_le_bytes());
buf[8..16].copy_from_slice(&self.byte_offset.to_le_bytes());
buf[16..20].copy_from_slice(&self.caplen.to_le_bytes());
buf
}
pub fn from_bytes(b: &[u8; Self::SIZE]) -> Self {
Self {
timestamp_ns: u64::from_le_bytes(b[0..8].try_into().unwrap()),
byte_offset: u64::from_le_bytes(b[8..16].try_into().unwrap()),
caplen: u32::from_le_bytes(b[16..20].try_into().unwrap()),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FilePacketIndex {
pub entry: PacketIndex,
pub file_id: usize,
}
pub enum IndexStore {
Memory(Vec<PacketIndex>),
Disk {
writer: BufWriter<File>,
path: PathBuf,
count: u64,
},
}
impl IndexStore {
pub fn memory() -> Self {
IndexStore::Memory(Vec::new())
}
pub fn disk(sidecar_path: &Path) -> Result<Self, SortError> {
let file = File::create(sidecar_path)?;
Ok(IndexStore::Disk {
writer: BufWriter::with_capacity(64 * 1024, file),
path: sidecar_path.to_owned(),
count: 0,
})
}
pub fn push(&mut self, entry: PacketIndex) -> Result<(), SortError> {
match self {
IndexStore::Memory(v) => {
v.push(entry);
Ok(())
}
IndexStore::Disk { writer, count, .. } => {
writer.write_all(&entry.to_bytes())?;
*count += 1;
Ok(())
}
}
}
pub fn sidecar_path(&self) -> Option<&Path> {
match self {
IndexStore::Disk { path, .. } => Some(path),
IndexStore::Memory(_) => None,
}
}
pub fn into_sorted(self) -> Result<Vec<PacketIndex>, SortError> {
match self {
IndexStore::Memory(mut v) => {
v.sort_unstable_by_key(|e| e.timestamp_ns);
Ok(v)
}
IndexStore::Disk {
mut writer,
path,
count,
} => {
writer.flush()?;
drop(writer);
let mut entries = read_disk_index(&path, count)?;
entries.sort_unstable_by_key(|e| e.timestamp_ns);
Ok(entries)
}
}
}
}
fn read_disk_index(path: &Path, expected: u64) -> Result<Vec<PacketIndex>, SortError> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut entries = Vec::with_capacity(expected as usize);
let mut buf = [0u8; PacketIndex::SIZE];
loop {
match reader.read_exact(&mut buf) {
Ok(()) => entries.push(PacketIndex::from_bytes(&buf)),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(SortError::Io(e)),
}
}
Ok(entries)
}
pub fn sidecar_path(input: &Path) -> PathBuf {
let name = input
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
input.with_file_name(format!("{name}.idx"))
}
pub fn read_packet_at(
file: &mut File,
offset: u64,
caplen: u32,
big_endian: bool,
) -> Result<(u32, Vec<u8>), SortError> {
file.seek(SeekFrom::Start(offset))?;
let mut hdr = [0u8; 16];
file.read_exact(&mut hdr)?;
let origlen = if big_endian {
u32::from_be_bytes(hdr[12..16].try_into().unwrap())
} else {
u32::from_le_bytes(hdr[12..16].try_into().unwrap())
};
let mut data = vec![0u8; caplen as usize];
file.read_exact(&mut data)?;
Ok((origlen, data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_packet_index_roundtrip() {
let idx = PacketIndex {
timestamp_ns: 1_700_000_000_123_456_789,
byte_offset: 4096,
caplen: 1500,
};
let bytes = idx.to_bytes();
assert_eq!(bytes.len(), 20);
assert_eq!(PacketIndex::from_bytes(&bytes), idx);
}
#[test]
fn test_memory_store_sorts_by_timestamp() {
let mut store = IndexStore::memory();
for (ts, off) in [(3000u64, 300u64), (1000, 100), (2000, 200)] {
store
.push(PacketIndex {
timestamp_ns: ts,
byte_offset: off,
caplen: 60,
})
.unwrap();
}
let sorted = store.into_sorted().unwrap();
assert_eq!(
sorted.iter().map(|e| e.timestamp_ns).collect::<Vec<_>>(),
[1000, 2000, 3000]
);
}
#[test]
fn test_disk_store_roundtrip() {
let path = std::env::temp_dir().join("pcap_toolkit_test_index.idx");
let mut store = IndexStore::disk(&path).unwrap();
for (ts, off) in [(300u64, 30u64), (100, 10), (200, 20)] {
store
.push(PacketIndex {
timestamp_ns: ts,
byte_offset: off,
caplen: 42,
})
.unwrap();
}
let sorted = store.into_sorted().unwrap();
assert_eq!(
sorted.iter().map(|e| e.timestamp_ns).collect::<Vec<_>>(),
[100, 200, 300]
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_sidecar_path() {
assert_eq!(
sidecar_path(Path::new("/tmp/traffic.pcap")),
PathBuf::from("/tmp/traffic.pcap.idx"),
);
}
}