From 1c24a94a09af4dbf4601175fcd3a27db5846c5e7 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 9 Feb 2026 10:32:33 +0100 Subject: [PATCH 1/5] feat(r8): Add has_minified_range and has_line_mapping metadata to members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track whether a mapping line had an explicit minified range prefix (e.g. `0:0:` or `1:5:`) and whether it had any line mapping at all. This metadata is needed for correctly resolving no-line entries in subsequent changes. - LineMapping: add has_minified_range field - Member (builder): add has_minified_range and has_line_mapping fields - MemberMapping (mapper): propagate both fields - cache Member (raw): replace _reserved bytes with the two new flags - Bump cache version 4 → 5 Co-Authored-By: Claude Opus 4.6 --- src/builder.rs | 13 +++++++++++++ src/cache/raw.rs | 23 +++++++++++++++++++---- src/mapper.rs | 6 ++++++ src/mapping.rs | 10 ++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 5fa8cc9..b4b8a60 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -157,6 +157,12 @@ pub(crate) struct Member<'s> { pub(crate) original_startline: usize, /// The original end line. pub(crate) original_endline: Option, + /// Whether the mapping line had an explicit minified range prefix (including `0:0:`). + /// `false` when the line was just `method():origLine -> obfuscated`. + pub(crate) has_minified_range: bool, + /// Whether the mapping line had any line mapping at all (`:origLine` or `startline:endline:`). + /// `false` only for bare method mappings like `void foo(int) -> a`. + pub(crate) has_line_mapping: bool, /// Optional outline callsite positions map attached to this member. pub(crate) outline_callsite_positions: Option>, /// Optional rewrite rules attached to this member. @@ -384,12 +390,19 @@ impl<'s> ParsedProguardMapping<'s> { records.next(); } + let has_minified_range = line_mapping + .as_ref() + .map_or(false, |lm| lm.has_minified_range); + let has_line_mapping = line_mapping.is_some(); + let member = Member { method, startline, endline, original_startline, original_endline, + has_minified_range, + has_line_mapping, outline_callsite_positions, rewrite_rules, }; diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 66a18a1..88c3fe8 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -19,7 +19,7 @@ pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES); pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes(); /// The current version of the ProguardCache format. -pub const PRGCACHE_VERSION: u32 = 4; +pub const PRGCACHE_VERSION: u32 = 5; /// The header of a proguard cache file. #[derive(Debug, Clone, PartialEq, Eq)] @@ -136,8 +136,14 @@ pub(crate) struct Member { /// /// `0` means `false`, all other values mean `true`. pub(crate) is_outline: u8, - /// Reserved space. - pub(crate) _reserved: [u8; 2], + /// Whether the mapping had an explicit minified range prefix (including `0:0:`). + /// + /// `0` means `false`, all other values mean `true`. + pub(crate) has_minified_range: u8, + /// Whether the mapping had any line mapping at all (`:origLine` or `startline:endline:`). + /// + /// `0` means `false`, all other values mean `true`. + pub(crate) has_line_mapping: u8, } impl Member { @@ -149,6 +155,14 @@ impl Member { pub(crate) fn is_outline(&self) -> bool { self.is_outline != 0 } + /// Returns true if the mapping had an explicit minified range prefix. + pub(crate) fn has_minified_range(&self) -> bool { + self.has_minified_range != 0 + } + /// Returns true if the mapping had any line mapping at all. + pub(crate) fn has_line_mapping(&self) -> bool { + self.has_line_mapping != 0 + } } unsafe impl Pod for Header {} @@ -627,11 +641,12 @@ impl<'data> ProguardCache<'data> { params_offset, is_synthesized, is_outline, + has_minified_range: member.has_minified_range as u8, + has_line_mapping: member.has_line_mapping as u8, outline_pairs_offset: 0, outline_pairs_len: 0, rewrite_rules_offset: 0, rewrite_rules_len: 0, - _reserved: [0; 2], }; MemberInProgress { diff --git a/src/mapper.rs b/src/mapper.rs index 6f7e18e..e238fd6 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -67,6 +67,10 @@ struct MemberMapping<'s> { original: &'s str, original_startline: usize, original_endline: Option, + /// Whether the mapping line had an explicit minified range prefix (including `0:0:`). + has_minified_range: bool, + /// Whether the mapping line had any line mapping at all. + has_line_mapping: bool, is_synthesized: bool, is_outline: bool, outline_callsite_positions: Option>, @@ -461,6 +465,8 @@ impl<'s> ProguardMapper<'s> { original: member.method.name.as_str(), original_startline: member.original_startline, original_endline: member.original_endline, + has_minified_range: member.has_minified_range, + has_line_mapping: member.has_line_mapping, is_synthesized, is_outline, outline_callsite_positions, diff --git a/src/mapping.rs b/src/mapping.rs index f9668d6..5df7dee 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -295,6 +295,9 @@ pub struct LineMapping { pub original_startline: Option, /// The original End Line. pub original_endline: Option, + /// Whether this mapping had an explicit minified line range prefix (e.g. `0:0:` or `1:5:`). + /// `false` when the mapping line had no range prefix (e.g. `void method():42 -> a`). + pub has_minified_range: bool, } /// An R8 header, as described in @@ -472,6 +475,7 @@ impl<'s> ProguardRecord<'s> { /// endline: 1016, /// original_startline: Some(16), /// original_endline: Some(16), + /// has_minified_range: true, /// }), /// }) /// ); @@ -641,6 +645,7 @@ fn parse_proguard_field_or_method( endline, original_startline, original_endline, + has_minified_range: true, }), // Preserve original line info even when no minified range is present. // This enables this crate to use the original line for no-line mappings. @@ -649,6 +654,7 @@ fn parse_proguard_field_or_method( endline: 0, original_startline: Some(original_startline), original_endline, + has_minified_range: false, }), _ => None, }; @@ -985,6 +991,7 @@ mod tests { endline: 15, original_startline: None, original_endline: None, + has_minified_range: true, }), }), ); @@ -1007,6 +1014,7 @@ mod tests { endline: 15, original_startline: Some(436), original_endline: None, + has_minified_range: true, }), }), ); @@ -1029,6 +1037,7 @@ mod tests { endline: 15, original_startline: Some(436), original_endline: Some(437), + has_minified_range: true, }), }), ); @@ -1165,6 +1174,7 @@ androidx.activity.OnBackPressedCallback endline: 4, original_startline: Some(184), original_endline: Some(187), + has_minified_range: true, }), }), Ok(ProguardRecord::R8Header(R8Header::Synthesized)), From 1c6f3207aad12210b67b17d30c8931011ca17a4c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 9 Feb 2026 10:51:23 +0100 Subject: [PATCH 2/5] Fix lint --- src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index b4b8a60..b7bd148 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -392,7 +392,7 @@ impl<'s> ParsedProguardMapping<'s> { let has_minified_range = line_mapping .as_ref() - .map_or(false, |lm| lm.has_minified_range); + .is_some_and(|lm| lm.has_minified_range); let has_line_mapping = line_mapping.is_some(); let member = Member { From cf6f54828fdbdda33e4264e1d75c391111176868 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 10 Feb 2026 13:10:44 +0100 Subject: [PATCH 3/5] refactor: Replace boolean flags with Option fields on Member structs Replace has_minified_range and has_line_mapping bools with Option fields on builder::Member and mapper::MemberMapping. startline/endline are now None when no minified range prefix was present, and original_startline is None when no line mapping existed. In cache/raw.rs, use u32::MAX sentinel for absent values and add Option-returning accessor methods (startline(), endline(), original_startline()) replacing the removed bool accessors. Co-Authored-By: Claude Opus 4.6 --- src/builder.rs | 67 ++++++++++++++++++++---------------------------- src/cache/mod.rs | 54 +++++++++++++++++++------------------- src/cache/raw.rs | 59 +++++++++++++++++++++++++----------------- src/mapper.rs | 61 +++++++++++++++++++++++-------------------- src/utils.rs | 6 ++--- 5 files changed, 127 insertions(+), 120 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index b7bd148..3627c49 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -149,20 +149,14 @@ pub(crate) struct RewriteRule<'s> { pub(crate) struct Member<'s> { /// The method the member refers to. pub(crate) method: MethodKey<'s>, - /// The obfuscated/minified start line. - pub(crate) startline: usize, - /// The obfuscated/minified end line. - pub(crate) endline: usize, - /// The original start line. - pub(crate) original_startline: usize, + /// The obfuscated/minified start line, `None` when no minified range prefix was present. + pub(crate) startline: Option, + /// The obfuscated/minified end line, `None` when no minified range prefix was present. + pub(crate) endline: Option, + /// The original start line, `None` when no line mapping was present. + pub(crate) original_startline: Option, /// The original end line. pub(crate) original_endline: Option, - /// Whether the mapping line had an explicit minified range prefix (including `0:0:`). - /// `false` when the line was just `method():origLine -> obfuscated`. - pub(crate) has_minified_range: bool, - /// Whether the mapping line had any line mapping at all (`:origLine` or `startline:endline:`). - /// `false` only for bare method mappings like `void foo(int) -> a`. - pub(crate) has_line_mapping: bool, /// Optional outline callsite positions map attached to this member. pub(crate) outline_callsite_positions: Option>, /// Optional rewrite rules attached to this member. @@ -297,29 +291,31 @@ impl<'s> ParsedProguardMapping<'s> { } else { None }; - // in case the mapping has no line records, we use `0` here. - let (mut startline, mut endline) = - line_mapping.as_ref().map_or((0, 0), |line_mapping| { - (line_mapping.startline, line_mapping.endline) - }); - let (mut original_startline, mut original_endline) = - line_mapping.map_or((0, None), |line_mapping| { - match line_mapping.original_startline { - Some(original_startline) => { - (original_startline, line_mapping.original_endline) - } - None => (line_mapping.startline, Some(line_mapping.endline)), - } - }); + let (mut startline, mut endline) = match line_mapping.as_ref() { + Some(lm) if lm.has_minified_range => { + (Some(lm.startline), Some(lm.endline)) + } + _ => (None, None), + }; + let (mut original_startline, mut original_endline) = match line_mapping { + None => (None, None), + Some(lm) => match lm.original_startline { + Some(os) => (Some(os), lm.original_endline), + None => (startline, endline), + }, + }; // Normalize inverted ranges independently. - if startline > endline { - std::mem::swap(&mut startline, &mut endline); + if let (Some(s), Some(e)) = (startline, endline) { + if s > e { + startline = Some(e); + endline = Some(s); + } } - if let Some(oe) = original_endline { - if original_startline > oe { - original_endline = Some(original_startline); - original_startline = oe; + if let (Some(os), Some(oe)) = (original_startline, original_endline) { + if os > oe { + original_startline = Some(oe); + original_endline = Some(os); } } @@ -390,19 +386,12 @@ impl<'s> ParsedProguardMapping<'s> { records.next(); } - let has_minified_range = line_mapping - .as_ref() - .is_some_and(|lm| lm.has_minified_range); - let has_line_mapping = line_mapping.is_some(); - let member = Member { method, startline, endline, original_startline, original_endline, - has_minified_range, - has_line_mapping, outline_callsite_positions, rewrite_rules, }; diff --git a/src/cache/mod.rs b/src/cache/mod.rs index ac8e0a7..de2a98a 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -390,8 +390,9 @@ impl<'data> ProguardCache<'data> { for member in mapping_entries { // Check if this member would produce a frame (line matching) let pf_line = prepared_frame.line.unwrap_or(0); - if member.endline == 0 - || (pf_line >= member.startline as usize && pf_line <= member.endline as usize) + if member.endline().unwrap_or(0) == 0 + || (pf_line >= member.startline().unwrap_or(0) as usize + && pf_line <= member.endline().unwrap_or(0) as usize) { had_mappings = true; rewrite_rules.extend(self.decode_rewrite_rules(member)); @@ -405,7 +406,9 @@ impl<'data> ProguardCache<'data> { } } - let has_line_info = mapping_entries.iter().any(|m| m.endline > 0); + let has_line_info = mapping_entries + .iter() + .any(|m| m.endline().unwrap_or(0) > 0); Some(( mapping_entries, @@ -631,9 +634,9 @@ impl<'data> ProguardCache<'data> { candidates .iter() .filter(|m| { - m.endline == 0 - || (callsite_line >= m.startline as usize - && callsite_line <= m.endline as usize) + m.endline().unwrap_or(0) == 0 + || (callsite_line >= m.startline().unwrap_or(0) as usize + && callsite_line <= m.endline().unwrap_or(0) as usize) }) .find_map(|m| { self.member_outline_pairs(m) @@ -917,7 +920,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { NoLineSelection::IterateBase => { let mut mapped = None; for member in members.by_ref() { - if member.endline == 0 { + if member.endline().unwrap_or(0) == 0 { mapped = map_member_without_lines( cache, &frame, @@ -985,28 +988,31 @@ fn iterate_with_lines<'a>( ) -> Option> { let frame_line = frame.line.unwrap_or(0); for member in members { + let member_endline = member.endline().unwrap_or(0) as usize; + let member_startline = member.startline().unwrap_or(0) as usize; // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame_line > 0 && member.endline == 0 { + if has_line_info && frame_line > 0 && member_endline == 0 { continue; } // If the mapping entry has no line range, preserve the input line number (if any). - if member.endline == 0 { + if member_endline == 0 { return map_member_without_lines(cache, frame, member, outer_source_file); } // skip any members which do not match our frames line - if member.endline > 0 - && (frame_line < member.startline as usize || frame_line > member.endline as usize) + if member_endline > 0 + && (frame_line < member_startline || frame_line > member_endline) { continue; } + let original_startline = member.original_startline().unwrap_or(0) as usize; // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. let line = if member.original_endline == u32::MAX - || member.original_endline == member.original_startline + || member.original_endline as usize == original_startline { - member.original_startline as usize + original_startline } else { - member.original_startline as usize + frame_line - member.startline as usize + original_startline + frame_line - member_startline }; let class = cache @@ -1058,7 +1064,7 @@ enum NoLineSelection<'a> { fn select_no_line_members<'a>(members: &'a [raw::Member]) -> Option> { // Prefer base entries (endline == 0) if present. - let mut base_members = members.iter().filter(|m| m.endline == 0); + let mut base_members = members.iter().filter(|m| m.endline().unwrap_or(0) == 0); if let Some(first_base) = base_members.next() { let all_same = base_members.all(|m| { m.original_class_offset == first_base.original_class_offset @@ -1097,10 +1103,7 @@ fn map_member_without_lines<'a>( let method = cache.read_string(member.original_name_offset).ok()?; let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); - let original_startline = match member.original_startline { - 0 | u32::MAX => None, - value => Some(value as usize), - }; + let original_startline = member.original_startline().map(|v| v as usize); Some(StackFrame { class, @@ -1109,8 +1112,8 @@ fn map_member_without_lines<'a>( line: Some(resolve_no_line_output_line( frame.line.unwrap_or(0), original_startline, - member.startline as usize, - member.endline as usize, + member.startline().map(|v| v as usize), + member.endline().map(|v| v as usize), )), parameters: frame.parameters, method_synthesized: member.is_synthesized(), @@ -1134,10 +1137,7 @@ fn iterate_without_lines<'a>( // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); - let original_startline = match member.original_startline { - 0 | u32::MAX => None, - value => Some(value as usize), - }; + let original_startline = member.original_startline().map(|v| v as usize); Some(StackFrame { class, @@ -1146,8 +1146,8 @@ fn iterate_without_lines<'a>( line: Some(resolve_no_line_output_line( frame.line.unwrap_or(0), original_startline, - member.startline as usize, - member.endline as usize, + member.startline().map(|v| v as usize), + member.endline().map(|v| v as usize), )), parameters: frame.parameters, method_synthesized: member.is_synthesized(), diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 88c3fe8..b30498d 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -98,15 +98,18 @@ impl Default for Class { } } +/// Sentinel value representing absent/`None` for u32 fields in the binary format. +const NONE_VALUE: u32 = u32::MAX; + /// An entry corresponding to a method line in a proguard cache file. #[derive(Debug, Clone, PartialEq, Eq, Default)] #[repr(C)] pub(crate) struct Member { /// The obfuscated method name (offset into the string section). pub(crate) obfuscated_name_offset: u32, - /// The start of the range covered by this entry (1-based). + /// The start of the range covered by this entry (1-based), `u32::MAX` if absent. pub(crate) startline: u32, - /// The end of the range covered by this entry (inclusive). + /// The end of the range covered by this entry (inclusive), `u32::MAX` if absent. pub(crate) endline: u32, /// The original class name (offset into the string section). pub(crate) original_class_offset: u32, @@ -114,7 +117,7 @@ pub(crate) struct Member { pub(crate) original_file_offset: u32, /// The original method name (offset into the string section). pub(crate) original_name_offset: u32, - /// The original start line (1-based). + /// The original start line (0-based), `u32::MAX` if absent. pub(crate) original_startline: u32, /// The original end line (inclusive). pub(crate) original_endline: u32, @@ -136,14 +139,9 @@ pub(crate) struct Member { /// /// `0` means `false`, all other values mean `true`. pub(crate) is_outline: u8, - /// Whether the mapping had an explicit minified range prefix (including `0:0:`). - /// - /// `0` means `false`, all other values mean `true`. - pub(crate) has_minified_range: u8, - /// Whether the mapping had any line mapping at all (`:origLine` or `startline:endline:`). - /// - /// `0` means `false`, all other values mean `true`. - pub(crate) has_line_mapping: u8, + + /// Reserved space. + pub(crate) _reserved: [u8; 2], } impl Member { @@ -155,13 +153,29 @@ impl Member { pub(crate) fn is_outline(&self) -> bool { self.is_outline != 0 } - /// Returns true if the mapping had an explicit minified range prefix. - pub(crate) fn has_minified_range(&self) -> bool { - self.has_minified_range != 0 + /// Returns the startline as `Option`, where `NONE_VALUE` maps to `None`. + pub(crate) fn startline(&self) -> Option { + if self.startline == NONE_VALUE { + None + } else { + Some(self.startline) + } } - /// Returns true if the mapping had any line mapping at all. - pub(crate) fn has_line_mapping(&self) -> bool { - self.has_line_mapping != 0 + /// Returns the endline as `Option`, where `NONE_VALUE` maps to `None`. + pub(crate) fn endline(&self) -> Option { + if self.endline == NONE_VALUE { + None + } else { + Some(self.endline) + } + } + /// Returns the original_startline as `Option`, where `NONE_VALUE` maps to `None`. + pub(crate) fn original_startline(&self) -> Option { + if self.original_startline == NONE_VALUE { + None + } else { + Some(self.original_startline) + } } } @@ -630,19 +644,18 @@ impl<'data> ProguardCache<'data> { .collect(); let member: Member = Member { - startline: member.startline as u32, - endline: member.endline as u32, + startline: member.startline.map_or(NONE_VALUE, |v| v as u32), + endline: member.endline.map_or(NONE_VALUE, |v| v as u32), original_class_offset, original_file_offset, original_name_offset, - original_startline: member.original_startline as u32, - original_endline: member.original_endline.map_or(u32::MAX, |l| l as u32), + original_startline: member.original_startline.map_or(NONE_VALUE, |v| v as u32), + original_endline: member.original_endline.map_or(NONE_VALUE, |l| l as u32), obfuscated_name_offset, params_offset, is_synthesized, is_outline, - has_minified_range: member.has_minified_range as u8, - has_line_mapping: member.has_line_mapping as u8, + _reserved: [0; 2], outline_pairs_offset: 0, outline_pairs_len: 0, rewrite_rules_offset: 0, diff --git a/src/mapper.rs b/src/mapper.rs index e238fd6..b24cd7e 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -60,17 +60,13 @@ impl fmt::Display for DeobfuscatedSignature { #[derive(Clone, Debug, PartialEq, Eq)] struct MemberMapping<'s> { - startline: usize, - endline: usize, + startline: Option, + endline: Option, original_class: Option<&'s str>, original_file: Option<&'s str>, original: &'s str, - original_startline: usize, + original_startline: Option, original_endline: Option, - /// Whether the mapping line had an explicit minified range prefix (including `0:0:`). - has_minified_range: bool, - /// Whether the mapping line had any line mapping at all. - has_line_mapping: bool, is_synthesized: bool, is_outline: bool, outline_callsite_positions: Option>, @@ -145,18 +141,21 @@ fn map_member_with_lines<'a>( member: &MemberMapping<'a>, ) -> Option> { let frame_line = frame.line.unwrap_or(0); - if member.endline > 0 && (frame_line < member.startline || frame_line > member.endline) { + let member_endline = member.endline.unwrap_or(0); + let member_startline = member.startline.unwrap_or(0); + if member_endline > 0 && (frame_line < member_startline || frame_line > member_endline) { return None; } + let original_startline = member.original_startline.unwrap_or(0); // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. let line = if member.original_endline.is_none() - || member.original_endline == Some(member.original_startline) + || member.original_endline == Some(original_startline) { - member.original_startline + original_startline } else { - member.original_startline + frame_line - member.startline + original_startline + frame_line - member_startline }; let class = member.original_class.unwrap_or(frame.class); @@ -190,14 +189,9 @@ fn map_member_without_lines<'a>( let class = member.original_class.unwrap_or(frame.class); // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned); - let original_startline = if member.original_startline > 0 { - Some(member.original_startline) - } else { - None - }; let line = resolve_no_line_output_line( frame.line.unwrap_or(0), - original_startline, + member.original_startline, member.startline, member.endline, ); @@ -241,7 +235,9 @@ fn select_no_line_members<'a>( mapping_entries: &'a [MemberMapping<'a>], has_line_info: bool, ) -> Option> { - let mut base_members = mapping_entries.iter().filter(|m| m.endline == 0); + let mut base_members = mapping_entries + .iter() + .filter(|m| m.endline.unwrap_or(0) == 0); if has_line_info { if let Some(first_base) = base_members.next() { let all_same = base_members.all(|m| m.original == first_base.original); @@ -302,12 +298,13 @@ fn iterate_with_lines<'a>( ) -> Option> { let frame_line = frame.line.unwrap_or(0); for member in members { + let member_endline = member.endline.unwrap_or(0); // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame_line > 0 && member.endline == 0 { + if has_line_info && frame_line > 0 && member_endline == 0 { continue; } // If the mapping entry has no line range, preserve the input line number (if any). - if member.endline == 0 { + if member_endline == 0 { return Some(map_member_without_lines(frame, member)); } if let Some(mapped) = map_member_with_lines(frame, member) { @@ -465,8 +462,6 @@ impl<'s> ProguardMapper<'s> { original: member.method.name.as_str(), original_startline: member.original_startline, original_endline: member.original_endline, - has_minified_range: member.has_minified_range, - has_line_mapping: member.has_line_mapping, is_synthesized, is_outline, outline_callsite_positions, @@ -499,7 +494,9 @@ impl<'s> ProguardMapper<'s> { candidates .iter() .filter(|m| { - m.endline == 0 || (callsite_line >= m.startline && callsite_line <= m.endline) + m.endline.unwrap_or(0) == 0 + || (callsite_line >= m.startline.unwrap_or(0) + && callsite_line <= m.endline.unwrap_or(0)) }) .find_map(|m| { m.outline_callsite_positions @@ -588,7 +585,9 @@ impl<'s> ProguardMapper<'s> { }; if frame.parameters.is_none() { - let has_line_info = mapping_entries.iter().any(|m| m.endline > 0); + let has_line_info = mapping_entries + .iter() + .any(|m| m.endline.unwrap_or(0) > 0); // If the stacktrace has no line number, treat it as unknown and remap without // applying line filters. If there are base (no-line) mappings present, prefer those. @@ -610,7 +609,10 @@ impl<'s> ProguardMapper<'s> { } } Some(NoLineSelection::IterateBase) => { - for member in mapping_entries.iter().filter(|m| m.endline == 0) { + for member in mapping_entries + .iter() + .filter(|m| m.endline.unwrap_or(0) == 0) + { collected .frames .push(map_member_without_lines(&frame, member)); @@ -623,10 +625,10 @@ impl<'s> ProguardMapper<'s> { } for member in mapping_entries { - if has_line_info && member.endline == 0 { + if has_line_info && member.endline.unwrap_or(0) == 0 { continue; } - if member.endline == 0 { + if member.endline.unwrap_or(0) == 0 { collected .frames .push(map_member_without_lines(&frame, member)); @@ -700,7 +702,10 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - let has_line_info = members.all_mappings.iter().any(|m| m.endline > 0); + let has_line_info = members + .all_mappings + .iter() + .any(|m| m.endline.unwrap_or(0) > 0); RemappedFrameIter::members(frame, mappings, has_line_info) } diff --git a/src/utils.rs b/src/utils.rs index fa0089c..a7db5b7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,12 +4,12 @@ pub(crate) fn resolve_no_line_output_line( frame_line: usize, original_startline: Option, - startline: usize, - endline: usize, + startline: Option, + endline: Option, ) -> usize { if frame_line > 0 { frame_line - } else if startline == 0 && endline == 0 { + } else if startline.unwrap_or(0) == 0 && endline.unwrap_or(0) == 0 { original_startline.unwrap_or(0) } else { 0 From a8e4f56d6b49883fa206bdd7e680a1fcba523746 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 10 Feb 2026 13:39:05 +0100 Subject: [PATCH 4/5] Formatting --- src/builder.rs | 4 +--- src/cache/mod.rs | 8 ++------ src/mapper.rs | 4 +--- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 3627c49..ff29b26 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -292,9 +292,7 @@ impl<'s> ParsedProguardMapping<'s> { None }; let (mut startline, mut endline) = match line_mapping.as_ref() { - Some(lm) if lm.has_minified_range => { - (Some(lm.startline), Some(lm.endline)) - } + Some(lm) if lm.has_minified_range => (Some(lm.startline), Some(lm.endline)), _ => (None, None), }; let (mut original_startline, mut original_endline) = match line_mapping { diff --git a/src/cache/mod.rs b/src/cache/mod.rs index de2a98a..23de9d1 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -406,9 +406,7 @@ impl<'data> ProguardCache<'data> { } } - let has_line_info = mapping_entries - .iter() - .any(|m| m.endline().unwrap_or(0) > 0); + let has_line_info = mapping_entries.iter().any(|m| m.endline().unwrap_or(0) > 0); Some(( mapping_entries, @@ -999,9 +997,7 @@ fn iterate_with_lines<'a>( return map_member_without_lines(cache, frame, member, outer_source_file); } // skip any members which do not match our frames line - if member_endline > 0 - && (frame_line < member_startline || frame_line > member_endline) - { + if member_endline > 0 && (frame_line < member_startline || frame_line > member_endline) { continue; } let original_startline = member.original_startline().unwrap_or(0) as usize; diff --git a/src/mapper.rs b/src/mapper.rs index b24cd7e..34673ce 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -585,9 +585,7 @@ impl<'s> ProguardMapper<'s> { }; if frame.parameters.is_none() { - let has_line_info = mapping_entries - .iter() - .any(|m| m.endline.unwrap_or(0) > 0); + let has_line_info = mapping_entries.iter().any(|m| m.endline.unwrap_or(0) > 0); // If the stacktrace has no line number, treat it as unknown and remap without // applying line filters. If there are base (no-line) mappings present, prefer those. From f99aadeec906344b2e4fd304dd0d338fa7bfe4c6 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 13 Feb 2026 17:53:01 +0100 Subject: [PATCH 5/5] Address PR review feedback - Revert cache version bump (4 stays, struct layout unchanged) - Make LineMapping.startline/endline Optional, remove has_minified_range - Extract startline/endline variables in cache find_members_and_rules Co-Authored-By: Claude Opus 4.6 --- src/builder.rs | 4 ++-- src/cache/mod.rs | 7 +++---- src/cache/raw.rs | 2 +- src/mapping.rs | 46 ++++++++++++++++++---------------------------- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index ff29b26..2f70138 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -292,8 +292,8 @@ impl<'s> ParsedProguardMapping<'s> { None }; let (mut startline, mut endline) = match line_mapping.as_ref() { - Some(lm) if lm.has_minified_range => (Some(lm.startline), Some(lm.endline)), - _ => (None, None), + Some(lm) => (lm.startline, lm.endline), + None => (None, None), }; let (mut original_startline, mut original_endline) = match line_mapping { None => (None, None), diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 23de9d1..a8f38f2 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -390,10 +390,9 @@ impl<'data> ProguardCache<'data> { for member in mapping_entries { // Check if this member would produce a frame (line matching) let pf_line = prepared_frame.line.unwrap_or(0); - if member.endline().unwrap_or(0) == 0 - || (pf_line >= member.startline().unwrap_or(0) as usize - && pf_line <= member.endline().unwrap_or(0) as usize) - { + let startline = member.startline().unwrap_or(0) as usize; + let endline = member.endline().unwrap_or(0) as usize; + if endline == 0 || (pf_line >= startline && pf_line <= endline) { had_mappings = true; rewrite_rules.extend(self.decode_rewrite_rules(member)); } diff --git a/src/cache/raw.rs b/src/cache/raw.rs index b30498d..c1fd467 100644 --- a/src/cache/raw.rs +++ b/src/cache/raw.rs @@ -19,7 +19,7 @@ pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES); pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes(); /// The current version of the ProguardCache format. -pub const PRGCACHE_VERSION: u32 = 5; +pub const PRGCACHE_VERSION: u32 = 4; /// The header of a proguard cache file. #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/mapping.rs b/src/mapping.rs index 5df7dee..ebafa83 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -287,17 +287,14 @@ impl<'s> Iterator for ProguardRecordIter<'s> { /// All line mappings are 1-based and inclusive. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct LineMapping { - /// Start Line, 1-based. - pub startline: usize, - /// End Line, inclusive. - pub endline: usize, + /// Start Line, 1-based. `None` when no minified range prefix was present. + pub startline: Option, + /// End Line, inclusive. `None` when no minified range prefix was present. + pub endline: Option, /// The original Start Line. pub original_startline: Option, /// The original End Line. pub original_endline: Option, - /// Whether this mapping had an explicit minified line range prefix (e.g. `0:0:` or `1:5:`). - /// `false` when the mapping line had no range prefix (e.g. `void method():42 -> a`). - pub has_minified_range: bool, } /// An R8 header, as described in @@ -471,11 +468,10 @@ impl<'s> ProguardRecord<'s> { /// arguments: "", /// original_class: Some("com.example1.domain.MyBean"), /// line_mapping: Some(proguard::LineMapping { - /// startline: 1016, - /// endline: 1016, + /// startline: Some(1016), + /// endline: Some(1016), /// original_startline: Some(16), /// original_endline: Some(16), - /// has_minified_range: true, /// }), /// }) /// ); @@ -641,20 +637,18 @@ fn parse_proguard_field_or_method( let line_mapping = match (startline, endline, original_startline) { (Some(startline), Some(endline), _) => Some(LineMapping { - startline, - endline, + startline: Some(startline), + endline: Some(endline), original_startline, original_endline, - has_minified_range: true, }), // Preserve original line info even when no minified range is present. // This enables this crate to use the original line for no-line mappings. (None, None, Some(original_startline)) => Some(LineMapping { - startline: 0, - endline: 0, + startline: None, + endline: None, original_startline: Some(original_startline), original_endline, - has_minified_range: false, }), _ => None, }; @@ -987,11 +981,10 @@ mod tests { arguments: "androidx.appcompat.widget.Toolbar", original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"), line_mapping: Some(LineMapping { - startline: 14, - endline: 15, + startline: Some(14), + endline: Some(15), original_startline: None, original_endline: None, - has_minified_range: true, }), }), ); @@ -1010,11 +1003,10 @@ mod tests { arguments: "androidx.appcompat.widget.Toolbar", original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"), line_mapping: Some(LineMapping { - startline: 14, - endline: 15, + startline: Some(14), + endline: Some(15), original_startline: Some(436), original_endline: None, - has_minified_range: true, }), }), ); @@ -1033,11 +1025,10 @@ mod tests { arguments: "androidx.appcompat.widget.Toolbar", original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"), line_mapping: Some(LineMapping { - startline: 14, - endline: 15, + startline: Some(14), + endline: Some(15), original_startline: Some(436), original_endline: Some(437), - has_minified_range: true, }), }), ); @@ -1170,11 +1161,10 @@ androidx.activity.OnBackPressedCallback arguments: "", original_class: None, line_mapping: Some(LineMapping { - startline: 1, - endline: 4, + startline: Some(1), + endline: Some(4), original_startline: Some(184), original_endline: Some(187), - has_minified_range: true, }), }), Ok(ProguardRecord::R8Header(R8Header::Synthesized)),