mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
- Refactor disassembly tests to dynamically resolve addresses and validate instruction schemas. - Introduce error handling tests for invalid inputs in disassembly. - Improve patching tests by validating output structure and ensuring graceful failure on invalid inputs. - Add snapshot tests for output format regression detection in patching and querying commands. - Update function list and memory map tests to validate JSON output against typed schemas. - Ensure all tests utilize dynamic address resolution instead of hardcoded values for robustness.
421 lines
10 KiB
Rust
421 lines
10 KiB
Rust
//! Test schema definitions and validation.
|
|
//!
|
|
//! Defines data structures matching the CLI's JSON output format.
|
|
//! These are used for type-safe parsing and validation in tests.
|
|
|
|
use serde::Deserialize;
|
|
|
|
/// Function information from `ghidra function list`.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Function {
|
|
pub name: String,
|
|
pub address: String,
|
|
pub size: u64,
|
|
#[serde(default)]
|
|
pub signature: Option<String>,
|
|
pub entry_point: String,
|
|
#[serde(default)]
|
|
pub calling_convention: Option<String>,
|
|
#[serde(default)]
|
|
pub parameters: Vec<Parameter>,
|
|
#[serde(default)]
|
|
pub local_variables: Vec<LocalVariable>,
|
|
#[serde(default)]
|
|
pub calls: Vec<String>,
|
|
#[serde(default)]
|
|
pub called_by: Vec<String>,
|
|
#[serde(default)]
|
|
pub decompiled: Option<String>,
|
|
#[serde(default)]
|
|
pub comment: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Parameter {
|
|
pub name: String,
|
|
pub data_type: String,
|
|
pub ordinal: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct LocalVariable {
|
|
pub name: String,
|
|
pub data_type: String,
|
|
#[serde(default)]
|
|
pub stack_offset: Option<i32>,
|
|
}
|
|
|
|
/// String data from `ghidra strings list`.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct StringData {
|
|
pub address: String,
|
|
pub value: String,
|
|
pub length: usize,
|
|
#[serde(default)]
|
|
pub encoding: Option<String>,
|
|
#[serde(default)]
|
|
pub references: Vec<String>,
|
|
}
|
|
|
|
/// Symbol from `ghidra symbol list`.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Symbol {
|
|
pub name: String,
|
|
pub address: String,
|
|
pub symbol_type: String,
|
|
#[serde(default)]
|
|
pub namespace: Option<String>,
|
|
#[serde(default)]
|
|
pub source: Option<String>,
|
|
}
|
|
|
|
/// Memory block from `ghidra memory map`.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct MemoryBlock {
|
|
pub name: String,
|
|
pub start: String,
|
|
pub end: String,
|
|
pub size: u64,
|
|
pub permissions: String,
|
|
#[serde(default)]
|
|
pub is_initialized: bool,
|
|
#[serde(default)]
|
|
pub is_loaded: bool,
|
|
}
|
|
|
|
/// Instruction from disassembly output.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Instruction {
|
|
pub address: String,
|
|
pub mnemonic: String,
|
|
#[serde(default)]
|
|
pub operands: Option<String>,
|
|
#[serde(default)]
|
|
pub bytes: Option<String>,
|
|
#[serde(default)]
|
|
pub length: Option<u32>,
|
|
#[serde(default)]
|
|
pub flow_type: Option<String>,
|
|
}
|
|
|
|
/// Comment from `ghidra comment` commands.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Comment {
|
|
pub address: String,
|
|
pub comment_type: String,
|
|
pub text: String,
|
|
}
|
|
|
|
/// Data type from `ghidra type` commands.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct DataType {
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub category: Option<String>,
|
|
#[serde(default)]
|
|
pub size: Option<u64>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
/// Cross-reference from `ghidra xref` commands.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct XRef {
|
|
pub from: String,
|
|
pub to: String,
|
|
pub ref_type: String,
|
|
#[serde(default)]
|
|
pub from_function: Option<String>,
|
|
#[serde(default)]
|
|
pub to_function: Option<String>,
|
|
}
|
|
|
|
/// Wrapper for lists of items with optional metadata.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ResultWrapper<T> {
|
|
pub results: Vec<T>,
|
|
#[serde(default)]
|
|
pub count: Option<usize>,
|
|
#[serde(default)]
|
|
pub truncated: Option<bool>,
|
|
}
|
|
|
|
/// Disassembly result from `ghidra disasm` command.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DisasmResult {
|
|
pub results: Vec<Instruction>,
|
|
#[serde(default)]
|
|
pub start_address: Option<String>,
|
|
#[serde(default)]
|
|
pub end_address: Option<String>,
|
|
}
|
|
|
|
/// Patch operation result.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PatchResult {
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub address: Option<String>,
|
|
#[serde(default)]
|
|
pub bytes_written: Option<usize>,
|
|
#[serde(default)]
|
|
pub original_bytes: Option<String>,
|
|
}
|
|
|
|
/// Export result from `ghidra patch export`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ExportResult {
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub path: Option<String>,
|
|
#[serde(default)]
|
|
pub size: Option<u64>,
|
|
}
|
|
|
|
/// Stats result from `ghidra stats`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct StatsResult {
|
|
#[serde(default)]
|
|
pub functions: Option<usize>,
|
|
#[serde(default)]
|
|
pub instructions: Option<usize>,
|
|
#[serde(default)]
|
|
pub strings: Option<usize>,
|
|
#[serde(default)]
|
|
pub symbols: Option<usize>,
|
|
#[serde(default)]
|
|
pub imports: Option<usize>,
|
|
#[serde(default)]
|
|
pub exports: Option<usize>,
|
|
#[serde(default)]
|
|
pub memory_blocks: Option<usize>,
|
|
}
|
|
|
|
/// Graph result for call graph operations.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct GraphResult {
|
|
pub nodes: Vec<GraphNode>,
|
|
pub edges: Vec<GraphEdge>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct GraphNode {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub label: Option<String>,
|
|
#[serde(default)]
|
|
pub address: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct GraphEdge {
|
|
pub from: String,
|
|
pub to: String,
|
|
#[serde(default)]
|
|
pub edge_type: Option<String>,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Validation trait and implementations
|
|
// ============================================================================
|
|
|
|
/// Validation trait for schema types.
|
|
pub trait Validate {
|
|
/// Perform validation and return any errors.
|
|
fn validate(&self) -> Vec<String>;
|
|
|
|
/// Check if valid (no errors).
|
|
fn is_valid(&self) -> bool {
|
|
self.validate().is_empty()
|
|
}
|
|
|
|
/// Assert validity, panicking with all errors if invalid.
|
|
fn assert_valid(&self) {
|
|
let errors = self.validate();
|
|
if !errors.is_empty() {
|
|
panic!("Validation failed:\n - {}", errors.join("\n - "));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_hex_address(s: &str) -> bool {
|
|
let bytes = s.as_bytes();
|
|
bytes.len() > 2
|
|
&& bytes[0] == b'0'
|
|
&& (bytes[1] == b'x' || bytes[1] == b'X')
|
|
&& bytes[2..].iter().all(|b| b.is_ascii_hexdigit())
|
|
}
|
|
|
|
impl Validate for Function {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = Vec::new();
|
|
|
|
if self.name.is_empty() {
|
|
errors.push("Function name is empty".to_string());
|
|
}
|
|
|
|
if !is_hex_address(&self.address) {
|
|
errors.push(format!(
|
|
"Function address '{}' should be hex format (0x...)",
|
|
self.address
|
|
));
|
|
}
|
|
|
|
if !is_hex_address(&self.entry_point) {
|
|
errors.push(format!(
|
|
"Function entry_point '{}' should be hex format",
|
|
self.entry_point
|
|
));
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|
|
|
|
impl Validate for Instruction {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = Vec::new();
|
|
|
|
if !is_hex_address(&self.address) {
|
|
errors.push(format!(
|
|
"Instruction address '{}' should be hex format",
|
|
self.address
|
|
));
|
|
}
|
|
|
|
if self.mnemonic.is_empty() {
|
|
errors.push("Instruction mnemonic is empty".to_string());
|
|
}
|
|
|
|
// Bytes should be hex string if present
|
|
if let Some(ref bytes) = self.bytes {
|
|
if !bytes.chars().all(|c| c.is_ascii_hexdigit()) {
|
|
errors.push(format!(
|
|
"Instruction bytes '{}' should be hex characters only",
|
|
bytes
|
|
));
|
|
}
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|
|
|
|
impl Validate for StringData {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = Vec::new();
|
|
|
|
if !is_hex_address(&self.address) {
|
|
errors.push(format!(
|
|
"String address '{}' should be hex format",
|
|
self.address
|
|
));
|
|
}
|
|
|
|
if self.length == 0 && !self.value.is_empty() {
|
|
errors.push("String length is 0 but value is not empty".to_string());
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|
|
|
|
impl Validate for Symbol {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = Vec::new();
|
|
|
|
if self.name.is_empty() {
|
|
errors.push("Symbol name is empty".to_string());
|
|
}
|
|
|
|
if !is_hex_address(&self.address) {
|
|
errors.push(format!(
|
|
"Symbol address '{}' should be hex format",
|
|
self.address
|
|
));
|
|
}
|
|
|
|
if self.symbol_type.is_empty() {
|
|
errors.push("Symbol type is empty".to_string());
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|
|
|
|
impl Validate for MemoryBlock {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = Vec::new();
|
|
|
|
if self.name.is_empty() {
|
|
errors.push("MemoryBlock name is empty".to_string());
|
|
}
|
|
|
|
if !is_hex_address(&self.start) {
|
|
errors.push(format!(
|
|
"MemoryBlock start '{}' should be hex format",
|
|
self.start
|
|
));
|
|
}
|
|
|
|
if !is_hex_address(&self.end) {
|
|
errors.push(format!(
|
|
"MemoryBlock end '{}' should be hex format",
|
|
self.end
|
|
));
|
|
}
|
|
|
|
if self.permissions.is_empty() {
|
|
errors.push("MemoryBlock permissions is empty".to_string());
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|
|
|
|
impl Validate for Comment {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = Vec::new();
|
|
|
|
if !is_hex_address(&self.address) {
|
|
errors.push(format!(
|
|
"Comment address '{}' should be hex format",
|
|
self.address
|
|
));
|
|
}
|
|
|
|
if self.comment_type.is_empty() {
|
|
errors.push("Comment type is empty".to_string());
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|
|
|
|
impl<T: Validate> Validate for Vec<T> {
|
|
fn validate(&self) -> Vec<String> {
|
|
self.iter()
|
|
.enumerate()
|
|
.flat_map(|(i, item)| {
|
|
item.validate()
|
|
.into_iter()
|
|
.map(move |e| format!("[{}] {}", i, e))
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl Validate for DisasmResult {
|
|
fn validate(&self) -> Vec<String> {
|
|
let mut errors = self.results.validate();
|
|
|
|
if let Some(ref addr) = self.start_address {
|
|
if !is_hex_address(addr) {
|
|
errors.push(format!("start_address '{}' should be hex format", addr));
|
|
}
|
|
}
|
|
|
|
errors
|
|
}
|
|
}
|