1use crate::{EngineError, Result};
4use asc_findrefs::{ClassQuery, MemberQuery, MemberSignature, Query, TypeMatch};
5
6pub fn format_class_name(name: &str) -> Result<String> {
10 if name.is_empty() {
11 return Err(EngineError::Query("Class name cannot be empty".into()));
12 }
13 if (name.starts_with('L') && name.ends_with(';') && name.contains('/')) || name.starts_with('[') {
14 return Ok(name.to_string());
15 }
16 let mut n = name.replace('.', "/");
17 if !n.starts_with('L') {
18 n.insert(0, 'L');
19 }
20 if !n.ends_with(';') {
21 n.push(';');
22 }
23 Ok(n)
24}
25
26pub fn class_inner(descriptor: &str) -> &str {
30 let d = descriptor.strip_prefix('L').unwrap_or(descriptor);
31 d.strip_suffix(';').unwrap_or(d)
32}
33
34pub fn dalvik_to_dot(name: &str) -> String {
36 if name.starts_with('L') && name.ends_with(';') { name[1..name.len() - 1].replace('/', ".") } else { name.to_string() }
37}
38
39pub fn class_prefix(prefix: &str) -> Result<String> {
42 let p = prefix.trim();
43 if p.is_empty() {
44 return Err(EngineError::Query("Class prefix cannot be empty".into()));
45 }
46 if p.starts_with('L') {
47 return Ok(p.to_string());
48 }
49 Ok(format!("L{}", p.replace('.', "/")))
50}
51
52pub fn normalize_class_query(name: &str, fuzzy: bool) -> Result<String> {
55 if fuzzy {
56 if name.contains('.') && !name.contains('/') {
57 return Ok(name.replace('.', "/"));
58 }
59 return Ok(name.to_string());
60 }
61 format_class_name(name)
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct MemberTarget<'a> {
68 pub class: Option<&'a str>,
70 pub name: &'a str,
72 pub signature: Option<&'a str>,
74}
75
76impl<'a> MemberTarget<'a> {
77 pub fn parse(spec: &'a str) -> MemberTarget<'a> {
83 let s = spec.trim();
84 let (class, member) = if let Some((c, m)) = s.split_once("->") {
85 (Some(c.trim()), m.trim())
86 } else if let Some((c, m)) = s.rsplit_once('#') {
87 (Some(c.trim()), m.trim())
88 } else if let Some(i) = dotted_member_split(s) {
89 (Some(&s[..i]), &s[i + 1..])
90 } else {
91 (None, s)
92 };
93 let (name, signature) = match member.find(['(', ':']) {
94 Some(i) if is_member_name(&member[..i]) => (&member[..i], Some(&member[i..])),
95 _ => (member, None),
96 };
97 MemberTarget { class: class.filter(|c| !c.is_empty()), name, signature }
98 }
99}
100
101fn is_identifier(s: &str) -> bool {
104 let mut chars = s.chars();
105 chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
106 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
107}
108
109fn is_member_name(s: &str) -> bool {
110 is_identifier(s) || s == "<init>" || s == "<clinit>"
111}
112
113fn dotted_member_split(spec: &str) -> Option<usize> {
116 let head_end = spec.find(['(', ':']).unwrap_or(spec.len());
117 let head = &spec[..head_end];
118 let i = head.rfind('.')?;
119 let (class, name) = (&head[..i], &head[i + 1..]);
120 let segments: Vec<&str> = class.split(['.', '/']).collect();
121 let capitalised = segments.last().is_some_and(|l| l.starts_with(|c: char| c.is_ascii_uppercase()));
122 let classlike = segments.iter().all(|s| is_identifier(s));
123 let member_evident = head_end < spec.len() || name.starts_with('<');
125 (classlike && is_member_name(name) && (capitalised || member_evident)).then_some(i)
126}
127
128pub fn looks_like_class(spec: &str) -> bool {
131 let s = spec.trim();
132 if s.starts_with('L') && s.ends_with(';') && !s.contains("->") {
133 return true;
134 }
135 let segments: Vec<&str> = s.split('.').collect();
136 segments.len() > 1
137 && segments.iter().all(|seg| is_identifier(seg))
138 && segments.last().is_some_and(|l| l.starts_with(|c: char| c.is_ascii_uppercase()))
139}
140
141fn simple_class_pattern(name: &str) -> Option<String> {
146 (is_identifier(name) && name != "L").then(|| format!("[/L]{};\\x00", regex::escape(name)))
147}
148
149pub fn member_query(kind: &str, class: Option<&str>, fuzzy_class: bool, name: Option<&str>, exact_name: bool) -> Result<MemberQuery> {
164 let mut class = class.map(str::trim).filter(|c| !c.is_empty());
165 let mut name = name.map(str::trim).filter(|n| !n.is_empty());
166 let mut exact_name = exact_name;
167 let mut signature = None;
168 if let (Some(c), None) = (class, name)
170 && (c.contains("->") || c.contains('#'))
171 {
172 (class, name) = (None, Some(c));
173 }
174 if let Some(n) = name {
175 let t = MemberTarget::parse(n);
176 match t.class {
177 Some(c) => {
178 class = class.or(Some(c));
179 name = Some(t.name).filter(|n| !n.is_empty());
180 }
181 None if class.is_none() && looks_like_class(n) => (class, name) = (Some(n), None),
182 None => name = Some(t.name),
183 }
184 if let Some(sig) = t.signature {
185 exact_name = true;
186 signature = Some(parse_signature(sig, kind == "method")?);
187 }
188 }
189 if let Some(n) = name.and_then(|n| n.strip_prefix('=')) {
190 (name, exact_name) = (Some(n), true);
191 }
192 if class.is_none() && name.is_none() {
193 return Err(EngineError::Query(format!(
194 "{kind} query needs a class or a {kind} name, e.g. Landroid/webkit/WebView;->loadUrl, android.webkit.WebView.loadUrl or just loadUrl"
195 )));
196 }
197 let class = match class {
198 Some(c) => Some(match c.strip_prefix('~') {
199 Some(f) => ClassQuery { pattern: normalize_class_query(f, true)?, precise: false },
200 None if fuzzy_class => ClassQuery { pattern: normalize_class_query(c, true)?, precise: false },
201 None => match simple_class_pattern(c) {
202 Some(pattern) => ClassQuery { pattern, precise: false },
203 None => ClassQuery { pattern: format_class_name(c)?, precise: true },
204 },
205 }),
206 None => None,
207 };
208 Ok(MemberQuery { class, name: name.map(String::from), exact_name, signature })
209}
210
211pub const KINDS: &[&str] = &["string", "literal", "type", "method", "field", "extends", "impl", "override", "annotation", "flag", "in"];
214
215pub fn parse_kind(kind: &str) -> Result<&'static str> {
219 let k = kind.trim().to_ascii_lowercase();
220 let alias = |k: &str| -> Option<&'static str> {
221 Some(match k {
222 "string" | "str" | "text" => "string",
223 "literal" => "literal",
224 "type" | "class" => "type",
225 "method" | "call" | "invoke" => "method",
226 "field" => "field",
227 "extends" | "extend" | "subclass" | "superclass" => "extends",
228 "impl" | "implements" | "implement" | "interface" | "implementer" => "impl",
229 "override" | "overrides" | "implementor" => "override",
230 "annotation" | "annotated" | "anno" => "annotation",
231 "flag" | "flags" | "access" | "modifier" => "flag",
232 "in" | "inclass" => "in",
233 _ => return None,
234 })
235 };
236 alias(&k).or_else(|| k.strip_suffix("es").and_then(alias)).or_else(|| k.strip_suffix('s').and_then(alias)).ok_or_else(|| {
237 let closest = KINDS.iter().min_by_key(|c| edit_distance(&k, c, usize::MAX).unwrap_or(usize::MAX)).copied().unwrap_or("string");
238 EngineError::Query(format!("unknown kind {kind:?}: use string, type, method, field, extends or impl (closest: {closest})"))
239 })
240}
241
242pub fn edit_distance(a: &str, b: &str, max: usize) -> Option<usize> {
244 let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
245 if a.len().abs_diff(b.len()) > max {
246 return None;
247 }
248 let mut row: Vec<usize> = (0..=b.len()).collect();
249 for (i, ca) in a.iter().enumerate() {
250 let mut prev = row[0];
251 row[0] = i + 1;
252 let mut best = row[0];
253 for (j, cb) in b.iter().enumerate() {
254 let cur = row[j + 1];
255 row[j + 1] = if ca == cb { prev } else { 1 + prev.min(cur).min(row[j]) };
256 prev = cur;
257 best = best.min(row[j + 1]);
258 }
259 if best > max {
260 return None;
261 }
262 }
263 Some(row[b.len()]).filter(|&d| d <= max)
264}
265
266pub fn java_signature_to_dalvik(signature: &str) -> Option<String> {
271 let (name, rest) = signature.trim().split_once('(')?;
272 let (params, ret) = rest.split_once(')')?;
273 let params: String = params.split(',').map(str::trim).filter(|p| !p.is_empty()).map(java_type).collect::<Option<_>>()?;
274 let ret = match ret.trim() {
275 "" => String::new(),
276 r => java_type(r)?,
277 };
278 Some(format!("{}({params}){ret}", name.trim()))
279}
280
281fn java_type(t: &str) -> Option<String> {
283 let t = t.trim();
284 if let Some(inner) = t.strip_suffix("[]") {
285 return Some(format!("[{}", java_type(inner)?));
286 }
287 let primitive = match t {
288 "void" => "V",
289 "boolean" => "Z",
290 "byte" => "B",
291 "short" => "S",
292 "char" => "C",
293 "int" => "I",
294 "long" => "J",
295 "float" => "F",
296 "double" => "D",
297 _ => "",
298 };
299 if !primitive.is_empty() {
300 return Some(primitive.to_string());
301 }
302 if (t.starts_with('L') && t.ends_with(';')) || t.starts_with('[') || (t.len() == 1 && "VZBSCIJFD".contains(t)) {
303 return Some(t.to_string());
304 }
305 t.split('.').all(is_identifier).then(|| format!("L{};", t.replace('.', "/")))
306}
307
308pub fn parse_signature(signature: &str, method: bool) -> Result<MemberSignature> {
314 let bad = |why: &str| {
315 EngineError::Query(format!(
316 "bad signature {signature:?}: {why}; write a method's as (params)ret, e.g. (ILjava/lang/String;)V or (int, String), \
317 a field's as :T, e.g. :I"
318 ))
319 };
320 let s = signature.trim();
321 if !method {
322 let t = s.strip_prefix(':').ok_or_else(|| bad("a field takes :T"))?;
323 return type_match(t).map(MemberSignature::Field).ok_or_else(|| bad("unknown type"));
324 }
325 let (params, ret) = s.strip_prefix('(').and_then(|r| r.split_once(')')).ok_or_else(|| bad("a method takes (params)"))?;
326 let params = params.trim();
327 let params = if params.is_empty() {
328 Vec::new()
329 } else if let Some(descriptors) = (!params.contains([',', ' '])).then(|| dalvik_types(params)).flatten() {
330 descriptors.into_iter().map(|d| TypeMatch::Descriptor(d.to_string())).collect()
331 } else {
332 params.split(',').map(type_match).collect::<Option<_>>().ok_or_else(|| bad("unknown parameter type"))?
333 };
334 let ret = match ret.trim() {
335 "" => None,
336 r => Some(type_match(r).ok_or_else(|| bad("unknown return type"))?),
337 };
338 Ok(MemberSignature::Method { params, ret })
339}
340
341pub(crate) fn dalvik_types(s: &str) -> Option<Vec<&str>> {
344 let mut out = Vec::new();
345 let mut rest = s;
346 while !rest.is_empty() {
347 let dims = rest.len() - rest.trim_start_matches('[').len();
348 let end = match rest[dims..].chars().next()? {
349 'Z' | 'B' | 'S' | 'C' | 'I' | 'J' | 'F' | 'D' | 'V' => dims + 1,
350 'L' => dims + rest[dims..].find(';')? + 1,
351 _ => return None,
352 };
353 out.push(&rest[..end]);
354 rest = &rest[end..];
355 }
356 Some(out)
357}
358
359fn type_match(t: &str) -> Option<TypeMatch> {
363 let t = t.trim();
364 if let Some([d]) = dalvik_types(t).as_deref() {
365 return Some(TypeMatch::Descriptor(d.to_string()));
366 }
367 let (mut base, mut dims) = (t, 0);
368 while let Some(b) = base.strip_suffix("[]") {
369 (base, dims) = (b.trim_end(), dims + 1);
370 }
371 let arrays = "[".repeat(dims);
372 let primitive = match base {
373 "void" => "V",
374 "boolean" => "Z",
375 "byte" => "B",
376 "short" => "S",
377 "char" => "C",
378 "int" => "I",
379 "long" => "J",
380 "float" => "F",
381 "double" => "D",
382 _ => "",
383 };
384 if !primitive.is_empty() {
385 return Some(TypeMatch::Descriptor(format!("{arrays}{primitive}")));
386 }
387 if base.contains(['.', '/']) {
388 return base.split(['.', '/']).all(is_identifier).then(|| TypeMatch::Descriptor(format!("{arrays}L{};", base.replace('.', "/"))));
389 }
390 is_identifier(base).then(|| TypeMatch::Simple { name: base.to_string(), dims })
391}
392
393pub fn exact_method(signature: &str) -> Result<MemberQuery> {
396 let bad = || EngineError::Query(format!("not a method signature: {signature:?} (want Lcls;->name(params)ret)"));
397 let (class, member) = signature.trim().split_once("->").ok_or_else(bad)?;
398 let p = member.find('(').ok_or_else(bad)?;
399 Ok(MemberQuery {
400 class: Some(ClassQuery { pattern: class.to_string(), precise: true }),
401 name: Some(member[..p].to_string()),
402 exact_name: true,
403 signature: Some(parse_signature(&member[p..], true)?),
404 })
405}
406
407pub fn parse_condition(spec: &str) -> Result<Query> {
422 crate::condition::parse_condition_in(spec, None)
423}
424
425pub fn parse_rule(spec: &str) -> Result<Query> {
429 crate::condition::parse_rule_in(spec, None)
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn class_names() {
438 assert_eq!(format_class_name("com.poc.Main").unwrap(), "Lcom/poc/Main;");
439 assert_eq!(format_class_name("Lcom/poc/Main;").unwrap(), "Lcom/poc/Main;");
440 assert_eq!(format_class_name("com/poc/Main").unwrap(), "Lcom/poc/Main;");
441 assert_eq!(format_class_name("[Ljava/lang/Object;").unwrap(), "[Ljava/lang/Object;");
442 assert_eq!(dalvik_to_dot("Lcom/poc/Main;"), "com.poc.Main");
443 assert_eq!(normalize_class_query("com.poc", true).unwrap(), "com/poc");
444 assert_eq!(normalize_class_query("poc", true).unwrap(), "poc");
445 assert!(format_class_name("").is_err());
446 for p in ["com.xxx", "com/xxx", "Lcom/xxx", " com.xxx\t"] {
447 assert_eq!(class_prefix(p).unwrap(), "Lcom/xxx");
448 }
449 assert_eq!(class_prefix("a").unwrap(), "La");
450 assert_eq!(class_prefix(" ").unwrap_err().to_string(), "Class prefix cannot be empty");
451 assert!(member_query("method", None, false, None, false).is_err());
452 let q = member_query("method", Some("com.poc.Main"), false, Some("on"), false).unwrap();
453 assert!(!q.exact_name);
454 assert_eq!(q.class.unwrap(), ClassQuery { pattern: "Lcom/poc/Main;".into(), precise: true });
455 assert!(member_query("field", None, false, Some("f"), true).unwrap().exact_name);
456 }
457
458 fn precise(c: &str) -> Option<ClassQuery> {
459 Some(ClassQuery { pattern: c.into(), precise: true })
460 }
461
462 #[test]
463 fn member_targets_in_every_spelling() {
464 let t = |s| MemberTarget::parse(s);
465 assert_eq!(t("Lcom/a/B;->run(I)V"), MemberTarget { class: Some("Lcom/a/B;"), name: "run", signature: Some("(I)V") });
466 assert_eq!(t("Lc;->f:I"), MemberTarget { class: Some("Lc;"), name: "f", signature: Some(":I") });
467 assert_eq!(t("com.a.B->run"), MemberTarget { class: Some("com.a.B"), name: "run", signature: None });
468 assert_eq!(t("com.a.B#run"), MemberTarget { class: Some("com.a.B"), name: "run", signature: None });
469 assert_eq!(
470 t("android.webkit.WebView.loadUrl"),
471 MemberTarget { class: Some("android.webkit.WebView"), name: "loadUrl", signature: None }
472 );
473 assert_eq!(t("com/a/B.run(I)V"), MemberTarget { class: Some("com/a/B"), name: "run", signature: Some("(I)V") });
474 assert_eq!(t("java.lang.Thread.<init>"), MemberTarget { class: Some("java.lang.Thread"), name: "<init>", signature: None });
475 assert_eq!(t("Build$VERSION.SDK_INT").class, Some("Build$VERSION"));
476 for name in ["get.*Id", "(get|set)Foo", "a.b", "exec"] {
478 assert_eq!(t(name), MemberTarget { class: None, name, signature: None }, "{name}");
479 }
480 assert!(looks_like_class("android.webkit.WebView") && looks_like_class("Lcom/a/B;"));
481 assert!(!looks_like_class("loadUrl") && !looks_like_class("com.a.b") && !looks_like_class("Lc;->m"));
482 }
483
484 #[test]
485 fn member_queries_from_any_spelling_agree() {
486 let want =
487 MemberQuery { class: precise("Landroid/webkit/WebView;"), name: Some("loadUrl".into()), exact_name: false, signature: None };
488 for spec in [
489 "Landroid/webkit/WebView;->loadUrl",
490 "android.webkit.WebView.loadUrl",
491 "android.webkit.WebView->loadUrl",
492 "android.webkit.WebView#loadUrl",
493 ] {
494 assert_eq!(member_query("method", None, false, Some(spec), false).unwrap(), want, "{spec}");
495 }
496 assert_eq!(member_query("method", Some("Landroid/webkit/WebView;->loadUrl"), false, None, false).unwrap(), want);
498 assert_eq!(member_query("method", Some("android.webkit.WebView"), false, Some("loadUrl"), false).unwrap(), want);
499 let class_only = member_query("method", None, false, Some("android.webkit.WebView"), false).unwrap();
500 assert_eq!(class_only, MemberQuery { class: precise("Landroid/webkit/WebView;"), name: None, exact_name: false, signature: None });
501 let q = member_query("method", None, false, Some("Lc;->run(I)V"), false).unwrap();
503 assert_eq!((q.name.as_deref(), q.exact_name), (Some("run"), true));
504 let q = member_query("field", Some("Lx;"), false, Some("Lc;->f:I"), false).unwrap();
505 assert_eq!((q.class, q.name.as_deref(), q.exact_name), (precise("Lx;"), Some("f"), true));
506 let q = member_query("method", None, false, Some("~Foo->=bar"), false).unwrap();
507 assert_eq!(
508 q,
509 MemberQuery {
510 class: Some(ClassQuery { pattern: "Foo".into(), precise: false }),
511 name: Some("bar".into()),
512 exact_name: true,
513 signature: None
514 }
515 );
516 let q = member_query("method", None, false, Some("WebView.loadUrl"), false).unwrap();
518 assert_eq!(q.class, Some(ClassQuery { pattern: "[/L]WebView;\\x00".into(), precise: false }));
519 let e = member_query("method", None, false, Some(" "), false).unwrap_err().to_string();
520 assert!(e.contains("needs a class or a method name"), "{e}");
521 }
522
523 #[test]
524 fn kinds_tolerate_case_plurals_and_synonyms() {
525 for (k, want) in [
526 ("Methods", "method"),
527 ("strings", "string"),
528 ("TYPE", "type"),
529 ("implements", "impl"),
530 ("classes", "type"),
531 ("fields", "field"),
532 ("literal", "literal"),
533 ] {
534 assert_eq!(parse_kind(k).unwrap(), want, "{k}");
535 }
536 let e = parse_kind("metod").unwrap_err().to_string();
537 assert!(e.contains("unknown kind \"metod\"") && e.contains("closest: method"), "{e}");
538 assert_eq!(
539 parse_condition("Methods:android.webkit.WebView.loadUrl").unwrap(),
540 parse_condition("method:Landroid/webkit/WebView;->loadUrl").unwrap()
541 );
542 assert!(matches!(parse_rule("string:a & method:b").unwrap(), Query::All { all, .. } if all.len() == 2));
543 }
544
545 #[test]
546 fn signatures_in_dalvik_and_java_form() {
547 let d = |s: &str| TypeMatch::Descriptor(s.into());
548 let method = |params: Vec<TypeMatch>, ret: Option<TypeMatch>| MemberSignature::Method { params, ret };
549 let sig = |s| parse_signature(s, true).unwrap();
550 assert_eq!(sig("(I)V"), method(vec![d("I")], Some(d("V"))));
551 assert_eq!(sig("(I)"), method(vec![d("I")], None));
552 assert_eq!(sig("()"), method(vec![], None));
553 assert_eq!(sig("(ILjava/lang/String;[B)Z"), method(vec![d("I"), d("Ljava/lang/String;"), d("[B")], Some(d("Z"))));
554 assert_eq!(sig("(int)"), method(vec![d("I")], None));
555 assert_eq!(sig("(int, java.lang.String[]) void"), method(vec![d("I"), d("[Ljava/lang/String;")], Some(d("V"))));
556 assert_eq!(
557 sig("(String, Map[][])"),
558 method(vec![TypeMatch::Simple { name: "String".into(), dims: 0 }, TypeMatch::Simple { name: "Map".into(), dims: 2 }], None)
559 );
560 assert_eq!(parse_signature(":I", false).unwrap(), MemberSignature::Field(d("I")));
561 assert_eq!(parse_signature(":java.lang.String", false).unwrap(), MemberSignature::Field(d("Ljava/lang/String;")));
562 for (bad, method) in [("(I", true), ("(%)V", true), (":I", true), ("(I)V", false), (":", false)] {
563 let e = parse_signature(bad, method).unwrap_err().to_string();
564 assert!(e.contains("bad signature"), "{bad}: {e}");
565 }
566 for spec in ["Lc;->run(I)V", "c.run(int)void", "Lc;->run(I)V "] {
568 let q = member_query("method", None, false, Some(spec), false).unwrap();
569 assert_eq!(q.signature, Some(method(vec![d("I")], Some(d("V")))), "{spec}");
570 }
571 assert_eq!(
572 member_query("method", None, false, Some("WebView.loadUrl(String)"), false).unwrap().signature.unwrap().to_string(),
573 "(String)"
574 );
575 assert_eq!(member_query("method", None, false, Some("loadUrl"), false).unwrap().signature, None);
576 assert!(member_query("field", None, false, Some("Lc;->f(I)V"), false).is_err());
577 assert_eq!(
578 parse_condition("method:Lc;->m(I)").unwrap(),
579 Query::Method(member_query("method", None, false, Some("Lc;->m(I)"), false).unwrap())
580 );
581 }
582
583 #[test]
584 fn java_signatures_and_edit_distance() {
585 assert_eq!(java_signature_to_dalvik("loadUrl(java.lang.String, int)").as_deref(), Some("loadUrl(Ljava/lang/String;I)"));
586 assert_eq!(java_signature_to_dalvik("f(byte[], Lx;) void").as_deref(), Some("f([BLx;)V"));
587 assert_eq!(java_signature_to_dalvik("run()").as_deref(), Some("run()"));
588 assert_eq!(java_signature_to_dalvik("run"), None);
589 assert_eq!(edit_distance("kitten", "sitting", 5), Some(3));
590 assert_eq!(edit_distance("kitten", "sitting", 2), None);
591 assert_eq!(edit_distance("a", "a", 0), Some(0));
592 }
593}