pub(crate) mod container;
pub(crate) mod l2;
pub(crate) mod l3;
pub(crate) mod l4;
pub(crate) mod l7;
pub(crate) mod oracle;
pub(crate) mod shadow_emit;
pub(crate) mod topology;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::Write as _;
use core::net::IpAddr;
use crate::ast::{L2Modifiers, L3Modifiers, L4Modifiers, Protocol, TransportProto};
use crate::error::CompilerError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReverseOutput {
pub fasm: String,
pub packet_count: usize,
pub host_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolDissection {
pub protocol: Protocol,
pub tokens: String,
pub residual_payload: Vec<u8>,
pub explicit_transport: Option<TransportProto>,
}
#[derive(Debug, Clone)]
pub(crate) struct DissectedPacket {
pub timestamp_ns: u64,
pub raw_eth_frame: Vec<u8>,
pub is_l3_only: bool,
pub src_mac: [u8; 6],
pub dst_mac: [u8; 6],
pub l2: L2Modifiers,
pub inner_ethertype: u16,
pub src_ip: Option<IpAddr>,
pub dst_ip: Option<IpAddr>,
pub has_ip_header: bool,
pub ip_proto: Option<u8>,
pub l3: L3Modifiers,
pub l4: L4Modifiers,
pub seq: Option<u32>,
pub ack_num: Option<u32>,
pub win: Option<u16>,
pub l4_payload: Vec<u8>,
pub l2_payload: Vec<u8>,
pub dissection: ProtocolDissection,
}
pub fn reverse_pcap_reader<R: std::io::Read>(reader: R) -> Result<ReverseOutput, CompilerError> {
let frames = container::read_capture_frames(reader).map_err(CompilerError::BackendError)?;
let packets: Vec<DissectedPacket> = frames.into_iter().map(l2::dissect_packet).collect();
let topo = topology::build_topology(&packets);
Ok(shadow_emit::emit_fasm_program(&packets, &topo))
}
pub fn reverse_pcap_bytes(bytes: &[u8]) -> Result<ReverseOutput, CompilerError> {
reverse_pcap_reader(std::io::Cursor::new(bytes))
}
pub fn reverse_pcap_file(path: impl AsRef<std::path::Path>) -> Result<ReverseOutput, CompilerError> {
let path_ref = path.as_ref();
let file = std::fs::File::open(path_ref).map_err(|e| {
CompilerError::BackendError(alloc::format!(
"Failed to open capture file '{}': {}",
path_ref.display(),
e
))
})?;
let reader = std::io::BufReader::new(file);
reverse_pcap_reader(reader)
}
pub(crate) fn format_mac_lower(mac: [u8; 6]) -> String {
alloc::format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0],
mac[1],
mac[2],
mac[3],
mac[4],
mac[5]
)
}
pub(crate) fn escape_fasm_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 || (c as u32) == 0x7f => {
let _ = write!(out, "\\x{:02x}", c as u32);
}
c => out.push(c),
}
}
out
}
pub(crate) fn bytes_to_hex_lower(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
let _ = write!(out, "{:02x}", b);
}
out
}
pub(crate) fn format_payload_modifier(bytes: &[u8]) -> String {
if bytes.is_empty() {
return String::new();
}
if bytes
.iter()
.all(|&b| (0x20..=0x7e).contains(&b) || b == b'\r' || b == b'\n' || b == b'\t')
&& let Ok(s) = core::str::from_utf8(bytes)
{
return alloc::format!("payload \"{}\"", escape_fasm_string(s));
}
alloc::format!("hexpayload \"{}\"", bytes_to_hex_lower(bytes))
}
pub(crate) fn be_u16(b: &[u8], off: usize) -> u16 {
u16::from_be_bytes([b[off], b[off + 1]])
}
pub(crate) fn be_u32(b: &[u8], off: usize) -> u32 {
u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
pub(crate) fn be_u64(b: &[u8], off: usize) -> u64 {
let mut arr = [0u8; 8];
arr.copy_from_slice(&b[off..off + 8]);
u64::from_be_bytes(arr)
}
pub(crate) fn le_u32(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
pub(crate) fn mac_at(b: &[u8], off: usize) -> [u8; 6] {
[
b[off],
b[off + 1],
b[off + 2],
b[off + 3],
b[off + 4],
b[off + 5],
]
}
pub(crate) fn ipv4_at(b: &[u8], off: usize) -> core::net::Ipv4Addr {
core::net::Ipv4Addr::new(b[off], b[off + 1], b[off + 2], b[off + 3])
}
pub(crate) fn ipv6_at(b: &[u8], off: usize) -> core::net::Ipv6Addr {
let mut arr = [0u8; 16];
arr.copy_from_slice(&b[off..off + 16]);
core::net::Ipv6Addr::from(arr)
}
pub(crate) fn push_if<T: PartialEq + core::fmt::Display>(
out: &mut String,
kw: &str,
val: T,
default: T,
) {
if val != default {
let _ = write!(out, " {kw} {val}");
}
}
pub(crate) fn push_hex(out: &mut String, kw: &str, val: u64, default: u64, width: usize) {
if val != default {
let _ = write!(out, " {kw} 0x{val:0width$x}");
}
}
#[allow(dead_code)]
pub(crate) fn push_str_if(out: &mut String, kw: &str, val: &str, default: &str) {
if val != default {
let needs_escape = {
let mut it = val.chars();
let mut esc = false;
while let Some(c) = it.next() {
if matches!(c, '\0' | '\r' | '\n' | '"')
|| (c == '\\' && it.next().is_none_or(|n| matches!(n, '\0' | '\r' | '\n')))
{
esc = true;
break;
}
}
esc
};
if needs_escape {
let _ = write!(out, " {kw} \"{}\"", escape_fasm_string(val));
} else {
let _ = write!(out, " {kw} \"{val}\"");
}
}
}
#[allow(dead_code)]
pub(crate) fn push_ip_if<T: PartialEq + core::fmt::Display>(
out: &mut String,
kw: &str,
ip: T,
default: T,
) {
if ip != default {
let _ = write!(out, " {kw} {ip}");
}
}
pub(crate) fn push_mac_if(out: &mut String, kw: &str, mac: [u8; 6], default: [u8; 6]) {
if mac != default {
let _ = write!(out, " {kw} {}", format_mac_lower(mac));
}
}
pub(crate) fn src_v4_or(
src_ip: Option<IpAddr>,
fallback: core::net::Ipv4Addr,
) -> core::net::Ipv4Addr {
match src_ip {
Some(IpAddr::V4(v4)) => v4,
_ => fallback,
}
}
pub(crate) fn sd(
protocol: Protocol,
tokens: String,
residual_payload: Vec<u8>,
) -> ProtocolDissection {
ProtocolDissection {
protocol,
tokens,
residual_payload,
explicit_transport: None,
}
}
#[allow(dead_code)]
pub(crate) fn sd_tp(
protocol: Protocol,
tokens: String,
residual_payload: Vec<u8>,
explicit_transport: impl Into<Option<TransportProto>>,
) -> ProtocolDissection {
ProtocolDissection {
protocol,
tokens,
residual_payload,
explicit_transport: explicit_transport.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::compile_program;
use crate::parser::parse_program;
use pcap_file::pcapng::PcapNgWriter;
use pcap_file::pcapng::blocks::Block;
use pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
use pcap_file::pcapng::blocks::interface_description::{
InterfaceDescriptionBlock, InterfaceDescriptionOption,
};
use std::borrow::Cow;
use std::time::Duration;
fn frames_to_pcapng(frames: &[(Vec<u8>, u64)]) -> Vec<u8> {
let mut buf = Vec::new();
let Ok(mut writer) = PcapNgWriter::new(&mut buf) else {
panic!("create PcapNgWriter");
};
let idb = InterfaceDescriptionBlock {
linktype: pcap_file::DataLink::ETHERNET,
snaplen: 65535,
options: alloc::vec![InterfaceDescriptionOption::IfTsResol(9)],
};
assert!(
writer
.write_block(&Block::InterfaceDescription(idb))
.is_ok()
);
for (data, ts) in frames {
let epb = EnhancedPacketBlock {
interface_id: 0,
timestamp: Duration::from_nanos(*ts),
original_len: data.len() as u32,
data: Cow::Borrowed(data),
options: Vec::new(),
};
assert!(writer.write_block(&Block::EnhancedPacket(epb)).is_ok());
}
buf
}
fn collect_fasm_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
let mut paths: Vec<_> = entries.filter_map(|e| e.ok().map(|e| e.path())).collect();
paths.sort();
for p in paths {
if p.is_dir() {
collect_fasm_files(&p, out);
} else if p.extension().and_then(|s| s.to_str()) == Some("fasm") {
out.push(p);
}
}
}
}
#[test]
fn test_l2_l3_l4_corpus_roundtrip_oracle() {
let mut files = Vec::new();
for sub in ["examples/l2", "examples/l3", "examples/l4"] {
collect_fasm_files(std::path::Path::new(sub), &mut files);
}
assert!(!files.is_empty(), "expected L2/L3/L4 corpus files");
for path in &files {
let Ok(src) = std::fs::read_to_string(path) else {
panic!("read corpus file");
};
let Ok(prog) = parse_program(&src) else {
panic!("parse original corpus file");
};
let Ok(orig_frames) = compile_program(&prog) else {
panic!("compile original corpus file");
};
let pcap_bytes = frames_to_pcapng(&orig_frames);
let out = reverse_pcap_bytes(&pcap_bytes)
.unwrap_or_else(|e| panic!("reverse failed on {}: {e}", path.display()));
assert!(
!out.fasm.contains("// ["),
"unexpected fallback comment in {}:\n{}",
path.display(),
out.fasm
);
let reprog = parse_program(&out.fasm).unwrap_or_else(|e| {
panic!(
"re-parse failed on {}: {e}\n--- FASM ---\n{}",
path.display(),
out.fasm
)
});
let reframes = compile_program(&reprog)
.unwrap_or_else(|e| panic!("re-compile failed on {}: {e}", path.display()));
assert_eq!(
orig_frames.len(),
reframes.len(),
"frame count mismatch on {}",
path.display()
);
let orig_base = orig_frames.first().map_or(0, |(_, ts)| *ts);
let re_base = reframes.first().map_or(0, |(_, ts)| *ts);
for (idx, ((_orig_b, orig_ts), (_re_b, re_ts))) in
orig_frames.iter().zip(reframes.iter()).enumerate()
{
assert_eq!(
orig_ts.saturating_sub(orig_base),
re_ts.saturating_sub(re_base),
"relative timestamp mismatch on {} frame #{}",
path.display(),
idx + 1
);
}
}
}
}