radare2 0.2.2

Rust integration helpers for radare2 core plugins
//! Safe-ish wrappers around a live radare2 core.

use crate::io_uri;
use crate::sys::{self, RConfig, RCore, RCorePluginSession};
use std::ffi::{CStr, CString};
use std::path::PathBuf;

/// Borrowed access to a live radare2 core.
#[derive(Clone, Copy)]
pub struct Core {
    ptr: *mut RCore,
}

impl Core {
    /// Wrap a raw core pointer.
    ///
    /// # Safety
    ///
    /// `ptr` must point to a live `RCore` for every use of the returned value.
    pub unsafe fn from_ptr(ptr: *mut RCore) -> Option<Self> {
        (!ptr.is_null()).then_some(Self { ptr })
    }

    /// Wrap the core belonging to a plugin session.
    ///
    /// # Safety
    ///
    /// `session` must point to a live session supplied by radare2, and the
    /// returned value must not outlive that session.
    pub unsafe fn from_session(session: *mut RCorePluginSession) -> Option<Self> {
        let session = unsafe { session.as_ref()? };
        unsafe { Self::from_ptr(session.core) }
    }

    /// Return the underlying core pointer.
    pub const fn as_ptr(self) -> *mut RCore {
        self.ptr
    }

    /// Return the core configuration pointer through the compiled headers.
    fn config(self) -> *mut RConfig {
        unsafe { sys::r2_rust_core_config(self.ptr) }
    }

    /// Run one radare2 command, discarding its output.
    pub fn cmd(self, command: &str) {
        let command = c_string(command);
        unsafe {
            sys::r_core_cmd0(self.ptr, command.as_ptr());
        }
    }

    /// Run a newline-separated radare2 command script.
    pub fn cmd_lines(self, commands: &str) {
        let commands = c_string(commands);
        unsafe {
            sys::r_core_cmd_lines(self.ptr, commands.as_ptr());
        }
    }

    /// Run a command and return its trimmed output, or an empty string on failure.
    pub fn cmd_str(self, command: &str) -> String {
        let command = c_string(command);
        unsafe {
            let raw = sys::r_core_cmd_str(self.ptr, command.as_ptr());
            if raw.is_null() {
                return String::new();
            }
            let output = CStr::from_ptr(raw).to_string_lossy().trim().to_owned();
            sys::free(raw.cast());
            output
        }
    }

    /// Print a single diagnostic line through radare2.
    pub fn echo(self, message: &str) {
        let escaped = message.replace(
            [
                '\'', '"', ';', '|', '@', '`', '[', ']', '*', '~', '?', '(', ')',
            ],
            " ",
        );
        let escaped = escaped.split_whitespace().collect::<Vec<_>>().join(" ");
        if !escaped.is_empty() {
            self.cmd(&format!("?e {escaped}"));
        }
    }

    /// Resolve the binary currently opened by radare2 to an on-disk path.
    pub fn current_file(self) -> Option<PathBuf> {
        let opened = first_nonempty_line(&self.cmd_str("o."))?;
        let opened = std::path::Path::new(&opened);
        if let Some(path) = io_uri::on_disk_path(opened) {
            return Some(path);
        }
        if let Some(path) = io_uri::ptrace_exe(opened) {
            return Some(path);
        }
        if let Some(path) = first_nonempty_line(&self.cmd_str("i~^file[1]")) {
            if is_plausible_path(&path) {
                return Some(path.into());
            }
        }
        if let Some(path) = file_info_path(&self.cmd_str("i~^file")) {
            if is_plausible_path(&path) {
                return Some(path.into());
            }
        }
        None
    }

    /// Return `bin.baddr`, or zero if it is unavailable.
    pub fn baddr(self) -> u64 {
        parse_r2_u64(&self.cmd_str("e bin.baddr")).unwrap_or(0)
    }

    /// Read a boolean configuration key, using `default` when it is absent.
    pub fn cfg_bool(self, key: &str, default: bool) -> bool {
        let config = self.config();
        if config.is_null() {
            return default;
        }
        let key = c_string(key);
        unsafe {
            if sys::r_config_node_get(config, key.as_ptr()).is_null() {
                default
            } else {
                sys::r_config_get_b(config, key.as_ptr())
            }
        }
    }

    /// Create or update a boolean configuration key and its description.
    pub fn set_cfg_bool(self, key: &str, value: bool, description: &str) {
        let config = self.config();
        if config.is_null() {
            return;
        }
        let key = c_string(key);
        unsafe {
            sys::r_config_lock(config, false);
            let node = sys::r_config_set_b(config, key.as_ptr(), value);
            if !node.is_null() && !description.is_empty() {
                let description = c_string(description);
                sys::r_config_desc(config, key.as_ptr(), description.as_ptr());
            }
            sys::r_config_lock(config, true);
        }
    }

    /// Read a string configuration key, returning an empty string when absent.
    pub fn cfg_str(self, key: &str) -> String {
        let config = self.config();
        if config.is_null() {
            return String::new();
        }
        let key = c_string(key);
        unsafe {
            let value = sys::r_config_get(config, key.as_ptr());
            if value.is_null() {
                String::new()
            } else {
                CStr::from_ptr(value).to_string_lossy().into_owned()
            }
        }
    }

    /// Update a string configuration key.
    pub fn set_cfg_str(self, key: &str, value: &str) {
        let config = self.config();
        if config.is_null() {
            return;
        }
        let key = c_string(key);
        let value = c_string(value);
        unsafe {
            sys::r_config_set(config, key.as_ptr(), value.as_ptr());
        }
    }
}

/// Parse radare2 numeric output such as `0x400000` or `4194304`.
pub fn parse_r2_u64(value: &str) -> Option<u64> {
    let value = value.trim();
    if value.is_empty() {
        return None;
    }
    if let Some(hex) = value
        .strip_prefix("0x")
        .or_else(|| value.strip_prefix("0X"))
    {
        u64::from_str_radix(hex, 16).ok()
    } else {
        value.parse().ok()
    }
}

fn c_string(value: &str) -> CString {
    CString::new(value.replace('\0', "")).expect("interior NULs were removed")
}

fn first_nonempty_line(value: &str) -> Option<String> {
    value
        .lines()
        .map(str::trim)
        .find(|line| !line.is_empty())
        .map(str::to_owned)
}

fn is_plausible_path(value: &str) -> bool {
    let value = value.trim();
    !value.is_empty()
        && !value.starts_with("ERROR")
        && !value.starts_with("WARN")
        && (value.starts_with('/') || value.starts_with('.') || value.contains('/'))
}

fn file_info_path(value: &str) -> Option<String> {
    value.lines().find_map(|line| {
        let rest = line.trim().strip_prefix("file")?.trim();
        (!rest.is_empty()).then(|| rest.to_owned())
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_radare2_numbers() {
        assert_eq!(parse_r2_u64("0x400000"), Some(0x400000));
        assert_eq!(parse_r2_u64("4194304"), Some(0x400000));
        assert_eq!(parse_r2_u64(""), None);
    }

    #[test]
    fn extracts_file_info_paths() {
        assert_eq!(
            file_info_path("file     /tmp/hello\ntype     EXEC"),
            Some("/tmp/hello".into())
        );
        assert_eq!(
            first_nonempty_line("/tmp/hello\nHello world!"),
            Some("/tmp/hello".into())
        );
        assert!(is_plausible_path("/tmp/ip-server"));
        assert!(!is_plausible_path("ERROR: No file selected"));
        assert!(!is_plausible_path("Hello world!"));
    }
}