wazabin-qcode 0.4.0

Typed SSA-style p-code IR for binary analysis, modelled after Ghidra's p-code
Documentation
//! Opaque compile-time byte blobs — constants wider than a [`Literal`](crate::value::literal::Literal) can hold.
//!
//! A numeric [`Literal`](crate::value::literal::Literal) is a single `u64`; constants
//! that exceed 64 bits (SSE/AVX register pools, wide stack/memory reads, the
//! result of coalescing several adjacent constant stores) cannot be represented
//! that way without breaking the u64-centric folding pipeline. A [`Bytes`]
//! value sidesteps that: it is a raw, opaque byte vector with **no arithmetic
//! meaning**, stored as a little-endian, memory-order snapshot (`data[i]` is the
//! byte at `base + i`, matching the target's fixed little-endian layout).
//!
//! Every `Bytes` carries a [`TypeId`] — typically an `Array(i8, len)` — with the
//! invariant `size_of(type_id) == data.len()`, enforced at construction. Unlike
//! numeric literals, `Bytes` values are **not interned**: each construction
//! produces a fresh [`BytesId`], so downstream equality must compare contents,
//! never IDs.

use crate::{
    context::Shared,
    types::TypeId,
    value::{
        Value, ValueId,
        util::base_ref::{BaseRef, WithShared},
    },
};
use jstd::Identifier;

#[derive(Identifier)]
pub struct BytesId(usize);

/// A compile-time opaque byte blob stored in a [`Context`](crate::context::Context).
///
/// The bytes are held in little-endian, memory-order layout. See the module
/// docs for the rationale and invariants.
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Bytes {
    /// Raw bytes in target memory order (`data[i]` = byte at `base + i`).
    pub data: Vec<u8>,
    /// The type of this constant; `size_of(type_id) == data.len()`.
    pub type_id: TypeId,
}

pub type BytesRef<'str, 'ctx> = BaseRef<&'ctx Shared<'str>, BytesId>;

impl<'s, 'ctx: 's, 'str: 'ctx> WithShared<'s, 'ctx, 'str> for BytesRef<'str, 'ctx> {
    fn shared(&'s self) -> &'ctx Shared<'str> {
        self.ctx
    }
}

impl<'s, 'ctx: 's, 'str: 'ctx, Ctx> BaseRef<Ctx, BytesId>
where
    Self: WithShared<'s, 'ctx, 'str>,
{
    fn inner(&'s self) -> &'ctx Bytes {
        &self.shared().values.bytes[self.id]
    }

    /// The raw bytes in target memory order.
    pub fn data(&'s self) -> &'ctx [u8] {
        &self.inner().data
    }

    /// Returns the [`TypeId`] of this blob.
    pub fn type_id(&'s self) -> TypeId {
        self.inner().type_id
    }
}

/// The encoding under which a byte blob was successfully read as text.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StringEncoding {
    /// Printable 7-bit ASCII (one byte per character).
    Ascii,
    /// Printable UTF-16, little-endian (two bytes per code unit).
    Utf16Le,
}

impl StringEncoding {
    /// Short human-readable label (e.g. for a UI column).
    pub fn label(self) -> &'static str {
        match self {
            StringEncoding::Ascii => "ascii",
            StringEncoding::Utf16Le => "utf16le",
        }
    }
}

/// Attempt to decode `data` as a printable ASCII or UTF-16LE string.
///
/// Both encodings tolerate a single trailing NUL terminator (the common C /
/// Windows-`W` convention). Returns the decoded text and the encoding it was
/// read under, but only when every character is printable; otherwise the blob
/// has no clean string reading and the caller should fall back to the `\xNN`
/// hex form.
pub fn decode_string(data: &[u8]) -> Option<(StringEncoding, String)> {
    if data.is_empty() {
        return None;
    }

    // ASCII, optionally NUL-terminated.
    let ascii = data.strip_suffix(&[0]).unwrap_or(data);
    if !ascii.is_empty() && ascii.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
        return Some((
            StringEncoding::Ascii,
            ascii.iter().map(|&b| b as char).collect(),
        ));
    }

    // UTF-16LE, optionally NUL-terminated.
    if data.len() >= 2 && data.len().is_multiple_of(2) {
        let (pairs, _) = data.as_chunks::<2>();
        let units: Vec<u16> = pairs.iter().map(|&pair| u16::from_le_bytes(pair)).collect();
        let units = units.strip_suffix(&[0]).unwrap_or(&units);
        // Require printable ASCII-range code units: random binary read as
        // UTF-16 lands in CJK / presentation-form ranges that decode to valid
        // but meaningless text (e.g. "凜ﯺ"). Genuine wide strings (the Windows
        // `W`-API convention, e.g. "ntdll.dll") are ASCII in the low byte with a
        // zero high byte, so this keeps real strings and drops the noise.
        if !units.is_empty()
            && units
                .iter()
                .all(|&u| u < 0x80 && (u as u8).is_ascii_graphic() || u == b' ' as u16)
        {
            let s: String = units.iter().map(|&u| u as u8 as char).collect();
            return Some((StringEncoding::Utf16Le, s));
        }
    }

    None
}

/// Escape a decoded string for display inside `b"..."` quotes.
pub fn escape_decoded(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '"' | '\\' => {
                out.push('\\');
                out.push(c);
            }
            _ => out.push(c),
        }
    }
    out
}

/// How a [`Bytes`] blob should be rendered as a `b"..."` literal.
///
/// [`Auto`](BytesDisplay::Auto) is the default and lets [`decode_string`] pick
/// the encoding (or fall back to hex). The remaining variants are user-forced
/// overrides — e.g. from the GUI Strings pane — and are applied even when the
/// blob is not cleanly printable, escaping any bytes that don't fit.
#[derive(
    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub enum BytesDisplay {
    /// Auto-detect ASCII / UTF-16LE, else hex.
    #[default]
    Auto,
    /// Force ASCII rendering.
    Ascii,
    /// Force UTF-16LE rendering.
    Utf16Le,
    /// Force raw `\xNN` hex.
    Raw,
}

/// Push one ASCII byte to `out`, either as a literal char or a `\xNN` escape.
fn push_ascii_byte(out: &mut String, b: u8) {
    match b {
        b'"' | b'\\' => {
            out.push('\\');
            out.push(b as char);
        }
        _ if b.is_ascii_graphic() || b == b' ' => out.push(b as char),
        _ => out.push_str(&format!("\\x{b:02x}")),
    }
}

/// Render `data` as the full `b"..."` literal text under `mode`.
///
/// Forced ASCII/UTF-16LE modes are best-effort: bytes (or code units) that
/// aren't printable are escaped rather than rejected, so the user always sees
/// the override they asked for.
pub fn render_bytes_literal(data: &[u8], mode: BytesDisplay) -> String {
    let mut out = String::new();
    out.push_str("b\"");
    match mode {
        BytesDisplay::Auto => {
            if let Some((_, s)) = decode_string(data) {
                out.push_str(&escape_decoded(&s));
            } else {
                for &b in data {
                    out.push_str(&format!("\\x{b:02x}"));
                }
            }
        }
        BytesDisplay::Ascii => {
            for &b in data {
                push_ascii_byte(&mut out, b);
            }
        }
        BytesDisplay::Utf16Le => {
            let (pairs, remainder) = data.as_chunks::<2>();
            for &pair in pairs {
                let unit = u16::from_le_bytes(pair);
                match char::from_u32(unit as u32) {
                    Some(ch) if !ch.is_control() => match ch {
                        '"' | '\\' => {
                            out.push('\\');
                            out.push(ch);
                        }
                        _ => out.push(ch),
                    },
                    _ => out.push_str(&format!("\\u{{{unit:04x}}}")),
                }
            }
            // Trailing odd byte, if any.
            for &b in remainder {
                out.push_str(&format!("\\x{b:02x}"));
            }
        }
        BytesDisplay::Raw => {
            for &b in data {
                out.push_str(&format!("\\x{b:02x}"));
            }
        }
    }
    out.push('"');
    out
}

impl std::fmt::Display for BytesRef<'_, '_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let data = &self.ctx.values.bytes[self.id].data;
        let mode = self.ctx.bytes_display(self.id);
        if mode != BytesDisplay::Auto {
            return f.write_str(&render_bytes_literal(data, mode));
        }
        if let Some((_, s)) = decode_string(data) {
            return write!(f, "b\"{}\"", escape_decoded(&s));
        }
        write!(f, "b\"")?;
        for &b in data {
            write!(f, "\\x{:02x}", b)?;
        }
        write!(f, "\"")
    }
}

impl<'str, 'ctx> Value<'str, 'ctx> for BytesRef<'str, 'ctx> {
    fn id(&self) -> ValueId {
        ValueId::Bytes(self.id)
    }

    fn size(&self) -> usize {
        self.ctx.values.bytes[self.id].data.len()
    }
}