use crate::command::{Command, Response};
use crate::commands::COMMANDS;
use exfiltrate_internal::args::ArgSpec;
use exfiltrate_internal::commands::list::ListItem;
pub(crate) struct List;
impl Command for List {
fn name(&self) -> &'static str {
"list"
}
fn short_description(&self) -> &'static str {
"List the currently available commands. Use this command to list currently available commands, which change depending on whether or not the debugged program is currently running."
}
fn full_description(&self) -> &'static str {
"List all available commands.
This CLI program debugs a remote application. Some commands are only available when the remote application is running.
Commands that declare an argument schema report it here, so `list` doubles as the machine-readable command surface of the application."
}
fn args(&self) -> &'static [ArgSpec] {
&[]
}
fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
let items: Vec<ListItem> = COMMANDS
.lock_sync_read()
.values()
.map(|command| ListItem {
name: command.name().to_string(),
short_description: command.short_description().to_string(),
full_description: super::help::describe(command.as_ref()),
args: command.args().iter().map(ArgSpec::to_info).collect(),
})
.collect();
Response::from_serialize(&items)
}
}
#[cfg(test)]
mod tests {
use super::*;
use exfiltrate_internal::args::ArgKind;
static SCHEMA_ARGS: &[ArgSpec] =
&[ArgSpec::positional("who", "whom to greet", ArgKind::String).required()];
struct WithSchema;
impl Command for WithSchema {
fn name(&self) -> &'static str {
"list_test_with_schema"
}
fn short_description(&self) -> &'static str {
"declares a parameter"
}
fn full_description(&self) -> &'static str {
"Declares a parameter."
}
fn args(&self) -> &'static [ArgSpec] {
SCHEMA_ARGS
}
fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
Ok("".into())
}
}
#[test]
fn the_list_carries_declared_signatures_over_the_wire() {
crate::commands::insert(Box::new(WithSchema)).ok();
let Response::Bytes(bytes) = List.execute(Vec::new()).unwrap() else {
panic!("list must answer with Bytes");
};
let items: Vec<ListItem> = rmp_serde::from_slice(&bytes).unwrap();
let item = items
.iter()
.find(|item| item.name == "list_test_with_schema")
.expect("the registered command must appear");
assert_eq!(item.args.len(), 1);
assert_eq!(item.args[0].name, "who");
assert!(item.args[0].required);
assert_eq!(
item.usage_line().unwrap(),
"exfiltrate list_test_with_schema <WHO>"
);
assert!(item.full_description.contains("Declares a parameter."));
}
#[test]
fn the_list_is_ordered_so_two_runs_can_be_diffed() {
let Response::Bytes(bytes) = List.execute(Vec::new()).unwrap() else {
panic!("list must answer with Bytes");
};
let items: Vec<ListItem> = rmp_serde::from_slice(&bytes).unwrap();
let names: Vec<&str> = items.iter().map(|item| item.name.as_str()).collect();
let mut sorted = names.clone();
sorted.sort_unstable();
assert_eq!(names, sorted);
}
}