asc-smali 0.2.1

baksmali-style disassembler for a single class of a DEX (the built-in decompile backend of asc-rs).
Documentation
//! Textual rendering of descriptors, literals and encoded values.

use crate::Result;
use asc_dex::Dex;
use asc_dex::encoded::{EncodedValue, VALUE_BYTE, VALUE_CHAR, VALUE_DOUBLE, VALUE_FLOAT, VALUE_INT, VALUE_LONG, VALUE_SHORT};

/// Escape a string for a smali string literal: control characters and
/// whatever else could disguise the line ([`asc_query::text::must_escape`])
/// as `\uXXXX`.
pub fn escape_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if asc_query::text::must_escape(c) => asc_query::text::push_unicode_escape(&mut out, c),
            c => out.push(c),
        }
    }
    out
}

/// `(params)ret` for a proto index ([`Dex::proto_descriptor`]).
///
/// # Errors
///
/// [`SmaliError::Dex`](crate::SmaliError::Dex) when the proto, its type list
/// or a type it names is out of range or malformed.
pub fn proto_descriptor(dex: &Dex<'_>, proto_idx: u32) -> Result<String> {
    Ok(dex.proto_descriptor(proto_idx)?)
}

/// `Lcls;->name(params)ret` for a method index ([`Dex::method_signature`]).
///
/// # Errors
///
/// [`SmaliError::Dex`](crate::SmaliError::Dex) when the method id, or a
/// string, type or proto it refers to, is out of range or malformed.
pub fn method_signature(dex: &Dex<'_>, method_idx: u32) -> Result<String> {
    Ok(dex.method_signature(method_idx)?)
}

/// `Lcls;->name:type` for a field index ([`Dex::field_signature`]).
///
/// # Errors
///
/// [`SmaliError::Dex`](crate::SmaliError::Dex) when the field id, or a
/// string or type it refers to, is out of range or malformed.
pub fn field_signature(dex: &Dex<'_>, field_idx: u32) -> Result<String> {
    Ok(dex.field_signature(field_idx)?)
}

/// baksmali's method handle literal: `invoke-static@Lcls;->m(I)V`,
/// `static-get@Lcls;->f:I`.
pub(crate) fn method_handle(dex: &Dex<'_>, handle_idx: u32) -> Result<String> {
    let handle = dex.method_handle(handle_idx)?;
    let member = u32::from(handle.field_or_method_idx);
    let target = if handle.kind.is_field_accessor() { field_signature(dex, member)? } else { method_signature(dex, member)? };
    Ok(format!("{}@{target}", handle.kind))
}

/// baksmali's call site reference:
/// `call_site_0("name", (args)Ret, extra...)@Lbootstrap;->m(...)Ljava/lang/invoke/CallSite;`.
pub(crate) fn call_site(dex: &Dex<'_>, site_idx: u32) -> Result<String> {
    let site = dex.call_site(site_idx)?;
    let mut out =
        format!("call_site_{site_idx}(\"{}\", {}", escape_string(&dex.string(site.method_name)?), proto_descriptor(dex, site.method_type)?);
    for arg in &site.extra_args {
        out.push_str(", ");
        out.push_str(&value(dex, arg)?);
    }
    let bootstrap = dex.method_handle(site.bootstrap)?;
    out.push_str(")@");
    out.push_str(&method_signature(dex, bootstrap.field_or_method_idx.into())?);
    Ok(out)
}

/// Signed hex literal (`0x1a`, `-0x1a`).
pub(crate) fn hex_i64(v: i64) -> String {
    if v < 0 { format!("-0x{:x}", (v as i128).unsigned_abs()) } else { format!("0x{v:x}") }
}

fn float_literal(f: f32) -> String {
    if f.is_nan() {
        "NaNf".into()
    } else if f.is_infinite() {
        if f > 0.0 { "Infinityf".into() } else { "-Infinityf".into() }
    } else if f.fract() == 0.0 && f.abs() < 1e16 {
        format!("{f:.1}f")
    } else {
        format!("{f}f")
    }
}

fn double_literal(f: f64) -> String {
    if f.is_nan() {
        "NaN".into()
    } else if f.is_infinite() {
        if f > 0.0 { "Infinity".into() } else { "-Infinity".into() }
    } else if f.fract() == 0.0 && f.abs() < 1e16 {
        format!("{f:.1}")
    } else {
        format!("{f}")
    }
}

/// Render an encoded value on one line (arrays/annotations use `depth`).
pub(crate) fn value(dex: &Dex<'_>, v: &EncodedValue) -> Result<String> {
    value_indented(dex, v, 0)
}

pub(crate) fn value_indented(dex: &Dex<'_>, v: &EncodedValue, depth: usize) -> Result<String> {
    Ok(match v {
        EncodedValue::Primitive { kind, .. } => match *kind {
            VALUE_BYTE => format!("{}t", hex_i64(v.as_i64().unwrap_or(0))),
            VALUE_SHORT => format!("{}s", hex_i64(v.as_i64().unwrap_or(0))),
            VALUE_CHAR => {
                let c = v.as_i64().unwrap_or(0) as u32;
                match char::from_u32(c) {
                    Some(ch) if (0x20..0x7f).contains(&c) && ch != '\'' && ch != '\\' => format!("'{ch}'"),
                    _ => format!("'\\u{c:04x}'"),
                }
            }
            VALUE_INT => hex_i64(v.as_i64().unwrap_or(0)),
            VALUE_LONG => format!("{}L", hex_i64(v.as_i64().unwrap_or(0))),
            VALUE_FLOAT => float_literal(v.as_f32().unwrap_or(0.0)),
            VALUE_DOUBLE => double_literal(v.as_f64().unwrap_or(0.0)),
            k => format!("<unknown value type {k:#x}>"),
        },
        EncodedValue::MethodType(p) => proto_descriptor(dex, *p)?,
        EncodedValue::MethodHandle(h) => method_handle(dex, *h).unwrap_or_else(|_| format!("method_handle@{h}")),
        EncodedValue::String(s) => format!("\"{}\"", escape_string(&dex.string(*s)?)),
        EncodedValue::Type(t) => dex.type_descriptor(*t)?,
        EncodedValue::Field(f) => field_signature(dex, *f)?,
        EncodedValue::Method(m) => method_signature(dex, *m)?,
        EncodedValue::Enum(f) => format!(".enum {}", field_signature(dex, *f)?),
        EncodedValue::Null => "null".into(),
        EncodedValue::Boolean(b) => {
            if *b {
                "true".into()
            } else {
                "false".into()
            }
        }
        EncodedValue::Array(items) => {
            let indent = "    ".repeat(depth);
            let mut s = String::from("{\n");
            for (i, item) in items.iter().enumerate() {
                s.push_str(&indent);
                s.push_str("    ");
                s.push_str(&value_indented(dex, item, depth + 1)?);
                if i + 1 < items.len() {
                    s.push(',');
                }
                s.push('\n');
            }
            s.push_str(&indent);
            s.push('}');
            s
        }
        EncodedValue::Annotation(ann) => {
            let indent = "    ".repeat(depth);
            let mut s = format!(".subannotation {}\n", dex.type_descriptor(ann.type_idx)?);
            for (name, val) in &ann.elements {
                s.push_str(&indent);
                s.push_str("    ");
                s.push_str(&dex.string(*name)?);
                s.push_str(" = ");
                s.push_str(&value_indented(dex, val, depth + 1)?);
                s.push('\n');
            }
            s.push_str(&indent);
            s.push_str(".end subannotation");
            s
        }
        // A value kind newer than this renderer.
        _ => "<unknown value>".into(),
    })
}

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

    #[test]
    fn string_literals_show_what_could_disguise_them() {
        assert_eq!(escape_string("a\"b\\\n\u{1b}é"), "a\\\"b\\\\\\n\\u001bé");
        // A right-to-left override, a zero-width space, a tag character.
        assert_eq!(escape_string("x\u{202e}y\u{200b}\u{e0041}"), "x\\u202ey\\u200b\\udb40\\udc41");
    }
}