1use std::collections::HashMap;
16use std::sync::OnceLock;
17
18#[derive(Debug, Clone)]
20struct ConstantDomain {
21 members: Vec<(String, String, i64)>,
22}
23
24impl ConstantDomain {
25 fn exact(&self, value: i64) -> Option<&(String, String, i64)> {
28 let mut hit = None;
29 for m in &self.members {
30 if m.2 == value {
31 if hit.is_some() {
32 return None;
33 }
34 hit = Some(m);
35 }
36 }
37 hit
38 }
39}
40
41#[derive(Debug, Default)]
42pub struct PlatformSymbols {
43 domains: HashMap<(String, String, String), Vec<Option<ConstantDomain>>>,
45}
46
47static SYMBOLS: OnceLock<PlatformSymbols> = OnceLock::new();
48
49pub fn set_symbols(s: PlatformSymbols) {
53 let _ = SYMBOLS.set(s);
54}
55
56fn active() -> Option<&'static PlatformSymbols> {
58 SYMBOLS.get().filter(|s| !s.domains.is_empty())
59}
60
61pub fn installed() -> bool {
63 active().is_some()
64}
65
66pub fn named_constant(cls: &str, name: &str, desc: &str, arg: usize, value: i64) -> Option<String> {
70 let syms = active()?;
71 let per_arg = syms.domains.get(&(cls.to_string(), name.to_string(), desc.to_string()))?;
72 let domain = per_arg.get(arg).or_else(|| per_arg.first()).cloned().flatten()?;
73 let (owner, field, _) = domain.exact(value)?;
74 Some(format!("{}.{}", owner.replace('/', "."), field))
75}
76
77impl PlatformSymbols {
78 pub fn from_metadata(xmls: &[&[u8]], constants: &HashMap<(String, String), i64>) -> Self {
81 let mut domains: HashMap<(String, String, String), Vec<Option<ConstantDomain>>> =
82 HashMap::new();
83 let mut on_domain = |class: &str, method: &str, desc: &str, param: usize,
84 ann: &str, value: &str| {
85 if !ann.contains("IntDef") && !ann.contains("LongDef") {
86 return;
87 }
88 let members: Vec<(String, String, i64)> = value
90 .trim_matches(|c| c == '{' || c == '}')
91 .split(',')
92 .filter_map(|m| {
93 let m = m.trim();
94 let (owner, field) = m.rsplit_once('.')?;
95 let owner = owner.replace('.', "/");
96 let v = constants.get(&(owner.clone(), field.to_string()))?;
97 Some((owner, field.to_string(), *v))
98 })
99 .collect();
100 if members.is_empty() {
101 return;
102 }
103 let entry = domains
104 .entry((class.to_string(), method.to_string(), desc.to_string()))
105 .or_default();
106 while entry.len() <= param {
107 entry.push(None);
108 }
109 entry[param] = Some(ConstantDomain { members });
110 };
111 for xml in xmls {
112 parse_annotations_xml(xml, &mut on_domain);
113 }
114 Self { domains }
115 }
116
117 pub fn domain_count(&self) -> usize {
119 self.domains.values().filter(|v| v.iter().any(|d| d.is_some())).count()
120 }
121
122 pub fn serialize(&self) -> Vec<u8> {
126 use std::collections::BTreeMap;
127 let ordered: BTreeMap<_, _> = self.domains.iter().collect(); let mut out = Vec::new();
129 out.extend_from_slice(&(ordered.len() as u32).to_le_bytes());
130 for ((cls, name, desc), per_arg) in ordered {
131 put_str(&mut out, cls);
132 put_str(&mut out, name);
133 put_str(&mut out, desc);
134 out.push(per_arg.len() as u8);
135 for d in per_arg {
136 match d {
137 Some(d) => {
138 out.push(1);
139 out.extend_from_slice(&(d.members.len() as u16).to_le_bytes());
140 for (owner, field, value) in &d.members {
141 put_str(&mut out, owner);
142 put_str(&mut out, field);
143 out.extend_from_slice(&value.to_le_bytes());
144 }
145 }
146 None => out.push(0),
147 }
148 }
149 }
150 out
151 }
152
153 pub fn from_text(text: &[u8]) -> Option<Self> {
158 let text = String::from_utf8_lossy(text);
159 let mut domains = HashMap::new();
160 for line in text.lines() {
161 let line = line.trim();
162 if line.is_empty() || line.starts_with('#') {
163 continue;
164 }
165 let mut parts = line.split(' ');
166 let cls = parts.next()?.replace('.', "/");
167 let name = parts.next()?.to_string();
168 let desc = parts.next()?.to_string();
169 let entry = domains
171 .entry((cls, name, desc))
172 .or_insert_with(Vec::new);
173 while let Some(idx) = parts.next() {
174 let param: usize = idx.parse().ok()?;
175 let members_s = parts.next()?;
176 while entry.len() <= param {
177 entry.push(None);
178 }
179 if members_s == "-" {
180 continue;
181 }
182 let members: Vec<(String, String, i64)> = members_s
183 .split(',')
184 .filter_map(|m| {
185 let (path, value) = m.rsplit_once('=')?;
187 let value: i64 = value.parse().ok()?;
188 let (owner_dot, field) = path.rsplit_once('.')?;
189 Some((owner_dot.replace('.', "/"), field.to_string(), value))
190 })
191 .collect();
192 if !members.is_empty() {
193 entry[param] = Some(ConstantDomain { members });
194 }
195 }
196 }
197 Some(Self { domains })
198 }
199
200 pub fn to_text(&self) -> String {
202 use std::collections::BTreeMap;
203 let ordered: BTreeMap<_, _> = self.domains.iter().collect();
204 let mut out = String::new();
205 for ((cls, name, desc), per_arg) in ordered {
206 let mut line = format!("{cls} {name} {desc}");
207 for (i, d) in per_arg.iter().enumerate() {
208 match d {
209 Some(d) => {
210 let members: Vec<String> = d
211 .members
212 .iter()
213 .map(|(owner, field, value)| {
214 format!("{}.{}={}", owner.replace('/', "."), field, value)
215 })
216 .collect();
217 line.push_str(&format!(" {i} {}", members.join(",")));
218 }
219 None => line.push_str(&format!(" {i} -")),
220 }
221 }
222 if line.split(' ').count() > 3 {
223 out.push_str(&line);
224 out.push('\n');
225 }
226 }
227 out
228 }
229
230 pub fn deserialize(bytes: &[u8]) -> Option<Self> {
231 let mut p = 0usize;
232 let rd_u32 = |p: &mut usize| -> Option<u32> {
233 let v = u32::from_le_bytes(bytes.get(*p..*p + 4)?.try_into().ok()?);
234 *p += 4;
235 Some(v)
236 };
237 let rd_u16 = |p: &mut usize| -> Option<u16> {
238 let v = u16::from_le_bytes(bytes.get(*p..*p + 2)?.try_into().ok()?);
239 *p += 2;
240 Some(v)
241 };
242 let rd_i64 = |p: &mut usize| -> Option<i64> {
243 let v = i64::from_le_bytes(bytes.get(*p..*p + 8)?.try_into().ok()?);
244 *p += 8;
245 Some(v)
246 };
247 let get_str = |p: &mut usize| -> Option<String> {
248 let len = rd_u16(p)? as usize;
249 let s = String::from_utf8(bytes.get(*p..*p + len)?.to_vec()).ok()?;
250 *p += len;
251 Some(s)
252 };
253 let count = rd_u32(&mut p)? as usize;
254 let mut domains = HashMap::with_capacity(count);
255 for _ in 0..count {
256 let cls = get_str(&mut p)?;
257 let name = get_str(&mut p)?;
258 let desc = get_str(&mut p)?;
259 let n = *bytes.get(p)?;
260 p += 1;
261 let mut per_arg = Vec::with_capacity(n as usize);
262 for _ in 0..n {
263 if *bytes.get(p)? == 1 {
264 p += 1;
265 let mc = rd_u16(&mut p)? as usize;
266 let mut members = Vec::with_capacity(mc);
267 for _ in 0..mc {
268 let owner = get_str(&mut p)?;
269 let field = get_str(&mut p)?;
270 let value = rd_i64(&mut p)?;
271 members.push((owner, field, value));
272 }
273 per_arg.push(Some(ConstantDomain { members }));
274 } else {
275 p += 1;
276 per_arg.push(None);
277 }
278 }
279 domains.insert((cls, name, desc), per_arg);
280 }
281 Some(Self { domains })
282 }
283}
284
285fn put_str(out: &mut Vec<u8>, s: &str) {
286 out.extend_from_slice(&(s.len() as u16).to_le_bytes());
287 out.extend_from_slice(s.as_bytes());
288}
289
290fn java_type_to_dex(t: &str) -> String {
293 let t = t.trim();
294 if t.contains('<') || t.contains("<") || t.starts_with("? ") || t.starts_with("?\t") {
298 return "Ljava/lang/Object;".to_string();
299 }
300 if let Some(inner) = t.strip_suffix("[]") {
301 return format!("[{}", java_type_to_dex(inner));
302 }
303 let prim = match t {
304 "void" => "V",
305 "int" => "I",
306 "long" => "J",
307 "float" => "F",
308 "double" => "D",
309 "boolean" => "Z",
310 "byte" => "B",
311 "char" => "C",
312 "short" => "S",
313 _ => "",
314 };
315 if !prim.is_empty() {
316 return prim.to_string();
317 }
318 if t.is_empty() {
319 return String::new();
320 }
321 format!("L{};", t.replace('.', "/"))
322}
323
324type DomainSink<'a> = dyn FnMut(&str, &str, &str, usize, &str, &str) + 'a;
329
330fn parse_annotations_xml(xml: &[u8], on_domain: &mut DomainSink<'_>) {
331 let text = String::from_utf8_lossy(xml);
332 let attr = |line: &str, key: &str| -> Option<String> {
333 let pat = format!("{key}=\"");
334 let start = line.find(&pat)? + pat.len();
335 let end = line[start..].find('"')? + start;
336 Some(line[start..end].to_string())
337 };
338 let mut item: Option<(String, String, String, usize)> = None;
340 let mut cur_ann: Option<String> = None;
341 for line in text.lines() {
342 let t = line.trim();
343 if t.starts_with("<item ") {
344 let name = attr(t, "name").unwrap_or_default()
347 .replace("<", "<")
348 .replace(">", ">")
349 .replace(""", "\"")
350 .replace("&", "&");
351 let mut tail = name.rsplitn(2, ' ');
352 let param = tail.next().and_then(|p| p.parse::<usize>().ok());
353 let rest = tail.next().unwrap_or("").trim().to_string();
354 let (class, rest) = match rest.split_once(' ') {
355 Some((c, r)) => (c.replace('.', "/"), r.to_string()),
356 None => (rest.clone(), String::new()),
357 };
358 let (ret, method, args) = match rest.find('(') {
364 Some(p) => {
365 let sig = &rest[..p];
366 let (ret, method) = match sig.rsplit_once(' ') {
367 Some((r, m)) => (r.to_string(), m.to_string()),
368 None => ("void".to_string(), sig.to_string()),
369 };
370 (ret, method, rest[p + 1..].trim_end_matches(')'))
371 }
372 None => (String::new(), String::new(), ""),
373 };
374 let mut desc = String::from("(");
375 for a in args.split(',') {
376 desc.push_str(&java_type_to_dex(a.trim()));
377 }
378 desc.push(')');
379 desc.push_str(&java_type_to_dex(&ret));
380 item = param.map(|p| (class, method, desc, p));
381 } else if t.starts_with("<annotation ") {
382 cur_ann = attr(t, "name");
383 } else if t.starts_with("<val ") {
384 if let (Some((class, method, desc, param)), Some(ann)) = (&item, &cur_ann) {
385 if attr(t, "name").as_deref() == Some("value") {
386 if let Some(v) = attr(t, "val") {
387 on_domain(class, method, desc, *param, ann, &v);
388 }
389 }
390 }
391 } else if t.starts_with("</item>") {
392 item = None;
393 cur_ann = None;
394 }
395 }
396}
397
398pub type ClassConstants = (HashMap<(String, String), i64>, Vec<(String, String, String)>);
408
409pub fn classfile_constants(bytes: &[u8]) -> Option<ClassConstants> {
410 if bytes.len() < 10 || &bytes[..4] != b"\xca\xfe\xba\xbe" {
411 return None;
412 }
413 let rd_u1 = |p: usize| -> Option<u8> { bytes.get(p).copied() };
414 let rd_u2 = |p: usize| -> Option<u16> {
415 Some(u16::from_be_bytes([bytes.get(p).copied()?, bytes.get(p + 1).copied()?]))
416 };
417 let rd_u4 = |p: usize| -> Option<u32> {
418 Some(u32::from_be_bytes([
419 bytes.get(p).copied()?,
420 bytes.get(p + 1).copied()?,
421 bytes.get(p + 2).copied()?,
422 bytes.get(p + 3).copied()?,
423 ]))
424 };
425 let rd_i64 = |p: usize| -> Option<i64> {
426 Some(i64::from_be_bytes([
427 bytes.get(p).copied()?,
428 bytes.get(p + 1).copied()?,
429 bytes.get(p + 2).copied()?,
430 bytes.get(p + 3).copied()?,
431 bytes.get(p + 4).copied()?,
432 bytes.get(p + 5).copied()?,
433 bytes.get(p + 6).copied()?,
434 bytes.get(p + 7).copied()?,
435 ]))
436 };
437
438 let count = rd_u2(8)? as usize;
440 let mut tags = vec![0u8; count];
442 let mut ints: HashMap<usize, i32> = HashMap::new();
443 let mut longs: HashMap<usize, i64> = HashMap::new();
444 let mut utf8s: HashMap<usize, (usize, usize)> = HashMap::new(); let mut classes: HashMap<usize, usize> = HashMap::new(); let mut p = 10usize;
447 let mut i = 1usize;
448 while i < count {
449 let tag = rd_u1(p)?;
450 p += 1;
451 tags[i] = tag;
452 match tag {
453 1 => {
454 let len = rd_u2(p)? as usize;
455 utf8s.insert(i, (p + 2, len));
456 p += 2 + len;
457 }
458 3 => {
459 ints.insert(i, i32::from_be_bytes(rd_u4(p)?.to_be_bytes()));
460 p += 4;
461 }
462 4 => p += 4,
465 5 | 6 | 7 | 8 | 16 | 19 | 20 => {
466 if tag == 5 {
467 longs.insert(i, rd_i64(p)?);
468 }
469 if tag == 7 {
470 classes.insert(i, rd_u2(p)? as usize);
471 }
472 p += if tag == 5 || tag == 6 { 8 } else { 2 };
473 }
474 9 | 10 | 11 | 12 | 17 | 18 => p += 4,
475 15 => p += 3,
476 _ => return None, }
478 i += if tag == 5 || tag == 6 { 2 } else { 1 };
482 let _ = i;
483 }
484 let utf8 = |slot: usize| -> Option<String> {
485 let (off, len) = *utf8s.get(&slot)?;
486 Some(String::from_utf8_lossy(bytes.get(off..off + len)?).into_owned())
489 };
490 let this_class = *classes.get(&(rd_u2(p + 2)? as usize))?;
494 p += 6;
495
496 let ifaces = rd_u2(p)? as usize;
498 p += 2 + 2 * ifaces;
499 let class_name = utf8(this_class)?.trim_start_matches('L').trim_end_matches(';').to_string();
500
501 let mut constants = HashMap::new();
502 let mut methods = Vec::new();
503 for read_fields in [true, false] {
504 let n = rd_u2(p)? as usize;
505 p += 2;
506 for _ in 0..n {
507 let _access = rd_u2(p)?;
508 let name_idx = rd_u2(p + 2)? as usize;
509 let desc_idx = rd_u2(p + 4)? as usize;
510 let nattrs = rd_u2(p + 6)? as usize;
511 p += 8;
512 let name = utf8(name_idx)?;
513 let desc = utf8(desc_idx)?;
514 for _ in 0..nattrs {
515 let attr_name = utf8(rd_u2(p)? as usize)?;
516 let len = rd_u4(p + 2)? as usize;
517 let body = p + 6;
518 if read_fields && attr_name == "ConstantValue" {
519 let idx = rd_u2(body)? as usize;
520 let value = if let Some(v) = ints.get(&idx) {
521 Some(*v as i64)
522 } else {
523 longs.get(&idx).copied()
524 };
525 if let Some(v) = value {
526 constants.insert((class_name.clone(), name.clone()), v);
527 }
528 }
529 if !read_fields {
530 methods.push((class_name.clone(), name.clone(), desc.clone()));
531 }
532 p = body + len;
533 }
534 }
535 }
536 Some((constants, methods))
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 const XML: &[u8] = br#"
544<root>
545 <item name="android.view.View void setVisibility(int) 0">
546 <annotation name="androidx.annotation.IntDef">
547 <val name="value" val="{android.view.View.VISIBLE, android.view.View.INVISIBLE, android.view.View.GONE}" />
548 </annotation>
549 </item>
550 <item name="android.content.Intent android.content.Intent addFlags(int) 0">
551 <annotation name="androidx.annotation.IntDef">
552 <val name="value" val="{android.content.Intent.FLAG_ACTIVITY_NEW_TASK, android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP}" />
553 </annotation>
554 </item>
555 <item name="android.view.View void setSomething(android.view.View) 1">
556 <annotation name="androidx.annotation.StringDef">
557 <val name="value" val="{android.view.View.NAME}" />
558 </annotation>
559 </item>
560</root>
561"#;
562
563 #[test]
564 fn domains_parse_and_match() {
565 let mut constants: HashMap<(String, String), i64> = HashMap::new();
566 for (c, f, v) in [
567 ("android/view/View", "VISIBLE", 0i64),
568 ("android/view/View", "INVISIBLE", 4),
569 ("android/view/View", "GONE", 8),
570 ("android/content/Intent", "FLAG_ACTIVITY_NEW_TASK", 0x10000000),
571 ("android/content/Intent", "FLAG_ACTIVITY_CLEAR_TOP", 0x04000000),
572 ] {
573 constants.insert((c.to_string(), f.to_string()), v);
574 }
575 let syms = PlatformSymbols::from_metadata(&[XML], &constants);
576 assert_eq!(syms.domain_count(), 2, "IntDef domains only");
577 set_symbols(PlatformSymbols::from_metadata(&[XML], &constants));
579
580 assert_eq!(
582 named_constant("android/view/View", "setVisibility", "(I)V", 0, 8),
583 Some("android.view.View.GONE".to_string())
584 );
585 assert!(named_constant("android/view/View", "setVisibility", "(I)V", 0, 7).is_none());
586 assert!(named_constant("android/view/View", "setAlpha", "(F)V", 0, 0).is_none());
588 assert!(named_constant("android/view/View", "setVisibility", "(J)V", 0, 0).is_none());
589 }
590
591 #[test]
592 fn text_roundtrip() {
593 let mut constants: HashMap<(String, String), i64> = HashMap::new();
594 for (c, f, v) in [
595 ("android/view/View", "VISIBLE", 0i64),
596 ("android/view/View", "GONE", 8),
597 ] {
598 constants.insert((c.to_string(), f.to_string()), v);
599 }
600 let syms = PlatformSymbols::from_metadata(&[XML], &constants);
601 let text = syms.to_text();
602 let back = PlatformSymbols::from_text(text.as_bytes()).expect("text parse");
603 assert_eq!(back.domain_count(), syms.domain_count());
604 let per_arg = back
608 .domains
609 .get(&("android/view/View".to_string(), "setVisibility".to_string(), "(I)V".to_string()))
610 .expect("domain present");
611 let domain = per_arg.first().and_then(|d| d.as_ref()).expect("param 0 domain");
612 assert_eq!(domain.exact(8).map(|m| m.1.as_str()), Some("GONE"));
613 assert_eq!(domain.exact(0).map(|m| m.1.as_str()), Some("VISIBLE"));
614 }
615
616 #[test]
617 fn serialize_roundtrip() {
618 let mut constants: HashMap<(String, String), i64> = HashMap::new();
619 for (c, f, v) in [
620 ("android/view/View", "VISIBLE", 0i64),
621 ("android/view/View", "GONE", 8),
622 ] {
623 constants.insert((c.to_string(), f.to_string()), v);
624 }
625 let syms = PlatformSymbols::from_metadata(&[XML], &constants);
626 let blob = syms.serialize();
627 let back = PlatformSymbols::deserialize(&blob).expect("roundtrip");
628 assert_eq!(back.domain_count(), syms.domain_count());
629 assert!(PlatformSymbols::deserialize(&blob[..blob.len() / 2]).is_none());
631 }
632
633 #[test]
634 fn classfile_constants_reads_constant_value() {
635 let mut entries: Vec<Vec<u8>> = Vec::new();
638 entries.push(vec![]); let add_utf8 = |entries: &mut Vec<Vec<u8>>, s: &str| {
640 let mut e = vec![1u8];
641 e.extend_from_slice(&(s.len() as u16).to_be_bytes());
642 e.extend_from_slice(s.as_bytes());
643 entries.push(e);
644 entries.len() as u16 - 1
645 };
646 let class_entry = add_utf8(&mut entries, "View");
647 let mut e = vec![7u8];
648 e.extend_from_slice(&class_entry.to_be_bytes());
649 entries.push(e);
650 let class_idx = (entries.len() - 1) as u16;
651 let field_name = add_utf8(&mut entries, "GONE");
652 let field_desc = add_utf8(&mut entries, "I");
653 let attr_name = add_utf8(&mut entries, "ConstantValue");
654 let mut e = vec![3u8];
655 e.extend_from_slice(&8i32.to_be_bytes());
656 entries.push(e);
657 let int_idx = (entries.len() - 1) as u16;
658
659 let mut class = Vec::new();
660 class.extend_from_slice(b"\xca\xfe\xba\xbe");
661 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());
664 for e in &entries[1..] {
665 class.extend_from_slice(e);
666 }
667 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());
674 class.extend_from_slice(&field_desc.to_be_bytes());
675 class.extend_from_slice(&1u16.to_be_bytes()); class.extend_from_slice(&attr_name.to_be_bytes());
677 class.extend_from_slice(&2u32.to_be_bytes()); class.extend_from_slice(&int_idx.to_be_bytes());
679 class.extend_from_slice(&0u16.to_be_bytes()); let (constants, _methods) = classfile_constants(&class).expect("parse");
682 assert_eq!(
683 constants.get(&("View".to_string(), "GONE".to_string())),
684 Some(&8i64),
685 "constants: {constants:?}"
686 );
687 }
688}