use super::*;
use std::io::Write;
use std::path::PathBuf;
const ARTIFACTS: [&str; 4] = [
"functions.json",
"xrefs.json",
"imports.json",
"findings.ndjson",
];
pub(super) fn build(
binary_path: &str,
data: &[u8],
args: &[String],
out_dir: &str,
) -> Result<u8, String> {
let limit = requested_agent_limit(args, AGENT_INDEX_MAX_FUNCTIONS, AGENT_INDEX_MAX_FUNCTIONS);
let analysis = build_agent_analysis(binary_path, data, limit, AGENT_INDEX_MAX_FINDINGS)?;
let status = analysis_status(&analysis);
let functions = json!({"schema":"rsleigh.functions/v1","source":binary_path,
"count":analysis.functions.len(),"total_discovered":analysis.total_functions,
"truncated":analysis.functions.len() < analysis.total_functions,
"functions":analysis.functions.iter().map(agent_function_json).collect::<Vec<_>>()});
let names: std::collections::HashMap<_, _> = analysis
.functions
.iter()
.map(|f| (f.address, f.name.as_str()))
.collect();
let mut called_by: std::collections::HashMap<u64, Vec<Value>> =
std::collections::HashMap::new();
for caller in &analysis.functions {
for target in &caller.direct_targets {
called_by
.entry(*target)
.or_default()
.push(json!({"name":caller.name,"addr":format!("0x{:x}",caller.address)}));
}
}
let xrefs = json!({"schema":"rsleigh.xrefs/v1","source":binary_path,
"scope":"returned function subset; direct calls only",
"functions":analysis.functions.iter().map(|f| json!({
"name":f.name,"addr":format!("0x{:x}",f.address),
"calls":f.direct_targets.iter().map(|t| json!({"name":names.get(t).copied().unwrap_or("unknown"),"addr":format!("0x{t:x}")})).collect::<Vec<_>>(),
"called_by":called_by.get(&f.address).cloned().unwrap_or_default()
})).collect::<Vec<_>>()});
let imports = json!({"schema":"rsleigh.imports/v1","source":binary_path,
"imports":analysis.imports.iter().map(|(a,n)| json!({"name":n,"addr":format!("0x{a:x}")})).collect::<Vec<_>>()});
let mut ndjson = String::new();
for f in &analysis.findings {
ndjson.push_str(&serde_json::to_string(f).map_err(|e| e.to_string())?);
ndjson.push('\n');
}
let artifacts = vec![
(
"functions.json",
serde_json::to_vec_pretty(&functions).unwrap(),
),
("xrefs.json", serde_json::to_vec_pretty(&xrefs).unwrap()),
("imports.json", serde_json::to_vec_pretty(&imports).unwrap()),
("findings.ndjson", ndjson.into_bytes()),
];
let manifest = json!({
"schema":"rsleigh.index/v2","status":status,"source":binary_path,
"file":file_identity(binary_path,data,&analysis), "tool_version":env!("CARGO_PKG_VERSION"),
"analysis_options":{"function_limit":limit,"finding_limit":AGENT_INDEX_MAX_FINDINGS,
"strings_per_function":5,"ranking":"incoming_calls,complexity,size,address",
"smt":false,"metadata":"decompile_with_binary"},
"trust":agent_trust_json(),"warnings":agent_arch_warnings(analysis.arch),
"limits":{"functions_returned":analysis.functions.len(),"functions_total":analysis.total_functions,
"functions_cap":AGENT_INDEX_MAX_FUNCTIONS,"findings_returned":analysis.findings.len(),
"findings_total":analysis.total_findings,"findings_cap":AGENT_INDEX_MAX_FINDINGS}
});
let manifest = publish(Path::new(out_dir), manifest, &artifacts)
.map_err(|e| format!("cannot publish index: {e}"))?;
emit(&manifest);
Ok(status_exit(status))
}
fn write_new(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
file.write_all(bytes)?;
file.sync_all()
}
pub(super) fn publish(
root: &Path,
mut manifest: Value,
artifacts: &[(&str, Vec<u8>)],
) -> std::io::Result<Value> {
use std::sync::atomic::{AtomicU64, Ordering};
static SERIAL: AtomicU64 = AtomicU64::new(0);
std::fs::create_dir_all(root.join("generations"))?;
let (generation, directory) = loop {
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let name = format!(
"{stamp}-{}-{}",
std::process::id(),
SERIAL.fetch_add(1, Ordering::Relaxed)
);
let directory = root.join("generations").join(&name);
match std::fs::create_dir(&directory) {
Ok(()) => break (name, directory),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
};
let mut files = Vec::new();
for (name, bytes) in artifacts {
write_new(&directory.join(name), bytes)?;
files.push(
json!({"name":name,"path":format!("generations/{generation}/{name}"),
"sha256":compute_sha256(bytes),"size":bytes.len()}),
);
}
manifest["generation"] = json!(generation);
manifest["files"] = json!(files);
let bytes = serde_json::to_vec_pretty(&manifest)?;
write_new(&directory.join("index.json"), &bytes)?;
sync_directory(&directory)?;
sync_directory(&root.join("generations"))?;
let pending = root.join(format!(".index-{generation}.tmp"));
write_new(&pending, &bytes)?;
if let Err(e) = std::fs::rename(&pending, root.join("index.json")) {
let _ = std::fs::remove_file(&pending);
return Err(e);
}
sync_directory(root)?;
Ok(manifest)
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> std::io::Result<()> {
std::fs::File::open(path)?.sync_all()
}
#[cfg(not(unix))]
fn sync_directory(_: &Path) -> std::io::Result<()> {
Ok(())
}
pub(super) fn verify(data: &[u8], out_dir: &str) -> Result<u8, String> {
let root = Path::new(out_dir);
let read = |p: &Path| std::fs::read(p).map_err(|e| format!("cannot read {}: {e}", p.display()));
let manifest_bytes = read(&root.join("index.json"))?;
let manifest: Value = serde_json::from_slice(&manifest_bytes)
.map_err(|e| format!("invalid index manifest: {e}"))?;
if manifest["schema"] != "rsleigh.index/v2" {
return Err("expected rsleigh.index/v2".into());
}
if manifest["file"]["hashes"]["sha256"] != compute_sha256(data) {
return Err("index binary SHA-256 does not match input".into());
}
if manifest["tool_version"] != env!("CARGO_PKG_VERSION") {
return Err("index tool version differs; rebuild before reuse".into());
}
let generation = manifest["generation"]
.as_str()
.ok_or("missing generation")?;
if generation.is_empty() || !generation.chars().all(|c| c.is_ascii_digit() || c == '-') {
return Err("invalid generation identifier".into());
}
let generation_manifest = read(&root.join("generations").join(generation).join("index.json"))?;
if generation_manifest != manifest_bytes {
return Err("published manifest differs from completed generation".into());
}
let files = manifest["files"]
.as_array()
.ok_or("missing artifact manifest")?;
if files.len() != ARTIFACTS.len() {
return Err("incomplete artifact manifest".into());
}
for name in ARTIFACTS {
let matches: Vec<_> = files.iter().filter(|f| f["name"] == name).collect();
if matches.len() != 1 {
return Err(format!("missing or duplicate artifact: {name}"));
}
let record = matches[0];
let expected_path = format!("generations/{generation}/{name}");
if record["path"] != expected_path {
return Err(format!("invalid artifact path for {name}"));
}
let bytes = read(&root.join(PathBuf::from(expected_path)))?;
if record["size"].as_u64() != Some(bytes.len() as u64)
|| record["sha256"] != compute_sha256(&bytes)
{
return Err(format!("artifact checksum or size mismatch: {name}"));
}
if name == "findings.ndjson" {
for line in bytes.split(|b| *b == b'\n').filter(|b| !b.is_empty()) {
let f: Value =
serde_json::from_slice(line).map_err(|e| format!("invalid finding: {e}"))?;
if f["schema"] != "rsleigh.finding/v1" {
return Err("invalid finding schema".into());
}
}
} else {
let value: Value =
serde_json::from_slice(&bytes).map_err(|e| format!("invalid {name}: {e}"))?;
let kind = name.trim_end_matches(".json");
if value["schema"] != format!("rsleigh.{kind}/v1") {
return Err(format!("invalid schema: {name}"));
}
}
}
let status = manifest["status"]
.as_str()
.ok_or("missing analysis status")?;
if !["ok", "partial", "failed"].contains(&status) {
return Err("invalid analysis status".into());
}
emit(
&json!({"schema":"rsleigh.index-verification/v1","status":"ok","valid":true,
"analysis_status":status,"generation":generation,"file_sha256":compute_sha256(data),"artifacts_verified":files.len()}),
);
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interrupted_artifact_write_keeps_previous_manifest() {
let root = std::env::temp_dir().join(format!(
"rsleigh-index-publish-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let first = publish(&root, json!({"schema":"test"}), &[("first", vec![1])]).unwrap();
let original = std::fs::read(root.join("index.json")).unwrap();
assert!(publish(
&root,
json!({"schema":"test"}),
&[("first", vec![2]), ("missing/second", vec![3])]
)
.is_err());
assert_eq!(std::fs::read(root.join("index.json")).unwrap(), original);
assert_eq!(
std::fs::read(root.join(first["files"][0]["path"].as_str().unwrap())).unwrap(),
vec![1]
);
std::fs::remove_dir_all(root).unwrap();
}
}