1use 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
49struct 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
69pub 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
81pub 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
94pub(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
116struct 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", 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 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 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 Mode::Exact => set,
191 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 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
213fn 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
221struct 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 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 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 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 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 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
385pub fn has_constraints(target: &str) -> bool {
387 constraint_start(target).is_some()
388}
389
390fn 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
400fn 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
419fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}