diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 9c9e26a..dc80e8a 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1144,10 +1144,12 @@ fn iterate_without_lines<'a>( /// Resolves frames for the no-line (frame_line==0) case. /// /// When the input frame has no line number, base entries (endline==0) are preferred -/// over line-mapped entries. Base entries are split into two groups by whether -/// `startline` is present (0:0 entries) or absent (no-range entries), and each -/// group's output lines are computed based on whether the group contains range -/// mappings, single-line mappings, or bare methods. +/// over line-mapped entries, since they are not keyed to a line either. Base entries +/// are split into two groups by whether `startline` is present (0:0 entries) or +/// absent (no-range entries), and each group's output lines are computed based on +/// whether the group contains range mappings, single-line mappings, or bare methods. +/// +/// This mirrors [`crate::mapper`]'s `resolve_no_line_frames`; keep the two in sync. fn resolve_no_line_frames<'a>( cache: &ProguardCache<'a>, frame: &StackFrame<'a>, @@ -1163,57 +1165,23 @@ fn resolve_no_line_frames<'a>( return resolve_base_entries(cache, frame, &base_entries, outer_source_file); } - // No base entries — check if the first range group forms an inline group - // (multiple entries sharing the same startline/endline). If so, resolve - // that group with proper output lines. Otherwise, fall back to emitting - // a single frame with line 0 (ambiguous non-inline case). - // - // This matches retrace's `allRangesForLine(0, true)` which picks the first - // range containing line 0 and returns all entries in that range group. - // Whether this is intentional retrace behavior or accidental is debatable, - // but we match it because users compare our output against retrace-based tools. + // Every entry is keyed by a minified line range the frame cannot match. Entries + // sharing a range are an inlined call stack R8 flattened into one place; entries + // with differing ranges are unrelated bodies merged under one name. Neither can + // be resolved without a line number, so a single frame is reported: the outermost + // entry of the first group, i.e. the method that physically exists on this class. + // See `resolve_no_line_frames` in `crate::mapper` for the reasoning. let mut frames = Vec::new(); if let Some(first) = members.first() { - let first_start = first.startline(); - let first_end = first.endline(); - let first_group: Vec<_> = members + let outermost = members .iter() - .take_while(|m| m.startline() == first_start && m.endline() == first_end) - .collect(); - - if first_group.len() > 1 { - // Inline group: multiple entries share the same range. - // Resolve each with its proper original line. - for member in &first_group { - let line = compute_member_output_line(member).or(Some(0)); - if let Some(f) = - map_member_without_lines(cache, frame, member, outer_source_file, line) - { - frames.push(f); - } - } - } else { - // Ambiguous: each entry has a different range. Collapse to one - // frame with line 0, matching retrace behavior. - let all_same = members.iter().all(|m| { - m.original_class_offset == first.original_class_offset - && m.original_name_offset == first.original_name_offset - }); - if all_same { - if let Some(f) = - map_member_without_lines(cache, frame, first, outer_source_file, Some(0)) - { - frames.push(f); - } - } else { - for member in members { - if let Some(f) = - map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) - { - frames.push(f); - } - } - } + .take_while(|m| m.startline() == first.startline() && m.endline() == first.endline()) + .last() + .unwrap_or(first); + if let Some(f) = + map_member_without_lines(cache, frame, outermost, outer_source_file, Some(0)) + { + frames.push(f); } } frames diff --git a/src/mapper.rs b/src/mapper.rs index 79f05ea..fcf0ab1 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -303,10 +303,13 @@ impl FusedIterator for RemappedFrameIter<'_> {} /// Resolves frames for the no-line (frame_line==0) case. /// /// When the input frame has no line number, base entries (endline==0) are preferred -/// over line-mapped entries. Base entries are split into two groups by whether -/// `startline` is present (0:0 entries) or absent (no-range entries), and each -/// group's output lines are computed based on whether the group contains range -/// mappings, single-line mappings, or bare methods. +/// over line-mapped entries, since they are not keyed to a line either. Base entries +/// are split into two groups by whether `startline` is present (0:0 entries) or +/// absent (no-range entries), and each group's output lines are computed based on +/// whether the group contains range mappings, single-line mappings, or bare methods. +/// +/// If there are no base entries, every entry is keyed by a minified range that the +/// frame cannot match, and the entries are resolved by shape instead: see below. fn resolve_no_line_frames<'s>( frame: &StackFrame<'s>, mapping_entries: &'s [MemberMapping<'s>], @@ -318,54 +321,38 @@ fn resolve_no_line_frames<'s>( return; } - // No base entries — check if the first range group forms an inline group - // (multiple entries sharing the same startline/endline). If so, resolve - // that group with proper output lines. Otherwise, fall back to emitting - // a single frame with line 0 (ambiguous non-inline case). + // Every entry is keyed by a minified line range, and the frame carries no line + // to select one with. R8 reuses a single obfuscated name for many unrelated + // method bodies, and it also flattens inlined call stacks into consecutive + // entries sharing one range. Neither can be resolved without a line number. // - // This matches retrace's `allRangesForLine(0, true)` which picks the first - // range containing line 0 and returns all entries in that range group. - // Whether this is intentional retrace behavior or accidental is debatable, - // but we match it because users compare our output against retrace-based tools. + // Retrace emits a single frame here: the method that physically exists on this + // class. It reaches that by taking the first range group and reporting only its + // outermost entry -- the caller the inlined bodies were spliced into. Inlinees + // are dropped, because which one ran is exactly what the missing line would have + // told us. We do the same, so that a frame without line information deobfuscates + // identically in both tools. let Some(first) = mapping_entries.first() else { return; }; - let first_start = first.startline; - let first_end = first.endline; - let first_group: Vec<_> = mapping_entries + // The outermost entry of a group is its last member; R8 lists inlinees + // innermost-first, with the enclosing method last. A group of one is its own + // outermost entry. + let outermost = mapping_entries .iter() - .take_while(|m| m.startline == first_start && m.endline == first_end) - .collect(); - - if first_group.len() > 1 { - // Inline group: multiple entries share the same range. - // Resolve each with its proper original line. - for member in &first_group { - let line = member.original_startline.filter(|&v| v > 0).or(Some(0)); - collected - .frames - .push(map_member_without_lines(frame, member, line)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - } else { - // Ambiguous: each entry has a different range. Collapse to one - // frame with line 0, matching retrace behavior. - let unambiguous = mapping_entries.iter().all(|m| m.original == first.original); - if unambiguous { - collected - .frames - .push(map_member_without_lines(frame, first, Some(0))); - collected.rewrite_rules.extend(first.rewrite_rules.iter()); - } else { - for member in mapping_entries { - collected - .frames - .push(map_member_without_lines(frame, member, Some(0))); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - } - } + .take_while(|m| m.startline == first.startline && m.endline == first.endline) + .last() + .unwrap_or(first); + + // The entry's rewrite rules are deliberately not collected. `removeInnerFrames` + // trims an inline chain, and we just established there is no chain to trim: the + // one frame we report is the outermost one, which the rule would never remove. + // Forwarding the rules would instead delete the sole frame and lose the + // stacktrace entirely. Retrace keeps the frame here too. + collected + .frames + .push(map_member_without_lines(frame, outermost, Some(0))); } /// Resolves output lines for base (endline==0) entries when the frame has no line number. diff --git a/tests/r8-ambiguous.rs b/tests/r8-ambiguous.rs index be1eb42..621ebd9 100644 --- a/tests/r8-ambiguous.rs +++ b/tests/r8-ambiguous.rs @@ -5,6 +5,10 @@ use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; +/// Excerpt of a real R8 mapping: the `a1.y` class block, trimmed to a representative +/// spread of the method bodies R8 merged into the obfuscated method `j`. +static MAPPING_AMBIGUOUS: &str = include_str!("res/mapping-ambiguous.txt"); + // ============================================================================= // AmbiguousStackTrace // ============================================================================= @@ -342,3 +346,36 @@ Exception in thread \"main\" java.lang.NullPointerException let actual = cache.remap_stacktrace(input).unwrap(); assert_eq!(actual.trim(), expected.trim()); } + +// ============================================================================= +// DistinctRangesNoLineNumberStackTrace +// ============================================================================= + +/// Tombstone stacktraces carry no line numbers, so a frame is just `a1.y.j`. +/// +/// R8 can merge many unrelated method bodies into the single obfuscated method `j`, +/// keeping them apart only by minified line range. Every entry has its own distinct +/// range, so a frame without a line matches none of them and nothing is left to +/// disambiguate them. +/// +/// Instead of emitting every entry, which fabricates a garbage stack, we only +/// report a single frame asserted below. +#[test] +fn test_distinct_ranges_no_line_number_stacktrace() { + let input = " at a1.y.j(Unknown Source)"; + + let expected = "at androidx.compose.foundation.text.TextFieldDelegate$Companion$updateTextLayoutResult$1$1$1.invoke(TextFieldDelegate.kt:0)"; + + let mapper = ProguardMapper::from(MAPPING_AMBIGUOUS); + let actual = mapper.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); + + let mapping = ProguardMapping::new(MAPPING_AMBIGUOUS.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + + let actual = cache.remap_stacktrace(input).unwrap(); + assert_eq!(actual.trim(), expected.trim()); +} diff --git a/tests/r8-inline.rs b/tests/r8-inline.rs index 6dc7ac9..a0b0375 100644 --- a/tests/r8-inline.rs +++ b/tests/r8-inline.rs @@ -787,3 +787,174 @@ fn test_inline_with_zero_original_line() { assert_eq!(frames[1].method(), "caller"); assert_eq!(frames[1].line(), Some(10)); } + +// ============================================================================= +// InlineChainNoLineNumberStackTrace +// ============================================================================= + +/// A three-deep inline chain: `crash(boolean)` and `crash()` were inlined into +/// `onFabClicked`, so all three entries share the minified range `1:8`. +const INLINE_CHAIN_NO_LINE_MAPPING: &str = r#"com.example.HomeFragment -> com.example.HomeFragment: + 1:8:void crash(boolean):184:184 -> onFabClicked + 1:8:void crash():178 -> onFabClicked + 1:8:void onFabClicked():174 -> onFabClicked +"#; + +/// With a line number the chain resolves and every inlined frame is reported. +#[test] +fn test_inline_chain_with_line_number() { + let input = " at com.example.HomeFragment.onFabClicked(SourceFile:5)"; + + let expected = "\ + at com.example.HomeFragment.crash(HomeFragment.java:184) + at com.example.HomeFragment.crash(HomeFragment.java:178) + at com.example.HomeFragment.onFabClicked(HomeFragment.java:174)"; + + let mapper = ProguardMapper::from(INLINE_CHAIN_NO_LINE_MAPPING); + assert_eq!( + mapper.remap_stacktrace(input).unwrap().trim(), + expected.trim() + ); + + let mapping = ProguardMapping::new(INLINE_CHAIN_NO_LINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + assert_eq!( + cache.remap_stacktrace(input).unwrap().trim(), + expected.trim() + ); +} + +/// Without a line number, which inlinee ran is unknowable, so only the outermost entry -- +/// the method that physically exists on the class -- is reported. Retrace prints +/// the same single frame (without the `:0`). +#[test] +fn test_inline_chain_no_line_number() { + let input = " at com.example.HomeFragment.onFabClicked(Unknown Source)"; + let expected = " at com.example.HomeFragment.onFabClicked(HomeFragment.java:0)"; + + let mapper = ProguardMapper::from(INLINE_CHAIN_NO_LINE_MAPPING); + assert_eq!( + mapper.remap_stacktrace(input).unwrap().trim(), + expected.trim() + ); + + let mapping = ProguardMapping::new(INLINE_CHAIN_NO_LINE_MAPPING.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&mapping, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + assert_eq!( + cache.remap_stacktrace(input).unwrap().trim(), + expected.trim() + ); +} + +// ============================================================================= +// InlineChainNoLineNumberStackTrace + rewriteFrame +// ============================================================================= + +/// The same three-deep chain, with a `removeInnerFrames(1)` rule attached to it. +const INLINE_CHAIN_REWRITE_MAPPING: &str = r#"com.example.HomeFragment -> com.example.HomeFragment: + 1:8:void crash(boolean):184:184 -> onFabClicked + 1:8:void crash():178 -> onFabClicked + 1:8:void onFabClicked():174 -> onFabClicked + # {"id":"com.android.tools.r8.rewriteFrame","conditions":["throws(Ljava/lang/NullPointerException;)"],"actions":["removeInnerFrames(1)"]} +"#; + +/// As above, but removing more frames than the no-line case reports at all. +const INLINE_CHAIN_REWRITE_TWO_MAPPING: &str = r#"com.example.HomeFragment -> com.example.HomeFragment: + 1:8:void crash(boolean):184:184 -> onFabClicked + 1:8:void crash():178 -> onFabClicked + 1:8:void onFabClicked():174 -> onFabClicked + # {"id":"com.android.tools.r8.rewriteFrame","conditions":["throws(Ljava/lang/NullPointerException;)"],"actions":["removeInnerFrames(2)"]} +"#; + +/// Asserts that both the mapper and the cache remap `input` to `expected`. +#[track_caller] +#[expect( + clippy::unwrap_used, + reason = "`allow-unwrap-in-tests` only covers `#[test]` fns, not helpers" +)] +fn assert_remaps(mapping: &str, input: &str, expected: &str) { + let mapper = ProguardMapper::from(mapping); + assert_eq!( + mapper.remap_stacktrace(input).unwrap().trim(), + expected.trim(), + "mapper" + ); + + let parsed = ProguardMapping::new(mapping.as_bytes()); + let mut buf = Vec::new(); + ProguardCache::write(&parsed, &mut buf).unwrap(); + let cache = ProguardCache::parse(&buf).unwrap(); + cache.test(); + assert_eq!( + cache.remap_stacktrace(input).unwrap().trim(), + expected.trim(), + "cache" + ); +} + +/// With a line number the chain resolves, so `removeInnerFrames(1)` has a real +/// inline chain to trim and drops the innermost frame. +#[test] +fn test_inline_chain_rewrite_with_line_number() { + assert_remaps( + INLINE_CHAIN_REWRITE_MAPPING, + "\ +java.lang.NullPointerException: Boom + at com.example.HomeFragment.onFabClicked(SourceFile:5)", + "\ +java.lang.NullPointerException: Boom + at com.example.HomeFragment.crash(HomeFragment.java:178) + at com.example.HomeFragment.onFabClicked(HomeFragment.java:174)", + ); +} + +/// Without a line number the chain is not reconstructed, so there are no inner +/// frames for the rule to trim and the outermost frame must survive. Retrace +/// 8.9.27 prints it too (without the `:0`). NPE tombstones are exactly this +/// shape, so dropping the frame here would lose the whole stacktrace. +#[test] +fn test_inline_chain_rewrite_no_line_number() { + assert_remaps( + INLINE_CHAIN_REWRITE_MAPPING, + "\ +java.lang.NullPointerException: Boom + at com.example.HomeFragment.onFabClicked(Unknown Source)", + "\ +java.lang.NullPointerException: Boom + at com.example.HomeFragment.onFabClicked(HomeFragment.java:0)", + ); +} + +/// Same, with a removal count exceeding the number of frames reported. +#[test] +fn test_inline_chain_rewrite_two_no_line_number() { + assert_remaps( + INLINE_CHAIN_REWRITE_TWO_MAPPING, + "\ +java.lang.NullPointerException: Boom + at com.example.HomeFragment.onFabClicked(Unknown Source)", + "\ +java.lang.NullPointerException: Boom + at com.example.HomeFragment.onFabClicked(HomeFragment.java:0)", + ); +} + +/// A thrown type the rule does not name leaves the frame alone either way. +#[test] +fn test_inline_chain_rewrite_condition_mismatch_no_line_number() { + assert_remaps( + INLINE_CHAIN_REWRITE_MAPPING, + "\ +java.lang.IllegalStateException: Boom + at com.example.HomeFragment.onFabClicked(Unknown Source)", + "\ +java.lang.IllegalStateException: Boom + at com.example.HomeFragment.onFabClicked(HomeFragment.java:0)", + ); +} diff --git a/tests/r8-method-overloading.rs b/tests/r8-method-overloading.rs index 3dae049..ef4183a 100644 --- a/tests/r8-method-overloading.rs +++ b/tests/r8-method-overloading.rs @@ -124,3 +124,36 @@ fn test_retrace_mapping_with_overloads_api_includes_sync_with_line() { let remapped: Vec<_> = cache.remap_frame(&frame).collect(); assert!(remapped.iter().any(|f| f.method() == "sync")); } + +// ============================================================================= +// OverloadsWithDistinctRangesNoLineNumberStackTrace +// ============================================================================= + +/// Real overloads: three signatures share one obfuscated name, kept apart only by +/// minified line range. +const OVERLOADS_DISTINCT_RANGES_MAPPING: &str = r#"androidx.activity.ComponentActivity -> androidx.activity.aux: + 1:2:void setContentView(int):379:380 -> setContentView + 3:4:void setContentView(android.view.View):385:386 -> setContentView + 5:6:void setContentView(android.view.View,android.view.ViewGroup$LayoutParams):393:394 -> setContentView +"#; + +/// With a line number the range selects exactly one overload. +#[test] +fn test_overloads_distinct_ranges_with_line_number() { + assert_remap_stacktrace( + OVERLOADS_DISTINCT_RANGES_MAPPING, + " at androidx.activity.aux.setContentView(SourceFile:3)", + " at androidx.activity.ComponentActivity.setContentView(ComponentActivity.java:385)", + ); +} + +/// Without one, no overload can be selected, so a single frame is reported. +/// Retrace prints the same frame (without the `:0`). +#[test] +fn test_overloads_distinct_ranges_no_line_number() { + assert_remap_stacktrace( + OVERLOADS_DISTINCT_RANGES_MAPPING, + " at androidx.activity.aux.setContentView(Unknown Source)", + " at androidx.activity.ComponentActivity.setContentView(ComponentActivity.java:0)", + ); +} diff --git a/tests/r8.rs b/tests/r8.rs index 8d3ba37..424b7a6 100644 --- a/tests/r8.rs +++ b/tests/r8.rs @@ -581,10 +581,10 @@ fn test_method_with_zero_zero_and_line_specific_mappings_cache() { #[test] fn test_inline_group_no_base_entries() { - // Regression test: when a frame has no line number and all mapping entries - // have non-zero endline (no base entries), the first range group should be - // detected as an inline chain and each entry should get its proper original - // line number instead of all being collapsed to line 0. + // When a frame has no line number and every mapping entry has a minified + // range, the inline chain cannot be resolved: which inlinee ran is exactly + // what the missing line would have told us. Only the outermost entry -- the + // method that physically exists on the class -- is reported, matching retrace. let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_INLINE_NO_BASE.as_bytes())); let test = mapper.remap_stacktrace( @@ -596,9 +596,7 @@ fn test_inline_group_no_base_entries() { assert_eq!( test.unwrap().trim(), r#"java.lang.RuntimeException: Crash - at com.example.app.MainActivity.innerCall(MainActivity.kt:54) - at com.example.app.MainActivity.middleCall(MainActivity.kt:44) - at com.example.app.MainActivity.onClick(MainActivity.kt:30)"# + at com.example.app.MainActivity.onClick(MainActivity.kt:0)"# .trim() ); } @@ -617,18 +615,8 @@ fn test_inline_group_no_base_entries_cache() { let frame1 = mapped.next().unwrap(); assert_eq!(frame1.class(), "com.example.app.MainActivity"); - assert_eq!(frame1.method(), "innerCall"); - assert_eq!(frame1.line(), Some(54)); - - let frame2 = mapped.next().unwrap(); - assert_eq!(frame2.class(), "com.example.app.MainActivity"); - assert_eq!(frame2.method(), "middleCall"); - assert_eq!(frame2.line(), Some(44)); - - let frame3 = mapped.next().unwrap(); - assert_eq!(frame3.class(), "com.example.app.MainActivity"); - assert_eq!(frame3.method(), "onClick"); - assert_eq!(frame3.line(), Some(30)); + assert_eq!(frame1.method(), "onClick"); + assert_eq!(frame1.line(), Some(0)); assert_eq!(mapped.next(), None); } diff --git a/tests/res/mapping-ambiguous.txt b/tests/res/mapping-ambiguous.txt new file mode 100644 index 0000000..5f76d95 --- /dev/null +++ b/tests/res/mapping-ambiguous.txt @@ -0,0 +1,30 @@ +androidx.compose.foundation.BorderModifierNode$drawWithCacheModifierNode$1 -> a1.y: +# {"id":"sourceFile","fileName":"Border.kt"} + 10:11:java.lang.Object androidx.compose.foundation.text.TextFieldDelegate$Companion$updateTextLayoutResult$1$1$1.invoke(java.lang.Object):256:256 -> j + 34:35:java.lang.Object androidx.compose.foundation.text.AndroidCursorHandle_androidKt$CursorHandle$finalModifier$1$1.invoke(java.lang.Object):54:54 -> j + 62:63:java.lang.Object coil.disk.DiskLruCache$newJournalWriter$faultHidingSink$1.invoke(java.lang.Object):247:247 -> j + 74:75:java.lang.Object androidx.compose.ui.platform.RenderNodeLayer$updateDisplayList$1$1.invoke(java.lang.Object):323:323 -> j + 87:88:java.lang.Object androidx.compose.ui.platform.InputMethodSession$createInputConnection$1$1.invoke(java.lang.Object):143:143 -> j + 144:145:java.lang.Object androidx.compose.ui.platform.GraphicsLayerOwnerLayer$recordLambda$1.invoke(java.lang.Object):259:259 -> j + 196:197:java.lang.Object androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$2$1.invoke(java.lang.Object):96:96 -> j + 209:210:java.lang.Object androidx.compose.ui.viewinterop.AndroidViewHolder$layoutNode$1$2.invoke(java.lang.Object):448:448 -> j + 221:222:java.lang.Object androidx.compose.ui.node.NodeChainKt$fillVector$1.invoke(java.lang.Object):801:801 -> j + 233:234:java.lang.Object androidx.compose.ui.node.AlignmentLines$recalculate$1.invoke(java.lang.Object):143:143 -> j + 389:390:java.lang.Object androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItemKt$LazyLayoutPinnableItem$1$1.invoke(java.lang.Object):55:55 -> j + 402:403:java.lang.Object androidx.compose.foundation.lazy.layout.LazyLayoutItemContentFactory$CachedItemContent$createContentLambda$1$1$1.invoke(java.lang.Object):104:104 -> j + 415:416:java.lang.Object io.sentry.okhttp.SentryOkHttpEventListener$responseHeadersEnd$1.invoke(java.lang.Object):300:300 -> j + 455:456:java.lang.Object io.sentry.okhttp.SentryOkHttpEventListener$proxySelectEnd$1.invoke(java.lang.Object):112:112 -> j + 492:498:java.lang.Object io.sentry.okhttp.SentryOkHttpEventListener$2.invoke(java.lang.Object):74:74 -> j + 508:509:java.lang.Object io.sentry.android.replay.ReplayIntegration$captureReplay$1.invoke(java.lang.Object):259:259 -> j + 642:647:java.lang.Object androidx.compose.foundation.lazy.grid.LazyGridState$scrollableState$1.invoke(java.lang.Object):225:225 -> j + 806:807:java.lang.Object androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$awaitPointerEventScope$2$2.invoke(java.lang.Object):793:793 -> j + 825:826:java.lang.Object androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$2.invoke(java.lang.Object):273:273 -> j + 843:844:java.lang.Object androidx.navigation.compose.NavHostControllerKt$NavControllerSaver$2.invoke(java.lang.Object):80:80 -> j + 1023:1024:java.lang.Object androidx.navigation.compose.NavBackStackEntryProviderKt$SaveableStateProvider$1.invoke(java.lang.Object):61:61 -> j + 1041:1042:java.lang.Object androidx.compose.foundation.gestures.ScrollingLogic$performScrollForOverscroll$1.invoke(java.lang.Object):697:697 -> j + 1063:1064:java.lang.Object androidx.compose.foundation.gestures.ScrollableNode$1.invoke(java.lang.Object):322:322 -> j + 1103:1104:java.lang.Object androidx.compose.foundation.gestures.DragGestureDetectorKt$detectDragGestures$6.invoke(java.lang.Object):177:177 -> j + 1115:1116:java.lang.Object androidx.navigation.Navigator$navigate$1.invoke(java.lang.Object):116:116 -> j + 1176:1181:java.lang.Object androidx.compose.foundation.ScrollState$scrollableState$1.invoke(java.lang.Object):130:130 -> j + 1247:1248:java.lang.Object androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(java.lang.Object):693:693 -> j + 1267:1268:java.lang.Object invoke(java.lang.Object):158:158 -> j