idakit 0.2.0

Idiomatic Rust bindings for IDA Pro's idalib kernel
Documentation
//! Mirrors IDA's operand-value-type byte as the closed [`OperandDataType`] enum.
//!
//! Discriminants are the raw values IDA reports for each type, so the `IntoPrimitive`/
//! `TryFromPrimitive` derives are the single source of truth for the mapping. This mirror
//! is pinned to one IDA version, where that set is fixed, so the enum is exhaustive and an
//! alignment test ties it to the facade. A later version that grows the set is a
//! deliberate, breaking widening. An out-of-domain value decodes to
//! [`DecodeError::UnsupportedDataType`](super::DecodeError), never a silent fallback.
//!
//! `data_type` is the *value* type, distinct from the addressing-mode size, since a float
//! and a dword are both four bytes but differ here. This is exactly why the operand keeps
//! `data_type` rather than only a byte count.

use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::VariantArray;

/// The value type of an operand.
#[derive(
    Clone,
    Copy,
    Debug,
    PartialEq,
    Eq,
    Hash,
    IntoPrimitive,
    TryFromPrimitive,
    VariantArray,
    Serialize,
    Deserialize,
)]
#[repr(u8)]
#[doc(alias("op_dtype_t"))]
pub enum OperandDataType {
    /// 8-bit integer.
    #[doc(alias("dt_byte"))]
    Byte = 0,
    /// 16-bit integer.
    #[doc(alias("dt_word"))]
    Word = 1,
    /// 32-bit integer.
    #[doc(alias("dt_dword"))]
    Dword = 2,
    /// 4-byte floating point.
    #[doc(alias("dt_float"))]
    Float = 3,
    /// 8-byte floating point.
    #[doc(alias("dt_double"))]
    Double = 4,
    /// Variable-size floating point (its width depends on the processor).
    #[doc(alias("dt_tbyte"))]
    Tbyte = 5,
    /// Packed real (mc68040).
    #[doc(alias("dt_packreal"))]
    PackReal = 6,
    /// 64-bit integer.
    #[doc(alias("dt_qword"))]
    Qword = 7,
    /// 128-bit integer.
    #[doc(alias("dt_byte16"))]
    Byte16 = 8,
    /// Pointer to code.
    #[doc(alias("dt_code"))]
    Code = 9,
    /// No value type.
    #[doc(alias("dt_void"))]
    Void = 10,
    /// 48-bit.
    #[doc(alias("dt_fword"))]
    Fword = 11,
    /// Bit field (mc680x0).
    #[doc(alias("dt_bitfild"))]
    BitField = 12,
    /// Pointer to an ASCIIZ string.
    #[doc(alias("dt_string"))]
    String = 13,
    /// Pointer to a Unicode string.
    #[doc(alias("dt_unicode"))]
    Unicode = 14,
    /// Long double, which may differ from [`Tbyte`](Self::Tbyte).
    #[doc(alias("dt_ldbl"))]
    Ldbl = 15,
    /// 256-bit integer.
    #[doc(alias("dt_byte32"))]
    Byte32 = 16,
    /// 512-bit integer.
    #[doc(alias("dt_byte64"))]
    Byte64 = 17,
    /// 2-byte floating point.
    #[doc(alias("dt_half"))]
    Half = 18,
}

impl OperandDataType {
    /// Fixed byte width, when the type has one.
    ///
    /// `None` for variable-size ([`Tbyte`](Self::Tbyte), [`Ldbl`](Self::Ldbl),
    /// [`PackReal`](Self::PackReal)), pointer ([`Code`](Self::Code), [`String`](Self::String),
    /// [`Unicode`](Self::Unicode)), or sizeless ([`Void`](Self::Void), [`BitField`](Self::BitField))
    /// types, whose true size is processor- or context-dependent and can't be answered off the
    /// kernel thread.
    #[must_use]
    pub fn bytes(self) -> Option<u32> {
        Some(match self {
            Self::Byte => 1,
            Self::Word | Self::Half => 2,
            Self::Dword | Self::Float => 4,
            Self::Fword => 6,
            Self::Double | Self::Qword => 8,
            Self::Byte16 => 16,
            Self::Byte32 => 32,
            Self::Byte64 => 64,
            _ => return None,
        })
    }

    /// Whether this is a floating-point value type.
    #[inline]
    #[must_use]
    pub fn is_float(self) -> bool {
        matches!(
            self,
            Self::Float | Self::Double | Self::Tbyte | Self::Ldbl | Self::Half
        )
    }
}

impl std::fmt::Display for OperandDataType {
    #[inline]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Byte => "byte",
            Self::Word => "word",
            Self::Dword => "dword",
            Self::Float => "float",
            Self::Double => "double",
            Self::Tbyte => "tbyte",
            Self::PackReal => "packed real",
            Self::Qword => "qword",
            Self::Byte16 => "byte16",
            Self::Code => "code pointer",
            Self::Void => "void",
            Self::Fword => "fword",
            Self::BitField => "bit field",
            Self::String => "string pointer",
            Self::Unicode => "unicode string pointer",
            Self::Ldbl => "long double",
            Self::Byte32 => "byte32",
            Self::Byte64 => "byte64",
            Self::Half => "half",
        })
    }
}

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

    use super::*;

    #[test]
    fn raw_roundtrips_every_variant() {
        for &d in OperandDataType::VARIANTS {
            assert!(OperandDataType::try_from(u8::from(d)).ok() == Some(d));
        }
    }

    // Pin the mirror to the facade's reported values: the facade reports each type in this
    // enum's discriminant order, so a header change (or a mistyped discriminant) mismatches.
    // Pure constant source, no kernel, so it runs as a unit test.
    #[test]
    fn dtype_ids_align_with_the_facade() {
        let ids = sys::op_dtype_ids();
        assert!(ids.len() == OperandDataType::VARIANTS.len());
        for (i, &d) in OperandDataType::VARIANTS.iter().enumerate() {
            assert!(
                ids[i] == u8::from(d),
                "data type {d:?}: facade dt_ {} != discriminant {}",
                ids[i],
                u8::from(d)
            );
        }
    }

    #[test]
    fn try_from_rejects_unknown() {
        assert!(OperandDataType::try_from(19).is_err());
        assert!(OperandDataType::try_from(20).is_err());
        assert!(OperandDataType::try_from(100).is_err());
        assert!(OperandDataType::try_from(255).is_err());
    }

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

        use super::*;

        proptest! {
            // Across the full u8 domain: `try_from` accepts a byte iff it is one of the 19
            // modelled discriminants, and rejects every other byte.
            #[test]
            fn try_from_matches_the_modelled_discriminant_set(byte: u8) {
                let modelled = OperandDataType::VARIANTS.iter().any(|&v| u8::from(v) == byte);
                prop_assert_eq!(OperandDataType::try_from(byte).is_ok(), modelled);
            }
        }
    }

    #[rstest]
    #[case::byte(OperandDataType::Byte, Some(1))]
    #[case::word(OperandDataType::Word, Some(2))]
    #[case::dword(OperandDataType::Dword, Some(4))]
    #[case::float(OperandDataType::Float, Some(4))]
    #[case::double(OperandDataType::Double, Some(8))]
    #[case::tbyte(OperandDataType::Tbyte, None)]
    #[case::pack_real(OperandDataType::PackReal, None)]
    #[case::qword(OperandDataType::Qword, Some(8))]
    #[case::byte16(OperandDataType::Byte16, Some(16))]
    #[case::code(OperandDataType::Code, None)]
    #[case::void(OperandDataType::Void, None)]
    #[case::fword(OperandDataType::Fword, Some(6))]
    #[case::bit_field(OperandDataType::BitField, None)]
    #[case::string(OperandDataType::String, None)]
    #[case::unicode(OperandDataType::Unicode, None)]
    #[case::ldbl(OperandDataType::Ldbl, None)]
    #[case::byte32(OperandDataType::Byte32, Some(32))]
    #[case::byte64(OperandDataType::Byte64, Some(64))]
    #[case::half(OperandDataType::Half, Some(2))]
    fn bytes_matches_every_variant(#[case] dtype: OperandDataType, #[case] expect: Option<u32>) {
        assert!(dtype.bytes() == expect);
    }

    #[test]
    fn float_classification() {
        assert!(OperandDataType::Float.is_float());
        assert!(OperandDataType::Half.is_float());
        assert!(OperandDataType::Ldbl.is_float());
        assert!(!OperandDataType::Dword.is_float());
        assert!(!OperandDataType::Qword.is_float());
    }

    // Every variant renders a non-empty, stable label; stability itself is enforced by the
    // exhaustive match in `Display` (a missed variant fails to compile).
    #[test]
    fn display_renders_every_variant() {
        for &d in OperandDataType::VARIANTS {
            assert!(!d.to_string().is_empty());
        }
        assert!(OperandDataType::Qword.to_string() == "qword");
        assert!(OperandDataType::Byte.to_string() == "byte");
    }

    #[test]
    fn serde_roundtrips_every_variant() {
        for &d in OperandDataType::VARIANTS {
            let json = serde_json::to_string(&d).expect("serialize");
            let back: OperandDataType = serde_json::from_str(&json).expect("deserialize");
            assert!(back == d);
        }
    }

    #[test]
    fn hash_usable_in_set() {
        use std::collections::HashSet;
        let set: HashSet<OperandDataType> = OperandDataType::VARIANTS.iter().copied().collect();
        assert!(set.len() == OperandDataType::VARIANTS.len());
    }
}