use crate::query::{CompiledClass, CompiledMember, CompiledName, CompiledQuery, CompiledSignature, CompiledType, Parts};
use crate::table::Table;
use crate::{MemberQuery, Result};
use asc_dex::Dex;
use regex::bytes::Regex;
use std::collections::BTreeSet;
pub struct StringLocator {
pub(crate) offsets: Table<[u32; 2]>,
pub(crate) strdata_start: usize,
pub(crate) strdata_end: usize,
}
impl StringLocator {
pub fn build(dex: &Dex<'_>) -> Result<StringLocator> {
let buf = dex.buf();
let count = dex.string_count();
let mut offsets = Vec::with_capacity((count as usize).min(buf.len() / 4));
for idx in 0..count {
offsets.push([dex.string_data_off(idx)?, idx]);
}
offsets.sort_unstable();
let (strdata_start, strdata_end) = match (offsets.first(), offsets.last()) {
(Some(&[first, _]), Some(&[last, _])) => {
let item = dex.string_item_at(last)?;
let end = (last as usize + asc_dex::leb128::uleb128_len(buf, last as usize)?) + item.bytes.len() + 1;
let end = end.min(buf.len());
((first as usize).min(end), end)
}
_ => (0, 0),
};
Ok(StringLocator { offsets: offsets.into(), strdata_start, strdata_end })
}
pub fn string_count(&self) -> usize {
self.offsets.len()
}
pub fn heap_bytes(&self) -> usize {
self.offsets.heap_bytes()
}
fn region<'b>(&self, buf: &'b [u8]) -> &'b [u8] {
let end = self.strdata_end.min(buf.len());
let start = self.strdata_start.min(end);
&buf[start..end]
}
pub fn locate(&self, buf: &[u8], re: &Regex) -> BTreeSet<u32> {
let mut out = BTreeSet::new();
if self.offsets.is_empty() {
return out;
}
let shape = PatternShape::of(re);
if shape.looks {
return self.locate_each(buf, re, shape.extent);
}
let region = self.region(buf);
let start_of = |p: usize| self.offsets.get(p).map_or(region.len(), |&[off, _]| (off as usize).saturating_sub(self.strdata_start));
let mut at = 0;
while at <= region.len() {
let Some(m) = re.find_at(region, at) else { break };
let abs = self.strdata_start + m.start();
let p = self.offsets.partition_point(|&[off, _]| (off as usize) <= abs);
if let Some((from, to)) = p.checked_sub(1).and_then(|q| self.extent(buf, q, shape.extent))
&& from <= abs
&& abs + m.len() <= to
{
out.insert(self.offsets[p - 1][1]);
at = start_of(p).max(m.end()).max(m.start() + 1);
continue;
}
let last = abs + m.len().max(1) - 1;
let q = self.offsets.partition_point(|&[off, _]| (off as usize) <= last);
for i in p.saturating_sub(1)..q {
if self.matches_alone(buf, i, re, shape.extent) {
out.insert(self.offsets[i][1]);
}
}
at = start_of(q).max(m.end()).max(m.start() + 1);
}
out
}
fn locate_each(&self, buf: &[u8], re: &Regex, extent: Extent) -> BTreeSet<u32> {
(0..self.offsets.len()).filter(|&i| self.matches_alone(buf, i, re, extent)).map(|i| self.offsets[i][1]).collect()
}
fn matches_alone(&self, buf: &[u8], i: usize, re: &Regex, extent: Extent) -> bool {
self.extent(buf, i, extent).is_some_and(|(from, to)| re.is_match(&buf[from..to]))
}
fn extent(&self, buf: &[u8], i: usize, extent: Extent) -> Option<(usize, usize)> {
let off = self.offsets[i][0] as usize;
let from = off + asc_dex::leb128::uleb128_len(buf, off).ok()?;
let data = buf.get(from..)?;
let to = match memchr::memchr(0, data) {
Some(n) => from + n + usize::from(extent == Extent::WithTerminator),
None => buf.len(),
};
Some((from, to))
}
pub fn locate_literal(&self, buf: &[u8], finder: &memchr::memmem::Finder<'_>) -> BTreeSet<u32> {
if self.offsets.is_empty() || finder.needle().is_empty() {
return BTreeSet::new();
}
let region = self.region(buf);
let needle = finder.needle();
let skip_rest = !needle.contains(&0);
let mut out = Vec::new();
let mut offset_pos = 0;
let mut at = 0;
while at < region.len() {
let Some(pos) = finder.find(®ion[at..]) else { break };
let start = at + pos;
let abs = self.strdata_start + start;
at = start + needle.len();
while offset_pos + 1 < self.offsets.len() && self.offsets[offset_pos + 1][0] as usize <= abs {
offset_pos += 1;
}
let off = self.offsets[offset_pos][0] as usize;
if asc_dex::leb128::uleb128_len(buf, off).map_or(true, |n| abs < off + n) {
at = start + 1;
continue;
}
out.push(self.offsets[offset_pos][1]);
if skip_rest && let Some(&[next, _]) = self.offsets.get(offset_pos + 1) {
at = at.max((next as usize).saturating_sub(self.strdata_start));
}
}
out.sort_unstable();
out.dedup();
out.into_iter().collect()
}
pub fn locate_pattern(&self, buf: &[u8], pattern: &str) -> Result<BTreeSet<u32>> {
if pattern.is_empty() {
return Ok(BTreeSet::new());
}
Ok(self.locate(buf, &Regex::new(pattern)?))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Extent {
Data,
WithTerminator,
}
struct PatternShape {
looks: bool,
extent: Extent,
}
impl PatternShape {
fn of(re: &Regex) -> PatternShape {
let pattern = re.as_str();
let looks =
regex_syntax::ParserBuilder::new().utf8(false).build().parse(pattern).is_ok_and(|hir| !hir.properties().look_set().is_empty());
let spells_nul =
regex_syntax::ast::parse::Parser::new().parse(pattern).is_ok_and(|ast| regex_syntax::ast::visit(&ast, NulFinder).is_err());
PatternShape { looks, extent: if spells_nul { Extent::WithTerminator } else { Extent::Data } }
}
}
struct NulFinder;
impl regex_syntax::ast::Visitor for NulFinder {
type Output = ();
type Err = ();
fn finish(self) -> std::result::Result<(), ()> {
Ok(())
}
fn visit_pre(&mut self, ast: ®ex_syntax::ast::Ast) -> std::result::Result<(), ()> {
match ast {
regex_syntax::ast::Ast::Literal(l) if l.c == '\0' => Err(()),
_ => Ok(()),
}
}
fn visit_class_set_item_pre(&mut self, item: ®ex_syntax::ast::ClassSetItem) -> std::result::Result<(), ()> {
match item {
regex_syntax::ast::ClassSetItem::Literal(l) if l.c == '\0' => Err(()),
regex_syntax::ast::ClassSetItem::Range(r) if r.start.c == '\0' => Err(()),
_ => Ok(()),
}
}
}
pub struct TypeLocator {
pub(crate) by_string: Table<[u32; 2]>,
}
impl TypeLocator {
pub fn build(dex: &Dex<'_>) -> Result<TypeLocator> {
let count = dex.type_count();
let mut by_string = Vec::with_capacity((count as usize).min(dex.buf().len() / 4));
for t in 0..count {
by_string.push([dex.type_string_idx(t)?, t]);
}
by_string.sort_unstable();
Ok(TypeLocator { by_string: by_string.into() })
}
pub fn heap_bytes(&self) -> usize {
self.by_string.heap_bytes()
}
pub fn types_for_string(&self, string_idx: u32) -> impl Iterator<Item = u32> + '_ {
let start = self.by_string.partition_point(|&[s, _]| s < string_idx);
self.by_string[start..].iter().take_while(move |&&[s, _]| s == string_idx).map(|&[_, t]| t)
}
pub fn locate(&self, buf: &[u8], strings: &StringLocator, re: &Regex) -> BTreeSet<u32> {
let mut out = BTreeSet::new();
for s in strings.locate(buf, re) {
out.extend(self.types_for_string(s));
}
out
}
pub fn locate_pattern(&self, buf: &[u8], strings: &StringLocator, pattern: &str) -> Result<BTreeSet<u32>> {
if pattern.is_empty() {
return Ok(BTreeSet::new());
}
Ok(self.locate(buf, strings, &Regex::new(pattern)?))
}
}
pub(crate) struct MemberLocator {
pub(crate) table_off: usize,
pub(crate) count: u32,
pub(crate) by_name: Table<[u32; 2]>,
pub(crate) by_class: Option<Table<[u32; 2]>>,
}
impl MemberLocator {
fn build(dex: &Dex<'_>, count: u32, table_off: u32) -> Result<MemberLocator> {
let buf = dex.buf();
let base = table_off as usize;
let cap = (count as usize).min(buf.len() / 8);
let mut by_name: Vec<[u32; 2]> = Vec::with_capacity(cap);
let mut by_class: Vec<[u32; 2]> = Vec::new();
let mut sorted = true;
let mut last_class = 0u32;
for i in 0..count {
let off = base + i as usize * 8;
let class_idx = asc_dex::read_u16(buf, off)? as u32;
let name_idx = asc_dex::read_u32(buf, off + 4)?;
by_name.push([name_idx, i]);
if class_idx < last_class {
sorted = false;
}
last_class = class_idx;
}
by_name.sort_unstable();
if !sorted {
by_class.reserve(cap);
for i in 0..count {
let off = base + i as usize * 8;
by_class.push([asc_dex::read_u16(buf, off)? as u32, i]);
}
by_class.sort_unstable();
}
Ok(MemberLocator { table_off: base, count, by_name: by_name.into(), by_class: (!sorted).then(|| by_class.into()) })
}
fn heap_bytes(&self) -> usize {
self.by_name.heap_bytes() + self.by_class.as_ref().map_or(0, Table::heap_bytes)
}
fn name_idx(&self, buf: &[u8], id: u32) -> Result<u32> {
Ok(asc_dex::read_u32(buf, self.table_off + id as usize * 8 + 4)?)
}
fn ids_by_name(&self, name_idx: u32, out: &mut BTreeSet<u32>) {
let start = self.by_name.partition_point(|&[n, _]| n < name_idx);
for &[n, id] in &self.by_name[start..] {
if n != name_idx {
break;
}
out.insert(id);
}
}
fn ids_by_class(&self, buf: &[u8], class_idx: u32, out: &mut BTreeSet<u32>) {
if let Some(v) = &self.by_class {
let start = v.partition_point(|&[c, _]| c < class_idx);
for &[c, id] in &v[start..] {
if c != class_idx {
break;
}
out.insert(id);
}
return;
}
let class_at = |i: u32| asc_dex::read_u16(buf, self.table_off + i as usize * 8).unwrap_or(u16::MAX) as u32;
let (mut lo, mut hi) = (0u32, self.count);
while lo < hi {
let mid = (lo + hi) / 2;
if class_at(mid) < class_idx {
lo = mid + 1;
} else {
hi = mid;
}
}
let mut i = lo;
while i < self.count && class_at(i) == class_idx {
out.insert(i);
i += 1;
}
}
fn locate(&self, dex: &Dex<'_>, strings: &StringLocator, types: &TypeLocator, query: &CompiledMember) -> Result<BTreeSet<u32>> {
let buf = dex.buf();
let mut out = BTreeSet::new();
let Some(class) = &query.class else {
match &query.name {
Some(CompiledName::Regex(re)) => {
for s in strings.locate(buf, re) {
self.ids_by_name(s, &mut out);
}
}
Some(CompiledName::Exact { bytes, .. }) => {
if let Some(s) = dex.find_string_idx(bytes)? {
self.ids_by_name(s, &mut out);
}
}
_ => {}
}
return Ok(out);
};
let mut class_members = BTreeSet::new();
match class {
CompiledClass::Precise(descriptor) => {
if let Some(t) = dex.find_type_idx(descriptor)? {
self.ids_by_class(buf, t, &mut class_members);
}
}
CompiledClass::Fuzzy(re) => {
for t in types.locate(buf, strings, re) {
self.ids_by_class(buf, t, &mut class_members);
}
}
}
if class_members.is_empty() {
return Ok(out);
}
match &query.name {
None => Ok(class_members),
Some(CompiledName::Substring(name)) => {
for id in class_members {
if dex.string(self.name_idx(buf, id)?)?.contains(name.as_str()) {
out.insert(id);
}
}
Ok(out)
}
Some(CompiledName::Exact { name, bytes }) => {
for id in class_members {
if exact_name_matches(dex, self.name_idx(buf, id)?, name, bytes) {
out.insert(id);
}
}
Ok(out)
}
Some(CompiledName::Regex(re)) => {
let mut named = BTreeSet::new();
for s in strings.locate(buf, re) {
self.ids_by_name(s, &mut named);
}
out.extend(named.intersection(&class_members));
Ok(out)
}
}
}
}
fn exact_name_matches(dex: &Dex<'_>, name_idx: u32, name: &str, bytes: &[u8]) -> bool {
match dex.string_bytes(name_idx) {
Ok(raw) => raw == bytes || (!raw.is_ascii() && asc_dex::mutf8::decode(raw) == name),
Err(_) => false,
}
}
fn method_has_signature(dex: &Dex<'_>, id: u32, params: &[CompiledType], ret: Option<&CompiledType>) -> Result<bool> {
let proto = dex.proto_id(dex.method_id(id)?.proto_idx as u32)?;
if let Some(ret) = ret
&& !ret.matches(dex.type_bytes(proto.return_type_idx)?)
{
return Ok(false);
}
let have = dex.type_list(proto.parameters_off)?;
if have.len() != params.len() {
return Ok(false);
}
for (&t, want) in have.iter().zip(params) {
if !want.matches(dex.type_bytes(t as u32)?) {
return Ok(false);
}
}
Ok(true)
}
fn retain_ok(ids: BTreeSet<u32>, mut keep: impl FnMut(u32) -> Result<bool>) -> Result<BTreeSet<u32>> {
let mut out = BTreeSet::new();
for id in ids {
if keep(id)? {
out.insert(id);
}
}
Ok(out)
}
pub struct MethodLocator(pub(crate) MemberLocator);
impl MethodLocator {
pub fn build(dex: &Dex<'_>) -> Result<MethodLocator> {
Ok(MethodLocator(MemberLocator::build(dex, dex.method_count(), dex.header().method_ids.off)?))
}
pub fn heap_bytes(&self) -> usize {
self.0.heap_bytes()
}
pub fn locate(&self, dex: &Dex<'_>, strings: &StringLocator, types: &TypeLocator, query: &MemberQuery) -> Result<BTreeSet<u32>> {
let compiled = CompiledQuery::new(&crate::Query::Method(query.clone()))?;
self.locate_compiled(dex, strings, types, &compiled)
}
pub fn locate_compiled(
&self,
dex: &Dex<'_>,
strings: &StringLocator,
types: &TypeLocator,
query: &CompiledQuery,
) -> Result<BTreeSet<u32>> {
match query.parts() {
Parts::Member(m) if query.kind() == crate::RefKind::Method => self.locate_member(dex, strings, types, m),
Parts::MemberSet(set) => {
let mut out = BTreeSet::new();
for m in set {
out.extend(self.locate_member(dex, strings, types, m)?);
}
Ok(out)
}
_ => Ok(BTreeSet::new()),
}
}
fn locate_member(&self, dex: &Dex<'_>, strings: &StringLocator, types: &TypeLocator, m: &CompiledMember) -> Result<BTreeSet<u32>> {
let ids = self.0.locate(dex, strings, types, m)?;
match &m.signature {
Some(CompiledSignature::Method { params, ret }) => retain_ok(ids, |id| method_has_signature(dex, id, params, ret.as_ref())),
_ => Ok(ids),
}
}
}
pub struct FieldLocator(pub(crate) MemberLocator);
impl FieldLocator {
pub fn build(dex: &Dex<'_>) -> Result<FieldLocator> {
Ok(FieldLocator(MemberLocator::build(dex, dex.field_count(), dex.header().field_ids.off)?))
}
pub fn heap_bytes(&self) -> usize {
self.0.heap_bytes()
}
pub fn locate(&self, dex: &Dex<'_>, strings: &StringLocator, types: &TypeLocator, query: &MemberQuery) -> Result<BTreeSet<u32>> {
let compiled = CompiledQuery::new(&crate::Query::Field(query.clone()))?;
self.locate_compiled(dex, strings, types, &compiled)
}
pub fn locate_compiled(
&self,
dex: &Dex<'_>,
strings: &StringLocator,
types: &TypeLocator,
query: &CompiledQuery,
) -> Result<BTreeSet<u32>> {
match query.parts() {
Parts::Member(m) if query.kind() == crate::RefKind::Field => {
let ids = self.0.locate(dex, strings, types, m)?;
match &m.signature {
Some(CompiledSignature::Field(t)) => {
retain_ok(ids, |id| Ok(t.matches(dex.type_bytes(dex.field_id(id)?.type_idx as u32)?)))
}
_ => Ok(ids),
}
}
_ => Ok(BTreeSet::new()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locators_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<StringLocator>();
assert_send_sync::<TypeLocator>();
assert_send_sync::<MethodLocator>();
assert_send_sync::<FieldLocator>();
}
#[test]
fn type_lookup_handles_duplicates_and_gaps() {
let mut by_string = vec![[1, 0], [3, 2], [3, 1], [7, 3]];
by_string.sort_unstable();
let loc = TypeLocator { by_string: by_string.into() };
assert_eq!(loc.types_for_string(0).count(), 0);
assert_eq!(loc.types_for_string(1).collect::<Vec<_>>(), vec![0]);
assert_eq!(loc.types_for_string(2).count(), 0);
assert_eq!(loc.types_for_string(3).collect::<Vec<_>>(), vec![1, 2]);
assert_eq!(loc.types_for_string(7).collect::<Vec<_>>(), vec![3]);
assert_eq!(loc.types_for_string(8).count(), 0);
}
#[test]
fn dense_regex_matches_insert_each_string_once() {
let offsets = vec![[0, 7], [4, 3], [8, 9]];
let loc = StringLocator { offsets: offsets.into(), strdata_start: 0, strdata_end: 12 };
let re = Regex::new(".").unwrap();
assert_eq!(loc.locate(b"\x02aa\0\x02bb\0\x02cc\0", &re), [3, 7, 9].into_iter().collect());
}
#[test]
fn a_length_prefix_is_not_part_of_its_string() {
let mut buf = vec![0x41];
buf.extend([b'x'; 65]);
buf.push(0);
let second = buf.len();
buf.push(0x41);
buf.push(b'A');
buf.extend([b'x'; 64]);
buf.push(0);
let offsets = vec![[0, 4], [second as u32, 5]];
let loc = StringLocator { offsets: offsets.into(), strdata_start: 0, strdata_end: buf.len() };
let finder = memchr::memmem::Finder::new(b"A");
assert_eq!(loc.locate_literal(&buf, &finder), [5].into_iter().collect());
assert_eq!(loc.locate(&buf, &Regex::new("A").unwrap()), [5].into_iter().collect());
assert_eq!(loc.locate_literal(&buf, &memchr::memmem::Finder::new(b"Ax")), [5].into_iter().collect());
}
}