use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
struct ConstantDomain {
members: Vec<(String, String, i64)>,
}
impl ConstantDomain {
fn exact(&self, value: i64) -> Option<&(String, String, i64)> {
let mut hit = None;
for m in &self.members {
if m.2 == value {
if hit.is_some() {
return None;
}
hit = Some(m);
}
}
hit
}
}
#[derive(Debug, Default)]
pub struct PlatformSymbols {
domains: HashMap<(String, String, String), Vec<Option<ConstantDomain>>>,
}
static SYMBOLS: OnceLock<PlatformSymbols> = OnceLock::new();
pub fn set_symbols(s: PlatformSymbols) {
let _ = SYMBOLS.set(s);
}
fn active() -> Option<&'static PlatformSymbols> {
SYMBOLS.get().filter(|s| !s.domains.is_empty())
}
pub fn installed() -> bool {
active().is_some()
}
pub fn named_constant(cls: &str, name: &str, desc: &str, arg: usize, value: i64) -> Option<String> {
let syms = active()?;
let per_arg = syms.domains.get(&(cls.to_string(), name.to_string(), desc.to_string()))?;
let domain = per_arg.get(arg).or_else(|| per_arg.first()).cloned().flatten()?;
let (owner, field, _) = domain.exact(value)?;
Some(format!("{}.{}", owner.replace('/', "."), field))
}
impl PlatformSymbols {
pub fn from_metadata(xmls: &[&[u8]], constants: &HashMap<(String, String), i64>) -> Self {
let mut domains: HashMap<(String, String, String), Vec<Option<ConstantDomain>>> =
HashMap::new();
let mut on_domain = |class: &str, method: &str, desc: &str, param: usize,
ann: &str, value: &str| {
if !ann.contains("IntDef") && !ann.contains("LongDef") {
return;
}
let members: Vec<(String, String, i64)> = value
.trim_matches(|c| c == '{' || c == '}')
.split(',')
.filter_map(|m| {
let m = m.trim();
let (owner, field) = m.rsplit_once('.')?;
let owner = owner.replace('.', "/");
let v = constants.get(&(owner.clone(), field.to_string()))?;
Some((owner, field.to_string(), *v))
})
.collect();
if members.is_empty() {
return;
}
let entry = domains
.entry((class.to_string(), method.to_string(), desc.to_string()))
.or_default();
while entry.len() <= param {
entry.push(None);
}
entry[param] = Some(ConstantDomain { members });
};
for xml in xmls {
parse_annotations_xml(xml, &mut on_domain);
}
Self { domains }
}
pub fn domain_count(&self) -> usize {
self.domains.values().filter(|v| v.iter().any(|d| d.is_some())).count()
}
pub fn serialize(&self) -> Vec<u8> {
use std::collections::BTreeMap;
let ordered: BTreeMap<_, _> = self.domains.iter().collect(); let mut out = Vec::new();
out.extend_from_slice(&(ordered.len() as u32).to_le_bytes());
for ((cls, name, desc), per_arg) in ordered {
put_str(&mut out, cls);
put_str(&mut out, name);
put_str(&mut out, desc);
out.push(per_arg.len() as u8);
for d in per_arg {
match d {
Some(d) => {
out.push(1);
out.extend_from_slice(&(d.members.len() as u16).to_le_bytes());
for (owner, field, value) in &d.members {
put_str(&mut out, owner);
put_str(&mut out, field);
out.extend_from_slice(&value.to_le_bytes());
}
}
None => out.push(0),
}
}
}
out
}
pub fn from_text(text: &[u8]) -> Option<Self> {
let text = String::from_utf8_lossy(text);
let mut domains = HashMap::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.split(' ');
let cls = parts.next()?.replace('.', "/");
let name = parts.next()?.to_string();
let desc = parts.next()?.to_string();
let entry = domains
.entry((cls, name, desc))
.or_insert_with(Vec::new);
while let Some(idx) = parts.next() {
let param: usize = idx.parse().ok()?;
let members_s = parts.next()?;
while entry.len() <= param {
entry.push(None);
}
if members_s == "-" {
continue;
}
let members: Vec<(String, String, i64)> = members_s
.split(',')
.filter_map(|m| {
let (path, value) = m.rsplit_once('=')?;
let value: i64 = value.parse().ok()?;
let (owner_dot, field) = path.rsplit_once('.')?;
Some((owner_dot.replace('.', "/"), field.to_string(), value))
})
.collect();
if !members.is_empty() {
entry[param] = Some(ConstantDomain { members });
}
}
}
Some(Self { domains })
}
pub fn to_text(&self) -> String {
use std::collections::BTreeMap;
let ordered: BTreeMap<_, _> = self.domains.iter().collect();
let mut out = String::new();
for ((cls, name, desc), per_arg) in ordered {
let mut line = format!("{cls} {name} {desc}");
for (i, d) in per_arg.iter().enumerate() {
match d {
Some(d) => {
let members: Vec<String> = d
.members
.iter()
.map(|(owner, field, value)| {
format!("{}.{}={}", owner.replace('/', "."), field, value)
})
.collect();
line.push_str(&format!(" {i} {}", members.join(",")));
}
None => line.push_str(&format!(" {i} -")),
}
}
if line.split(' ').count() > 3 {
out.push_str(&line);
out.push('\n');
}
}
out
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let mut p = 0usize;
let rd_u32 = |p: &mut usize| -> Option<u32> {
let v = u32::from_le_bytes(bytes.get(*p..*p + 4)?.try_into().ok()?);
*p += 4;
Some(v)
};
let rd_u16 = |p: &mut usize| -> Option<u16> {
let v = u16::from_le_bytes(bytes.get(*p..*p + 2)?.try_into().ok()?);
*p += 2;
Some(v)
};
let rd_i64 = |p: &mut usize| -> Option<i64> {
let v = i64::from_le_bytes(bytes.get(*p..*p + 8)?.try_into().ok()?);
*p += 8;
Some(v)
};
let get_str = |p: &mut usize| -> Option<String> {
let len = rd_u16(p)? as usize;
let s = String::from_utf8(bytes.get(*p..*p + len)?.to_vec()).ok()?;
*p += len;
Some(s)
};
let count = rd_u32(&mut p)? as usize;
let mut domains = HashMap::with_capacity(count);
for _ in 0..count {
let cls = get_str(&mut p)?;
let name = get_str(&mut p)?;
let desc = get_str(&mut p)?;
let n = *bytes.get(p)?;
p += 1;
let mut per_arg = Vec::with_capacity(n as usize);
for _ in 0..n {
if *bytes.get(p)? == 1 {
p += 1;
let mc = rd_u16(&mut p)? as usize;
let mut members = Vec::with_capacity(mc);
for _ in 0..mc {
let owner = get_str(&mut p)?;
let field = get_str(&mut p)?;
let value = rd_i64(&mut p)?;
members.push((owner, field, value));
}
per_arg.push(Some(ConstantDomain { members }));
} else {
p += 1;
per_arg.push(None);
}
}
domains.insert((cls, name, desc), per_arg);
}
Some(Self { domains })
}
}
fn put_str(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(&(s.len() as u16).to_le_bytes());
out.extend_from_slice(s.as_bytes());
}
fn java_type_to_dex(t: &str) -> String {
let t = t.trim();
if t.contains('<') || t.contains("<") || t.starts_with("? ") || t.starts_with("?\t") {
return "Ljava/lang/Object;".to_string();
}
if let Some(inner) = t.strip_suffix("[]") {
return format!("[{}", java_type_to_dex(inner));
}
let prim = match t {
"void" => "V",
"int" => "I",
"long" => "J",
"float" => "F",
"double" => "D",
"boolean" => "Z",
"byte" => "B",
"char" => "C",
"short" => "S",
_ => "",
};
if !prim.is_empty() {
return prim.to_string();
}
if t.is_empty() {
return String::new();
}
format!("L{};", t.replace('.', "/"))
}
type DomainSink<'a> = dyn FnMut(&str, &str, &str, usize, &str, &str) + 'a;
fn parse_annotations_xml(xml: &[u8], on_domain: &mut DomainSink<'_>) {
let text = String::from_utf8_lossy(xml);
let attr = |line: &str, key: &str| -> Option<String> {
let pat = format!("{key}=\"");
let start = line.find(&pat)? + pat.len();
let end = line[start..].find('"')? + start;
Some(line[start..end].to_string())
};
let mut item: Option<(String, String, String, usize)> = None;
let mut cur_ann: Option<String> = None;
for line in text.lines() {
let t = line.trim();
if t.starts_with("<item ") {
let name = attr(t, "name").unwrap_or_default()
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("&", "&");
let mut tail = name.rsplitn(2, ' ');
let param = tail.next().and_then(|p| p.parse::<usize>().ok());
let rest = tail.next().unwrap_or("").trim().to_string();
let (class, rest) = match rest.split_once(' ') {
Some((c, r)) => (c.replace('.', "/"), r.to_string()),
None => (rest.clone(), String::new()),
};
let (ret, method, args) = match rest.find('(') {
Some(p) => {
let sig = &rest[..p];
let (ret, method) = match sig.rsplit_once(' ') {
Some((r, m)) => (r.to_string(), m.to_string()),
None => ("void".to_string(), sig.to_string()),
};
(ret, method, rest[p + 1..].trim_end_matches(')'))
}
None => (String::new(), String::new(), ""),
};
let mut desc = String::from("(");
for a in args.split(',') {
desc.push_str(&java_type_to_dex(a.trim()));
}
desc.push(')');
desc.push_str(&java_type_to_dex(&ret));
item = param.map(|p| (class, method, desc, p));
} else if t.starts_with("<annotation ") {
cur_ann = attr(t, "name");
} else if t.starts_with("<val ") {
if let (Some((class, method, desc, param)), Some(ann)) = (&item, &cur_ann) {
if attr(t, "name").as_deref() == Some("value") {
if let Some(v) = attr(t, "val") {
on_domain(class, method, desc, *param, ann, &v);
}
}
}
} else if t.starts_with("</item>") {
item = None;
cur_ann = None;
}
}
}
pub type ClassConstants = (HashMap<(String, String), i64>, Vec<(String, String, String)>);
pub fn classfile_constants(bytes: &[u8]) -> Option<ClassConstants> {
if bytes.len() < 10 || &bytes[..4] != b"\xca\xfe\xba\xbe" {
return None;
}
let rd_u1 = |p: usize| -> Option<u8> { bytes.get(p).copied() };
let rd_u2 = |p: usize| -> Option<u16> {
Some(u16::from_be_bytes([bytes.get(p).copied()?, bytes.get(p + 1).copied()?]))
};
let rd_u4 = |p: usize| -> Option<u32> {
Some(u32::from_be_bytes([
bytes.get(p).copied()?,
bytes.get(p + 1).copied()?,
bytes.get(p + 2).copied()?,
bytes.get(p + 3).copied()?,
]))
};
let rd_i64 = |p: usize| -> Option<i64> {
Some(i64::from_be_bytes([
bytes.get(p).copied()?,
bytes.get(p + 1).copied()?,
bytes.get(p + 2).copied()?,
bytes.get(p + 3).copied()?,
bytes.get(p + 4).copied()?,
bytes.get(p + 5).copied()?,
bytes.get(p + 6).copied()?,
bytes.get(p + 7).copied()?,
]))
};
let count = rd_u2(8)? as usize;
let mut tags = vec![0u8; count];
let mut ints: HashMap<usize, i32> = HashMap::new();
let mut longs: HashMap<usize, i64> = HashMap::new();
let mut utf8s: HashMap<usize, (usize, usize)> = HashMap::new(); let mut classes: HashMap<usize, usize> = HashMap::new(); let mut p = 10usize;
let mut i = 1usize;
while i < count {
let tag = rd_u1(p)?;
p += 1;
tags[i] = tag;
match tag {
1 => {
let len = rd_u2(p)? as usize;
utf8s.insert(i, (p + 2, len));
p += 2 + len;
}
3 => {
ints.insert(i, i32::from_be_bytes(rd_u4(p)?.to_be_bytes()));
p += 4;
}
4 => p += 4,
5 | 6 | 7 | 8 | 16 | 19 | 20 => {
if tag == 5 {
longs.insert(i, rd_i64(p)?);
}
if tag == 7 {
classes.insert(i, rd_u2(p)? as usize);
}
p += if tag == 5 || tag == 6 { 8 } else { 2 };
}
9 | 10 | 11 | 12 | 17 | 18 => p += 4,
15 => p += 3,
_ => return None, }
i += if tag == 5 || tag == 6 { 2 } else { 1 };
let _ = i;
}
let utf8 = |slot: usize| -> Option<String> {
let (off, len) = *utf8s.get(&slot)?;
Some(String::from_utf8_lossy(bytes.get(off..off + len)?).into_owned())
};
let this_class = *classes.get(&(rd_u2(p + 2)? as usize))?;
p += 6;
let ifaces = rd_u2(p)? as usize;
p += 2 + 2 * ifaces;
let class_name = utf8(this_class)?.trim_start_matches('L').trim_end_matches(';').to_string();
let mut constants = HashMap::new();
let mut methods = Vec::new();
for read_fields in [true, false] {
let n = rd_u2(p)? as usize;
p += 2;
for _ in 0..n {
let _access = rd_u2(p)?;
let name_idx = rd_u2(p + 2)? as usize;
let desc_idx = rd_u2(p + 4)? as usize;
let nattrs = rd_u2(p + 6)? as usize;
p += 8;
let name = utf8(name_idx)?;
let desc = utf8(desc_idx)?;
for _ in 0..nattrs {
let attr_name = utf8(rd_u2(p)? as usize)?;
let len = rd_u4(p + 2)? as usize;
let body = p + 6;
if read_fields && attr_name == "ConstantValue" {
let idx = rd_u2(body)? as usize;
let value = if let Some(v) = ints.get(&idx) {
Some(*v as i64)
} else {
longs.get(&idx).copied()
};
if let Some(v) = value {
constants.insert((class_name.clone(), name.clone()), v);
}
}
if !read_fields {
methods.push((class_name.clone(), name.clone(), desc.clone()));
}
p = body + len;
}
}
}
Some((constants, methods))
}
#[cfg(test)]
mod tests {
use super::*;
const XML: &[u8] = br#"
<root>
<item name="android.view.View void setVisibility(int) 0">
<annotation name="androidx.annotation.IntDef">
<val name="value" val="{android.view.View.VISIBLE, android.view.View.INVISIBLE, android.view.View.GONE}" />
</annotation>
</item>
<item name="android.content.Intent android.content.Intent addFlags(int) 0">
<annotation name="androidx.annotation.IntDef">
<val name="value" val="{android.content.Intent.FLAG_ACTIVITY_NEW_TASK, android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP}" />
</annotation>
</item>
<item name="android.view.View void setSomething(android.view.View) 1">
<annotation name="androidx.annotation.StringDef">
<val name="value" val="{android.view.View.NAME}" />
</annotation>
</item>
</root>
"#;
#[test]
fn domains_parse_and_match() {
let mut constants: HashMap<(String, String), i64> = HashMap::new();
for (c, f, v) in [
("android/view/View", "VISIBLE", 0i64),
("android/view/View", "INVISIBLE", 4),
("android/view/View", "GONE", 8),
("android/content/Intent", "FLAG_ACTIVITY_NEW_TASK", 0x10000000),
("android/content/Intent", "FLAG_ACTIVITY_CLEAR_TOP", 0x04000000),
] {
constants.insert((c.to_string(), f.to_string()), v);
}
let syms = PlatformSymbols::from_metadata(&[XML], &constants);
assert_eq!(syms.domain_count(), 2, "IntDef domains only");
set_symbols(PlatformSymbols::from_metadata(&[XML], &constants));
assert_eq!(
named_constant("android/view/View", "setVisibility", "(I)V", 0, 8),
Some("android.view.View.GONE".to_string())
);
assert!(named_constant("android/view/View", "setVisibility", "(I)V", 0, 7).is_none());
assert!(named_constant("android/view/View", "setAlpha", "(F)V", 0, 0).is_none());
assert!(named_constant("android/view/View", "setVisibility", "(J)V", 0, 0).is_none());
}
#[test]
fn text_roundtrip() {
let mut constants: HashMap<(String, String), i64> = HashMap::new();
for (c, f, v) in [
("android/view/View", "VISIBLE", 0i64),
("android/view/View", "GONE", 8),
] {
constants.insert((c.to_string(), f.to_string()), v);
}
let syms = PlatformSymbols::from_metadata(&[XML], &constants);
let text = syms.to_text();
let back = PlatformSymbols::from_text(text.as_bytes()).expect("text parse");
assert_eq!(back.domain_count(), syms.domain_count());
let per_arg = back
.domains
.get(&("android/view/View".to_string(), "setVisibility".to_string(), "(I)V".to_string()))
.expect("domain present");
let domain = per_arg.first().and_then(|d| d.as_ref()).expect("param 0 domain");
assert_eq!(domain.exact(8).map(|m| m.1.as_str()), Some("GONE"));
assert_eq!(domain.exact(0).map(|m| m.1.as_str()), Some("VISIBLE"));
}
#[test]
fn serialize_roundtrip() {
let mut constants: HashMap<(String, String), i64> = HashMap::new();
for (c, f, v) in [
("android/view/View", "VISIBLE", 0i64),
("android/view/View", "GONE", 8),
] {
constants.insert((c.to_string(), f.to_string()), v);
}
let syms = PlatformSymbols::from_metadata(&[XML], &constants);
let blob = syms.serialize();
let back = PlatformSymbols::deserialize(&blob).expect("roundtrip");
assert_eq!(back.domain_count(), syms.domain_count());
assert!(PlatformSymbols::deserialize(&blob[..blob.len() / 2]).is_none());
}
#[test]
fn classfile_constants_reads_constant_value() {
let mut entries: Vec<Vec<u8>> = Vec::new();
entries.push(vec![]); let add_utf8 = |entries: &mut Vec<Vec<u8>>, s: &str| {
let mut e = vec![1u8];
e.extend_from_slice(&(s.len() as u16).to_be_bytes());
e.extend_from_slice(s.as_bytes());
entries.push(e);
entries.len() as u16 - 1
};
let class_entry = add_utf8(&mut entries, "View");
let mut e = vec![7u8];
e.extend_from_slice(&class_entry.to_be_bytes());
entries.push(e);
let class_idx = (entries.len() - 1) as u16;
let field_name = add_utf8(&mut entries, "GONE");
let field_desc = add_utf8(&mut entries, "I");
let attr_name = add_utf8(&mut entries, "ConstantValue");
let mut e = vec![3u8];
e.extend_from_slice(&8i32.to_be_bytes());
entries.push(e);
let int_idx = (entries.len() - 1) as u16;
let mut class = Vec::new();
class.extend_from_slice(b"\xca\xfe\xba\xbe");
class.extend_from_slice(&0u16.to_be_bytes()); class.extend_from_slice(&52u16.to_be_bytes()); class.extend_from_slice(&(entries.len() as u16).to_be_bytes());
for e in &entries[1..] {
class.extend_from_slice(e);
}
class.extend_from_slice(&0x0008u16.to_be_bytes()); class.extend_from_slice(&class_idx.to_be_bytes()); class.extend_from_slice(&0u16.to_be_bytes()); class.extend_from_slice(&0u16.to_be_bytes()); class.extend_from_slice(&1u16.to_be_bytes()); class.extend_from_slice(&0x0019u16.to_be_bytes()); class.extend_from_slice(&field_name.to_be_bytes());
class.extend_from_slice(&field_desc.to_be_bytes());
class.extend_from_slice(&1u16.to_be_bytes()); class.extend_from_slice(&attr_name.to_be_bytes());
class.extend_from_slice(&2u32.to_be_bytes()); class.extend_from_slice(&int_idx.to_be_bytes());
class.extend_from_slice(&0u16.to_be_bytes());
let (constants, _methods) = classfile_constants(&class).expect("parse");
assert_eq!(
constants.get(&("View".to_string(), "GONE".to_string())),
Some(&8i64),
"constants: {constants:?}"
);
}
}