use crate::names::{exact_method, format_class_name, member_query, parse_kind};
use crate::{EngineError, Result};
use asc_findrefs::{CallConstraint, Certainty, Defined, MemberQuery, Query, ResultWant, Slot, Want};
use asc_query::rule::{Grammar, Mode, Part, Rule, Set, SetFile, Shape, SyntaxError, Text, Value};
use std::path::Path;
pub use asc_query::rule::MAX_DEPTH;
struct Dex;
impl Grammar for Dex {
type Kind = &'static str;
const KINDS: &'static str = "string, literal, type, method, field, extends, impl";
fn kind(word: &str) -> std::result::Result<&'static str, String> {
parse_kind(word).map_err(|e| e.to_string())
}
fn shape(kind: &'static str) -> Shape {
match kind {
"method" => Shape::Set { bare_file: true },
"in" => Shape::Set { bare_file: false },
_ => Shape::Text,
}
}
}
pub fn parse_rule_in(spec: &str, dir: Option<&Path>) -> Result<Query> {
let rule = Rule::parse::<Dex>(spec).map_err(syntax)?;
Reader { src: spec, dir }.rule(&rule)
}
pub fn parse_condition_in(spec: &str, dir: Option<&Path>) -> Result<Query> {
match parse_rule_in(spec, dir)? {
Query::All { .. } => Err(EngineError::Query(format!("{spec:?} is several conditions; give one KIND:VALUE here"))),
q => Ok(q),
}
}
pub(crate) fn method_set(text: &str) -> Result<Vec<MemberQuery>> {
let mut targets: Vec<&str> = text
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(|l| l.split('|').flat_map(str::split_whitespace).find(|t| t.len() > 2 && t.contains("->")).unwrap_or(l))
.collect();
targets.sort_unstable();
targets.dedup();
targets
.into_iter()
.map(|t| if t.contains(";->") && t.contains('(') { exact_method(t) } else { member_query("method", None, false, Some(t), false) })
.collect()
}
fn syntax(error: SyntaxError) -> EngineError {
EngineError::Query(error.to_string())
}
struct Reader<'a> {
src: &'a str,
dir: Option<&'a Path>,
}
impl Reader<'_> {
fn error(&self, at: usize, message: &str) -> EngineError {
syntax(SyntaxError::new(self.src, at, message))
}
fn rule(&self, rule: &Rule<'_, &'static str>) -> Result<Query> {
let mut all = Vec::new();
let mut none = Vec::new();
for part in &rule.parts {
let query = self.condition(part)?;
if part.negated { none.push(query) } else { all.push(query) }
}
match (all.len(), none.is_empty()) {
(0, _) => Err(self.error(rule.parts[0].at, "a rule needs a condition without `!` (`!` only removes methods)")),
(1, true) => Ok(all.pop().expect("one condition")),
_ => Ok(Query::All { all, none }),
}
}
fn condition(&self, part: &Part<'_, &'static str>) -> Result<Query> {
let text = match (part.kind, &part.value) {
("method", value) => return self.method(value),
("in", Value::Set { mode: Mode::Plain, set: Set::Rule(inner), tail }) if tail.text.is_empty() => {
return match inner.parts.as_slice() {
[one] if !one.negated => Ok(Query::Defined(Defined::In(Box::new(self.condition(one)?)))),
_ => Err(self.error(part.at, "in: takes one class condition, e.g. in:{extends:android.app.Activity}")),
};
}
(_, Value::Text(text)) if part.kind != "in" => text.text,
_ => return Err(self.error(part.at, "in: takes a class condition in braces, e.g. in:{extends:android.app.Activity}")),
};
Ok(match part.kind {
"string" => Query::String(text.to_string()),
"literal" => Query::StringLiteral(text.to_string()),
"type" => Query::Type(text.to_string()),
"extends" => Query::Extends(format_class_name(text)?),
"impl" => Query::Implements(format_class_name(text)?),
"override" => Query::Defined(Defined::Override(member_query("method", None, false, Some(text), false)?)),
"annotation" => Query::Defined(Defined::Annotation(crate::defs::annotation_descriptor(text)?)),
"flag" => Query::Defined(Defined::Flags(crate::defs::parse_flags(text)?)),
_ => Query::Field(member_query("field", None, false, Some(text), false)?),
})
}
fn method(&self, value: &Value<'_, &'static str>) -> Result<Query> {
let (target, calls) = match value {
Value::Text(text) => {
let (target, calls) = split_constraints(*text);
if target.text.is_empty() {
return Err(self.error(target.at, "empty value"));
}
(Query::Method(member_query("method", None, false, Some(target.text), false)?), calls)
}
Value::Set { mode, set, tail } => {
let (set, calls) = match set {
Set::Rule(rule) => (Query::Nested(Box::new(self.rule(rule)?)), *tail),
Set::File(file) if tail.text.is_empty() => {
let (path, calls) = split_constraints(file.path);
(self.method_file(SetFile { path })?, calls)
}
Set::File(file) => (self.method_file(*file)?, *tail),
};
let dispatched = match mode {
Mode::Exact => set,
Mode::Every => Query::Virtual { target: Box::new(set), via: Default::default(), fan_out: None },
Mode::Plain => {
Query::Virtual { target: Box::new(set), via: Default::default(), fan_out: Some(crate::compose::DEFAULT_FAN_OUT) }
}
};
(dispatched, calls)
}
};
let constraints = Constraints { src: self.src, pos: calls.at, end: calls.at + calls.text.len() }.all()?;
Ok(if constraints.is_empty() { target } else { Query::Call { target: Box::new(target), constraints } })
}
fn method_file(&self, file: SetFile<'_>) -> Result<Query> {
let path = file.path.text;
let text = file.read(self.dir).map_err(|e| EngineError::Query(format!("cannot read method set {path}: {e}")))?;
Ok(Query::MethodSet(method_set(&text).map_err(|e| EngineError::Query(format!("{path}: {e}")))?))
}
}
fn split_constraints(text: Text<'_>) -> (Text<'_>, Text<'_>) {
let at = constraint_start(text.text).unwrap_or(text.text.len());
let calls = &text.text[at..];
(Text { text: text.text[..at].trim_end(), at: text.at }, Text { text: calls, at: text.at + at })
}
struct Constraints<'a> {
src: &'a str,
pos: usize,
end: usize,
}
impl<'a> Constraints<'a> {
fn rest(&self) -> &'a str {
&self.src[self.pos..self.end]
}
fn skip_ws(&mut self) {
self.pos = self.end - self.rest().trim_start().len();
}
fn peek(&mut self) -> Option<char> {
self.skip_ws();
self.rest().chars().next()
}
fn eat(&mut self, c: char) -> bool {
let found = self.peek() == Some(c);
if found {
self.pos += c.len_utf8();
}
found
}
fn error(&self, what: &str) -> EngineError {
syntax(SyntaxError::new(self.src, self.pos, what))
}
fn all(mut self) -> Result<Vec<CallConstraint>> {
let mut constraints = Vec::new();
while self.eat('[') {
loop {
constraints.push(self.constraint()?);
if self.eat(']') {
break;
}
if !self.eat(',') {
return Err(self.error("expected `,` or `]` after a constraint"));
}
}
}
if self.peek().is_some() {
return Err(self.error("unexpected text after the method"));
}
Ok(constraints)
}
fn constraint(&mut self) -> Result<CallConstraint> {
self.skip_ws();
let key_len = self.rest().find(|c: char| !c.is_ascii_alphanumeric()).unwrap_or(self.rest().len());
let key = &self.rest()[..key_len];
let slot = match key {
"ret" => None,
"this" => Some(Slot::This),
_ => match key.strip_prefix("arg").and_then(|n| n.parse().ok()) {
Some(n) => Some(Slot::Arg(n)),
None => return Err(self.error("expected argN, this or ret in a constraint")),
},
};
self.pos += key_len;
let certainty = if self.eat('=') {
if self.rest().starts_with('=') {
self.pos += 1;
Certainty::Must
} else {
Certainty::May
}
} else {
return Err(self.error("expected `=` or `==` in a constraint"));
};
self.skip_ws();
let Some(slot) = slot else {
let len = item_len(self.rest());
let want = match self.rest()[..len].trim() {
"used" => ResultWant::Used,
"unused" => ResultWant::Unused,
_ => return Err(self.error("ret takes used or unused")),
};
self.pos += len;
return Ok(CallConstraint::Result { want, certainty });
};
let want = self.want()?;
Ok(CallConstraint::Value { slot, want, certainty })
}
fn want(&mut self) -> Result<Want> {
if self.rest().starts_with('"') {
return Ok(Want::Str(self.quoted()?));
}
let regex = self.rest().starts_with('~');
if regex {
self.pos += 1;
if self.rest().starts_with('"') {
return Ok(Want::StrRegex(self.quoted()?));
}
}
let len = item_len(self.rest());
let src: &'a str = self.src;
let raw = src[self.pos..self.pos + len].trim();
if raw.is_empty() {
return Err(self.error("empty value in a constraint"));
}
self.pos += len;
if regex {
return Ok(Want::StrRegex(raw.to_string()));
}
let bad = |what: &str| EngineError::Query(format!("bad rule {:?}: {what} in constraint value {raw:?}", self.src));
if let Some((kind, target)) = raw.split_once(':') {
let target = target.trim();
return Ok(match kind {
"class" => Want::Class(format_class_name(target)?),
"new" => Want::New(format_class_name(target)?),
"ret" => Want::Ret(member_query("method", None, false, Some(target), false)?),
"field" => Want::Field(member_query("field", None, false, Some(target), false)?),
_ => return Err(bad("expected class:, new:, ret: or field:")),
});
}
Ok(match raw {
"*" => Want::Any,
"true" => Want::Int(1),
"false" | "null" => Want::Int(0),
"param" => Want::Param(None),
_ => match raw.strip_prefix("param") {
Some(k) => Want::Param(Some(k.parse().map_err(|_| bad("expected paramK"))?)),
None => Want::Int(
parse_int(raw).ok_or_else(|| bad("expected *, a number, \"text\", ~regex, param, class:, new:, ret: or field:"))?,
),
},
})
}
fn quoted(&mut self) -> Result<String> {
let mut out = String::new();
let mut chars = self.rest().char_indices().skip(1);
while let Some((i, c)) = chars.next() {
match c {
'"' => {
self.pos += i + 1;
return Ok(out);
}
'\\' => match chars.next() {
Some((_, 'n')) => out.push('\n'),
Some((_, 't')) => out.push('\t'),
Some((_, c)) => out.push(c),
None => break,
},
c => out.push(c),
}
}
Err(self.error("unterminated `\"`"))
}
}
pub fn has_constraints(target: &str) -> bool {
constraint_start(target).is_some()
}
fn constraint_start(s: &str) -> Option<usize> {
s.match_indices('[').map(|(i, _)| i).find(|&i| {
let c = s[i + 1..].trim_start();
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()))
})
}
fn item_len(s: &str) -> usize {
let mut depth = 0usize;
let mut chars = s.char_indices();
while let Some((i, c)) = chars.next() {
match c {
'\\' => {
chars.next();
}
'(' | '{' => depth += 1,
')' | '}' if depth > 0 => depth -= 1,
',' | ']' if depth == 0 => return i,
_ => {}
}
}
s.len()
}
fn parse_int(s: &str) -> Option<i64> {
let (neg, digits) = s.strip_prefix('-').map_or((false, s), |d| (true, d));
let n = match digits.strip_prefix("0x").or_else(|| digits.strip_prefix("0X")) {
Some(hex) => {
let n = u64::from_str_radix(hex, 16).ok()?;
match u32::try_from(n) {
Ok(int) => i64::from(i32::from_ne_bytes(int.to_ne_bytes())),
Err(_) => i64::from_ne_bytes(n.to_ne_bytes()),
}
}
None => digits.parse().ok()?,
};
Some(if neg { n.checked_neg()? } else { n })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::names::{format_class_name, parse_condition, parse_rule};
fn method_query(spec: &str) -> MemberQuery {
member_query("method", None, false, Some(spec), false).unwrap()
}
fn method(spec: &str) -> Query {
Query::Method(method_query(spec))
}
fn vq(target: Query) -> Query {
Query::Virtual { target: Box::new(target), via: Default::default(), fan_out: Some(crate::compose::DEFAULT_FAN_OUT) }
}
#[test]
fn parts_split_at_top_level_ampersands_only() {
let q = parse_rule(" string:a & !literal:b&method:Lc;->m(I)V ").unwrap();
assert_eq!(
q,
Query::All { all: vec![Query::String("a".into()), method("Lc;->m(I)V")], none: vec![Query::StringLiteral("b".into())] }
);
assert_eq!(parse_rule("string:(a&b)").unwrap(), Query::String("(a&b)".into()));
assert_eq!(
parse_rule("string:a&b & literal:c&d").unwrap(),
Query::All { all: vec![Query::String("a&b".into()), Query::StringLiteral("c&d".into())], none: vec![] }
);
assert_eq!(parse_rule("string:a & ").unwrap(), Query::String("a &".into()));
assert_eq!(parse_rule(r"string:a{1}\&b").unwrap(), Query::String(r"a{1}\&b".into()));
assert_eq!(
parse_condition("extends:android.app.Activity").unwrap(),
Query::Extends(format_class_name("android.app.Activity").unwrap())
);
assert_eq!(
parse_rule("method:Lc;->m([B)V & type:X").unwrap(),
Query::All { all: vec![method("Lc;->m([B)V"), Query::Type("X".into())], none: vec![] }
);
}
#[test]
fn method_bodies_nest_whole_rules() {
let inner = Query::All { all: vec![Query::StringLiteral("imei".into())], none: vec![Query::Type("Lx;".into())] };
assert_eq!(parse_rule("method:{literal:imei & !type:Lx;}").unwrap(), vq(Query::Nested(Box::new(inner.clone()))));
let q = parse_rule("method:{ method:{literal:imei & !type:Lx;} } & string:http").unwrap();
assert_eq!(
q,
Query::All {
all: vec![vq(Query::Nested(Box::new(vq(Query::Nested(Box::new(inner)))))), Query::String("http".into())],
none: vec![]
}
);
assert_eq!(parse_rule("method:{string:a{2}}").unwrap(), vq(Query::Nested(Box::new(Query::String("a{2}".into())))));
}
fn call(target: Query, constraints: Vec<CallConstraint>) -> Query {
Query::Call { target: Box::new(target), constraints }
}
fn arg(n: u16, want: Want, certainty: Certainty) -> CallConstraint {
CallConstraint::Value { slot: Slot::Arg(n), want, certainty }
}
#[test]
fn method_targets_take_call_constraints() {
use Certainty::{May, Must};
let target = || method("Lc;->m([BLjava/lang/String;)V");
assert_eq!(
parse_rule(r#"method:Lc;->m([BLjava/lang/String;)V [arg1="a,]\"b", this==new:x.Y][ret=unused]"#).unwrap(),
call(
target(),
vec![
arg(1, Want::Str("a,]\"b".into()), May),
CallConstraint::Value { slot: Slot::This, want: Want::New("Lx/Y;".into()), certainty: Must },
CallConstraint::Result { want: ResultWant::Unused, certainty: May },
]
)
);
for (value, want) in [
("*", Want::Any),
("-12", Want::Int(-12)),
("0x10", Want::Int(16)),
("0xffffffff", Want::Int(-1)),
("true", Want::Int(1)),
("null", Want::Int(0)),
("param", Want::Param(None)),
("param3", Want::Param(Some(3))),
("~^ro\\.(serial|boot){1,2}", Want::StrRegex("^ro\\.(serial|boot){1,2}".into())),
("~\"a]b\"", Want::StrRegex("a]b".into())),
("class:java.lang.String", Want::Class("Ljava/lang/String;".into())),
("ret:android.content.Context->getSystemService", Want::Ret(method_query("android.content.Context->getSystemService"))),
("field:Lc;->f:I", Want::Field(member_query("field", None, false, Some("Lc;->f:I"), false).unwrap())),
] {
assert_eq!(parse_rule(&format!("method:x[arg0={value}]")).unwrap(), call(method("x"), vec![arg(0, want, May)]), "{value}");
}
assert_eq!(
parse_rule("method:x[arg0=~a&b] & string:s").unwrap(),
Query::All {
all: vec![call(method("x"), vec![arg(0, Want::StrRegex("a&b".into()), May)]), Query::String("s".into())],
none: vec![]
}
);
let q = parse_rule("method:{literal:imei}[arg0=*]").unwrap();
assert_eq!(q, call(vq(Query::Nested(Box::new(Query::StringLiteral("imei".into())))), vec![arg(0, Want::Any, May)]));
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("set.txt"), "Lc;->m()V\n").unwrap();
let q = parse_rule_in("method:@set.txt[ret==used]", Some(dir.path())).unwrap();
assert!(matches!(&q, Query::Call { target, .. } if matches!(**target, Query::Virtual { .. })), "{q:?}");
assert!(!has_constraints("Lc;->m([B[Ljava/lang/String;)V") && has_constraints("x[ret=used]"));
assert_eq!(parse_rule("string:[arg0]").unwrap(), Query::String("[arg0]".into()));
}
#[test]
fn definition_predicates_parse_into_defined_queries() {
use asc_dex::access;
assert_eq!(parse_rule("flag:public,static").unwrap(), Query::Defined(Defined::Flags(access::PUBLIC | access::STATIC)));
assert_eq!(parse_rule("annotation:a.B").unwrap(), Query::Defined(Defined::Annotation("La/B;".into())));
let Query::Defined(Defined::Override(m)) = parse_rule("override:a.B->run").unwrap() else { panic!("an override") };
assert_eq!((m.name.as_deref(), m.class.unwrap().pattern.as_str()), (Some("run"), "La/B;"));
assert_eq!(parse_rule("in:{extends:a.Base}").unwrap(), Query::Defined(Defined::In(Box::new(format_extends("a.Base")))));
let Query::All { all, none } = parse_rule("string:x & flag:native & !annotation:a.B").unwrap() else { panic!("a compound") };
assert_eq!(all.len(), 2);
assert_eq!(none, vec![Query::Defined(Defined::Annotation("La/B;".into()))]);
for (spec, want) in [("flag:bogus", "use public"), ("in:extends:a.B", "in: takes a class condition")] {
assert!(parse_rule(spec).unwrap_err().to_string().contains(want), "{spec}");
}
assert!(matches!(parse_rule("in:{string:x}").unwrap(), Query::Defined(Defined::In(_))));
}
fn format_extends(c: &str) -> Query {
Query::Extends(format_class_name(c).unwrap())
}
#[test]
fn method_sets_follow_dispatch_unless_prefixed_with_eq() {
use crate::compose::DEFAULT_FAN_OUT;
let Query::Virtual { target, via, fan_out } = parse_rule("method:{literal:x}").unwrap() else { panic!("a dispatch set") };
assert!(matches!(*target, Query::Nested(_)) && via.is_empty() && fan_out == Some(DEFAULT_FAN_OUT));
let Query::Virtual { fan_out, .. } = parse_rule("method:*{literal:x}").unwrap() else { panic!("a dispatch set") };
assert_eq!(fan_out, None);
assert!(matches!(parse_rule("method:={literal:x}").unwrap(), Query::Nested(_)));
let Query::Call { target, .. } = parse_rule("method:{literal:x}[arg0=1]").unwrap() else { panic!("constrained") };
assert!(matches!(*target, Query::Virtual { .. }));
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("s.txt"), "La/B;->m()V\n").unwrap();
assert!(matches!(parse_rule_in("method:@s.txt", Some(dir.path())).unwrap(), Query::Virtual { .. }));
assert!(matches!(parse_rule_in("method:=@s.txt", Some(dir.path())).unwrap(), Query::MethodSet(_)));
assert!(matches!(parse_rule_in("method:*@s.txt", Some(dir.path())).unwrap(), Query::Virtual { fan_out: None, .. }));
assert_eq!(parse_rule_in("method:{@s.txt}", Some(dir.path())).unwrap(), parse_rule_in("method:@s.txt", Some(dir.path())).unwrap());
assert_eq!(
parse_rule_in("method:={ @s.txt }[arg0=1]", Some(dir.path())).unwrap(),
parse_rule_in("method:=@s.txt[arg0=1]", Some(dir.path())).unwrap()
);
}
#[test]
fn errors_name_the_problem() {
for (spec, want) in [
("bogus", "expected KIND:VALUE"),
("method:", "empty value"),
("metod:x", "closest: method"),
("method:{string:a", "expected `}`"),
("method:{string:a} x", "unexpected text after the method"),
("method:x[arg0=1] y", "unexpected text after the method"),
("method:x[arg0=1", "expected `,` or `]`"),
("method:x[arg0=1, foo=2]", "expected argN, this or ret"),
("method:x[arg0 1]", "expected `=` or `==`"),
("method:x[arg0=]", "empty value"),
("method:x[arg0=\"a]", "unterminated"),
("method:x[arg0=bogus:y]", "expected class:, new:, ret: or field:"),
("method:x[arg0=paramx]", "expected paramK"),
("method:x[arg0=1.5]", "expected *, a number"),
("method:x[ret=maybe]", "ret takes used or unused"),
("!string:a", "needs a condition without `!`"),
("string:a & bogus:b", "unknown kind \"bogus\""),
("in:{extends:a.B & !extends:c.D}", "in: takes one class condition"),
("method:@", "empty value"),
("method:@/nonexistent/set.txt", "cannot read method set"),
] {
let e = parse_rule(spec).unwrap_err().to_string();
assert!(e.contains(want), "{spec}: {e}");
}
assert!(parse_condition("string:a & string:b").unwrap_err().to_string().contains("several conditions"));
}
#[test]
fn bodies_nest_up_to_the_limit() {
let nest = |open: &str, n: usize, leaf: &str| format!("{}{leaf}{}", open.repeat(n), "}".repeat(n));
for open in ["method:{", "method:={", "method:*{", "in:{"] {
assert!(parse_rule(&nest(open, MAX_DEPTH, "string:a")).is_ok(), "{open} x{MAX_DEPTH}");
for n in [MAX_DEPTH + 1, 100_000] {
let e = parse_rule(&nest(open, n, "string:a")).unwrap_err().to_string();
assert!(e.contains(&format!("nested deeper than {MAX_DEPTH} levels")), "{open} x{n}: {}", &e[e.len().saturating_sub(80)..]);
}
}
let mixed = format!("{}string:a{}", "method:{in:{".repeat(MAX_DEPTH / 2 + 1), "}}".repeat(MAX_DEPTH / 2 + 1));
assert!(parse_rule(&mixed).is_err());
let wide = vec![nest("method:{", MAX_DEPTH, "string:a"); 50].join(" & ");
assert!(parse_rule(&wide).is_ok());
}
#[test]
fn method_files_take_any_listing() {
let dir = tempfile::tempdir().unwrap();
let text = "# from findrefs\n\
classes2.dex | Lb/Lib;->sink()V | \"x->y\"\n\
r1 | classes.dex | La/A;->run(I)V | Lb/Lib;->sink()V\n\
\n\
\x20 2 <- La/A;->run(I)V (via Lx;)\n\
com.a.B.go\n";
std::fs::write(dir.path().join("set.txt"), text).unwrap();
let Query::Virtual { target, .. } = parse_rule_in("method:@set.txt", Some(dir.path())).unwrap() else { panic!("a dispatch set") };
let Query::MethodSet(set) = *target else { panic!("a method set") };
let want = [exact_method("La/A;->run(I)V").unwrap(), exact_method("Lb/Lib;->sink()V").unwrap(), method_query("com.a.B.go")];
assert_eq!(set, want);
let q = exact_method("LX/0TRC;->ʻ(I)Ljava/lang/String;").unwrap();
assert_eq!((q.name.as_deref(), q.exact_name, q.class.unwrap().pattern.as_str()), (Some("ʻ"), true, "LX/0TRC;"));
}
}