1use super::{class_inner, format_class_name, java_signature_to_dalvik};
17use asc_findrefs::{ClassQuery, MemberQuery, MemberSignature, Query, TypeMatch};
18
19pub fn is_java_reserved(value: &str) -> bool {
22 matches!(
23 value,
24 "abstract"
25 | "assert"
26 | "boolean"
27 | "break"
28 | "byte"
29 | "case"
30 | "catch"
31 | "char"
32 | "class"
33 | "const"
34 | "continue"
35 | "default"
36 | "do"
37 | "double"
38 | "else"
39 | "enum"
40 | "extends"
41 | "final"
42 | "finally"
43 | "float"
44 | "for"
45 | "goto"
46 | "if"
47 | "implements"
48 | "import"
49 | "instanceof"
50 | "int"
51 | "interface"
52 | "long"
53 | "native"
54 | "new"
55 | "package"
56 | "private"
57 | "protected"
58 | "public"
59 | "return"
60 | "short"
61 | "static"
62 | "strictfp"
63 | "super"
64 | "switch"
65 | "synchronized"
66 | "this"
67 | "throw"
68 | "throws"
69 | "transient"
70 | "try"
71 | "void"
72 | "volatile"
73 | "while"
74 | "true"
75 | "false"
76 | "null"
77 | "_"
78 )
79}
80
81fn partial_signature(spec: &str) -> Option<(&str, &str)> {
85 let open = spec.find('(')?;
86 let tail = &spec[open..];
87 let balanced = tail.ends_with(')') && tail.matches('(').count() == 1 && tail.matches(')').count() == 1;
88 let name = spec[..open].trim();
89 (balanced && !name.is_empty()).then_some((name, tail))
90}
91
92pub(crate) fn partial_proto(spec: &str) -> Option<(String, String)> {
97 let (name, tail) = partial_signature(spec)?;
98 let tail = java_signature_to_dalvik(spec)
99 .and_then(|dalvik| dalvik.find('(').map(|i| dalvik[i..].to_string()))
100 .unwrap_or_else(|| tail.split_whitespace().collect());
101 Some((name.to_string(), denormalize_signature_types(&tail)))
102}
103
104pub(crate) fn proto_matches(have: &str, want: &str) -> bool {
107 have == want || (want.ends_with(')') && have.len() > want.len() && have.starts_with(want))
108}
109
110fn push_unique(out: &mut Vec<String>, item: String) {
111 if !item.is_empty() && !out.contains(&item) {
112 out.push(item);
113 }
114}
115
116fn anonymous_number(name: &str) -> Option<&str> {
118 name.strip_prefix("Anonymous").filter(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
119}
120
121fn decode_kotlin_dex_hex(s: &str) -> Option<String> {
124 let rest = s.strip_prefix("_dex_")?;
125 if rest == "empty" {
126 return Some(String::new());
127 }
128 let rest = rest.strip_suffix('_')?;
129 let mut out = String::new();
130 for part in rest.split('_') {
131 let code = u32::from_str_radix(part, 16).ok()?;
132 out.push(char::from_u32(code)?);
133 }
134 (!out.is_empty()).then_some(out)
135}
136
137pub fn denormalize_identifier(ident: &str) -> Vec<String> {
144 let mut out = Vec::new();
145 let stripped = ident.strip_prefix('`').and_then(|s| s.strip_suffix('`')).unwrap_or(ident);
146 let mut seeds = Vec::new();
147 if stripped != ident {
148 push_unique(&mut out, stripped.to_string());
149 }
150 seeds.push(stripped.to_string());
151 if let Some(decoded) = decode_kotlin_dex_hex(stripped) {
152 push_unique(&mut out, decoded.clone());
153 seeds.push(decoded);
154 }
155 if let Some((base, num)) = stripped.rsplit_once('$')
156 && !base.is_empty()
157 && num.parse::<u32>().is_ok_and(|n| n >= 2)
158 {
159 push_unique(&mut out, base.to_string());
160 seeds.push(base.to_string());
161 }
162 for s in seeds {
163 if let Some(rest) = s.strip_prefix('C')
164 && (is_java_reserved(rest) || rest.starts_with(|c: char| c.is_ascii_digit()))
165 {
166 push_unique(&mut out, rest.to_string());
167 }
168 }
169 out.retain(|c| c != ident);
170 out
171}
172
173const MAX_CLASS_SEQUENCES: usize = 16;
175
176pub fn denormalize_class_candidates(input: &str) -> Vec<String> {
182 let Ok(formatted) = format_class_name(input) else {
183 return Vec::new();
184 };
185 let dims = formatted.len() - formatted.trim_start_matches('[').len();
186 let array_prefix = &formatted[..dims];
187 let core = &formatted[dims..];
188 if !core.starts_with('L') || !core.ends_with(';') {
189 return Vec::new();
190 }
191 let inner = class_inner(core);
192 let mut tokens: Vec<(char, &str)> = Vec::new();
194 for slash_seg in inner.split('/') {
195 for (j, dollar_seg) in slash_seg.split('$').enumerate() {
196 tokens.push((if j > 0 { '$' } else { '/' }, dollar_seg));
197 }
198 }
199
200 let token_variants: Vec<Vec<(String, bool)>> = tokens
203 .iter()
204 .map(|&(_, raw)| {
205 let mut vars = Vec::new();
206 let unquoted = raw.strip_prefix('`').and_then(|s| s.strip_suffix('`')).unwrap_or(raw);
207 if let Some(num) = anonymous_number(unquoted) {
208 vars.push((num.to_string(), true));
209 }
210 for d in denormalize_identifier(raw) {
211 if !vars.iter().any(|(v, _)| v == &d) {
212 vars.push((d, false));
213 }
214 }
215 if !vars.iter().any(|(v, _)| v == raw) {
216 vars.push((raw.to_string(), false));
217 }
218 vars
219 })
220 .collect();
221
222 let mut seqs: Vec<Vec<(String, bool)>> = vec![Vec::new()];
224 for vars in &token_variants {
225 let mut next = Vec::new();
226 for prefix in &seqs {
227 for v in vars.iter().take(2) {
228 let mut p = prefix.clone();
229 p.push(v.clone());
230 next.push(p);
231 }
232 }
233 next.truncate(MAX_CLASS_SEQUENCES);
234 seqs = next;
235 }
236
237 let mut out = Vec::new();
238 for seq in &seqs {
239 let build = |dollar_from: Option<usize>| -> String {
241 let mut s = format!("{array_prefix}L");
242 for (idx, (tok, force_dollar)) in seq.iter().enumerate() {
243 if idx > 0 {
244 let nested = tokens[idx].0 == '$' || *force_dollar || dollar_from.is_some_and(|df| idx > df);
245 s.push(if nested { '$' } else { '/' });
246 }
247 s.push_str(tok);
248 }
249 s.push(';');
250 s
251 };
252 let mut push = |cand: String| {
253 if cand != formatted {
254 push_unique(&mut out, cand);
255 }
256 };
257
258 push(build(None));
260
261 let upper = seq
266 .iter()
267 .position(|(t, _)| t.starts_with(|c: char| c.is_ascii_uppercase()))
268 .or_else(|| tokens.iter().position(|&(_, raw)| raw.trim_matches('`').starts_with(|c: char| c.is_ascii_uppercase())));
269 let mut splits: Vec<usize> = upper.map(|first| (first..seq.len().saturating_sub(1)).collect()).unwrap_or_default();
270 if seq.len() >= 3 {
273 splits.push(seq.len() - 2);
274 }
275 splits.sort_unstable();
276 splits.dedup();
277 for split_at in splits {
278 push(build(Some(split_at)));
279 }
280 }
281 out
282}
283
284fn class_constructor_names(class_descriptor: &str) -> Vec<String> {
286 let mut names = vec!["constructor".to_string()];
287 let inner = class_inner(class_descriptor.trim_start_matches('['));
288 let binary_cls = inner.rsplit('/').next().unwrap_or(inner);
289 let last = binary_cls.rsplit('$').next().unwrap_or(binary_cls);
290 if !last.is_empty() {
291 push_unique(&mut names, last.to_string());
292 if last.bytes().all(|b| b.is_ascii_digit()) {
293 push_unique(&mut names, format!("Anonymous{last}"));
294 }
295 if is_java_reserved(last) || last.starts_with(|c: char| c.is_ascii_digit()) {
296 push_unique(&mut names, format!("C{last}"));
297 }
298 }
299 if binary_cls != last {
300 push_unique(&mut names, binary_cls.to_string());
301 }
302 names
303}
304
305pub fn denormalize_method_candidates(class_descriptor: Option<&str>, method: &str) -> Vec<String> {
311 let trimmed = method.trim();
312 let member = trimmed.rsplit_once("->").map_or(trimmed, |(_, m)| m);
313 let (raw_name, sig_tail) = match member.find('(') {
314 Some(i) => (member[..i].trim(), Some(&member[i..])),
315 None => (member.trim(), None),
316 };
317 let unquoted = raw_name.trim_matches('`');
318 let mut name_cands = vec![raw_name.to_string()];
319 for d in denormalize_identifier(raw_name) {
320 push_unique(&mut name_cands, d);
321 }
322 let constructor = match class_descriptor {
323 Some(cls) => {
324 let as_written = class_constructor_names(cls);
325 as_written.iter().any(|n| n == raw_name || n == unquoted)
326 || denormalize_class_candidates(cls)
327 .iter()
328 .any(|c| class_constructor_names(c).iter().any(|n| !as_written.contains(n) && (n == raw_name || n == unquoted)))
329 }
330 None => raw_name == "constructor" || anonymous_number(raw_name).is_some(),
331 };
332 if constructor {
333 push_unique(&mut name_cands, "<init>".to_string());
334 }
335
336 let mut sig_cands = Vec::new();
337 match sig_tail {
338 None => sig_cands.push(String::new()),
339 Some(sig) => {
340 sig_cands.push(sig.to_string());
341 push_unique(&mut sig_cands, denormalize_signature_types(sig));
342 if let Some(dalvik_sig) = java_signature_to_dalvik(&format!("m{sig}")).and_then(|d| d.strip_prefix('m').map(str::to_string)) {
343 push_unique(&mut sig_cands, denormalize_signature_types(&dalvik_sig));
344 push_unique(&mut sig_cands, dalvik_sig);
345 }
346 }
347 }
348
349 let mut out = Vec::new();
350 for n in &name_cands {
351 for s in &sig_cands {
352 let combined = format!("{n}{s}");
353 if combined != member && combined != trimmed {
354 push_unique(&mut out, combined.clone());
355 }
356 if n == "<init>" && s.ends_with(')') {
359 push_unique(&mut out, format!("{combined}V"));
360 }
361 }
362 }
363 out
364}
365
366pub(crate) fn denormalize_signature_types(sig: &str) -> String {
369 let mut out = String::with_capacity(sig.len());
370 let mut rest = sig;
371 while let Some(l_pos) = rest.find('L') {
372 out.push_str(&rest[..l_pos]);
373 let after_l = &rest[l_pos..];
374 let Some(semi) = after_l.find(';') else {
375 out.push_str(after_l);
376 rest = "";
377 break;
378 };
379 let desc = &after_l[..=semi];
380 match denormalize_class_candidates(desc).into_iter().next() {
381 Some(first) => out.push_str(&first),
382 None => out.push_str(desc),
383 }
384 rest = &after_l[semi + 1..];
385 }
386 out.push_str(rest);
387 out
388}
389
390fn denormalize_type_match(tm: &TypeMatch) -> Option<TypeMatch> {
391 match tm {
392 TypeMatch::Descriptor(d) => denormalize_class_candidates(d).into_iter().next().map(TypeMatch::Descriptor),
393 TypeMatch::Simple { name, dims } => {
394 let name = match anonymous_number(name) {
395 Some(num) => format!("${num}"),
396 None => denormalize_identifier(name).into_iter().next()?,
397 };
398 Some(TypeMatch::Simple { name, dims: *dims })
399 }
400 }
401}
402
403fn denormalize_member_signature(sig: &MemberSignature) -> Option<MemberSignature> {
404 match sig {
405 MemberSignature::Field(tm) => denormalize_type_match(tm).map(MemberSignature::Field),
406 MemberSignature::Method { params, ret } => {
407 let mut changed = false;
408 let mut map = |t: &TypeMatch| match denormalize_type_match(t) {
409 Some(n) => {
410 changed = true;
411 n
412 }
413 None => t.clone(),
414 };
415 let params = params.iter().map(&mut map).collect();
416 let ret = ret.as_ref().map(&mut map);
417 changed.then_some(MemberSignature::Method { params, ret })
418 }
419 }
420}
421
422fn simple_class_name(pattern: &str) -> Option<String> {
425 pattern.strip_prefix("[/L]").and_then(|s| s.strip_suffix(";\\x00")).map(|s| s.replace('\\', ""))
426}
427
428fn denormalize_member_query(mq: &MemberQuery, is_method: bool) -> Vec<MemberQuery> {
430 let mut class_cands: Vec<Option<ClassQuery>> = vec![mq.class.clone()];
431 let mut class_descriptor = None;
433 if let Some(cq) = &mq.class {
434 if cq.precise {
435 class_descriptor = Some(cq.pattern.clone());
436 class_cands.extend(denormalize_class_candidates(&cq.pattern).into_iter().map(|c| Some(ClassQuery::exact(c))));
437 } else if let Some(simple) = simple_class_name(&cq.pattern) {
438 class_descriptor = Some(format!("L{simple};"));
439 if let Some(num) = anonymous_number(&simple) {
440 class_cands.push(Some(ClassQuery::fuzzy(format!("\\${num};\\x00"))));
441 }
442 for d in denormalize_identifier(&simple) {
443 class_cands.push(Some(ClassQuery::fuzzy(format!("[/L]{};\\x00", regex::escape(&d)))));
444 }
445 } else {
446 for c in denormalize_class_candidates(&cq.pattern) {
447 class_cands.push(Some(ClassQuery::fuzzy(regex::escape(class_inner(&c)))));
448 }
449 }
450 }
451
452 let mut name_cands: Vec<Option<String>> = vec![mq.name.clone()];
453 if let Some(name) = &mq.name {
454 let extras =
455 if is_method { denormalize_method_candidates(class_descriptor.as_deref(), name) } else { denormalize_identifier(name) };
456 for e in extras {
457 let e = if is_method { e.split('(').next().unwrap_or(&e).to_string() } else { e };
460 let opt = Some(e);
461 if !name_cands.contains(&opt) {
462 name_cands.push(opt);
463 }
464 }
465 }
466
467 let mut sig_cands: Vec<Option<MemberSignature>> = vec![mq.signature.clone()];
468 if let Some(ns) = mq.signature.as_ref().and_then(denormalize_member_signature) {
469 sig_cands.push(Some(ns));
470 }
471
472 let mut out = vec![mq.clone()];
473 for c in &class_cands {
474 for n in &name_cands {
475 for s in &sig_cands {
476 let exact_name = mq.exact_name || n != &mq.name;
480 let cand =
481 MemberQuery::default().with_class(c.clone()).with_name(n.clone()).with_exact_name(exact_name).with_signature(s.clone());
482 if !out.contains(&cand) {
483 out.push(cand);
484 }
485 }
486 }
487 }
488 out
489}
490
491pub fn denormalize_query(q: &Query) -> Option<Query> {
497 match q {
498 Query::Type(pat) => {
499 let mut cands = denormalize_class_candidates(pat);
500 if cands.is_empty() {
501 cands = denormalize_identifier(pat);
502 }
503 if cands.is_empty() {
504 return None;
505 }
506 let alternatives: Vec<String> = std::iter::once(format!("(?:{pat})")).chain(cands.iter().map(|c| regex::escape(c))).collect();
507 Some(Query::Type(format!("(?:{})", alternatives.join("|"))))
508 }
509 Query::Extends(cls) => denormalize_class_candidates(cls).into_iter().next().map(Query::Extends),
510 Query::Implements(cls) => denormalize_class_candidates(cls).into_iter().next().map(Query::Implements),
511 Query::Method(mq) => {
512 let vars = denormalize_member_query(mq, true);
513 (vars.len() > 1).then_some(Query::MethodSet(vars))
514 }
515 Query::Field(mq) => {
516 let vars = denormalize_member_query(mq, false);
517 (vars.len() > 1).then(|| collapse_field_variants(&vars)).map(Query::Field)
518 }
519 Query::FieldAccess { field, access } => {
520 let vars = denormalize_member_query(field, false);
521 (vars.len() > 1).then(|| Query::FieldAccess { field: collapse_field_variants(&vars), access: *access })
522 }
523 Query::Call { target, constraints } => {
524 denormalize_query(target).map(|t| Query::Call { target: Box::new(t), constraints: constraints.clone() })
525 }
526 Query::All { all, none } => {
527 let mut changed = false;
528 let all = all
529 .iter()
530 .map(|part| match denormalize_query(part) {
531 Some(np) => {
532 changed = true;
533 np
534 }
535 None => part.clone(),
536 })
537 .collect();
538 changed.then(|| Query::All { all, none: none.clone() })
539 }
540 _ => None,
541 }
542}
543
544fn collapse_field_variants(vars: &[MemberQuery]) -> MemberQuery {
548 let anchored = |s: &str| format!("^{}\\x00", regex::escape(s));
549 let mut classes = Vec::new();
550 let mut names = Vec::new();
551 for v in vars {
552 if let Some(c) = &v.class {
553 push_unique(&mut classes, if c.precise { anchored(&c.pattern) } else { c.pattern.clone() });
554 }
555 if let Some(n) = &v.name {
556 push_unique(&mut names, if v.exact_name { anchored(n) } else { regex::escape(n) });
557 }
558 }
559 let first = &vars[0];
560 let signature = vars.iter().rev().find_map(|v| v.signature.clone());
561 let single_precise = classes.len() == 1 && first.class.as_ref().is_some_and(|c| c.precise);
562 if single_precise && names.len() <= 1 {
563 return first.clone().with_signature(signature);
565 }
566 let class = (!classes.is_empty()).then(|| ClassQuery::fuzzy(format!("(?:{})", classes.join("|"))));
567 let name = (!names.is_empty()).then(|| format!("(?:{})", names.join("|")));
568 MemberQuery::default().with_class(class).with_name(name).with_signature(signature)
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn identifiers_lose_their_source_escapes() {
577 assert_eq!(denormalize_identifier("Cdo"), ["do"]);
578 assert_eq!(denormalize_identifier("C_"), ["_"]);
579 assert_eq!(denormalize_identifier("C1a"), ["1a"]);
580 assert_eq!(denormalize_identifier("`in`"), ["in"]);
581 assert_eq!(denormalize_identifier("_dex_61_2D_62_"), ["a-b"]);
582 assert_eq!(denormalize_identifier("m$2"), ["m"]);
583 assert_eq!(denormalize_identifier("Cdo$2"), ["Cdo", "do"]);
584 assert!(denormalize_identifier("Class").is_empty());
585 assert!(denormalize_identifier("m$1").is_empty());
586 assert!(denormalize_identifier("plain").is_empty());
587 }
588
589 #[test]
590 fn dotted_nested_and_anonymous_classes_become_descriptors() {
591 let c = denormalize_class_candidates("com.app.Outer.Inner");
592 assert_eq!(c.first().map(String::as_str), Some("Lcom/app/Outer$Inner;"));
593 let c = denormalize_class_candidates("com.app.Outer.Inner.Deep");
594 assert_eq!(c.first().map(String::as_str), Some("Lcom/app/Outer$Inner$Deep;"));
595 assert!(denormalize_class_candidates("com.app.Outer.Anonymous1").contains(&"Lcom/app/Outer$1;".to_string()));
596 assert!(denormalize_class_candidates("com.Cpkg.Cdo.Cif").contains(&"Lcom/Cpkg/do/if;".to_string()));
597 assert!(denormalize_class_candidates("com.kt.`in`.When").contains(&"Lcom/kt/in/When;".to_string()));
598 assert_eq!(denormalize_class_candidates("[Lcom/a/Outer/Inner;").first().map(String::as_str), Some("[Lcom/a/Outer$Inner;"));
600 assert!(!denormalize_class_candidates("Lcom/a/Outer$Inner;").contains(&"Lcom/a/Outer$Inner;".to_string()));
601 assert!(denormalize_class_candidates("I").is_empty());
602 }
603
604 #[test]
605 fn methods_read_as_constructors_escapes_and_java_signatures() {
606 let m = denormalize_method_candidates(Some("Lcom/app/Outer;"), "Outer");
607 assert!(m.contains(&"<init>".to_string()), "{m:?}");
608 let m = denormalize_method_candidates(Some("Lcom/app/Outer;"), "Outer(int)");
609 assert!(m.contains(&"<init>(I)V".to_string()), "{m:?}");
610 let m = denormalize_method_candidates(Some("Lcom/app/Outer$1;"), "Anonymous1");
611 assert!(m.contains(&"<init>".to_string()), "{m:?}");
612 assert_eq!(denormalize_method_candidates(None, "Cwhile"), ["while"]);
613 let m = denormalize_method_candidates(None, "entry(com.app.Outer.Inner)");
614 assert!(m.contains(&"entry(Lcom/app/Outer$Inner;)".to_string()), "{m:?}");
615 assert!(denormalize_method_candidates(Some("Lcom/app/Outer;"), "run").is_empty());
616 }
617
618 #[test]
619 fn return_less_signatures_become_proto_prefixes() {
620 assert_eq!(partial_proto("run(int)"), Some(("run".into(), "(I)".into())));
621 assert_eq!(partial_proto("entry(com.app.Outer.Inner)"), Some(("entry".into(), "(Lcom/app/Outer$Inner;)".into())));
622 assert_eq!(partial_proto("run(I"), None);
623 assert_eq!(partial_proto("run"), None);
624 assert!(proto_matches("(I)V", "(I)"));
625 assert!(proto_matches("(I)V", "(I)V"));
626 assert!(!proto_matches("(II)V", "(I)"));
627 assert!(!proto_matches("(I)V", "(I)Z"));
628 }
629
630 #[test]
631 fn queries_keep_what_they_matched_and_add_the_readings() {
632 let mq = MemberQuery::default().with_class(Some(ClassQuery::exact("Lcom/app/Outer/Inner;"))).with_name(Some("work".into()));
633 let Some(Query::MethodSet(set)) = denormalize_query(&Query::Method(mq.clone())) else { panic!("a method set") };
634 assert_eq!(set[0], mq);
635 assert!(set.iter().any(|m| m.class == Some(ClassQuery::exact("Lcom/app/Outer$Inner;"))), "{set:?}");
636 let plain = MemberQuery::default().with_name(Some("work".into()));
638 assert_eq!(denormalize_query(&Query::Method(plain)), None);
639 assert_eq!(denormalize_query(&Query::String("x".into())), None);
640 let Some(Query::Field(f)) = denormalize_query(&Query::Field(
641 MemberQuery::default().with_class(Some(ClassQuery::exact("Lcom/app/C;"))).with_name(Some("a$2".into())).with_exact_name(true),
642 )) else {
643 panic!("a field query")
644 };
645 assert_eq!(f.name.as_deref(), Some("(?:^a\\$2\\x00|^a\\x00)"));
646 assert!(!f.exact_name);
647 }
648}