From a273c7a46903d55d74d7942dbc3cbb914e08a15b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 20 Apr 2026 10:15:48 +0200 Subject: [PATCH 1/2] fix(mapping): Parse Compose mappings with flexible indentation Accept indented member lines instead of requiring exactly four spaces so raw ComposeStackTrace -> 98418compose blocks parse correctly. Bump the proguard cache format version so existing caches built from truncated Compose mappings are recalculated with the fixed parser. Co-Authored-By: Codex --- src/cache/raw.rs | 2 +- src/mapping.rs | 29 +++++++++++-------------- tests/retrace.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 0eb8c7f..32bad26 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)] diff --git a/src/mapping.rs b/src/mapping.rs index ebafa83..4166b51 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -161,13 +161,7 @@ impl<'s> ProguardMapping<'s> { /// let valid = ProguardMapping::new(b"a -> b:\n void method() -> b"); /// assert_eq!(valid.is_valid(), true); /// - /// let invalid = ProguardMapping::new( - /// br#" - /// # looks: like - /// a -> proguard: - /// mapping but(is) -> not - /// "#, - /// ); + /// let invalid = ProguardMapping::new(b"a -> proguard:\n not a valid proguard member line"); /// assert_eq!(invalid.is_valid(), false); /// ``` pub fn is_valid(&self) -> bool { @@ -505,7 +499,7 @@ fn parse_proguard_record(bytes: &[u8]) -> (Result, ParseError } else { parse_proguard_header(bytes) } - } else if bytes.starts_with(b" ") { + } else if matches!(bytes.first(), Some(b' ' | b'\t')) { parse_proguard_field_or_method(bytes) } else { parse_proguard_class(bytes) @@ -568,7 +562,7 @@ fn parse_proguard_field_or_method( // field line or method line: // `originalfieldtype originalfieldname -> obfuscatedfieldname` // `[startline:endline:]originalreturntype [originalclassname.]originalmethodname(originalargumenttype,...)[:originalstartline[:originalendline]] -> obfuscatedmethodname` - let bytes = parse_prefix(bytes, b" ")?; + let bytes = bytes.trim_ascii_start(); let (startline, bytes) = match parse_usize(bytes) { Ok((startline, bytes)) => (Some(startline), bytes), @@ -1063,15 +1057,15 @@ mod tests { } #[test] - fn try_parse_field_insufficient_leading_spaces() { - // only 2 leading spaces instead of 4 + fn try_parse_field_with_two_space_indentation() { let bytes = b" android.app.Activity mActivity -> a"; let parsed = ProguardRecord::try_parse(bytes); assert_eq!( parsed, - Err(ParseError { - line: bytes, - kind: ParseErrorKind::ParseError("line is not a valid proguard record"), + Ok(ProguardRecord::Field { + ty: "android.app.Activity", + original: "mActivity", + obfuscated: "a", }), ); } @@ -1145,9 +1139,10 @@ androidx.activity.OnBackPressedCallback original: "mEnabled", obfuscated: "a", }), - Err(ParseError { - line: b" boolean mEnabled -> a\n", - kind: ParseErrorKind::ParseError("line is not a valid proguard record"), + Ok(ProguardRecord::Field { + ty: "boolean", + original: "mEnabled", + obfuscated: "a", }), Ok(ProguardRecord::Field { ty: "java.util.ArrayDeque", diff --git a/tests/retrace.rs b/tests/retrace.rs index ca54fae..3b877c1 100644 --- a/tests/retrace.rs +++ b/tests/retrace.rs @@ -1,4 +1,4 @@ -use proguard::{ProguardMapper, StackFrame}; +use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame}; #[test] fn test_remap() { @@ -130,3 +130,56 @@ fn test_remap_just_method() { let ambiguous = mapper.remap_method("a.b.c.d", "buttonClicked"); assert_eq!(ambiguous, None); } + +#[test] +fn test_remap_compose_stacktrace_group_keys() { + let mapping = r#"ComposeStackTrace -> $$compose: + 1:1:androidx.compose.runtime.State androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(float,androidx.compose.animation.core.AnimationSpec,float,java.lang.String,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,int,int):71:71 -> m$1125598679 + 1:1:androidx.compose.runtime.State androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(float,androidx.compose.animation.core.AnimationSpec,float,java.lang.String,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,int,int):73:73 -> m$1125708605"#; + let mapper = ProguardMapper::from(mapping); + + let mapped = mapper + .remap_stacktrace( + r#"androidx.compose.runtime.ComposeTraceException: + at $$compose.m$1125598679(SourceFile:1) + at $$compose.m$1125708605(SourceFile:1)"#, + ) + .unwrap(); + + assert_eq!( + mapped.trim(), + r#"androidx.compose.runtime.ComposeTraceException: + at androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(AnimateAsState.kt:71) + at androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(AnimateAsState.kt:73)"# + .trim() + ); +} + +#[test] +fn test_remap_compose_stacktrace_group_keys_cache() { + let mapping = ProguardMapping::new( + br#"ComposeStackTrace -> $$compose: + 1:1:androidx.compose.runtime.State androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(float,androidx.compose.animation.core.AnimationSpec,float,java.lang.String,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,int,int):71:71 -> m$1125598679 + 1:1:androidx.compose.runtime.State androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(float,androidx.compose.animation.core.AnimationSpec,float,java.lang.String,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,int,int):73:73 -> m$1125708605"#, + ); + + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + + let mapped = cache + .remap_stacktrace( + r#"androidx.compose.runtime.ComposeTraceException: + at $$compose.m$1125598679(SourceFile:1) + at $$compose.m$1125708605(SourceFile:1)"#, + ) + .unwrap(); + + assert_eq!( + mapped.trim(), + r#"androidx.compose.runtime.ComposeTraceException: + at androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(AnimateAsState.kt:71) + at androidx.compose.animation.core.AnimateAsStateKt.animateFloatAsState(AnimateAsState.kt:73)"# + .trim() + ); +} From 200b1cdeadf8eb92443c5665699fb946703b4547 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 21 Apr 2026 10:49:13 +0200 Subject: [PATCH 2/2] fix(cache): Keep PRGCACHE_VERSION unchanged Leave the cache format version alone in rust-proguard because the parser compatibility fix does not change the serialized cache layout. Cache invalidation for existing symbolicator caches will happen on the consumer side instead. Co-Authored-By: Codex --- src/cache/raw.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cache/raw.rs b/src/cache/raw.rs index 32bad26..0eb8c7f 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)]