Files
ghidra-cli/tests/common/schemas.rs
T
Alexander KiselevandClaude Opus 4.6 41355aee72 fix: handle Ghidra overlay memory block addresses in validation
MemoryBlock addresses can be in overlay format (e.g., ".comment::00000000")
rather than plain hex. Also allow empty permissions for non-loaded sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 18:20:56 -08:00

442 lines
11 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 - "));
}
}
}
/// Check if a string is a valid memory address.
/// Accepts both plain hex ("001174b0") and overlay format (".comment::00000000").
fn is_memory_address(s: &str) -> bool {
if is_hex_address(s) {
return true;
}
// Ghidra overlay sections use "section_name::hex_addr" format
if let Some((_section, addr)) = s.rsplit_once("::") {
return is_hex_address(addr);
}
false
}
fn is_hex_address(s: &str) -> bool {
let s = s.trim();
if s.is_empty() {
return false;
}
// Accept both "0x001174b0" and "001174b0" formats
// Ghidra returns addresses without 0x prefix
let hex_part = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
!hex_part.is_empty() && hex_part.bytes().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());
}
// Ghidra may return addresses as "section::hex" for overlay blocks
if !is_memory_address(&self.start) {
errors.push(format!(
"MemoryBlock start '{}' should be hex or overlay format",
self.start
));
}
if !is_memory_address(&self.end) {
errors.push(format!(
"MemoryBlock end '{}' should be hex or overlay format",
self.end
));
}
// Permissions may be empty for non-loaded/overlay sections
if self.permissions.is_empty() && self.is_loaded {
errors.push("Loaded 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
}
}