use crate::cli::responses::*;
use crate::cli::{detect_repo_path, resolve_db_path, Cli, OutputFormat, PathsArgs};
use crate::output;
use anyhow::Result;
pub fn paths(args: &PathsArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{enumerate_paths_incremental, get_or_enumerate_paths, PathKind, PathLimits};
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::{get_function_hash_db, MirageDb};
let db_path = resolve_db_path(cli.db.clone())?;
let repo_path = detect_repo_path(&db_path);
if args.incremental {
let since = args
.since
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--since required with --incremental"))?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let result = match enumerate_paths_incremental(
&args.function,
&db,
&repo_path,
since,
args.max_length,
) {
Ok(r) => r,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"IncrementalAnalysisError",
&format!("Incremental analysis failed: {}", e),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Incremental analysis failed: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
};
match cli.output {
OutputFormat::Human => {
println!("Incremental path enumeration (since {}):", since);
println!(" Analyzed functions: {}", result.analyzed_functions);
println!(" Total paths: {}", result.paths.len());
if args.show_errors {
let error_count = result
.paths
.iter()
.filter(|p| matches!(p.kind, PathKind::Error))
.count();
println!(" Error paths: {}", error_count);
}
if !result.paths.is_empty() {
println!("\nPaths:");
for path in &result.paths {
if args.show_errors || !matches!(path.kind, PathKind::Error) {
println!(" {}", path);
}
}
}
}
OutputFormat::Json => {
let response = serde_json::json!({
"incremental": true,
"since": since,
"analyzed_functions": result.analyzed_functions,
"skipped_functions": result.skipped_functions,
"total_paths": result.paths.len(),
"paths": result.paths,
});
println!("{}", serde_json::to_string(&response)?);
}
OutputFormat::Pretty => {
let response = serde_json::json!({
"incremental": true,
"since": since,
"analyzed_functions": result.analyzed_functions,
"skipped_functions": result.skipped_functions,
"total_paths": result.paths.len(),
"paths": result.paths,
});
println!("{}", serde_json::to_string_pretty(&response)?);
}
}
return Ok(());
}
let mut db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let mut limits = PathLimits::default();
if let Some(max_length) = args.max_length {
limits = limits.with_max_length(max_length);
}
let mut paths = if db.is_sqlite() {
let function_hash = match get_function_hash_db(&db, function_id) {
Some(hash) => hash,
None => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"HashNotFound",
&format!("Function hash not found for '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Function hash not found for '{}'", args.function));
output::info(
"The function data may be incomplete. Try re-running 'magellan watch'",
);
std::process::exit(output::EXIT_DATABASE);
}
}
};
get_or_enumerate_paths(&cfg, function_id, &function_hash, &limits, db.conn_mut()?)
.map_err(|e| anyhow::anyhow!("Path enumeration failed: {}", e))?
} else {
crate::cfg::enumerate_paths(&cfg, &limits)
};
if args.show_errors {
paths.retain(|p| p.kind == PathKind::Error);
}
if args.by_coverage {
let coverage_map: std::collections::HashMap<i64, i64> = db
.conn()
.ok()
.and_then(|conn| {
let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
let mut stmt = conn.prepare(sql).ok()?;
let rows = stmt.query_map([function_id], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
});
let mut map = std::collections::HashMap::new();
if let Ok(iter) = rows {
for (block_id, hit_count) in iter.flatten() {
map.insert(block_id, hit_count);
}
}
if map.is_empty() {
None
} else {
Some(map)
}
})
.unwrap_or_default();
let node_hits: std::collections::HashMap<usize, i64> = cfg
.node_indices()
.filter_map(|idx| {
cfg.node_weight(idx).and_then(|b| {
b.db_id
.and_then(|db_id| coverage_map.get(&db_id).copied())
.map(|hits| (b.id, hits))
})
})
.collect();
paths.sort_by(|a, b| {
let total_a: i64 = a
.blocks
.iter()
.map(|bid| node_hits.get(bid).copied().unwrap_or(0))
.sum();
let total_b: i64 = b
.blocks
.iter()
.map(|bid| node_hits.get(bid).copied().unwrap_or(0))
.sum();
total_b.cmp(&total_a) });
}
let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Total paths: {}", paths.len());
if args.show_errors {
println!("(Showing error paths only)");
} else {
println!("Error paths: {}", error_count);
}
println!();
if paths.is_empty() {
output::info("No paths found");
return Ok(());
}
for (i, path) in paths.iter().enumerate() {
println!("Path {}: {}", i + 1, path.path_id);
println!(" Kind: {:?}", path.kind);
println!(" Length: {} blocks", path.len());
if args.with_blocks {
println!(
" Blocks: {}",
path.blocks
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(" -> ")
);
}
println!();
}
}
OutputFormat::Json => {
let response = PathsResponse {
function: args.function.clone(),
total_paths: paths.len(),
error_paths: error_count,
paths: paths
.iter()
.map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
.collect(),
};
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_json());
}
OutputFormat::Pretty => {
let response = PathsResponse {
function: args.function.clone(),
total_paths: paths.len(),
error_paths: error_count,
paths: paths
.iter()
.map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
.collect(),
};
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_pretty_json());
}
}
Ok(())
}