fix: make help and version successful discovery
Some checks failed
checks / flake (push) Has been cancelled
Some checks failed
checks / flake (push) Has been cancelled
This commit is contained in:
parent
ffab80a1f2
commit
5c81b469e5
5 changed files with 88 additions and 1 deletions
6
PRD.md
6
PRD.md
|
|
@ -813,6 +813,12 @@ JSON mode is a strict framing protocol:
|
||||||
- An error includes the path, byte size, and content digest of its Diagnostic
|
- An error includes the path, byte size, and content digest of its Diagnostic
|
||||||
Log when one is available.
|
Log when one is available.
|
||||||
|
|
||||||
|
CLI discovery is the sole framing exception. `ghidr --help`, every
|
||||||
|
`ghidr <command> --help`, and `ghidr --version` are successful metadata
|
||||||
|
requests rather than operations: they emit plain UTF-8 text on stdout, leave
|
||||||
|
stderr empty, exit with status 0, and never initialize Ghidra, access the
|
||||||
|
Analysis Store, or execute a command.
|
||||||
|
|
||||||
Human-readable mode may present progress and concise diagnostics on stderr.
|
Human-readable mode may present progress and concise diagnostics on stderr.
|
||||||
|
|
||||||
Every Ghidra/adapter invocation captures diagnostics into a private temporary
|
Every Ghidra/adapter invocation captures diagnostics into a private temporary
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -21,6 +21,17 @@ Each command will identify the sample by its content, reuse compatible cached
|
||||||
analysis, and produce versioned JSON by default. Human-readable presentation is
|
analysis, and produce versioned JSON by default. Human-readable presentation is
|
||||||
available explicitly through `--format human`.
|
available explicitly through `--format human`.
|
||||||
|
|
||||||
|
Agents can discover the complete command surface without initializing Ghidra:
|
||||||
|
|
||||||
|
```console
|
||||||
|
ghidr --help
|
||||||
|
ghidr decompile --help
|
||||||
|
ghidr --version
|
||||||
|
```
|
||||||
|
|
||||||
|
These discovery requests return plain text on stdout with exit status 0 and
|
||||||
|
leave stderr empty.
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
|
|
||||||
- [PRD.md](./PRD.md) defines the product requirements and initial delivery
|
- [PRD.md](./PRD.md) defines the product requirements and initial delivery
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,10 @@ whether stdout is a terminal or pipe; human-readable presentation requires
|
||||||
command has the same framing and semantics in every execution environment.
|
command has the same framing and semantics in every execution environment.
|
||||||
JSON mode never emits interactive prompts; required confirmation is represented
|
JSON mode never emits interactive prompts; required confirmation is represented
|
||||||
as a structured error and satisfied by an explicit flag.
|
as a structured error and satisfied by an explicit flag.
|
||||||
|
|
||||||
|
Clap discovery requests are intentionally outside the operation-result
|
||||||
|
protocol. Root and subcommand `--help`, plus `--version`, return conventional
|
||||||
|
plain UTF-8 text on stdout with status 0 and no stderr. They do not initialize
|
||||||
|
the runtime or execute an operation. This narrow exception keeps the CLI
|
||||||
|
self-describing for both people and automation agents without weakening JSON
|
||||||
|
framing for analysis and cleanup commands.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser as _;
|
use clap::{Parser as _, error::ErrorKind};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -46,6 +46,15 @@ where
|
||||||
let arguments: Vec<OsString> = arguments.into_iter().map(Into::into).collect();
|
let arguments: Vec<OsString> = arguments.into_iter().map(Into::into).collect();
|
||||||
match Cli::try_parse_from(arguments.clone()) {
|
match Cli::try_parse_from(arguments.clone()) {
|
||||||
Ok(cli) => run_parsed(&cli, executor, stdout, stderr),
|
Ok(cli) => run_parsed(&cli, executor, stdout, stderr),
|
||||||
|
Err(error)
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
stdout.write_all(error.to_string().as_bytes())?;
|
||||||
|
Ok(ExitStatus::Success)
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if requests_human_format(&arguments) {
|
if requests_human_format(&arguments) {
|
||||||
stderr.write_all(error.to_string().as_bytes())?;
|
stderr.write_all(error.to_string().as_bytes())?;
|
||||||
|
|
@ -173,6 +182,35 @@ mod tests {
|
||||||
assert_eq!(document["error"]["code"], "invalid_arguments");
|
assert_eq!(document["error"]["code"], "invalid_arguments");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovery_requests_use_plain_stdout_and_succeed() {
|
||||||
|
for (arguments, expected) in [
|
||||||
|
(&["ghidr", "--help"][..], "Usage: ghidr"),
|
||||||
|
(
|
||||||
|
&["ghidr", "decompile", "--help"][..],
|
||||||
|
"Usage: ghidr decompile",
|
||||||
|
),
|
||||||
|
(&["ghidr", "--version"][..], "ghidr 0.1.0"),
|
||||||
|
] {
|
||||||
|
let mut stdout = Vec::new();
|
||||||
|
let mut stderr = Vec::new();
|
||||||
|
let status = run_from(
|
||||||
|
arguments.iter().copied(),
|
||||||
|
&SuccessExecutor,
|
||||||
|
&mut stdout,
|
||||||
|
&mut stderr,
|
||||||
|
)
|
||||||
|
.expect("write succeeds");
|
||||||
|
assert_eq!(status, ExitStatus::Success);
|
||||||
|
assert!(stderr.is_empty());
|
||||||
|
assert!(
|
||||||
|
String::from_utf8(stdout)
|
||||||
|
.expect("help is UTF-8")
|
||||||
|
.contains(expected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oversized_executor_success_cannot_escape_inline_bound() {
|
fn oversized_executor_success_cannot_escape_inline_bound() {
|
||||||
let mut stdout = Vec::new();
|
let mut stdout = Vec::new();
|
||||||
|
|
|
||||||
25
tests/cli.rs
25
tests/cli.rs
|
|
@ -10,6 +10,31 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use rustix::process::{Pid, Signal, kill_process};
|
use rustix::process::{Pid, Signal, kill_process};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_and_version_are_successful_discovery_output() {
|
||||||
|
let cases = [
|
||||||
|
(vec!["--help"], "Usage: ghidr"),
|
||||||
|
(vec!["doctor", "--help"], "Usage: ghidr doctor"),
|
||||||
|
(vec!["inspect", "--help"], "Usage: ghidr inspect"),
|
||||||
|
(vec!["functions", "--help"], "Usage: ghidr functions"),
|
||||||
|
(vec!["decompile", "--help"], "Usage: ghidr decompile"),
|
||||||
|
(vec!["clean", "--help"], "Usage: ghidr clean"),
|
||||||
|
(vec!["--version"], "ghidr 0.1.0"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (arguments, expected) in cases {
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
|
||||||
|
.args(arguments)
|
||||||
|
.output()
|
||||||
|
.expect("run discovery request");
|
||||||
|
assert_eq!(output.status.code(), Some(0), "{:?}", output.stderr);
|
||||||
|
assert!(output.stderr.is_empty());
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("discovery output is UTF-8");
|
||||||
|
assert!(stdout.contains(expected), "{stdout:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doctor_reports_ready_with_closed_runtime_paths() {
|
fn doctor_reports_ready_with_closed_runtime_paths() {
|
||||||
let temp = tempfile::tempdir().expect("temp");
|
let temp = tempfile::tempdir().expect("temp");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue