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
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,
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
4455mod debug;
4556mod 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