Files
ghidra-cli/tests/common/schemas.rs
T
Alexander Kiselev 35716fe468 Enhance tests for disassembly, patching, and querying commands
- 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.
2026-01-25 19:07:58 -08:00

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
}
}