idakit 0.2.0

Idiomatic Rust bindings for IDA Pro's idalib kernel
Documentation
//! Enumerates IDA's string list through the [`StringLiteral`] view and [`Strings`].
//!
//! The natural singular name `String` collides with [`std::string::String`], so the view is
//! [`StringLiteral`] while the iterator and the [`Database::strings`] method keep the ergonomic
//! `strings` stem.

use idakit_sys as sys;

use crate::Database;
use crate::address::Address;

impl Database {
    /// Iterate every string literal IDA located in the database.
    ///
    /// This (re)builds IDA's string list first, an O(database) scan, then walks it. Collect the
    /// result once if you iterate repeatedly, rather than calling this again.
    #[must_use]
    #[doc(alias("build_strlist"))]
    pub fn strings(&self) -> Strings<'_> {
        self.strlist_build();
        Strings::new(self)
    }
}

/// A borrowed view of one string IDA located, keyed by address.
///
/// Carries the [`address`](Self::address), octet [`len`](Self::len), and decoded
/// [`text`](Self::text). The raw type fields are read once at iteration; the text is decoded
/// on demand.
#[derive(Clone, Copy)]
#[doc(alias("string_info_t"))]
pub struct StringLiteral<'db> {
    address: Address,
    length: usize,
    raw_type: i32,
    db: &'db Database,
}

impl<'db> StringLiteral<'db> {
    #[inline]
    pub(crate) fn new(address: Address, length: usize, raw_type: i32, db: &'db Database) -> Self {
        Self {
            address,
            length,
            raw_type,
            db,
        }
    }

    /// The string's address.
    #[inline]
    #[must_use]
    pub const fn address(&self) -> Address {
        self.address
    }

    /// The string's length in octets (raw bytes), excluding any terminator. Divide by
    /// [`char_width`](Self::char_width) for a character count.
    #[inline]
    #[must_use]
    pub const fn len(&self) -> usize {
        self.length
    }

    /// Whether the string is empty.
    #[inline]
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.length == 0
    }

    /// Bytes per character: `1` (byte / UTF-8), `2` (UTF-16), or `4` (UTF-32).
    #[inline]
    #[must_use]
    pub fn char_width(&self) -> u8 {
        char_width_of(self.raw_type)
    }

    /// Whether the string is length-prefixed (Pascal-style) rather than terminated.
    #[inline]
    #[must_use]
    pub fn is_pascal(&self) -> bool {
        is_pascal_of(self.raw_type)
    }

    /// The decoded string as UTF-8, or `None` if the bytes can't be read.
    ///
    /// This is the semantic form, for search, matching, and analysis: undecodable units become the
    /// Unicode replacement character (U+FFFD) rather than failing. For the pseudocode-faithful
    /// rendering with control bytes shown as C escapes, use [`escaped`](Self::escaped).
    #[must_use]
    #[doc(alias("get_strlit_contents"))]
    pub fn text(&self) -> Option<String> {
        self.db
            .strlit_contents(self.address, self.length, self.raw_type)
    }

    /// The string in its C-escaped display form, or `None` if the bytes can't be read.
    ///
    /// This is what the decompiler renders in pseudocode: non-printable and undecodable bytes show
    /// as C escapes (`\n`, `\xNN`, `\uNNNN`). Use [`text`](Self::text) for the semantic form.
    #[must_use]
    #[doc(alias("get_strlit_contents"))]
    pub fn escaped(&self) -> Option<String> {
        self.db
            .strlit_escaped(self.address, self.length, self.raw_type)
    }
}

impl std::fmt::Debug for StringLiteral<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("StringLiteral")
            .field("address", &self.address)
            .field("len", &self.length)
            .field("char_width", &self.char_width())
            .field("text", &self.text())
            .finish()
    }
}

key_identity!(StringLiteral, address, ord);

/// Bytes per character encoded in the raw string-type code: 1, 2, or 4.
///
/// Only the low byte carries width/layout; the high byte's encoding index is irrelevant here.
fn char_width_of(raw_type: i32) -> u8 {
    1u8 << ((raw_type & sys::STRWIDTH_MASK) as u8)
}

/// Whether a string's layout is Pascal (length-prefixed) rather than terminated.
fn is_pascal_of(raw_type: i32) -> bool {
    let layout = (raw_type & sys::STRLYT_MASK) >> sys::STRLYT_SHIFT;
    (1..=3).contains(&layout)
}

/// A lazy iterator over IDA's string list, in list order, from [`Database::strings`].
///
/// Borrows `&Database`, so it can't coexist with a write. `size_hint`'s lower bound is `0`: a
/// list entry with no readable address is skipped.
#[doc(alias("get_strlist_qty", "get_strlist_item"))]
pub struct Strings<'db> {
    db: &'db Database,
    next: usize,
    count: usize,
}

impl<'db> Strings<'db> {
    #[inline]
    pub(crate) fn new(db: &'db Database) -> Self {
        Self {
            db,
            next: 0,
            count: db.strlist_qty(),
        }
    }

    /// Read list entry `n` into a view, or `None` if it is out of range or has no valid address.
    fn item(&self, n: usize) -> Option<StringLiteral<'db>> {
        let item = self.db.strlist_item(n)?;
        let address = Address::try_new(item.ea)?;
        Some(StringLiteral::new(
            address,
            item.length.max(0) as usize,
            item.type_,
            self.db,
        ))
    }
}

impl<'db> Iterator for Strings<'db> {
    type Item = StringLiteral<'db>;

    fn next(&mut self) -> Option<Self::Item> {
        while self.next < self.count {
            let n = self.next;
            self.next += 1;
            if let Some(string) = self.item(n) {
                return Some(string);
            }
        }
        None
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (0, Some(self.count - self.next))
    }
}

#[cfg(test)]
mod tests {
    use assert2::assert;
    use rstest::rstest;

    use super::*;

    // Raw string-type codes: width in bits 0-1, layout in bits 2+.
    const STRTYPE_C: i32 = 0x00; // 1-byte, terminated
    const STRTYPE_C_16: i32 = 0x01; // 2-byte, terminated
    const STRTYPE_C_32: i32 = 0x02; // 4-byte, terminated
    const STRTYPE_PASCAL: i32 = 0x04; // 1-byte, 1-byte length prefix
    const STRTYPE_PASCAL_16: i32 = 0x05; // 2-byte, 1-byte length prefix
    const STRTYPE_LEN2: i32 = 0x08; // 1-byte, 2-byte length prefix

    /// Width comes from the low bits, and a nonzero encoding index (high byte) does not
    /// disturb it.
    #[rstest]
    #[case(STRTYPE_C, 1)]
    #[case(STRTYPE_C_16, 2)]
    #[case(STRTYPE_C_32, 4)]
    #[case(STRTYPE_PASCAL, 1)]
    #[case(STRTYPE_PASCAL_16, 2)]
    #[case(STRTYPE_C | 0x5500_0000, 1)] // encoding index in the high byte is ignored
    fn char_width_reads_the_strwidth_field(#[case] raw: i32, #[case] width: u8) {
        assert!(char_width_of(raw) == width);
    }

    /// Terminated layouts are not Pascal; the three length-prefixed layouts are.
    #[rstest]
    #[case(STRTYPE_C, false)]
    #[case(STRTYPE_C_16, false)]
    #[case(STRTYPE_PASCAL, true)]
    #[case(STRTYPE_PASCAL_16, true)]
    #[case(STRTYPE_LEN2, true)]
    fn is_pascal_reads_the_strlyt_field(#[case] raw: i32, #[case] pascal: bool) {
        assert!(is_pascal_of(raw) == pascal);
    }

    /// `len`/`is_empty` read the stored length back verbatim, zero or not.
    #[rstest]
    #[case(0, true)]
    #[case(1, false)]
    #[case(5, false)]
    fn len_and_is_empty_reflect_the_stored_length(#[case] length: usize, #[case] empty: bool) {
        let db = Database::new();
        let literal = StringLiteral::new(Address::new_const(0x1000), length, STRTYPE_C, &db);
        assert!(literal.len() == length);
        assert!(literal.is_empty() == empty);
    }

    /// `char_width`/`is_pascal` forward to the free functions above over the stored raw type.
    #[rstest]
    #[case(STRTYPE_C, 1, false)]
    #[case(STRTYPE_C_16, 2, false)]
    #[case(STRTYPE_PASCAL, 1, true)]
    fn char_width_and_is_pascal_forward_the_raw_type(
        #[case] raw: i32,
        #[case] width: u8,
        #[case] pascal: bool,
    ) {
        let db = Database::new();
        let literal = StringLiteral::new(Address::new_const(0x1000), 0, raw, &db);
        assert!(literal.char_width() == width);
        assert!(literal.is_pascal() == pascal);
    }

    /// Identity is the address alone: two entries at the same address are equal even with
    /// different lengths/types, and a different address is never equal.
    #[test]
    fn string_literal_identity_compares_by_address() {
        let db = Database::new();
        let a = Address::new_const(0x1000);
        let b = Address::new_const(0x2000);
        assert!(
            StringLiteral::new(a, 4, STRTYPE_C, &db)
                == StringLiteral::new(a, 8, STRTYPE_PASCAL, &db)
        );
        assert!(
            StringLiteral::new(a, 4, STRTYPE_C, &db) != StringLiteral::new(b, 4, STRTYPE_C, &db)
        );
    }

    #[test]
    fn string_literal_ord_sorts_by_address() {
        let db = Database::new();
        let hi = Address::new_const(0x2000);
        let lo = Address::new_const(0x1000);
        let mut strings = [
            StringLiteral::new(hi, 1, STRTYPE_C, &db),
            StringLiteral::new(lo, 1, STRTYPE_C, &db),
        ];
        strings.sort();
        assert!(strings[0].address() == lo);
        assert!(strings[1].address() == hi);
    }

    /// `size_hint`'s upper bound is the remaining `count - next` span, with no live kernel
    /// involved: it never walks the list itself.
    #[test]
    fn strings_size_hint_reports_the_remaining_span() {
        let db = Database::new();
        let strings = Strings {
            db: &db,
            next: 2,
            count: 6,
        };
        assert!(strings.size_hint() == (0, Some(4)));
    }

    mod proptests {
        use proptest::prelude::*;

        use super::*;

        proptest! {
            // Across the full raw STRTYPE domain: width comes only from the low two bits, so it
            // is always a power of two in {1, 2, 4, 8} (8 is STRWIDTH's unassigned reserved
            // value; no real IDA database emits it, but the parser must not panic on it either),
            // and it is never disturbed by any bit outside STRWIDTH_MASK.
            #[test]
            fn char_width_of_never_panics_and_is_a_power_of_two(raw in any::<i32>()) {
                let width = char_width_of(raw);
                prop_assert!(matches!(width, 1 | 2 | 4 | 8));
                prop_assert_eq!(width, char_width_of(raw & sys::STRWIDTH_MASK));
            }

            // is_pascal_of never panics, and only depends on the STRLYT field (bits outside
            // STRWIDTH_MASK).
            #[test]
            fn is_pascal_of_never_panics_and_ignores_width_bits(raw in any::<i32>(), width_bits in 0i32..4) {
                prop_assert_eq!(
                    is_pascal_of(raw),
                    is_pascal_of((raw & !sys::STRWIDTH_MASK) | width_bits)
                );
            }
        }
    }
}