use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::NamedTempFile;
use std::io::Write;
fn build_pcap(n: usize) -> Vec<u8> {
let mut pcap = Vec::new();
pcap.extend_from_slice(&0xA1B2C3D4u32.to_le_bytes());
pcap.extend_from_slice(&2u16.to_le_bytes()); pcap.extend_from_slice(&4u16.to_le_bytes()); pcap.extend_from_slice(&0i32.to_le_bytes()); pcap.extend_from_slice(&0u32.to_le_bytes()); pcap.extend_from_slice(&65535u32.to_le_bytes()); pcap.extend_from_slice(&1u32.to_le_bytes());
let pkt: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x08, 0x00, 0x45, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x02, 0x10, 0x00, 0x10, 0x01, 0x00, 0x08, 0x00, 0x00, ];
for i in 0..n {
let ts_sec = i as u32;
pcap.extend_from_slice(&ts_sec.to_le_bytes());
pcap.extend_from_slice(&0u32.to_le_bytes()); pcap.extend_from_slice(&(pkt.len() as u32).to_le_bytes());
pcap.extend_from_slice(&(pkt.len() as u32).to_le_bytes());
pcap.extend_from_slice(pkt);
}
pcap
}
fn write_pcap(n: usize) -> NamedTempFile {
let pcap = build_pcap(n);
let mut tmp = NamedTempFile::with_suffix(".pcap").unwrap();
tmp.write_all(&pcap).unwrap();
tmp
}
#[test]
fn pcapng_minimum_file_is_read_successfully() {
let mut png = Vec::new();
png.extend_from_slice(&0x0A0D_0D0Au32.to_le_bytes()); png.extend_from_slice(&28u32.to_le_bytes()); png.extend_from_slice(&0x1A2B_3C4Du32.to_le_bytes()); png.extend_from_slice(&1u16.to_le_bytes()); png.extend_from_slice(&0u16.to_le_bytes()); png.extend_from_slice(&(-1i64).to_le_bytes()); png.extend_from_slice(&28u32.to_le_bytes());
png.extend_from_slice(&0x0000_0001u32.to_le_bytes()); png.extend_from_slice(&20u32.to_le_bytes()); png.extend_from_slice(&1u16.to_le_bytes()); png.extend_from_slice(&0u16.to_le_bytes()); png.extend_from_slice(&65535u32.to_le_bytes()); png.extend_from_slice(&20u32.to_le_bytes());
let mut tmp = NamedTempFile::with_suffix(".pcapng").unwrap();
tmp.write_all(&png).unwrap();
let output = Command::cargo_bin("dsct")
.unwrap()
.args(["read", tmp.path().to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"dsct read failed on minimal pcapng: stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.trim().is_empty(), "unexpected stdout: {stdout:?}");
}
#[test]
fn truncated_packet_emits_warning_and_no_panic() {
let mut pcap = build_pcap(0);
pcap.extend_from_slice(&0u32.to_le_bytes()); pcap.extend_from_slice(&0u32.to_le_bytes()); pcap.extend_from_slice(&10u32.to_le_bytes()); pcap.extend_from_slice(&42u32.to_le_bytes()); pcap.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33]);
let mut tmp = NamedTempFile::with_suffix(".pcap").unwrap();
tmp.write_all(&pcap).unwrap();
let output = Command::cargo_bin("dsct")
.unwrap()
.args(["read", tmp.path().to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"dsct should not panic on truncated packet: stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.trim().is_empty(),
"truncated packet should be skipped, got stdout: {stdout:?}"
);
let stderr = String::from_utf8(output.stderr).unwrap();
let warning_lines: Vec<&str> = stderr
.lines()
.filter(|l| l.contains("\"warning\""))
.collect();
assert_eq!(
warning_lines.len(),
1,
"expected exactly one structured warning line, stderr = {stderr:?}"
);
}
#[test]
fn unsupported_link_type_skips_packets_and_continues() {
let mut pcap = Vec::new();
pcap.extend_from_slice(&0xA1B2C3D4u32.to_le_bytes()); pcap.extend_from_slice(&2u16.to_le_bytes()); pcap.extend_from_slice(&4u16.to_le_bytes()); pcap.extend_from_slice(&0i32.to_le_bytes()); pcap.extend_from_slice(&0u32.to_le_bytes()); pcap.extend_from_slice(&65535u32.to_le_bytes()); pcap.extend_from_slice(&0x0000_7FFFu32.to_le_bytes());
let junk: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0x00];
for i in 0..2u32 {
pcap.extend_from_slice(&i.to_le_bytes()); pcap.extend_from_slice(&0u32.to_le_bytes()); pcap.extend_from_slice(&(junk.len() as u32).to_le_bytes());
pcap.extend_from_slice(&(junk.len() as u32).to_le_bytes());
pcap.extend_from_slice(junk);
}
let mut tmp = NamedTempFile::with_suffix(".pcap").unwrap();
tmp.write_all(&pcap).unwrap();
let output = Command::cargo_bin("dsct")
.unwrap()
.args(["read", tmp.path().to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"dsct should keep running on unsupported link type: stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.trim().is_empty(),
"all packets should be skipped, got stdout: {stdout:?}"
);
let stderr = String::from_utf8(output.stderr).unwrap();
let warning_lines: Vec<&str> = stderr
.lines()
.filter(|l| l.contains("\"warning\""))
.collect();
assert_eq!(
warning_lines.len(),
2,
"expected one warning per skipped packet, stderr = {stderr:?}"
);
}
#[test]
fn stdin_partial_pcap_header_returns_exit_code_4() {
let full = build_pcap(0);
let partial = full[..16].to_vec();
Command::cargo_bin("dsct")
.unwrap()
.args(["read", "-"])
.write_stdin(partial)
.assert()
.failure()
.code(4)
.stderr(predicate::str::contains("invalid_format"));
}
#[test]
fn zero_packet_pcap_exits_clean() {
let tmp = write_pcap(0);
let output = Command::cargo_bin("dsct")
.unwrap()
.args(["read", tmp.path().to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"dsct read should exit 0 on empty pcap: stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"stdout should be empty, got: {:?}",
String::from_utf8_lossy(&output.stdout)
);
}