Skip to main content

asc_engine/
names.rs

1//! Class-name normalisation shared by the CLI and MCP.
2
3use crate::{EngineError, Result};
4use asc_findrefs::{ClassQuery, MemberQuery, MemberSignature, Query, TypeMatch};
5
6/// Turn `com.poc.Main` / `com/poc/Main` / `Lcom/poc/Main;` into a Dalvik descriptor.
7/// Array descriptors (`[Ljava/lang/Object;`, whose methods such as `clone`
8/// are referenced like any class's) are kept as they are.
9pub 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
26/// `Lcom/poc/Main;` → `com/poc/Main`: the descriptor's own `L` / `;` removed,
27/// package separators kept. Only the leading `L` is syntax, so
28/// `Lpkg/LLoader;` → `pkg/LLoader` while `LLoader;` is class `Loader`.
29pub 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
34/// `Lcom/poc/Main;` → `com.poc.Main` (other inputs unchanged).
35pub 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
39/// Normalise a `listclass` prefix: `com.poc` / `com/poc` become `Lcom/poc`,
40/// a prefix already starting with `L` is used as is (like the Python tool).
41pub 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
52/// Normalise a class query: fuzzy patterns only get their dots swapped for
53/// slashes, precise names become full descriptors.
54pub 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/// A method / field target as agents write it, split into its parts. Every
65/// field borrows from the spec.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct MemberTarget<'a> {
68    /// The class part, as written (`Lcom/a/B;`, `com.a.B`, `B`, `~pattern`).
69    pub class: Option<&'a str>,
70    /// The member name (or name pattern).
71    pub name: &'a str,
72    /// What followed the name: `(params)ret` of a method, `:T` of a field.
73    pub signature: Option<&'a str>,
74}
75
76impl<'a> MemberTarget<'a> {
77    /// Split `Lcom/a/B;->name(sig)ret`, `Lcom/a/B;->name`, `Lc;->f:T`,
78    /// `com.a.B->name`, `com.a.B#name`, `com.a.B.name` or `B.name`; a bare
79    /// name (or name pattern) has no class. A dot only separates the class
80    /// when what precedes it reads as a class (identifiers, the last one
81    /// capitalised: `WebView.loadUrl`, not the regex `get.*Id`).
82    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
101/// `[A-Za-z_$][A-Za-z0-9_$]*`, the identifiers of Java class paths and members
102/// (obfuscators stay within it; non-ASCII names are left to `->`).
103fn 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
113/// The index of the dot separating `com.a.B` from `name` in `com.a.B.name`
114/// (also `com/a/B.name`), if `spec` reads that way.
115fn 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    // A signature or a constructor name settles it; otherwise Java naming does.
124    let member_evident = head_end < spec.len() || name.starts_with('<');
125    (classlike && is_member_name(name) && (capitalised || member_evident)).then_some(i)
126}
127
128/// Whether `spec` names a class rather than a member: `Lcom/a/B;` or a dotted
129/// `com.a.B` whose last part is capitalised.
130pub 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
141/// A class written without its package (`WebView`, `Build$VERSION`): a fuzzy
142/// pattern for every class of that simple name. Fuzzy patterns run over the
143/// whole string data, so the descriptor's end is its `;` and NUL terminator.
144/// A descriptor (`Lx;`) is exact even without a package.
145fn simple_class_pattern(name: &str) -> Option<String> {
146    (is_identifier(name) && name != "L").then(|| format!("[/L]{};\\x00", regex::escape(name)))
147}
148
149/// Build a member (method/field) query from CLI-style arguments; `exact_name`
150/// matches the member name as a whole instead of as a substring / regex.
151///
152/// Lenient about where the class is written: `name` may carry it
153/// ([`MemberTarget::parse`] forms, used when `class` is not given), `class`
154/// may carry `->name`, and a `name` that is only a class becomes the class.
155/// A signature after the name (`run(I)V`, `f:I`) makes the name exact; the
156/// rows show each overload's signature. In the class, `~` in front means
157/// fuzzy; in the name, `=` in front means exact. A class without package
158/// (`WebView`) matches that simple name in any package.
159///
160/// A signature after the name also narrows the query to it
161/// ([`parse_signature`]): `run(I)V` is that overload, `run(I)` any return
162/// type, `f:I` the field of that type.
163pub 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    // `class: "Lc;->m"`: the whole target, read as one below.
169    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
211/// The findrefs kinds, as conditions spell them (`literal` is a string
212/// matched as plain text).
213pub const KINDS: &[&str] = &["string", "literal", "type", "method", "field", "extends", "impl", "override", "annotation", "flag", "in"];
214
215/// The canonical kind for what an agent wrote: case and plurals do not
216/// matter (`Methods`), nor common synonyms (`implements`, `class`, `call`).
217/// Unknown kinds fail with the valid ones and the closest of them.
218pub 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
242/// The Levenshtein distance of `a` and `b`, or `None` once it exceeds `max`.
243pub 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
266/// `loadUrl(java.lang.String, int)` → `loadUrl(Ljava/lang/String;I)`: a
267/// signature with Java parameter types in Dalvik form (a return type, if
268/// any, likewise). `None` when it has no parameter list or a type is not
269/// recognisable.
270pub 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
281/// `int` → `I`, `java.lang.String[]` → `[Ljava/lang/String;`, descriptors as they are.
282fn 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
308/// The signature written after a member name: `(params)ret` or `(params)`
309/// of a method, `:T` of a field. Types are Dalvik descriptors run together
310/// (`(ILjava/lang/String;)V`) or Java types separated by commas
311/// (`(int, java.lang.String) void`); a Java class without package
312/// (`String`) is that simple name in any package.
313pub 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
341/// `ILjava/lang/String;[B` → `["I", "Ljava/lang/String;", "[B"]`; `None`
342/// unless all of `s` is descriptors.
343pub(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
359/// One type as written in a signature: a descriptor, a Java primitive, a
360/// dotted or slashed class (`java.lang.String`), a class without package
361/// (`String`, any package), each optionally followed by `[]`s.
362fn 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
393/// The query of exactly the method `signature` names, `Lcls;->name(params)ret`
394/// as findrefs and callgraph print it (whatever characters the name holds).
395pub 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
407/// Parse a one-word findrefs condition, as the CLI's compound and rule
408/// options take them:
409///
410/// - `string:REGEX`, `literal:TEXT`, `type:PATTERN`
411/// - `method:TARGET`, `field:TARGET`: any [`MemberTarget::parse`] form, e.g.
412///   `android.webkit.WebView->loadUrl`; the class is exact unless prefixed
413///   `~` (fuzzy) or written without package, the name a regex / substring
414///   unless prefixed `=` or followed by a signature (exact)
415/// - `method:{RULE}`: calls of the methods whose code matches RULE
416/// - `method:@FILE`: calls of the methods FILE lists, one per line
417/// - `extends:CLASS`, `impl:CLASS`
418///
419/// The kind is read by [`parse_kind`] (`methods:`, `Method:` work too); the
420/// grammar is [`crate::condition`]'s.
421pub fn parse_condition(spec: &str) -> Result<Query> {
422    crate::condition::parse_condition_in(spec, None)
423}
424
425/// One rule of a rule set: a [`parse_condition`] condition, or several
426/// joined with `&` (a compound: every part must match in one method, none
427/// of the `!` parts).
428pub 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        // Patterns and lowercase dotted names stay names.
477        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        // The class may also carry the member, or come on its own.
497        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        // A signature or `=` makes the name exact; an explicit class wins.
502        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        // Without a package: that simple name in any package.
517        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        // A signature after the name narrows the query to it.
567        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}