Skip to main content

asc_engine/
condition.rs

1//! The conditions of findrefs rules and callgraph endpoints on DEX code.
2//!
3//! The syntax is the one both engines read ([`asc_query::rule`]); here are
4//! the kinds and what their values mean:
5//!
6//! ```text
7//! method: ['=' | '*'] '{' rule '}' calls    calls of the methods whose code matches the rule
8//!         ['=' | '*'] '{@' PATH '}' calls   calls of the methods listed in a file
9//!         ['=' | '*'] '@' PATH calls        the same
10//!         TARGET calls                      a method target, e.g. Lc;->m(I)V
11//! in:     '{' CONDITION '}'                 the methods of a class the condition matches
12//! other:  TEXT                              the kind's own syntax, e.g. a regex
13//! calls := ( '[' constraint ( ',' constraint )* ']' )*
14//! constraint := ( 'arg' N | 'this' ) ( '=' | '==' ) arg
15//!             | 'ret' ( '=' | '==' ) ( 'used' | 'unused' )
16//! arg   := '*' | INT | 'true' | 'false' | 'null' | '"' TEXT '"' | '~' REGEX
17//!        | 'param' [ K ] | 'class:' TYPE | 'new:' TYPE | 'ret:' METHOD | 'field:' FIELD
18//! ```
19//!
20//! A rule of one positive part is that condition; several parts make a
21//! compound ([`Query::All`]) whose `!` parts are its `none` list.
22//!
23//! A `{rule}` or `@PATH` set also takes the calls that may dispatch to its
24//! methods ([`Query::Virtual`]): those naming a method on a supertype of its
25//! class, or on a subclass inheriting it. A supertype method more than
26//! [`DEFAULT_FAN_OUT`](crate::compose::DEFAULT_FAN_OUT) classes define is
27//! not followed (the search notes it); `*` in front follows every one, `=`
28//! keeps only the calls naming the set's methods themselves.
29//!
30//! Constraints keep the calls whose values they accept ([`Query::Call`]):
31//! `argN` is the Nth declared parameter of the called method (from 0),
32//! `this` its receiver, `paramK` the calling method's own `pK` (smali
33//! numbering), `ret:` / `field:` the result of a call / a field read the
34//! target matches. Values are what the call site may hold: `=` keeps a site
35//! the analysis cannot decide (marked `?`), `==` drops it. `*` only shows
36//! the values. A REGEX runs to the next `,` / `]` outside `()` / `{}`, or is
37//! quoted like TEXT (`\"`, `\\` escape).
38//!
39//! Sets (`method:{...}`, `in:{...}`) nest at most [`MAX_DEPTH`] deep.
40
41use crate::names::{exact_method, format_class_name, member_query, parse_kind};
42use crate::{EngineError, Result};
43use asc_findrefs::{CallConstraint, Certainty, Defined, MemberQuery, Query, ResultWant, Slot, Want};
44use asc_query::rule::{Grammar, Mode, Part, Rule, Set, SetFile, Shape, SyntaxError, Text, Value};
45use std::path::Path;
46
47pub use asc_query::rule::MAX_DEPTH;
48
49/// The kinds of DEX code.
50struct Dex;
51
52impl Grammar for Dex {
53    type Kind = &'static str;
54    const KINDS: &'static str = "string, literal, type, method, field, extends, impl";
55
56    fn kind(word: &str) -> std::result::Result<&'static str, String> {
57        parse_kind(word).map_err(|e| e.to_string())
58    }
59
60    fn shape(kind: &'static str) -> Shape {
61        match kind {
62            "method" => Shape::Set { bare_file: true },
63            "in" => Shape::Set { bare_file: false },
64            _ => Shape::Text,
65        }
66    }
67}
68
69/// Parse a rule (see the module docs). `@PATH` files are read relative to
70/// `dir` (the current directory when `None`).
71///
72/// # Errors
73///
74/// [`EngineError::Query`] for a syntax error, an invalid value, or a
75/// `@PATH` file that cannot be read or parsed.
76pub fn parse_rule_in(spec: &str, dir: Option<&Path>) -> Result<Query> {
77    let rule = Rule::parse::<Dex>(spec).map_err(syntax)?;
78    Reader { src: spec, dir }.rule(&rule)
79}
80
81/// Parse one condition (a rule of a single part without `!`).
82///
83/// # Errors
84///
85/// As [`parse_rule_in`], plus [`EngineError::Query`] when `spec` holds several
86/// conditions.
87pub fn parse_condition_in(spec: &str, dir: Option<&Path>) -> Result<Query> {
88    match parse_rule_in(spec, dir)? {
89        Query::All { .. } => Err(EngineError::Query(format!("{spec:?} is several conditions; give one KIND:VALUE here"))),
90        q => Ok(q),
91    }
92}
93
94/// The methods of an `@file`: one per line, blank lines and `#` comments
95/// skipped. A line may be any method target spelling, or a line findrefs or
96/// callgraph printed: its first `Lcls;->name...` token is the method.
97pub(crate) fn method_set(text: &str) -> Result<Vec<MemberQuery>> {
98    let mut targets: Vec<&str> = text
99        .lines()
100        .map(str::trim)
101        .filter(|l| !l.is_empty() && !l.starts_with('#'))
102        .map(|l| l.split('|').flat_map(str::split_whitespace).find(|t| t.len() > 2 && t.contains("->")).unwrap_or(l))
103        .collect();
104    targets.sort_unstable();
105    targets.dedup();
106    targets
107        .into_iter()
108        .map(|t| if t.contains(";->") && t.contains('(') { exact_method(t) } else { member_query("method", None, false, Some(t), false) })
109        .collect()
110}
111
112fn syntax(error: SyntaxError) -> EngineError {
113    EngineError::Query(error.to_string())
114}
115
116/// What the parts of a parsed rule mean on DEX code.
117struct Reader<'a> {
118    src: &'a str,
119    dir: Option<&'a Path>,
120}
121
122impl Reader<'_> {
123    fn error(&self, at: usize, message: &str) -> EngineError {
124        syntax(SyntaxError::new(self.src, at, message))
125    }
126
127    fn rule(&self, rule: &Rule<'_, &'static str>) -> Result<Query> {
128        let mut all = Vec::new();
129        let mut none = Vec::new();
130        for part in &rule.parts {
131            let query = self.condition(part)?;
132            if part.negated { none.push(query) } else { all.push(query) }
133        }
134        match (all.len(), none.is_empty()) {
135            (0, _) => Err(self.error(rule.parts[0].at, "a rule needs a condition without `!` (`!` only removes methods)")),
136            (1, true) => Ok(all.pop().expect("one condition")),
137            _ => Ok(Query::All { all, none }),
138        }
139    }
140
141    fn condition(&self, part: &Part<'_, &'static str>) -> Result<Query> {
142        let text = match (part.kind, &part.value) {
143            ("method", value) => return self.method(value),
144            // `in:{CLASS-CONDITION}`: the methods of a matching class.
145            ("in", Value::Set { mode: Mode::Plain, set: Set::Rule(inner), tail }) if tail.text.is_empty() => {
146                return match inner.parts.as_slice() {
147                    [one] if !one.negated => Ok(Query::Defined(Defined::In(Box::new(self.condition(one)?)))),
148                    _ => Err(self.error(part.at, "in: takes one class condition, e.g. in:{extends:android.app.Activity}")),
149                };
150            }
151            (_, Value::Text(text)) if part.kind != "in" => text.text,
152            _ => return Err(self.error(part.at, "in: takes a class condition in braces, e.g. in:{extends:android.app.Activity}")),
153        };
154        Ok(match part.kind {
155            "string" => Query::String(text.to_string()),
156            "literal" => Query::StringLiteral(text.to_string()),
157            "type" => Query::Type(text.to_string()),
158            "extends" => Query::Extends(format_class_name(text)?),
159            "impl" => Query::Implements(format_class_name(text)?),
160            "override" => Query::Defined(Defined::Override(member_query("method", None, false, Some(text), false)?)),
161            "annotation" => Query::Defined(Defined::Annotation(crate::defs::annotation_descriptor(text)?)),
162            "flag" => Query::Defined(Defined::Flags(crate::defs::parse_flags(text)?)),
163            _ => Query::Field(member_query("field", None, false, Some(text), false)?),
164        })
165    }
166
167    /// The value of a `method:` condition: a set or a target, then its
168    /// constraints.
169    fn method(&self, value: &Value<'_, &'static str>) -> Result<Query> {
170        let (target, calls) = match value {
171            Value::Text(text) => {
172                let (target, calls) = split_constraints(*text);
173                if target.text.is_empty() {
174                    return Err(self.error(target.at, "empty value"));
175                }
176                (Query::Method(member_query("method", None, false, Some(target.text), false)?), calls)
177            }
178            Value::Set { mode, set, tail } => {
179                let (set, calls) = match set {
180                    Set::Rule(rule) => (Query::Nested(Box::new(self.rule(rule)?)), *tail),
181                    // A bare `@PATH` runs to the end of the part, its constraints included.
182                    Set::File(file) if tail.text.is_empty() => {
183                        let (path, calls) = split_constraints(file.path);
184                        (self.method_file(SetFile { path })?, calls)
185                    }
186                    Set::File(file) => (self.method_file(*file)?, *tail),
187                };
188                let dispatched = match mode {
189                    // `={...}` / `=@PATH`: the set's own methods, without dispatch.
190                    Mode::Exact => set,
191                    // `*{...}` / `*@PATH`: dispatch through every supertype.
192                    Mode::Every => Query::Virtual { target: Box::new(set), via: Default::default(), fan_out: None },
193                    Mode::Plain => {
194                        Query::Virtual { target: Box::new(set), via: Default::default(), fan_out: Some(crate::compose::DEFAULT_FAN_OUT) }
195                    }
196                };
197                (dispatched, calls)
198            }
199        };
200        let constraints = Constraints { src: self.src, pos: calls.at, end: calls.at + calls.text.len() }.all()?;
201        Ok(if constraints.is_empty() { target } else { Query::Call { target: Box::new(target), constraints } })
202    }
203
204    /// The method set of `method:@PATH`, read now so the query (and its
205    /// cache key) holds the file's content.
206    fn method_file(&self, file: SetFile<'_>) -> Result<Query> {
207        let path = file.path.text;
208        let text = file.read(self.dir).map_err(|e| EngineError::Query(format!("cannot read method set {path}: {e}")))?;
209        Ok(Query::MethodSet(method_set(&text).map_err(|e| EngineError::Query(format!("{path}: {e}")))?))
210    }
211}
212
213/// `text` split before its first call constraint: the target and the
214/// constraints, each without the blanks around it.
215fn split_constraints(text: Text<'_>) -> (Text<'_>, Text<'_>) {
216    let at = constraint_start(text.text).unwrap_or(text.text.len());
217    let calls = &text.text[at..];
218    (Text { text: text.text[..at].trim_end(), at: text.at }, Text { text: calls, at: text.at + at })
219}
220
221/// The call constraints after a method target or set: `[...][...]`, the
222/// cursor and end offsets in the rule `src`.
223struct Constraints<'a> {
224    src: &'a str,
225    pos: usize,
226    end: usize,
227}
228
229impl<'a> Constraints<'a> {
230    fn rest(&self) -> &'a str {
231        &self.src[self.pos..self.end]
232    }
233
234    fn skip_ws(&mut self) {
235        self.pos = self.end - self.rest().trim_start().len();
236    }
237
238    /// The next non-blank character, not consumed.
239    fn peek(&mut self) -> Option<char> {
240        self.skip_ws();
241        self.rest().chars().next()
242    }
243
244    fn eat(&mut self, c: char) -> bool {
245        let found = self.peek() == Some(c);
246        if found {
247            self.pos += c.len_utf8();
248        }
249        found
250    }
251
252    fn error(&self, what: &str) -> EngineError {
253        syntax(SyntaxError::new(self.src, self.pos, what))
254    }
255
256    /// Every constraint, up to the end.
257    fn all(mut self) -> Result<Vec<CallConstraint>> {
258        let mut constraints = Vec::new();
259        while self.eat('[') {
260            loop {
261                constraints.push(self.constraint()?);
262                if self.eat(']') {
263                    break;
264                }
265                if !self.eat(',') {
266                    return Err(self.error("expected `,` or `]` after a constraint"));
267                }
268            }
269        }
270        if self.peek().is_some() {
271            return Err(self.error("unexpected text after the method"));
272        }
273        Ok(constraints)
274    }
275
276    /// One constraint inside `[...]`.
277    fn constraint(&mut self) -> Result<CallConstraint> {
278        self.skip_ws();
279        let key_len = self.rest().find(|c: char| !c.is_ascii_alphanumeric()).unwrap_or(self.rest().len());
280        let key = &self.rest()[..key_len];
281        let slot = match key {
282            "ret" => None,
283            "this" => Some(Slot::This),
284            _ => match key.strip_prefix("arg").and_then(|n| n.parse().ok()) {
285                Some(n) => Some(Slot::Arg(n)),
286                None => return Err(self.error("expected argN, this or ret in a constraint")),
287            },
288        };
289        self.pos += key_len;
290        let certainty = if self.eat('=') {
291            if self.rest().starts_with('=') {
292                self.pos += 1;
293                Certainty::Must
294            } else {
295                Certainty::May
296            }
297        } else {
298            return Err(self.error("expected `=` or `==` in a constraint"));
299        };
300        self.skip_ws();
301        let Some(slot) = slot else {
302            let len = item_len(self.rest());
303            let want = match self.rest()[..len].trim() {
304                "used" => ResultWant::Used,
305                "unused" => ResultWant::Unused,
306                _ => return Err(self.error("ret takes used or unused")),
307            };
308            self.pos += len;
309            return Ok(CallConstraint::Result { want, certainty });
310        };
311        let want = self.want()?;
312        Ok(CallConstraint::Value { slot, want, certainty })
313    }
314
315    /// The value an argument has to hold.
316    fn want(&mut self) -> Result<Want> {
317        if self.rest().starts_with('"') {
318            return Ok(Want::Str(self.quoted()?));
319        }
320        let regex = self.rest().starts_with('~');
321        if regex {
322            self.pos += 1;
323            if self.rest().starts_with('"') {
324                return Ok(Want::StrRegex(self.quoted()?));
325            }
326        }
327        let len = item_len(self.rest());
328        let src: &'a str = self.src;
329        let raw = src[self.pos..self.pos + len].trim();
330        if raw.is_empty() {
331            return Err(self.error("empty value in a constraint"));
332        }
333        self.pos += len;
334        if regex {
335            return Ok(Want::StrRegex(raw.to_string()));
336        }
337        let bad = |what: &str| EngineError::Query(format!("bad rule {:?}: {what} in constraint value {raw:?}", self.src));
338        if let Some((kind, target)) = raw.split_once(':') {
339            let target = target.trim();
340            return Ok(match kind {
341                "class" => Want::Class(format_class_name(target)?),
342                "new" => Want::New(format_class_name(target)?),
343                "ret" => Want::Ret(member_query("method", None, false, Some(target), false)?),
344                "field" => Want::Field(member_query("field", None, false, Some(target), false)?),
345                _ => return Err(bad("expected class:, new:, ret: or field:")),
346            });
347        }
348        Ok(match raw {
349            "*" => Want::Any,
350            "true" => Want::Int(1),
351            "false" | "null" => Want::Int(0),
352            "param" => Want::Param(None),
353            _ => match raw.strip_prefix("param") {
354                Some(k) => Want::Param(Some(k.parse().map_err(|_| bad("expected paramK"))?)),
355                None => Want::Int(
356                    parse_int(raw).ok_or_else(|| bad("expected *, a number, \"text\", ~regex, param, class:, new:, ret: or field:"))?,
357                ),
358            },
359        })
360    }
361
362    /// A `"..."` string at the cursor, its `\"` / `\\` unescaped.
363    fn quoted(&mut self) -> Result<String> {
364        let mut out = String::new();
365        let mut chars = self.rest().char_indices().skip(1);
366        while let Some((i, c)) = chars.next() {
367            match c {
368                '"' => {
369                    self.pos += i + 1;
370                    return Ok(out);
371                }
372                '\\' => match chars.next() {
373                    Some((_, 'n')) => out.push('\n'),
374                    Some((_, 't')) => out.push('\t'),
375                    Some((_, c)) => out.push(c),
376                    None => break,
377                },
378                c => out.push(c),
379            }
380        }
381        Err(self.error("unterminated `\"`"))
382    }
383}
384
385/// Whether a method target carries call constraints (`X[arg0=1]`).
386pub fn has_constraints(target: &str) -> bool {
387    constraint_start(target).is_some()
388}
389
390/// The offset of the first `[argN`, `[this` or `[ret` in a method target.
391/// Dalvik names hold no `[`, and in a descriptor one is followed by a type
392/// (`[B`, `[Lx;`, `[[I`), never by these words.
393fn constraint_start(s: &str) -> Option<usize> {
394    s.match_indices('[').map(|(i, _)| i).find(|&i| {
395        let c = s[i + 1..].trim_start();
396        c.starts_with("ret") || c.starts_with("this") || c.strip_prefix("arg").is_some_and(|n| n.starts_with(|d: char| d.is_ascii_digit()))
397    })
398}
399
400/// Bytes of a constraint value at the start of `s`: up to a `,` or `]`
401/// outside `()` / `{}`; `\` keeps the next character in.
402fn item_len(s: &str) -> usize {
403    let mut depth = 0usize;
404    let mut chars = s.char_indices();
405    while let Some((i, c)) = chars.next() {
406        match c {
407            '\\' => {
408                chars.next();
409            }
410            '(' | '{' => depth += 1,
411            ')' | '}' if depth > 0 => depth -= 1,
412            ',' | ']' if depth == 0 => return i,
413            _ => {}
414        }
415    }
416    s.len()
417}
418
419/// A decimal or `0x` integer, maybe negative.
420fn parse_int(s: &str) -> Option<i64> {
421    let (neg, digits) = s.strip_prefix('-').map_or((false, s), |d| (true, d));
422    let n = match digits.strip_prefix("0x").or_else(|| digits.strip_prefix("0X")) {
423        // Hex is the bits of the value: 0xffffffff is -1 as an int.
424        Some(hex) => {
425            let n = u64::from_str_radix(hex, 16).ok()?;
426            match u32::try_from(n) {
427                Ok(int) => i64::from(i32::from_ne_bytes(int.to_ne_bytes())),
428                Err(_) => i64::from_ne_bytes(n.to_ne_bytes()),
429            }
430        }
431        None => digits.parse().ok()?,
432    };
433    Some(if neg { n.checked_neg()? } else { n })
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::names::{format_class_name, parse_condition, parse_rule};
440
441    fn method_query(spec: &str) -> MemberQuery {
442        member_query("method", None, false, Some(spec), false).unwrap()
443    }
444
445    fn method(spec: &str) -> Query {
446        Query::Method(method_query(spec))
447    }
448
449    /// A method set wrapped in its default (unresolved) dispatch query.
450    fn vq(target: Query) -> Query {
451        Query::Virtual { target: Box::new(target), via: Default::default(), fan_out: Some(crate::compose::DEFAULT_FAN_OUT) }
452    }
453
454    #[test]
455    fn parts_split_at_top_level_ampersands_only() {
456        let q = parse_rule(" string:a & !literal:b&method:Lc;->m(I)V ").unwrap();
457        assert_eq!(
458            q,
459            Query::All { all: vec![Query::String("a".into()), method("Lc;->m(I)V")], none: vec![Query::StringLiteral("b".into())] }
460        );
461        // Inside `{}` / `()`, after `\` or where no KIND: follows, `&` is part of the value.
462        assert_eq!(parse_rule("string:(a&b)").unwrap(), Query::String("(a&b)".into()));
463        assert_eq!(
464            parse_rule("string:a&b & literal:c&d").unwrap(),
465            Query::All { all: vec![Query::String("a&b".into()), Query::StringLiteral("c&d".into())], none: vec![] }
466        );
467        assert_eq!(parse_rule("string:a & ").unwrap(), Query::String("a &".into()));
468        assert_eq!(parse_rule(r"string:a{1}\&b").unwrap(), Query::String(r"a{1}\&b".into()));
469        assert_eq!(
470            parse_condition("extends:android.app.Activity").unwrap(),
471            Query::Extends(format_class_name("android.app.Activity").unwrap())
472        );
473        // Array descriptors are not brackets.
474        assert_eq!(
475            parse_rule("method:Lc;->m([B)V & type:X").unwrap(),
476            Query::All { all: vec![method("Lc;->m([B)V"), Query::Type("X".into())], none: vec![] }
477        );
478    }
479
480    #[test]
481    fn method_bodies_nest_whole_rules() {
482        let inner = Query::All { all: vec![Query::StringLiteral("imei".into())], none: vec![Query::Type("Lx;".into())] };
483        // A `{...}` body nests the rule and, by default, follows dispatch.
484        assert_eq!(parse_rule("method:{literal:imei & !type:Lx;}").unwrap(), vq(Query::Nested(Box::new(inner.clone()))));
485        let q = parse_rule("method:{ method:{literal:imei & !type:Lx;} } & string:http").unwrap();
486        assert_eq!(
487            q,
488            Query::All {
489                all: vec![vq(Query::Nested(Box::new(vq(Query::Nested(Box::new(inner)))))), Query::String("http".into())],
490                none: vec![]
491            }
492        );
493        // A regex brace inside the body stays in its value.
494        assert_eq!(parse_rule("method:{string:a{2}}").unwrap(), vq(Query::Nested(Box::new(Query::String("a{2}".into())))));
495    }
496
497    fn call(target: Query, constraints: Vec<CallConstraint>) -> Query {
498        Query::Call { target: Box::new(target), constraints }
499    }
500
501    fn arg(n: u16, want: Want, certainty: Certainty) -> CallConstraint {
502        CallConstraint::Value { slot: Slot::Arg(n), want, certainty }
503    }
504
505    #[test]
506    fn method_targets_take_call_constraints() {
507        use Certainty::{May, Must};
508        let target = || method("Lc;->m([BLjava/lang/String;)V");
509        assert_eq!(
510            parse_rule(r#"method:Lc;->m([BLjava/lang/String;)V [arg1="a,]\"b", this==new:x.Y][ret=unused]"#).unwrap(),
511            call(
512                target(),
513                vec![
514                    arg(1, Want::Str("a,]\"b".into()), May),
515                    CallConstraint::Value { slot: Slot::This, want: Want::New("Lx/Y;".into()), certainty: Must },
516                    CallConstraint::Result { want: ResultWant::Unused, certainty: May },
517                ]
518            )
519        );
520        for (value, want) in [
521            ("*", Want::Any),
522            ("-12", Want::Int(-12)),
523            ("0x10", Want::Int(16)),
524            ("0xffffffff", Want::Int(-1)),
525            ("true", Want::Int(1)),
526            ("null", Want::Int(0)),
527            ("param", Want::Param(None)),
528            ("param3", Want::Param(Some(3))),
529            ("~^ro\\.(serial|boot){1,2}", Want::StrRegex("^ro\\.(serial|boot){1,2}".into())),
530            ("~\"a]b\"", Want::StrRegex("a]b".into())),
531            ("class:java.lang.String", Want::Class("Ljava/lang/String;".into())),
532            ("ret:android.content.Context->getSystemService", Want::Ret(method_query("android.content.Context->getSystemService"))),
533            ("field:Lc;->f:I", Want::Field(member_query("field", None, false, Some("Lc;->f:I"), false).unwrap())),
534        ] {
535            assert_eq!(parse_rule(&format!("method:x[arg0={value}]")).unwrap(), call(method("x"), vec![arg(0, want, May)]), "{value}");
536        }
537        // `&` inside a constraint stays in it; after `]` it joins the next part.
538        assert_eq!(
539            parse_rule("method:x[arg0=~a&b] & string:s").unwrap(),
540            Query::All {
541                all: vec![call(method("x"), vec![arg(0, Want::StrRegex("a&b".into()), May)]), Query::String("s".into())],
542                none: vec![]
543            }
544        );
545        // Nested bodies and method sets take them too, under the dispatch wrapper.
546        let q = parse_rule("method:{literal:imei}[arg0=*]").unwrap();
547        assert_eq!(q, call(vq(Query::Nested(Box::new(Query::StringLiteral("imei".into())))), vec![arg(0, Want::Any, May)]));
548        let dir = tempfile::tempdir().unwrap();
549        std::fs::write(dir.path().join("set.txt"), "Lc;->m()V\n").unwrap();
550        let q = parse_rule_in("method:@set.txt[ret==used]", Some(dir.path())).unwrap();
551        assert!(matches!(&q, Query::Call { target, .. } if matches!(**target, Query::Virtual { .. })), "{q:?}");
552        // Array descriptors and regex classes are not constraints.
553        assert!(!has_constraints("Lc;->m([B[Ljava/lang/String;)V") && has_constraints("x[ret=used]"));
554        assert_eq!(parse_rule("string:[arg0]").unwrap(), Query::String("[arg0]".into()));
555    }
556
557    #[test]
558    fn definition_predicates_parse_into_defined_queries() {
559        use asc_dex::access;
560        assert_eq!(parse_rule("flag:public,static").unwrap(), Query::Defined(Defined::Flags(access::PUBLIC | access::STATIC)));
561        assert_eq!(parse_rule("annotation:a.B").unwrap(), Query::Defined(Defined::Annotation("La/B;".into())));
562        let Query::Defined(Defined::Override(m)) = parse_rule("override:a.B->run").unwrap() else { panic!("an override") };
563        assert_eq!((m.name.as_deref(), m.class.unwrap().pattern.as_str()), (Some("run"), "La/B;"));
564        assert_eq!(parse_rule("in:{extends:a.Base}").unwrap(), Query::Defined(Defined::In(Box::new(format_extends("a.Base")))));
565        // A definition predicate joins a code condition in a compound.
566        let Query::All { all, none } = parse_rule("string:x & flag:native & !annotation:a.B").unwrap() else { panic!("a compound") };
567        assert_eq!(all.len(), 2);
568        assert_eq!(none, vec![Query::Defined(Defined::Annotation("La/B;".into()))]);
569        // Bad spellings are reported at parse time; a non-class `in:{...}` is
570        // caught later, when the engine resolves it over the hierarchy.
571        for (spec, want) in [("flag:bogus", "use public"), ("in:extends:a.B", "in: takes a class condition")] {
572            assert!(parse_rule(spec).unwrap_err().to_string().contains(want), "{spec}");
573        }
574        assert!(matches!(parse_rule("in:{string:x}").unwrap(), Query::Defined(Defined::In(_))));
575    }
576
577    fn format_extends(c: &str) -> Query {
578        Query::Extends(format_class_name(c).unwrap())
579    }
580
581    #[test]
582    fn method_sets_follow_dispatch_unless_prefixed_with_eq() {
583        // `{...}` / `@` wrap the set in a dispatch query with the default
584        // fan-out; `*` lifts it; `=` keeps the set exact.
585        use crate::compose::DEFAULT_FAN_OUT;
586        let Query::Virtual { target, via, fan_out } = parse_rule("method:{literal:x}").unwrap() else { panic!("a dispatch set") };
587        assert!(matches!(*target, Query::Nested(_)) && via.is_empty() && fan_out == Some(DEFAULT_FAN_OUT));
588        let Query::Virtual { fan_out, .. } = parse_rule("method:*{literal:x}").unwrap() else { panic!("a dispatch set") };
589        assert_eq!(fan_out, None);
590        assert!(matches!(parse_rule("method:={literal:x}").unwrap(), Query::Nested(_)));
591        // A constraint applies to the dispatched calls.
592        let Query::Call { target, .. } = parse_rule("method:{literal:x}[arg0=1]").unwrap() else { panic!("constrained") };
593        assert!(matches!(*target, Query::Virtual { .. }));
594        let dir = tempfile::tempdir().unwrap();
595        std::fs::write(dir.path().join("s.txt"), "La/B;->m()V\n").unwrap();
596        assert!(matches!(parse_rule_in("method:@s.txt", Some(dir.path())).unwrap(), Query::Virtual { .. }));
597        assert!(matches!(parse_rule_in("method:=@s.txt", Some(dir.path())).unwrap(), Query::MethodSet(_)));
598        assert!(matches!(parse_rule_in("method:*@s.txt", Some(dir.path())).unwrap(), Query::Virtual { fan_out: None, .. }));
599        // The file may also go in the braces, as the HarmonyOS side writes it.
600        assert_eq!(parse_rule_in("method:{@s.txt}", Some(dir.path())).unwrap(), parse_rule_in("method:@s.txt", Some(dir.path())).unwrap());
601        assert_eq!(
602            parse_rule_in("method:={ @s.txt }[arg0=1]", Some(dir.path())).unwrap(),
603            parse_rule_in("method:=@s.txt[arg0=1]", Some(dir.path())).unwrap()
604        );
605    }
606
607    #[test]
608    fn errors_name_the_problem() {
609        for (spec, want) in [
610            ("bogus", "expected KIND:VALUE"),
611            ("method:", "empty value"),
612            ("metod:x", "closest: method"),
613            ("method:{string:a", "expected `}`"),
614            ("method:{string:a} x", "unexpected text after the method"),
615            ("method:x[arg0=1] y", "unexpected text after the method"),
616            ("method:x[arg0=1", "expected `,` or `]`"),
617            ("method:x[arg0=1, foo=2]", "expected argN, this or ret"),
618            ("method:x[arg0 1]", "expected `=` or `==`"),
619            ("method:x[arg0=]", "empty value"),
620            ("method:x[arg0=\"a]", "unterminated"),
621            ("method:x[arg0=bogus:y]", "expected class:, new:, ret: or field:"),
622            ("method:x[arg0=paramx]", "expected paramK"),
623            ("method:x[arg0=1.5]", "expected *, a number"),
624            ("method:x[ret=maybe]", "ret takes used or unused"),
625            ("!string:a", "needs a condition without `!`"),
626            ("string:a & bogus:b", "unknown kind \"bogus\""),
627            ("in:{extends:a.B & !extends:c.D}", "in: takes one class condition"),
628            ("method:@", "empty value"),
629            ("method:@/nonexistent/set.txt", "cannot read method set"),
630        ] {
631            let e = parse_rule(spec).unwrap_err().to_string();
632            assert!(e.contains(want), "{spec}: {e}");
633        }
634        assert!(parse_condition("string:a & string:b").unwrap_err().to_string().contains("several conditions"));
635    }
636
637    #[test]
638    fn bodies_nest_up_to_the_limit() {
639        let nest = |open: &str, n: usize, leaf: &str| format!("{}{leaf}{}", open.repeat(n), "}".repeat(n));
640        for open in ["method:{", "method:={", "method:*{", "in:{"] {
641            assert!(parse_rule(&nest(open, MAX_DEPTH, "string:a")).is_ok(), "{open} x{MAX_DEPTH}");
642            // Far past the limit is an error, not a stack overflow.
643            for n in [MAX_DEPTH + 1, 100_000] {
644                let e = parse_rule(&nest(open, n, "string:a")).unwrap_err().to_string();
645                assert!(e.contains(&format!("nested deeper than {MAX_DEPTH} levels")), "{open} x{n}: {}", &e[e.len().saturating_sub(80)..]);
646            }
647        }
648        // Mixed kinds share one budget; siblings do not add up.
649        let mixed = format!("{}string:a{}", "method:{in:{".repeat(MAX_DEPTH / 2 + 1), "}}".repeat(MAX_DEPTH / 2 + 1));
650        assert!(parse_rule(&mixed).is_err());
651        let wide = vec![nest("method:{", MAX_DEPTH, "string:a"); 50].join(" & ");
652        assert!(parse_rule(&wide).is_ok());
653    }
654
655    #[test]
656    fn method_files_take_any_listing() {
657        let dir = tempfile::tempdir().unwrap();
658        let text = "# from findrefs\n\
659            classes2.dex | Lb/Lib;->sink()V | \"x->y\"\n\
660            r1 | classes.dex | La/A;->run(I)V | Lb/Lib;->sink()V\n\
661            \n\
662            \x20 2 <- La/A;->run(I)V (via Lx;)\n\
663            com.a.B.go\n";
664        std::fs::write(dir.path().join("set.txt"), text).unwrap();
665        let Query::Virtual { target, .. } = parse_rule_in("method:@set.txt", Some(dir.path())).unwrap() else { panic!("a dispatch set") };
666        let Query::MethodSet(set) = *target else { panic!("a method set") };
667        let want = [exact_method("La/A;->run(I)V").unwrap(), exact_method("Lb/Lib;->sink()V").unwrap(), method_query("com.a.B.go")];
668        assert_eq!(set, want);
669        // Obfuscated names go by the arrow, not as identifiers.
670        let q = exact_method("LX/0TRC;->ʻ(I)Ljava/lang/String;").unwrap();
671        assert_eq!((q.name.as_deref(), q.exact_name, q.class.unwrap().pattern.as_str()), (Some("ʻ"), true, "LX/0TRC;"));
672    }
673}