Skip to main content

asc_engine/
names.rs

1//! Class-name normalisation shared by the CLI and MCP, the readings of names
2//! as decompiled Java / Kotlin source spells them ([`denormalize_query`]),
3//! and the name mappings handed to decompiler backends ([`NameMappings`]).
4
5use crate::{EngineError, Result};
6use asc_findrefs::{ClassQuery, MemberQuery, MemberSignature, Query, TypeMatch};
7
8mod denormalize;
9mod mappings;
10mod proguard;
11
12pub use denormalize::{
13    denormalize_class_candidates, denormalize_identifier, denormalize_method_candidates, denormalize_query, is_java_reserved,
14};
15pub(crate) use denormalize::{partial_proto, proto_matches};
16
17pub use mappings::{FieldRef, MappingError, MappingsHash, MethodRef, NameMappings, NameResolver};
18pub use proguard::{ProguardError, ProguardMapping, ProguardWarning, WarningKind};
19
20/// Turn `com.poc.Main` / `com/poc/Main` / `Lcom/poc/Main;` into a Dalvik descriptor.
21/// Array descriptors (`[Ljava/lang/Object;`, whose methods such as `clone`
22/// are referenced like any class's) are kept as they are.
23///
24/// # Errors
25///
26/// [`EngineError::Query`] for an empty name.
27pub fn format_class_name(name: &str) -> Result<String> {
28    if name.is_empty() {
29        return Err(EngineError::Query("Class name cannot be empty".into()));
30    }
31    if (name.starts_with('L') && name.ends_with(';') && name.contains('/')) || name.starts_with('[') {
32        return Ok(name.to_string());
33    }
34    let mut n = name.replace('.', "/");
35    if !n.starts_with('L') {
36        n.insert(0, 'L');
37    }
38    if !n.ends_with(';') {
39        n.push(';');
40    }
41    Ok(n)
42}
43
44/// `Lcom/poc/Main;` → `com/poc/Main`: the descriptor's own `L` / `;` removed,
45/// package separators kept. Only the leading `L` is syntax, so
46/// `Lpkg/LLoader;` → `pkg/LLoader` while `LLoader;` is class `Loader`.
47pub(crate) fn class_inner(descriptor: &str) -> &str {
48    let d = descriptor.strip_prefix('L').unwrap_or(descriptor);
49    d.strip_suffix(';').unwrap_or(d)
50}
51
52/// Normalise a `listclass` prefix: `com.poc` / `com/poc` become `Lcom/poc`,
53/// a prefix already starting with `L` is used as is (like the Python tool).
54pub(crate) fn class_prefix(prefix: &str) -> Result<String> {
55    let p = prefix.trim();
56    if p.is_empty() {
57        return Err(EngineError::Query("Class prefix cannot be empty".into()));
58    }
59    if p.starts_with('L') {
60        return Ok(p.to_string());
61    }
62    Ok(format!("L{}", p.replace('.', "/")))
63}
64
65/// Normalise a class query: fuzzy patterns only get their dots swapped for
66/// slashes, precise names become full descriptors.
67pub(crate) fn normalize_class_query(name: &str, fuzzy: bool) -> Result<String> {
68    if fuzzy {
69        if name.contains('.') && !name.contains('/') {
70            return Ok(name.replace('.', "/"));
71        }
72        return Ok(name.to_string());
73    }
74    format_class_name(name)
75}
76
77/// A method / field target as agents write it, split into its parts. Every
78/// field borrows from the spec.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub(crate) struct MemberTarget<'a> {
81    /// The class part, as written (`Lcom/a/B;`, `com.a.B`, `B`, `~pattern`).
82    pub class: Option<&'a str>,
83    /// The member name (or name pattern).
84    pub name: &'a str,
85    /// What followed the name: `(params)ret` of a method, `:T` of a field.
86    pub signature: Option<&'a str>,
87}
88
89impl<'a> MemberTarget<'a> {
90    /// Split `Lcom/a/B;->name(sig)ret`, `Lcom/a/B;->name`, `Lc;->f:T`,
91    /// `com.a.B->name`, `com.a.B#name`, `com.a.B.name` or `B.name`; a bare
92    /// name (or name pattern) has no class. A dot only separates the class
93    /// when what precedes it reads as a class (identifiers, the last one
94    /// capitalised: `WebView.loadUrl`, not the regex `get.*Id`).
95    pub(crate) fn parse(spec: &'a str) -> MemberTarget<'a> {
96        let s = spec.trim();
97        let (class, member) = if let Some((c, m)) = s.split_once("->") {
98            (Some(c.trim()), m.trim())
99        } else if let Some((c, m)) = s.rsplit_once('#') {
100            (Some(c.trim()), m.trim())
101        } else if let Some(i) = dotted_member_split(s) {
102            (Some(&s[..i]), &s[i + 1..])
103        } else {
104            (None, s)
105        };
106        let (name, signature) = match member.find(['(', ':']) {
107            Some(i) if is_member_name(&member[..i]) => (&member[..i], Some(&member[i..])),
108            _ => (member, None),
109        };
110        MemberTarget { class: class.filter(|c| !c.is_empty()), name, signature }
111    }
112}
113
114/// `[A-Za-z_$][A-Za-z0-9_$]*`, the identifiers of Java class paths and members
115/// (obfuscators stay within it; non-ASCII names are left to `->`), or a
116/// Kotlin backticked name (`` `in` ``).
117fn is_identifier(s: &str) -> bool {
118    if let Some(inner) = s.strip_prefix('`').and_then(|r| r.strip_suffix('`')) {
119        return !inner.is_empty() && !inner.contains('`');
120    }
121    let mut chars = s.chars();
122    chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
123        && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
124}
125
126fn is_member_name(s: &str) -> bool {
127    is_identifier(s) || s == "<init>" || s == "<clinit>"
128}
129
130/// The index of the dot separating `com.a.B` from `name` in `com.a.B.name`
131/// (also `com/a/B.name`), if `spec` reads that way.
132fn dotted_member_split(spec: &str) -> Option<usize> {
133    let head_end = spec.find(['(', ':']).unwrap_or(spec.len());
134    let head = &spec[..head_end];
135    let i = head.rfind('.')?;
136    let (class, name) = (&head[..i], &head[i + 1..]);
137    let segments: Vec<&str> = class.split(['.', '/']).collect();
138    let capitalised = segments.last().is_some_and(|l| l.trim_matches('`').starts_with(|c: char| c.is_ascii_uppercase()));
139    let classlike = segments.iter().all(|s| is_identifier(s));
140    // A signature or a constructor name settles it; otherwise Java naming does.
141    let member_evident = head_end < spec.len() || name.starts_with('<');
142    (classlike && is_member_name(name) && (capitalised || member_evident)).then_some(i)
143}
144
145/// Whether `spec` names a class rather than a member: `Lcom/a/B;` or a dotted
146/// `com.a.B` whose last part is capitalised.
147pub(crate) fn looks_like_class(spec: &str) -> bool {
148    let s = spec.trim();
149    if s.starts_with('L') && s.ends_with(';') && !s.contains("->") {
150        return true;
151    }
152    let segments: Vec<&str> = s.split('.').collect();
153    segments.len() > 1
154        && segments.iter().all(|seg| is_identifier(seg))
155        && segments.last().is_some_and(|l| l.trim_matches('`').starts_with(|c: char| c.is_ascii_uppercase()))
156}
157
158/// A class written without its package (`WebView`, `Build$VERSION`): a fuzzy
159/// pattern for every class of that simple name. Fuzzy patterns run over the
160/// whole string data, so the descriptor's end is its `;` and NUL terminator.
161/// A descriptor (`Lx;`) is exact even without a package.
162fn simple_class_pattern(name: &str) -> Option<String> {
163    (is_identifier(name) && name != "L").then(|| format!("[/L]{};\\x00", regex::escape(name)))
164}
165
166/// Build a member (method/field) query from CLI-style arguments; `exact_name`
167/// matches the member name as a whole instead of as a substring / regex.
168///
169/// Lenient about where the class is written: `name` may carry it
170/// (`Lcom/a/B;->name`, `com.a.B->name`, `com.a.B#name`, `B.name`, ...; used
171/// when `class` is not given), `class`
172/// may carry `->name`, and a `name` that is only a class becomes the class.
173/// A signature after the name (`run(I)V`, `f:I`) makes the name exact; the
174/// rows show each overload's signature. In the class, `~` in front means
175/// fuzzy; in the name, `=` in front means exact. A class without package
176/// (`WebView`) matches that simple name in any package.
177///
178/// A signature after the name also narrows the query to it: `run(I)V` is
179/// that overload, `run(I)` any return type, `f:I` the field of that type.
180/// Types are Dalvik descriptors run together or comma-separated Java types
181/// (`(int, java.lang.String) void`).
182///
183/// # Errors
184///
185/// [`EngineError::Query`] when neither a class nor a name is given, the
186/// class is empty, or the signature is malformed.
187pub fn member_query(kind: &str, class: Option<&str>, fuzzy_class: bool, name: Option<&str>, exact_name: bool) -> Result<MemberQuery> {
188    let mut class = class.map(str::trim).filter(|c| !c.is_empty());
189    let mut name = name.map(str::trim).filter(|n| !n.is_empty());
190    let mut exact_name = exact_name;
191    let mut signature = None;
192    // `class: "Lc;->m"`: the whole target, read as one below.
193    if let (Some(c), None) = (class, name)
194        && (c.contains("->") || c.contains('#'))
195    {
196        (class, name) = (None, Some(c));
197    }
198    if let Some(n) = name {
199        let t = MemberTarget::parse(n);
200        match t.class {
201            Some(c) => {
202                class = class.or(Some(c));
203                name = Some(t.name).filter(|n| !n.is_empty());
204            }
205            None if class.is_none() && looks_like_class(n) => (class, name) = (Some(n), None),
206            None => name = Some(t.name),
207        }
208        if let Some(sig) = t.signature {
209            exact_name = true;
210            signature = Some(parse_signature(sig, kind == "method")?);
211        }
212    }
213    if let Some(n) = name.and_then(|n| n.strip_prefix('=')) {
214        (name, exact_name) = (Some(n), true);
215    }
216    if class.is_none() && name.is_none() {
217        return Err(EngineError::Query(format!(
218            "{kind} query needs a class or a {kind} name, e.g. Landroid/webkit/WebView;->loadUrl, android.webkit.WebView.loadUrl or just loadUrl"
219        )));
220    }
221    let class = match class {
222        Some(c) => Some(match c.strip_prefix('~') {
223            Some(f) => ClassQuery::fuzzy(normalize_class_query(f, true)?),
224            None if fuzzy_class => ClassQuery::fuzzy(normalize_class_query(c, true)?),
225            None => match simple_class_pattern(c) {
226                Some(pattern) => ClassQuery::fuzzy(pattern),
227                None => ClassQuery::exact(format_class_name(c)?),
228            },
229        }),
230        None => None,
231    };
232    Ok(MemberQuery::default().with_class(class).with_name(name.map(String::from)).with_exact_name(exact_name).with_signature(signature))
233}
234
235/// The findrefs kinds, as conditions spell them (`literal` is a string
236/// matched as plain text).
237pub const KINDS: &[&str] = &["string", "literal", "type", "method", "field", "extends", "impl", "override", "annotation", "flag", "in"];
238
239/// The canonical kind for what an agent wrote: case and plurals do not
240/// matter (`Methods`), nor common synonyms (`implements`, `class`, `call`).
241/// Unknown kinds fail with the valid ones and the closest of them.
242///
243/// # Errors
244///
245/// [`EngineError::Query`] for an unknown kind.
246pub fn parse_kind(kind: &str) -> Result<&'static str> {
247    let k = kind.trim().to_ascii_lowercase();
248    let alias = |k: &str| -> Option<&'static str> {
249        Some(match k {
250            "string" | "str" | "text" => "string",
251            "literal" => "literal",
252            "type" | "class" => "type",
253            "method" | "call" | "invoke" => "method",
254            "field" => "field",
255            "extends" | "extend" | "subclass" | "superclass" => "extends",
256            "impl" | "implements" | "implement" | "interface" | "implementer" => "impl",
257            "override" | "overrides" | "implementor" => "override",
258            "annotation" | "annotated" | "anno" => "annotation",
259            "flag" | "flags" | "access" | "modifier" => "flag",
260            "in" | "inclass" => "in",
261            _ => return None,
262        })
263    };
264    alias(&k).or_else(|| k.strip_suffix("es").and_then(alias)).or_else(|| k.strip_suffix('s').and_then(alias)).ok_or_else(|| {
265        let closest = KINDS.iter().min_by_key(|c| edit_distance(&k, c, usize::MAX).unwrap_or(usize::MAX)).copied().unwrap_or("string");
266        EngineError::Query(format!("unknown kind {kind:?}: use string, type, method, field, extends or impl (closest: {closest})"))
267    })
268}
269
270/// The Levenshtein distance of `a` and `b`, or `None` once it exceeds `max`.
271pub(crate) fn edit_distance(a: &str, b: &str, max: usize) -> Option<usize> {
272    let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
273    if a.len().abs_diff(b.len()) > max {
274        return None;
275    }
276    let mut row: Vec<usize> = (0..=b.len()).collect();
277    for (i, ca) in a.iter().enumerate() {
278        let mut prev = row[0];
279        row[0] = i + 1;
280        let mut best = row[0];
281        for (j, cb) in b.iter().enumerate() {
282            let cur = row[j + 1];
283            row[j + 1] = if ca == cb { prev } else { 1 + prev.min(cur).min(row[j]) };
284            prev = cur;
285            best = best.min(row[j + 1]);
286        }
287        if best > max {
288            return None;
289        }
290    }
291    Some(row[b.len()]).filter(|&d| d <= max)
292}
293
294/// `loadUrl(java.lang.String, int)` → `loadUrl(Ljava/lang/String;I)`: a
295/// signature with Java parameter types in Dalvik form (a return type, if
296/// any, likewise). `None` when it has no parameter list or a type is not
297/// recognisable.
298pub fn java_signature_to_dalvik(signature: &str) -> Option<String> {
299    let (name, rest) = signature.trim().split_once('(')?;
300    let (params, ret) = rest.split_once(')')?;
301    let params: String = params.split(',').map(str::trim).filter(|p| !p.is_empty()).map(java_type).collect::<Option<_>>()?;
302    let ret = match ret.trim() {
303        "" => String::new(),
304        r => java_type(r)?,
305    };
306    Some(format!("{}({params}){ret}", name.trim()))
307}
308
309/// `int` → `I`, `java.lang.String[]` → `[Ljava/lang/String;`, descriptors as they are.
310fn java_type(t: &str) -> Option<String> {
311    // A loop, not recursion: the dimensions come from the user's query.
312    let mut t = t.trim();
313    let mut dims = 0;
314    while let Some(inner) = t.strip_suffix("[]") {
315        t = inner.trim();
316        dims += 1;
317    }
318    let element = java_element_type(t)?;
319    Some("[".repeat(dims) + &element)
320}
321
322/// [`java_type`] of a type that is not a Java array.
323fn java_element_type(t: &str) -> Option<String> {
324    let primitive = match t {
325        "void" => "V",
326        "boolean" => "Z",
327        "byte" => "B",
328        "short" => "S",
329        "char" => "C",
330        "int" => "I",
331        "long" => "J",
332        "float" => "F",
333        "double" => "D",
334        _ => "",
335    };
336    if !primitive.is_empty() {
337        return Some(primitive.to_string());
338    }
339    if (t.starts_with('L') && t.ends_with(';')) || t.starts_with('[') || (t.len() == 1 && "VZBSCIJFD".contains(t)) {
340        return Some(t.to_string());
341    }
342    t.split('.').all(is_identifier).then(|| format!("L{};", t.replace('.', "/")))
343}
344
345/// The signature written after a member name: `(params)ret` or `(params)`
346/// of a method, `:T` of a field. Types are Dalvik descriptors run together
347/// (`(ILjava/lang/String;)V`) or Java types separated by commas
348/// (`(int, java.lang.String) void`); a Java class without package
349/// (`String`) is that simple name in any package.
350pub(crate) fn parse_signature(signature: &str, method: bool) -> Result<MemberSignature> {
351    let bad = |why: &str| {
352        EngineError::Query(format!(
353            "bad signature {signature:?}: {why}; write a method's as (params)ret, e.g. (ILjava/lang/String;)V or (int, String), \
354             a field's as :T, e.g. :I"
355        ))
356    };
357    let s = signature.trim();
358    if !method {
359        let t = s.strip_prefix(':').ok_or_else(|| bad("a field takes :T"))?;
360        return type_match(t).map(MemberSignature::Field).ok_or_else(|| bad("unknown type"));
361    }
362    let (params, ret) = s.strip_prefix('(').and_then(|r| r.split_once(')')).ok_or_else(|| bad("a method takes (params)"))?;
363    let params = params.trim();
364    let params = if params.is_empty() {
365        Vec::new()
366    } else if let Some(descriptors) = (!params.contains([',', ' '])).then(|| dalvik_types(params)).flatten() {
367        descriptors.into_iter().map(|d| TypeMatch::Descriptor(d.to_string())).collect()
368    } else {
369        params.split(',').map(type_match).collect::<Option<_>>().ok_or_else(|| bad("unknown parameter type"))?
370    };
371    let ret = match ret.trim() {
372        "" => None,
373        r => Some(type_match(r).ok_or_else(|| bad("unknown return type"))?),
374    };
375    Ok(MemberSignature::Method { params, ret })
376}
377
378/// `ILjava/lang/String;[B` → `["I", "Ljava/lang/String;", "[B"]`; `None`
379/// unless all of `s` is descriptors.
380pub(crate) fn dalvik_types(s: &str) -> Option<Vec<&str>> {
381    let mut out = Vec::new();
382    let mut rest = s;
383    while !rest.is_empty() {
384        let dims = rest.len() - rest.trim_start_matches('[').len();
385        let end = match rest[dims..].chars().next()? {
386            'Z' | 'B' | 'S' | 'C' | 'I' | 'J' | 'F' | 'D' | 'V' => dims + 1,
387            'L' => dims + rest[dims..].find(';')? + 1,
388            _ => return None,
389        };
390        out.push(&rest[..end]);
391        rest = &rest[end..];
392    }
393    Some(out)
394}
395
396/// One type as written in a signature: a descriptor, a Java primitive, a
397/// dotted or slashed class (`java.lang.String`), a class without package
398/// (`String`, any package), each optionally followed by `[]`s.
399fn type_match(t: &str) -> Option<TypeMatch> {
400    let t = t.trim();
401    if let Some([d]) = dalvik_types(t).as_deref() {
402        return Some(TypeMatch::Descriptor(d.to_string()));
403    }
404    let (mut base, mut dims) = (t, 0);
405    while let Some(b) = base.strip_suffix("[]") {
406        (base, dims) = (b.trim_end(), dims + 1);
407    }
408    let arrays = "[".repeat(dims);
409    let primitive = match base {
410        "void" => "V",
411        "boolean" => "Z",
412        "byte" => "B",
413        "short" => "S",
414        "char" => "C",
415        "int" => "I",
416        "long" => "J",
417        "float" => "F",
418        "double" => "D",
419        _ => "",
420    };
421    if !primitive.is_empty() {
422        return Some(TypeMatch::Descriptor(format!("{arrays}{primitive}")));
423    }
424    if base.contains(['.', '/']) {
425        return base.split(['.', '/']).all(is_identifier).then(|| TypeMatch::Descriptor(format!("{arrays}L{};", base.replace('.', "/"))));
426    }
427    is_identifier(base).then(|| TypeMatch::Simple { name: base.to_string(), dims })
428}
429
430/// The query of exactly the method `signature` names, `Lcls;->name(params)ret`
431/// as findrefs and callgraph print it (whatever characters the name holds).
432pub(crate) fn exact_method(signature: &str) -> Result<MemberQuery> {
433    let bad = || EngineError::Query(format!("not a method signature: {signature:?} (want Lcls;->name(params)ret)"));
434    let (class, member) = signature.trim().split_once("->").ok_or_else(bad)?;
435    let p = member.find('(').ok_or_else(bad)?;
436    Ok(MemberQuery::default()
437        .with_class(Some(ClassQuery::exact(class.to_string())))
438        .with_name(Some(member[..p].to_string()))
439        .with_exact_name(true)
440        .with_signature(Some(parse_signature(&member[p..], true)?)))
441}
442
443/// Parse a one-word findrefs condition, as the CLI's compound and rule
444/// options take them:
445///
446/// - `string:REGEX`, `literal:TEXT`, `type:PATTERN`
447/// - `method:TARGET`, `field:TARGET`: any member target form (as in
448///   [`member_query`]), e.g.
449///   `android.webkit.WebView->loadUrl`; the class is exact unless prefixed
450///   `~` (fuzzy) or written without package, the name a regex / substring
451///   unless prefixed `=` or followed by a signature (exact)
452/// - `method:{RULE}`: calls of the methods whose code matches RULE
453/// - `method:@FILE`: calls of the methods FILE lists, one per line
454/// - `extends:CLASS`, `impl:CLASS`
455///
456/// The kind is read by [`parse_kind`] (`methods:`, `Method:` work too); the
457/// grammar is [`crate::condition`]'s.
458///
459/// # Errors
460///
461/// [`EngineError::Query`] for a syntax error, an unknown kind, an invalid
462/// value, or a rule of several conditions.
463pub fn parse_condition(spec: &str) -> Result<Query> {
464    crate::condition::parse_condition_in(spec, None)
465}
466
467/// One rule of a rule set: a [`parse_condition`] condition, or several
468/// joined with `&` (a compound: every part must match in one method, none
469/// of the `!` parts).
470///
471/// # Errors
472///
473/// [`EngineError::Query`] for a syntax error, an unknown kind or an invalid
474/// value.
475pub fn parse_rule(spec: &str) -> Result<Query> {
476    crate::condition::parse_rule_in(spec, None)
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn class_names() {
485        assert_eq!(format_class_name("com.poc.Main").unwrap(), "Lcom/poc/Main;");
486        assert_eq!(format_class_name("Lcom/poc/Main;").unwrap(), "Lcom/poc/Main;");
487        assert_eq!(format_class_name("com/poc/Main").unwrap(), "Lcom/poc/Main;");
488        assert_eq!(format_class_name("[Ljava/lang/Object;").unwrap(), "[Ljava/lang/Object;");
489        assert_eq!(normalize_class_query("com.poc", true).unwrap(), "com/poc");
490        assert_eq!(normalize_class_query("poc", true).unwrap(), "poc");
491        assert!(format_class_name("").is_err());
492        for p in ["com.xxx", "com/xxx", "Lcom/xxx", " com.xxx\t"] {
493            assert_eq!(class_prefix(p).unwrap(), "Lcom/xxx");
494        }
495        assert_eq!(class_prefix("a").unwrap(), "La");
496        assert_eq!(class_prefix(" ").unwrap_err().to_string(), "Class prefix cannot be empty");
497        assert!(member_query("method", None, false, None, false).is_err());
498        let q = member_query("method", Some("com.poc.Main"), false, Some("on"), false).unwrap();
499        assert!(!q.exact_name);
500        assert_eq!(q.class.unwrap(), ClassQuery::exact("Lcom/poc/Main;"));
501        assert!(member_query("field", None, false, Some("f"), true).unwrap().exact_name);
502    }
503
504    fn precise(c: &str) -> Option<ClassQuery> {
505        Some(ClassQuery::exact(c))
506    }
507
508    #[test]
509    fn member_targets_in_every_spelling() {
510        let t = |s| MemberTarget::parse(s);
511        assert_eq!(t("Lcom/a/B;->run(I)V"), MemberTarget { class: Some("Lcom/a/B;"), name: "run", signature: Some("(I)V") });
512        assert_eq!(t("Lc;->f:I"), MemberTarget { class: Some("Lc;"), name: "f", signature: Some(":I") });
513        assert_eq!(t("com.a.B->run"), MemberTarget { class: Some("com.a.B"), name: "run", signature: None });
514        assert_eq!(t("com.a.B#run"), MemberTarget { class: Some("com.a.B"), name: "run", signature: None });
515        assert_eq!(
516            t("android.webkit.WebView.loadUrl"),
517            MemberTarget { class: Some("android.webkit.WebView"), name: "loadUrl", signature: None }
518        );
519        assert_eq!(t("com/a/B.run(I)V"), MemberTarget { class: Some("com/a/B"), name: "run", signature: Some("(I)V") });
520        assert_eq!(t("java.lang.Thread.<init>"), MemberTarget { class: Some("java.lang.Thread"), name: "<init>", signature: None });
521        assert_eq!(t("Build$VERSION.SDK_INT").class, Some("Build$VERSION"));
522        // Patterns and lowercase dotted names stay names.
523        for name in ["get.*Id", "(get|set)Foo", "a.b", "exec"] {
524            assert_eq!(t(name), MemberTarget { class: None, name, signature: None }, "{name}");
525        }
526        assert!(looks_like_class("android.webkit.WebView") && looks_like_class("Lcom/a/B;"));
527        assert!(!looks_like_class("loadUrl") && !looks_like_class("com.a.b") && !looks_like_class("Lc;->m"));
528    }
529
530    #[test]
531    fn member_queries_from_any_spelling_agree() {
532        let want = MemberQuery::default().with_class(precise("Landroid/webkit/WebView;")).with_name(Some("loadUrl".into()));
533        for spec in [
534            "Landroid/webkit/WebView;->loadUrl",
535            "android.webkit.WebView.loadUrl",
536            "android.webkit.WebView->loadUrl",
537            "android.webkit.WebView#loadUrl",
538        ] {
539            assert_eq!(member_query("method", None, false, Some(spec), false).unwrap(), want, "{spec}");
540        }
541        // The class may also carry the member, or come on its own.
542        assert_eq!(member_query("method", Some("Landroid/webkit/WebView;->loadUrl"), false, None, false).unwrap(), want);
543        assert_eq!(member_query("method", Some("android.webkit.WebView"), false, Some("loadUrl"), false).unwrap(), want);
544        let class_only = member_query("method", None, false, Some("android.webkit.WebView"), false).unwrap();
545        assert_eq!(class_only, MemberQuery::default().with_class(precise("Landroid/webkit/WebView;")));
546        // A signature or `=` makes the name exact; an explicit class wins.
547        let q = member_query("method", None, false, Some("Lc;->run(I)V"), false).unwrap();
548        assert_eq!((q.name.as_deref(), q.exact_name), (Some("run"), true));
549        let q = member_query("field", Some("Lx;"), false, Some("Lc;->f:I"), false).unwrap();
550        assert_eq!((q.class, q.name.as_deref(), q.exact_name), (precise("Lx;"), Some("f"), true));
551        let q = member_query("method", None, false, Some("~Foo->=bar"), false).unwrap();
552        assert_eq!(
553            q,
554            MemberQuery::default().with_class(Some(ClassQuery::fuzzy("Foo"))).with_name(Some("bar".into())).with_exact_name(true)
555        );
556        // Without a package: that simple name in any package.
557        let q = member_query("method", None, false, Some("WebView.loadUrl"), false).unwrap();
558        assert_eq!(q.class, Some(ClassQuery::fuzzy("[/L]WebView;\\x00")));
559        let e = member_query("method", None, false, Some(" "), false).unwrap_err().to_string();
560        assert!(e.contains("needs a class or a method name"), "{e}");
561    }
562
563    #[test]
564    fn kinds_tolerate_case_plurals_and_synonyms() {
565        for (k, want) in [
566            ("Methods", "method"),
567            ("strings", "string"),
568            ("TYPE", "type"),
569            ("implements", "impl"),
570            ("classes", "type"),
571            ("fields", "field"),
572            ("literal", "literal"),
573        ] {
574            assert_eq!(parse_kind(k).unwrap(), want, "{k}");
575        }
576        let e = parse_kind("metod").unwrap_err().to_string();
577        assert!(e.contains("unknown kind \"metod\"") && e.contains("closest: method"), "{e}");
578        assert_eq!(
579            parse_condition("Methods:android.webkit.WebView.loadUrl").unwrap(),
580            parse_condition("method:Landroid/webkit/WebView;->loadUrl").unwrap()
581        );
582        assert!(matches!(parse_rule("string:a & method:b").unwrap(), Query::All { all, .. } if all.len() == 2));
583    }
584
585    #[test]
586    fn signatures_in_dalvik_and_java_form() {
587        let d = |s: &str| TypeMatch::Descriptor(s.into());
588        let method = |params: Vec<TypeMatch>, ret: Option<TypeMatch>| MemberSignature::Method { params, ret };
589        let sig = |s| parse_signature(s, true).unwrap();
590        assert_eq!(sig("(I)V"), method(vec![d("I")], Some(d("V"))));
591        assert_eq!(sig("(I)"), method(vec![d("I")], None));
592        assert_eq!(sig("()"), method(vec![], None));
593        assert_eq!(sig("(ILjava/lang/String;[B)Z"), method(vec![d("I"), d("Ljava/lang/String;"), d("[B")], Some(d("Z"))));
594        assert_eq!(sig("(int)"), method(vec![d("I")], None));
595        assert_eq!(sig("(int, java.lang.String[]) void"), method(vec![d("I"), d("[Ljava/lang/String;")], Some(d("V"))));
596        assert_eq!(
597            sig("(String, Map[][])"),
598            method(vec![TypeMatch::Simple { name: "String".into(), dims: 0 }, TypeMatch::Simple { name: "Map".into(), dims: 2 }], None)
599        );
600        assert_eq!(parse_signature(":I", false).unwrap(), MemberSignature::Field(d("I")));
601        assert_eq!(parse_signature(":java.lang.String", false).unwrap(), MemberSignature::Field(d("Ljava/lang/String;")));
602        for (bad, method) in [("(I", true), ("(%)V", true), (":I", true), ("(I)V", false), (":", false)] {
603            let e = parse_signature(bad, method).unwrap_err().to_string();
604            assert!(e.contains("bad signature"), "{bad}: {e}");
605        }
606        // A signature after the name narrows the query to it.
607        for spec in ["Lc;->run(I)V", "c.run(int)void", "Lc;->run(I)V "] {
608            let q = member_query("method", None, false, Some(spec), false).unwrap();
609            assert_eq!(q.signature, Some(method(vec![d("I")], Some(d("V")))), "{spec}");
610        }
611        assert_eq!(
612            member_query("method", None, false, Some("WebView.loadUrl(String)"), false).unwrap().signature.unwrap().to_string(),
613            "(String)"
614        );
615        assert_eq!(member_query("method", None, false, Some("loadUrl"), false).unwrap().signature, None);
616        assert!(member_query("field", None, false, Some("Lc;->f(I)V"), false).is_err());
617        assert_eq!(
618            parse_condition("method:Lc;->m(I)").unwrap(),
619            Query::Method(member_query("method", None, false, Some("Lc;->m(I)"), false).unwrap())
620        );
621    }
622
623    #[test]
624    fn java_signatures_and_edit_distance() {
625        assert_eq!(java_signature_to_dalvik("loadUrl(java.lang.String, int)").as_deref(), Some("loadUrl(Ljava/lang/String;I)"));
626        assert_eq!(java_signature_to_dalvik("f(byte[], Lx;) void").as_deref(), Some("f([BLx;)V"));
627        assert_eq!(java_signature_to_dalvik("run()").as_deref(), Some("run()"));
628        assert_eq!(java_signature_to_dalvik("run"), None);
629        assert_eq!(java_signature_to_dalvik("f(int [] [])").as_deref(), Some("f([[I)"));
630        // Array dimensions are counted, not recursed into.
631        let deep = format!("f(int{})", "[]".repeat(1_000_000));
632        assert_eq!(java_signature_to_dalvik(&deep).map(|s| s.len()), Some("f()I".len() + 1_000_000));
633        assert_eq!(edit_distance("kitten", "sitting", 5), Some(3));
634        assert_eq!(edit_distance("kitten", "sitting", 2), None);
635        assert_eq!(edit_distance("a", "a", 0), Some(0));
636    }
637}