exfiltrate 0.3.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Implements the remote command that lists registered commands.
use crate::command::{Command, Response};
use crate::commands::COMMANDS;
use exfiltrate_internal::args::ArgSpec;
use exfiltrate_internal::commands::list::ListItem;

/// The remote `list` command.
///
/// Returns every command registered in the application, with its descriptions
/// and its declared parameter signature. The signature is what makes the list
/// useful to something other than a human reader: the CLI validates against it
/// before a round trip, and `init-skill` renders it into a real signature table.
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);
    }
}