use crate::spec::{BpfProgramKind, BpfProgramSpec};
use std::fmt::Write;
#[derive(Debug, thiserror::Error)]
pub enum CodegenError {
#[error("source `{0}`: shape not recognized — expected `*.rs`, `*.bpf.o`, or `*.tlisp:<fn>`")]
UnrecognizedSource(String),
#[error("tatara-lisp body source `{0}` is not yet supported in this codegen MVP — the Lisp → Rust lowering pass lands next phase")]
LispBodyNotYet(String),
}
#[must_use]
pub fn aya_attribute(kind: &BpfProgramKind) -> &'static str {
match kind {
BpfProgramKind::Xdp => "#[xdp]",
BpfProgramKind::Tc => "#[classifier]",
BpfProgramKind::SocketFilter => "#[socket_filter]",
BpfProgramKind::Kprobe => "#[kprobe]",
BpfProgramKind::Tracepoint => "#[tracepoint]",
BpfProgramKind::CgroupSkb => "#[cgroup_skb]",
BpfProgramKind::Lsm => "#[lsm]",
BpfProgramKind::PerfEvent => "#[perf_event]",
}
}
#[must_use]
pub fn aya_context_type(kind: &BpfProgramKind) -> &'static str {
match kind {
BpfProgramKind::Xdp => "aya_ebpf::programs::XdpContext",
BpfProgramKind::Tc => "aya_ebpf::programs::TcContext",
BpfProgramKind::SocketFilter => "aya_ebpf::programs::SkBuffContext",
BpfProgramKind::Kprobe => "aya_ebpf::programs::ProbeContext",
BpfProgramKind::Tracepoint => "aya_ebpf::programs::TracePointContext",
BpfProgramKind::CgroupSkb => "aya_ebpf::programs::SkBuffContext",
BpfProgramKind::Lsm => "aya_ebpf::programs::LsmContext",
BpfProgramKind::PerfEvent => "aya_ebpf::programs::PerfEventContext",
}
}
#[must_use]
pub fn aya_return_type(kind: &BpfProgramKind) -> &'static str {
match kind {
BpfProgramKind::Xdp => "u32",
BpfProgramKind::Tc => "i32",
BpfProgramKind::SocketFilter => "u32",
BpfProgramKind::Kprobe | BpfProgramKind::Tracepoint => "u32",
BpfProgramKind::CgroupSkb => "i32",
BpfProgramKind::Lsm => "i32",
BpfProgramKind::PerfEvent => "u32",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceShape {
RustFile(String),
PrecompiledObject(String),
LispBody { path: String, body_fn: String },
}
pub fn classify_source(source: &str) -> Result<SourceShape, CodegenError> {
if source.ends_with(".rs") {
return Ok(SourceShape::RustFile(source.to_string()));
}
if source.ends_with(".bpf.o") || source.ends_with(".o") {
return Ok(SourceShape::PrecompiledObject(source.to_string()));
}
if let Some((path, body_fn)) = source.split_once(':') {
if path.ends_with(".tlisp") {
return Ok(SourceShape::LispBody {
path: path.to_string(),
body_fn: body_fn.to_string(),
});
}
}
Err(CodegenError::UnrecognizedSource(source.to_string()))
}
#[must_use]
pub fn emit_aya_program(spec: &BpfProgramSpec, body_block: &str) -> String {
let attr = aya_attribute(&spec.kind);
let ctx_ty = aya_context_type(&spec.kind);
let ret_ty = aya_return_type(&spec.kind);
let mut out = String::new();
let _ = writeln!(out, "// Auto-generated by tatara-ebpf. Do not hand-edit.");
let _ = writeln!(
out,
"// Source spec: {} ({:?})",
spec.name, spec.kind
);
let _ = writeln!(out, "// License: {}", spec.license);
let _ = writeln!(out);
let _ = writeln!(out, "{attr}");
let _ = writeln!(out, "pub fn {name}(ctx: {ctx_ty}) -> {ret_ty} {{", name = spec.name);
for line in body_block.lines() {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(out, "}}");
out
}
#[must_use]
pub fn emit_precompiled_stub(spec: &BpfProgramSpec, object_path: &str) -> String {
let mut out = String::new();
let _ = writeln!(out, "// Auto-generated stub for precompiled BPF object.");
let _ = writeln!(
out,
"// Spec: {} ({:?}) Object: {}",
spec.name, spec.kind, object_path
);
let _ = writeln!(
out,
"pub const {}_OBJECT_PATH: &str = {object_path:?};",
spec.name.to_uppercase().replace('-', "_")
);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{BpfAttachPoint, BpfProgramKind};
fn sample_spec(kind: BpfProgramKind) -> BpfProgramSpec {
BpfProgramSpec {
name: "drop_syn".into(),
kind,
attach: BpfAttachPoint {
target: "eth0".into(),
direction: None,
},
source: "bpf/drop_syn.rs".into(),
license: "GPL".into(),
pin_path: None,
uses_maps: vec!["syn_counter".into()],
}
}
#[test]
fn classifies_rust_source() {
assert_eq!(
classify_source("bpf/drop_syn.rs").unwrap(),
SourceShape::RustFile("bpf/drop_syn.rs".into())
);
}
#[test]
fn classifies_precompiled_object() {
assert_eq!(
classify_source("bpf/drop_syn.bpf.o").unwrap(),
SourceShape::PrecompiledObject("bpf/drop_syn.bpf.o".into())
);
}
#[test]
fn classifies_lisp_body() {
assert_eq!(
classify_source("bpf/policies.tlisp:drop-syn").unwrap(),
SourceShape::LispBody {
path: "bpf/policies.tlisp".into(),
body_fn: "drop-syn".into(),
}
);
}
#[test]
fn rejects_unknown_source() {
assert!(matches!(
classify_source("bpf/drop_syn.bin").unwrap_err(),
CodegenError::UnrecognizedSource(_)
));
}
#[test]
fn emits_xdp_wrapper_with_correct_attribute() {
let spec = sample_spec(BpfProgramKind::Xdp);
let src = emit_aya_program(&spec, "Ok(xdp_action::XDP_PASS)");
assert!(src.contains("#[xdp]"));
assert!(src.contains("pub fn drop_syn(ctx: aya_ebpf::programs::XdpContext) -> u32"));
assert!(src.contains("Ok(xdp_action::XDP_PASS)"));
}
#[test]
fn emits_kprobe_wrapper_with_probe_context() {
let spec = sample_spec(BpfProgramKind::Kprobe);
let src = emit_aya_program(&spec, "0");
assert!(src.contains("#[kprobe]"));
assert!(src.contains("ProbeContext"));
}
#[test]
fn precompiled_stub_exposes_object_path_const() {
let spec = sample_spec(BpfProgramKind::Xdp);
let stub = emit_precompiled_stub(&spec, "/nix/store/xxx-drop-syn.bpf.o");
assert!(stub.contains("DROP_SYN_OBJECT_PATH"));
assert!(stub.contains("/nix/store/xxx-drop-syn.bpf.o"));
}
}