Skip to content

Commit 305eabd

Browse files
authored
feat(r8): Support outline and outlineCallsite annotations in ProguardCache (#62)
Same as #60 but implements the necessary changes for `outline` and `outlineCallsite` in ProguardCache Closes #59
1 parent ce3f3c9 commit 305eabd

File tree

4 files changed

+438
-51
lines changed

4 files changed

+438
-51
lines changed

src/cache/debug.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ impl fmt::Debug for MemberDebug<'_, '_> {
107107
.field("original_endline", &self.original_endline())
108108
.field("params", &self.params())
109109
.field("is_synthesized", &self.raw.is_synthesized())
110+
.field("is_outline", &self.raw.is_outline())
110111
.finish()
111112
}
112113
}

src/cache/mod.rs

Lines changed: 200 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
//!
33
//! # Structure
44
//! A [`ProguardCache`] file comprises the following parts:
5-
//! * A [header](ProguardCache::header), containing the version number, the numbers of class, member, and
6-
//! member-by-params entries, and the length of the string section;
5+
//! * A [header](ProguardCache::header), containing:
6+
//! - the format version,
7+
//! - the number of class, member, and member-by-params entries,
8+
//! - the number of outline mapping pairs,
9+
//! - and the length of the string section;
710
//! * A [list](ProguardCache::classes) of [`Class`](raw::Class) entries;
811
//! * A [list](ProguardCache::members) of [`Member`](raw::Member) entries;
9-
//! * Another [list](Proguard_cache::members_by_params) of `Member` entries, sorted
10-
//! by parameter strings;
11-
//! * A [string section](ProguardCache::string_bytes) in which class names, method
12-
//! names, &c. are collected. Whenever a class or member entry references a string,
13-
//! it is by offset into this section.
12+
//! * Another [list](Proguard_cache::members_by_params) of `Member` entries, sorted by parameter strings;
13+
//! * A [list] of outline mapping pairs shared by all members;
14+
//! * A [string section](ProguardCache::string_bytes) in which class names, method names, &c. are collected.
15+
//! Whenever a class or member entry references a string, it is by offset into this section.
1416
//!
1517
//! ## Class entries
1618
//! A class entry contains
@@ -26,7 +28,10 @@
2628
//! * an obfuscated and an original method name,
2729
//! * a start and end line (1- based and inclusive),
2830
//! * a params string,
29-
//! * and an `is_synthesized` flag.
31+
//! * an `is_synthesized` flag,
32+
//! * an `is_outline` flag designating outline methods,
33+
//! * an `outline_pairs_offset` and `outline_pairs_len` which slice into the global outline
34+
//! pairs section.
3035
//!
3136
//! It may also contain
3237
//! * an original class name,
@@ -37,9 +42,15 @@
3742
//! obfuscated method name, and finally by the order in which they were encountered
3843
//! in the original proguard file.
3944
//!
40-
//! Member entries in `members_by_params` are sorted by the class they belong to,
41-
//! then by obfuscated method name, then by params string, and finally
42-
//! by the order in which they were encountered in the original proguard file.
45+
//! Member entries in `members_by_params` are sorted by the class they belong to, then by obfuscated
46+
//! method name, then by params string, and finally by the order in which they were encountered in the
47+
//! original proguard file.
48+
//!
49+
//! ## Outline pairs section
50+
//! The outline pairs section is a flat array of pairs mapping an outline-position to a callsite line.
51+
//! Each [`Member`](raw::Member) that carries outline callsite information references a sub-slice of this
52+
//! section via its `outline_pairs_offset` and `outline_pairs_len`. This keeps members fixed-size and
53+
//! enables zero-copy parsing while supporting variable-length metadata.
4354
4455
mod debug;
4556
mod raw;
@@ -332,17 +343,164 @@ impl<'data> ProguardCache<'data> {
332343
})
333344
}
334345

346+
/// Returns the outline mapping pairs slice for a given member.
347+
fn member_outline_pairs(&self, member: &raw::Member) -> &'data [raw::OutlinePair] {
348+
let start = member.outline_pairs_offset as usize;
349+
let end = start + member.outline_pairs_len as usize;
350+
if start >= self.outline_pairs.len() || end > self.outline_pairs.len() {
351+
&self.outline_pairs[0..0]
352+
} else {
353+
&self.outline_pairs[start..end]
354+
}
355+
}
356+
357+
/// If the previous frame was an outline and carried a position, attempt to
358+
/// map that outline position to a callsite position for the given method.
359+
fn map_outline_position(
360+
&self,
361+
class: &str,
362+
method: &str,
363+
callsite_line: usize,
364+
pos: usize,
365+
parameters: Option<&str>,
366+
) -> Option<usize> {
367+
let class = self.get_class(class)?;
368+
369+
let candidates: &[raw::Member] = if let Some(params) = parameters {
370+
let members = self.get_class_members_by_params(class)?;
371+
Self::find_range_by_binary_search(members, |m| {
372+
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
373+
return Ordering::Greater;
374+
};
375+
let p = self.read_string(m.params_offset).unwrap_or_default();
376+
(obfuscated_name, p).cmp(&(method, params))
377+
})?
378+
} else {
379+
let members = self.get_class_members(class)?;
380+
Self::find_range_by_binary_search(members, |m| {
381+
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
382+
return Ordering::Greater;
383+
};
384+
obfuscated_name.cmp(method)
385+
})?
386+
};
387+
388+
candidates
389+
.iter()
390+
.filter(|m| {
391+
m.endline == 0
392+
|| (callsite_line >= m.startline as usize
393+
&& callsite_line <= m.endline as usize)
394+
})
395+
.find_map(|m| {
396+
self.member_outline_pairs(m)
397+
.iter()
398+
.find(|pair| pair.outline_pos as usize == pos)
399+
.map(|pair| pair.callsite_line as usize)
400+
})
401+
}
402+
403+
/// Determines if a frame refers to an outline method, either via the
404+
/// method-level flag or via any matching mapping entry for the frame line.
405+
fn is_outline_frame(
406+
&self,
407+
class: &str,
408+
method: &str,
409+
line: usize,
410+
parameters: Option<&str>,
411+
) -> bool {
412+
let Some(class) = self.get_class(class) else {
413+
return false;
414+
};
415+
416+
let candidates: &[raw::Member] = if let Some(params) = parameters {
417+
let Some(members) = self.get_class_members_by_params(class) else {
418+
return false;
419+
};
420+
let Some(range) = Self::find_range_by_binary_search(members, |m| {
421+
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
422+
return Ordering::Greater;
423+
};
424+
let p = self.read_string(m.params_offset).unwrap_or_default();
425+
(obfuscated_name, p).cmp(&(method, params))
426+
}) else {
427+
return false;
428+
};
429+
range
430+
} else {
431+
let Some(members) = self.get_class_members(class) else {
432+
return false;
433+
};
434+
let Some(range) = Self::find_range_by_binary_search(members, |m| {
435+
let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else {
436+
return Ordering::Greater;
437+
};
438+
obfuscated_name.cmp(method)
439+
}) else {
440+
return false;
441+
};
442+
range
443+
};
444+
445+
candidates.iter().any(|m| {
446+
m.is_outline()
447+
&& (m.endline == 0 || (line >= m.startline as usize && line <= m.endline as usize))
448+
})
449+
}
450+
451+
/// Applies any carried outline position to the frame line and returns the adjusted frame.
452+
fn prepare_frame_for_mapping<'a>(
453+
&self,
454+
frame: &StackFrame<'a>,
455+
carried_outline_pos: &mut Option<usize>,
456+
) -> StackFrame<'a> {
457+
let mut effective = frame.clone();
458+
if let Some(pos) = carried_outline_pos.take() {
459+
if let Some(mapped) = self.map_outline_position(
460+
effective.class,
461+
effective.method,
462+
effective.line,
463+
pos,
464+
effective.parameters,
465+
) {
466+
effective.line = mapped;
467+
}
468+
}
469+
470+
effective
471+
}
472+
335473
/// Remaps a complete Java StackTrace, similar to [`Self::remap_stacktrace_typed`] but instead works on
336474
/// strings as input and output.
337475
pub fn remap_stacktrace(&self, input: &str) -> Result<String, std::fmt::Error> {
338476
let mut stacktrace = String::new();
339477
let mut lines = input.lines();
340478

479+
let mut carried_outline_pos: Option<usize> = None;
480+
341481
if let Some(line) = lines.next() {
342482
match stacktrace::parse_throwable(line) {
343483
None => match stacktrace::parse_frame(line) {
344484
None => writeln!(&mut stacktrace, "{line}")?,
345-
Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?,
485+
Some(frame) => {
486+
if self.is_outline_frame(
487+
frame.class,
488+
frame.method,
489+
frame.line,
490+
frame.parameters,
491+
) {
492+
carried_outline_pos = Some(frame.line);
493+
} else {
494+
let effective_frame =
495+
self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos);
496+
497+
format_frames(
498+
&mut stacktrace,
499+
line,
500+
self.remap_frame(&effective_frame),
501+
)?;
502+
}
503+
}
346504
},
347505
Some(throwable) => {
348506
format_throwable(&mut stacktrace, line, self.remap_throwable(&throwable))?
@@ -361,7 +519,21 @@ impl<'data> ProguardCache<'data> {
361519
format_cause(&mut stacktrace, line, self.remap_throwable(&cause))?
362520
}
363521
},
364-
Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?,
522+
Some(frame) => {
523+
if self.is_outline_frame(
524+
frame.class,
525+
frame.method,
526+
frame.line,
527+
frame.parameters,
528+
) {
529+
carried_outline_pos = Some(frame.line);
530+
continue;
531+
}
532+
533+
let effective_frame =
534+
self.prepare_frame_for_mapping(&frame, &mut carried_outline_pos);
535+
format_frames(&mut stacktrace, line, self.remap_frame(&effective_frame))?;
536+
}
365537
}
366538
}
367539
Ok(stacktrace)
@@ -374,20 +546,22 @@ impl<'data> ProguardCache<'data> {
374546
.as_ref()
375547
.and_then(|t| self.remap_throwable(t));
376548

377-
let frames =
378-
trace
379-
.frames
380-
.iter()
381-
.fold(Vec::with_capacity(trace.frames.len()), |mut frames, f| {
382-
let mut peek_frames = self.remap_frame(f).peekable();
383-
if peek_frames.peek().is_some() {
384-
frames.extend(peek_frames);
385-
} else {
386-
frames.push(f.clone());
387-
}
549+
let mut carried_outline_pos: Option<usize> = None;
550+
let mut frames: Vec<StackFrame<'a>> = Vec::with_capacity(trace.frames.len());
551+
for f in trace.frames.iter() {
552+
if self.is_outline_frame(f.class, f.method, f.line, f.parameters) {
553+
carried_outline_pos = Some(f.line);
554+
continue;
555+
}
388556

389-
frames
390-
});
557+
let effective = self.prepare_frame_for_mapping(f, &mut carried_outline_pos);
558+
let mut iter = self.remap_frame(&effective).peekable();
559+
if iter.peek().is_some() {
560+
frames.extend(iter);
561+
} else {
562+
frames.push(f.clone());
563+
}
564+
}
391565

392566
let cause = trace
393567
.cause

0 commit comments

Comments
 (0)