├── .craft.yml ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ ├── enforce-license-compliance.yml │ ├── release.yml │ └── weekly.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── benches ├── proguard_mapping.rs └── proguard_parsing.rs ├── rustfmt.toml ├── scripts └── bump-version.sh ├── src ├── cache │ ├── debug.rs │ ├── mod.rs │ └── raw.rs ├── java.rs ├── lib.rs ├── mapper.rs ├── mapping.rs └── stacktrace.rs └── tests ├── basic.rs ├── callback.rs ├── r8.rs ├── res ├── mapping-callback-extra-class.txt ├── mapping-callback-extra-class_EditActivity.kt ├── mapping-callback-extra-class_TestSourceContext.kt ├── mapping-callback-inner-class.txt ├── mapping-callback-inner-class_EditActivity.kt ├── mapping-callback.txt ├── mapping-callback_EditActivity.kt ├── mapping-inlines.txt ├── mapping-r8-symbolicated_file_names.txt ├── mapping-r8.txt └── mapping.txt └── retrace.rs /.craft.yml: -------------------------------------------------------------------------------- 1 | minVersion: "0.15.0" 2 | github: 3 | owner: getsentry 4 | repo: rust-proguard 5 | changelogPolicy: auto 6 | 7 | statusProvider: 8 | name: github 9 | artifactProvider: 10 | name: none 11 | 12 | targets: 13 | - name: crates 14 | - name: github 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/res/*.txt text eol=lf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @getsentry/processing 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - "release/**" 8 | pull_request: 9 | 10 | env: 11 | RUSTFLAGS: -Dwarnings 12 | 13 | jobs: 14 | lints: 15 | name: Style/Linting 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy --no-self-update 22 | - uses: Swatinem/rust-cache@v2 23 | 24 | - run: cargo fmt --all -- --check 25 | - run: cargo clippy --all-features --workspace --tests --examples -- -D clippy::all 26 | - run: cargo doc --workspace --all-features --document-private-items --no-deps 27 | 28 | test: 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [ubuntu-latest] 33 | 34 | name: Tests on ${{ matrix.os }} 35 | runs-on: ${{ matrix.os }} 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - run: rustup toolchain install stable --profile minimal --no-self-update 41 | - uses: Swatinem/rust-cache@v2 42 | 43 | - run: cargo test --workspace --all-features --all-targets 44 | - run: cargo test --workspace --all-features --doc 45 | 46 | codecov: 47 | name: Code Coverage 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - run: rustup toolchain install stable --profile minimal --component llvm-tools-preview --no-self-update 54 | - uses: Swatinem/rust-cache@v2 55 | - uses: taiki-e/install-action@cargo-llvm-cov 56 | 57 | - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 58 | 59 | - uses: codecov/codecov-action@v3 60 | with: 61 | files: lcov.info 62 | -------------------------------------------------------------------------------- /.github/workflows/enforce-license-compliance.yml: -------------------------------------------------------------------------------- 1 | name: Enforce License Compliance 2 | 3 | on: 4 | push: 5 | branches: [master, release/*] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | enforce-license-compliance: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: "Enforce License Compliance" 14 | uses: getsentry/action-enforce-license-compliance@main 15 | with: 16 | fossa_api_key: ${{ secrets.FOSSA_API_KEY }} 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: Version to release 7 | required: false 8 | force: 9 | description: Force a release even when there are release-blockers (optional) 10 | required: false 11 | merge_target: 12 | description: Target branch to merge into. Uses the default branch as a fallback (optional) 13 | required: false 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | name: "Release a new version" 18 | steps: 19 | - name: Get auth token 20 | id: token 21 | uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 22 | with: 23 | app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} 24 | private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} 25 | - uses: actions/checkout@v4 26 | with: 27 | # Fetch all commits so we can determine previous version 28 | fetch-depth: 0 29 | token: ${{ steps.token.outputs.token }} 30 | - name: Prepare release 31 | uses: getsentry/action-prepare-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ steps.token.outputs.token }} 34 | with: 35 | version: ${{ github.event.inputs.version }} 36 | force: ${{ github.event.inputs.force }} 37 | -------------------------------------------------------------------------------- /.github/workflows/weekly.yml: -------------------------------------------------------------------------------- 1 | name: Weekly CI 2 | 3 | on: 4 | schedule: 5 | - cron: "14 3 * * 5" # every friday at 03:14 6 | workflow_dispatch: 7 | 8 | env: 9 | RUSTFLAGS: -Dwarnings 10 | 11 | jobs: 12 | weekly-ci: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | rust: [nightly, beta] 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - run: | 24 | rustup toolchain install ${{ matrix.rust }} --profile minimal --component clippy --no-self-update 25 | rustup default ${{ matrix.rust }} 26 | 27 | - run: cargo clippy --all-features --workspace --tests --examples -- -D clippy::all 28 | - run: cargo test --workspace --all-features --all-targets 29 | - run: cargo test --workspace --all-features --doc 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.5.0 4 | 5 | ### Various fixes & improvements 6 | 7 | - Commit Cargo.lock (#43) by @loewenheim 8 | - Update edition to 2021 (#43) by @loewenheim 9 | - Don't rename uuid dep (#43) by @loewenheim 10 | - Remove ClassIndex (#43) by @loewenheim 11 | - Use chars (#43) by @loewenheim 12 | - Add module docs (#42) by @loewenheim 13 | - Write vectors at once (#42) by @loewenheim 14 | - Feedback (#42) by @loewenheim 15 | - Abstract out the binary search and use it in remap_method (#42) by @loewenheim 16 | - cache.rs -> cache/mod.rs (#42) by @loewenheim 17 | - Add parsing + mapping benchmarks (#42) by @loewenheim 18 | - Missed something (#42) by @loewenheim 19 | - Use correct name of cache everywhere (#42) by @loewenheim 20 | - Rename error types (#42) by @loewenheim 21 | - debug: use references (#42) by @loewenheim 22 | - Update src/cache/raw.rs (#42) by @loewenheim 23 | - Add cache to parsing benchmark (#42) by @loewenheim 24 | - Remove unwrap in binary search (#42) by @loewenheim 25 | - Remove test I committed by mistake (#42) by @loewenheim 26 | - Typo (#42) by @loewenheim 27 | - Cleanup (#42) by @loewenheim 28 | - Expand test coverage (#42) by @loewenheim 29 | - Add remapping benchmark (#42) by @loewenheim 30 | - Use binary search more aggressively (#42) by @loewenheim 31 | 32 | _Plus 10 more_ 33 | 34 | ## 5.4.1 35 | 36 | ### Various fixes & improvements 37 | 38 | - Update CI definitions (#37) by @Swatinem 39 | - feat: add method signature parsing (#35) by @viglia 40 | - Support symbolicated file names (#36) by @wzieba 41 | 42 | ## 5.4.0 43 | 44 | ### Various fixes & improvements 45 | 46 | - enhance: make mapping by params initialization optional (#34) by @viglia 47 | - Clear `unique_methods` per `class` (#33) by @Swatinem 48 | 49 | ## 5.3.0 50 | 51 | ### Various fixes & improvements 52 | 53 | - Add getter method for private parameters struct field (#32) by @viglia 54 | - release: 5.2.0 (8032053d) by @getsentry-bot 55 | 56 | ## 5.2.0 57 | 58 | - No documented changes. 59 | 60 | ## 5.1.0 61 | 62 | ### Various fixes & improvements 63 | 64 | - Allow remapping just a method without line numbers (#30) by @Swatinem 65 | 66 | ## 5.0.2 67 | 68 | ### Various fixes & improvements 69 | 70 | - Fix line number mismatch for callbacks (#27) by @adinauer 71 | 72 | ## 5.0.1 73 | 74 | ### Various fixes & improvements 75 | 76 | - perf(proguard): Try to optimize proguard mapping parsing (#26) by @Zylphrex 77 | 78 | ## 5.0.0 79 | 80 | **Breaking Changes**: 81 | 82 | - Update `uuid` dependency to version `1.0.0`. ([#22](https://github.com/getsentry/rust-proguard/pull/22)) 83 | 84 | **Thank you**: 85 | 86 | Features, fixes and improvements in this release have been contributed by: 87 | 88 | - [@jhpratt](https://github.com/jhpratt) 89 | 90 | ## 4.1.1 91 | 92 | **Fixes**: 93 | 94 | - Removed overly conservative limit in `has_line_info`. 95 | 96 | **Thank you**: 97 | 98 | Features, fixes and improvements in this release have been contributed by: 99 | 100 | - [@JaCzekanski](https://github.com/JaCzekanski) 101 | 102 | ## 4.1.0 103 | 104 | **Features**: 105 | 106 | - A new `MappingSummary` was added providing some information about the mapping file. 107 | - Added support for remapping a complete Java `StackTrace`, including the `Throwable` and cause chain. 108 | 109 | **Thank you**: 110 | 111 | Features, fixes and improvements in this release have been contributed by: 112 | 113 | - [@dnaka91](https://github.com/dnaka91) 114 | 115 | ## 4.0.1 116 | 117 | **Fixes** 118 | 119 | - Fix `has_line_info` to not short-circuit when it found lines _without_ line-info. 120 | 121 | ## 4.0.0 122 | 123 | This is a complete rewrite of the crate. 124 | It focuses on two types, `ProguardMapping` and `ProguardMapper`. 125 | 126 | **ProguardMapping** 127 | 128 | Provides high level metadata about a proguard mapping file, and allows iterating 129 | over the contained `ProguardRecord`s. 130 | 131 | This is a replacement for the previous `Parser`. For example, 132 | `Parser::has_line_info()` becomes `ProguardMapping::has_line_info()`. 133 | 134 | **ProguardMapper** 135 | 136 | Allows re-mapping class names and entire frames, with support for inlined frames. 137 | 138 | This is a replacement for the previous `MappingView`, and allows easier 139 | re-mapping of both class-names and complete frames. 140 | 141 | `MappingView::find_class("obfuscated").map(Class::class_name)` becomes 142 | `ProguardMapper::remap_class("obfuscated")`, and the 143 | `ProguardMapper::remap_frame` function replaces manually collecting and 144 | processing the results of `Class::get_methods`. 145 | 146 | ## 3.0.0 147 | 148 | - Update `uuid` to `0.8.1`. 149 | 150 | ## 2.0.0 151 | 152 | - Implement support for R8. 153 | - Update `uuid` to `0.7.2`. 154 | 155 | ## 1.1.0 156 | 157 | - Update `uuid` to `0.6.0`. 158 | 159 | ## 1.0.0 160 | 161 | - Initial stable release. 162 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.1.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.3.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.3.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 42 | 43 | [[package]] 44 | name = "bumpalo" 45 | version = "3.16.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 48 | 49 | [[package]] 50 | name = "cast" 51 | version = "0.3.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 54 | 55 | [[package]] 56 | name = "cfg-if" 57 | version = "1.0.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 60 | 61 | [[package]] 62 | name = "ciborium" 63 | version = "0.2.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 66 | dependencies = [ 67 | "ciborium-io", 68 | "ciborium-ll", 69 | "serde", 70 | ] 71 | 72 | [[package]] 73 | name = "ciborium-io" 74 | version = "0.2.2" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 77 | 78 | [[package]] 79 | name = "ciborium-ll" 80 | version = "0.2.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 83 | dependencies = [ 84 | "ciborium-io", 85 | "half", 86 | ] 87 | 88 | [[package]] 89 | name = "clap" 90 | version = "3.2.25" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 93 | dependencies = [ 94 | "bitflags", 95 | "clap_lex", 96 | "indexmap", 97 | "textwrap", 98 | ] 99 | 100 | [[package]] 101 | name = "clap_lex" 102 | version = "0.2.4" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 105 | dependencies = [ 106 | "os_str_bytes", 107 | ] 108 | 109 | [[package]] 110 | name = "criterion" 111 | version = "0.4.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" 114 | dependencies = [ 115 | "anes", 116 | "atty", 117 | "cast", 118 | "ciborium", 119 | "clap", 120 | "criterion-plot", 121 | "itertools", 122 | "lazy_static", 123 | "num-traits", 124 | "oorandom", 125 | "plotters", 126 | "rayon", 127 | "regex", 128 | "serde", 129 | "serde_derive", 130 | "serde_json", 131 | "tinytemplate", 132 | "walkdir", 133 | ] 134 | 135 | [[package]] 136 | name = "criterion-plot" 137 | version = "0.5.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 140 | dependencies = [ 141 | "cast", 142 | "itertools", 143 | ] 144 | 145 | [[package]] 146 | name = "crossbeam-deque" 147 | version = "0.8.5" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 150 | dependencies = [ 151 | "crossbeam-epoch", 152 | "crossbeam-utils", 153 | ] 154 | 155 | [[package]] 156 | name = "crossbeam-epoch" 157 | version = "0.9.18" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 160 | dependencies = [ 161 | "crossbeam-utils", 162 | ] 163 | 164 | [[package]] 165 | name = "crossbeam-utils" 166 | version = "0.8.20" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 169 | 170 | [[package]] 171 | name = "crunchy" 172 | version = "0.2.2" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 175 | 176 | [[package]] 177 | name = "either" 178 | version = "1.12.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 181 | 182 | [[package]] 183 | name = "half" 184 | version = "2.4.1" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 187 | dependencies = [ 188 | "cfg-if", 189 | "crunchy", 190 | ] 191 | 192 | [[package]] 193 | name = "hashbrown" 194 | version = "0.12.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 197 | 198 | [[package]] 199 | name = "hermit-abi" 200 | version = "0.1.19" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 203 | dependencies = [ 204 | "libc", 205 | ] 206 | 207 | [[package]] 208 | name = "indexmap" 209 | version = "1.9.3" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 212 | dependencies = [ 213 | "autocfg", 214 | "hashbrown", 215 | ] 216 | 217 | [[package]] 218 | name = "itertools" 219 | version = "0.10.5" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 222 | dependencies = [ 223 | "either", 224 | ] 225 | 226 | [[package]] 227 | name = "itoa" 228 | version = "1.0.11" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 231 | 232 | [[package]] 233 | name = "js-sys" 234 | version = "0.3.69" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 237 | dependencies = [ 238 | "wasm-bindgen", 239 | ] 240 | 241 | [[package]] 242 | name = "lazy_static" 243 | version = "1.4.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 246 | 247 | [[package]] 248 | name = "leb128" 249 | version = "0.2.5" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 252 | 253 | [[package]] 254 | name = "libc" 255 | version = "0.2.155" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 258 | 259 | [[package]] 260 | name = "log" 261 | version = "0.4.21" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 264 | 265 | [[package]] 266 | name = "memchr" 267 | version = "2.7.4" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 270 | 271 | [[package]] 272 | name = "num-traits" 273 | version = "0.2.19" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 276 | dependencies = [ 277 | "autocfg", 278 | ] 279 | 280 | [[package]] 281 | name = "once_cell" 282 | version = "1.19.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 285 | 286 | [[package]] 287 | name = "oorandom" 288 | version = "11.1.3" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 291 | 292 | [[package]] 293 | name = "os_str_bytes" 294 | version = "6.6.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 297 | 298 | [[package]] 299 | name = "plotters" 300 | version = "0.3.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" 303 | dependencies = [ 304 | "num-traits", 305 | "plotters-backend", 306 | "plotters-svg", 307 | "wasm-bindgen", 308 | "web-sys", 309 | ] 310 | 311 | [[package]] 312 | name = "plotters-backend" 313 | version = "0.3.6" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" 316 | 317 | [[package]] 318 | name = "plotters-svg" 319 | version = "0.3.6" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" 322 | dependencies = [ 323 | "plotters-backend", 324 | ] 325 | 326 | [[package]] 327 | name = "proc-macro2" 328 | version = "1.0.85" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 331 | dependencies = [ 332 | "unicode-ident", 333 | ] 334 | 335 | [[package]] 336 | name = "proguard" 337 | version = "5.5.0" 338 | dependencies = [ 339 | "criterion", 340 | "lazy_static", 341 | "thiserror", 342 | "uuid", 343 | "watto", 344 | ] 345 | 346 | [[package]] 347 | name = "quote" 348 | version = "1.0.36" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 351 | dependencies = [ 352 | "proc-macro2", 353 | ] 354 | 355 | [[package]] 356 | name = "rayon" 357 | version = "1.10.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 360 | dependencies = [ 361 | "either", 362 | "rayon-core", 363 | ] 364 | 365 | [[package]] 366 | name = "rayon-core" 367 | version = "1.12.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 370 | dependencies = [ 371 | "crossbeam-deque", 372 | "crossbeam-utils", 373 | ] 374 | 375 | [[package]] 376 | name = "regex" 377 | version = "1.10.5" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 380 | dependencies = [ 381 | "aho-corasick", 382 | "memchr", 383 | "regex-automata", 384 | "regex-syntax", 385 | ] 386 | 387 | [[package]] 388 | name = "regex-automata" 389 | version = "0.4.7" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 392 | dependencies = [ 393 | "aho-corasick", 394 | "memchr", 395 | "regex-syntax", 396 | ] 397 | 398 | [[package]] 399 | name = "regex-syntax" 400 | version = "0.8.4" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 403 | 404 | [[package]] 405 | name = "ryu" 406 | version = "1.0.18" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 409 | 410 | [[package]] 411 | name = "same-file" 412 | version = "1.0.6" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 415 | dependencies = [ 416 | "winapi-util", 417 | ] 418 | 419 | [[package]] 420 | name = "serde" 421 | version = "1.0.203" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 424 | dependencies = [ 425 | "serde_derive", 426 | ] 427 | 428 | [[package]] 429 | name = "serde_derive" 430 | version = "1.0.203" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 433 | dependencies = [ 434 | "proc-macro2", 435 | "quote", 436 | "syn", 437 | ] 438 | 439 | [[package]] 440 | name = "serde_json" 441 | version = "1.0.117" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 444 | dependencies = [ 445 | "itoa", 446 | "ryu", 447 | "serde", 448 | ] 449 | 450 | [[package]] 451 | name = "sha1_smol" 452 | version = "1.0.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" 455 | 456 | [[package]] 457 | name = "syn" 458 | version = "2.0.66" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 461 | dependencies = [ 462 | "proc-macro2", 463 | "quote", 464 | "unicode-ident", 465 | ] 466 | 467 | [[package]] 468 | name = "textwrap" 469 | version = "0.16.1" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 472 | 473 | [[package]] 474 | name = "thiserror" 475 | version = "1.0.61" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 478 | dependencies = [ 479 | "thiserror-impl", 480 | ] 481 | 482 | [[package]] 483 | name = "thiserror-impl" 484 | version = "1.0.61" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 487 | dependencies = [ 488 | "proc-macro2", 489 | "quote", 490 | "syn", 491 | ] 492 | 493 | [[package]] 494 | name = "tinytemplate" 495 | version = "1.2.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 498 | dependencies = [ 499 | "serde", 500 | "serde_json", 501 | ] 502 | 503 | [[package]] 504 | name = "unicode-ident" 505 | version = "1.0.12" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 508 | 509 | [[package]] 510 | name = "uuid" 511 | version = "1.8.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" 514 | dependencies = [ 515 | "sha1_smol", 516 | ] 517 | 518 | [[package]] 519 | name = "walkdir" 520 | version = "2.5.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 523 | dependencies = [ 524 | "same-file", 525 | "winapi-util", 526 | ] 527 | 528 | [[package]] 529 | name = "wasm-bindgen" 530 | version = "0.2.92" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 533 | dependencies = [ 534 | "cfg-if", 535 | "wasm-bindgen-macro", 536 | ] 537 | 538 | [[package]] 539 | name = "wasm-bindgen-backend" 540 | version = "0.2.92" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 543 | dependencies = [ 544 | "bumpalo", 545 | "log", 546 | "once_cell", 547 | "proc-macro2", 548 | "quote", 549 | "syn", 550 | "wasm-bindgen-shared", 551 | ] 552 | 553 | [[package]] 554 | name = "wasm-bindgen-macro" 555 | version = "0.2.92" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 558 | dependencies = [ 559 | "quote", 560 | "wasm-bindgen-macro-support", 561 | ] 562 | 563 | [[package]] 564 | name = "wasm-bindgen-macro-support" 565 | version = "0.2.92" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 568 | dependencies = [ 569 | "proc-macro2", 570 | "quote", 571 | "syn", 572 | "wasm-bindgen-backend", 573 | "wasm-bindgen-shared", 574 | ] 575 | 576 | [[package]] 577 | name = "wasm-bindgen-shared" 578 | version = "0.2.92" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 581 | 582 | [[package]] 583 | name = "watto" 584 | version = "0.1.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "6746b5315e417144282a047ebb82260d45c92d09bf653fa9ec975e3809be942b" 587 | dependencies = [ 588 | "leb128", 589 | "thiserror", 590 | ] 591 | 592 | [[package]] 593 | name = "web-sys" 594 | version = "0.3.69" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 597 | dependencies = [ 598 | "js-sys", 599 | "wasm-bindgen", 600 | ] 601 | 602 | [[package]] 603 | name = "winapi" 604 | version = "0.3.9" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 607 | dependencies = [ 608 | "winapi-i686-pc-windows-gnu", 609 | "winapi-x86_64-pc-windows-gnu", 610 | ] 611 | 612 | [[package]] 613 | name = "winapi-i686-pc-windows-gnu" 614 | version = "0.4.0" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 617 | 618 | [[package]] 619 | name = "winapi-util" 620 | version = "0.1.8" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 623 | dependencies = [ 624 | "windows-sys", 625 | ] 626 | 627 | [[package]] 628 | name = "winapi-x86_64-pc-windows-gnu" 629 | version = "0.4.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 632 | 633 | [[package]] 634 | name = "windows-sys" 635 | version = "0.52.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 638 | dependencies = [ 639 | "windows-targets", 640 | ] 641 | 642 | [[package]] 643 | name = "windows-targets" 644 | version = "0.52.5" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 647 | dependencies = [ 648 | "windows_aarch64_gnullvm", 649 | "windows_aarch64_msvc", 650 | "windows_i686_gnu", 651 | "windows_i686_gnullvm", 652 | "windows_i686_msvc", 653 | "windows_x86_64_gnu", 654 | "windows_x86_64_gnullvm", 655 | "windows_x86_64_msvc", 656 | ] 657 | 658 | [[package]] 659 | name = "windows_aarch64_gnullvm" 660 | version = "0.52.5" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 663 | 664 | [[package]] 665 | name = "windows_aarch64_msvc" 666 | version = "0.52.5" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 669 | 670 | [[package]] 671 | name = "windows_i686_gnu" 672 | version = "0.52.5" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 675 | 676 | [[package]] 677 | name = "windows_i686_gnullvm" 678 | version = "0.52.5" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 681 | 682 | [[package]] 683 | name = "windows_i686_msvc" 684 | version = "0.52.5" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 687 | 688 | [[package]] 689 | name = "windows_x86_64_gnu" 690 | version = "0.52.5" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 693 | 694 | [[package]] 695 | name = "windows_x86_64_gnullvm" 696 | version = "0.52.5" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 699 | 700 | [[package]] 701 | name = "windows_x86_64_msvc" 702 | version = "0.52.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 705 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proguard" 3 | version = "5.5.0" 4 | authors = ["Sentry "] 5 | keywords = ["proguard", "retrace", "android", "r8"] 6 | description = "Basic proguard mapping file handling for Rust" 7 | repository = "https://github.com/getsentry/rust-proguard" 8 | homepage = "https://sentry.io/welcome/" 9 | license = "BSD-3-Clause" 10 | readme = "README.md" 11 | edition = "2021" 12 | 13 | [features] 14 | uuid = ["dep:uuid", "lazy_static"] 15 | 16 | [dependencies] 17 | lazy_static = { version = "1.4.0", optional = true } 18 | thiserror = "1.0.61" 19 | uuid = { version = "1.0.0", features = ["v5"], optional = true } 20 | watto = { version = "0.1.0", features = ["writer", "strings"] } 21 | 22 | [dev-dependencies] 23 | lazy_static = "1.4.0" 24 | criterion = "0.4" 25 | 26 | [[bench]] 27 | name = "proguard_parsing" 28 | harness = false 29 | 30 | [[bench]] 31 | name = "proguard_mapping" 32 | harness = false 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Sentry (https://sentry.io) and individual contributors. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: style lint test 2 | .PHONY: all 3 | 4 | check: style lint 5 | .PHONY: check 6 | 7 | build: 8 | @cargo build 9 | .PHONY: build 10 | 11 | doc: 12 | @cargo doc 13 | .PHONY: doc 14 | 15 | test: 16 | @cargo test --no-default-features 17 | @cargo test --all-features 18 | .PHONY: test 19 | 20 | style: 21 | @rustup component add rustfmt --toolchain stable 2> /dev/null 22 | cargo +stable fmt -- --check 23 | .PHONY: style 24 | 25 | format: 26 | @rustup component add rustfmt --toolchain stable 2> /dev/null 27 | @cargo +stable fmt 28 | .PHONY: format 29 | 30 | lint: 31 | @rustup component add clippy --toolchain stable 2> /dev/null 32 | @cargo +stable clippy --all-features --tests --examples -- -D clippy::all 33 | .PHONY: lint 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Proguard Parser 2 | 3 | A simple Rust library that implements basic proguard handling. 4 | 5 | [Documentation](https://docs.rs/proguard) 6 | 7 | ### Release Management 8 | 9 | We use [craft](https://github.com/getsentry/craft) to release new versions. 10 | -------------------------------------------------------------------------------- /benches/proguard_mapping.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; 3 | 4 | static MAPPING: &[u8] = include_bytes!("../tests/res/mapping-inlines.txt"); 5 | 6 | static RAW: &str = r#"java.lang.RuntimeException: Button press caused an exception! 7 | at io.sentry.sample.MainActivity.t(MainActivity.java:1) 8 | at e.a.c.a.onClick 9 | at android.view.View.performClick(View.java:7125) 10 | at android.view.View.performClickInternal(View.java:7102) 11 | at android.view.View.access$3500(View.java:801) 12 | at android.view.View$PerformClick.run(View.java:27336) 13 | at android.os.Handler.handleCallback(Handler.java:883) 14 | at android.os.Handler.dispatchMessage(Handler.java:100) 15 | at android.os.Looper.loop(Looper.java:214) 16 | at android.app.ActivityThread.main(ActivityThread.java:7356) 17 | at java.lang.reflect.Method.invoke(Method.java) 18 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 19 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"#; 20 | 21 | fn benchmark_remapping(c: &mut Criterion) { 22 | let mut cache_buf = Vec::new(); 23 | let mapping = ProguardMapping::new(MAPPING); 24 | ProguardCache::write(&mapping, &mut cache_buf).unwrap(); 25 | let cache = ProguardCache::parse(&cache_buf).unwrap(); 26 | let mapper = ProguardMapper::new(mapping); 27 | 28 | let mut group = c.benchmark_group("Proguard Remapping"); 29 | 30 | group.bench_function("Cache, preparsed", |b| { 31 | b.iter(|| cache.remap_stacktrace(black_box(RAW))) 32 | }); 33 | group.bench_function("Mapper, preparsed", |b| { 34 | b.iter(|| mapper.remap_stacktrace(black_box(RAW))) 35 | }); 36 | 37 | group.bench_function("Cache", |b| { 38 | b.iter(|| { 39 | let cache = ProguardCache::parse(black_box(&cache_buf)).unwrap(); 40 | cache.remap_stacktrace(black_box(RAW)) 41 | }) 42 | }); 43 | group.bench_function("Mapper", |b| { 44 | b.iter(|| { 45 | let mapper = ProguardMapper::new(black_box(ProguardMapping::new(MAPPING))); 46 | mapper.remap_stacktrace(black_box(RAW)) 47 | }) 48 | }); 49 | 50 | group.finish(); 51 | } 52 | 53 | criterion_group!(benches, benchmark_remapping); 54 | criterion_main!(benches); 55 | -------------------------------------------------------------------------------- /benches/proguard_parsing.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use proguard::{ProguardCache, ProguardMapper, ProguardMapping}; 3 | 4 | static MAPPING: &[u8] = include_bytes!("../tests/res/mapping.txt"); 5 | 6 | fn proguard_mapper(mapping: ProguardMapping) -> ProguardMapper { 7 | ProguardMapper::new(mapping) 8 | } 9 | 10 | fn proguard_cache(cache: &[u8]) -> ProguardCache { 11 | ProguardCache::parse(cache).unwrap() 12 | } 13 | 14 | fn criterion_benchmark(c: &mut Criterion) { 15 | let mut cache = Vec::new(); 16 | let mapping = ProguardMapping::new(MAPPING); 17 | ProguardCache::write(&mapping, &mut cache).unwrap(); 18 | 19 | let mut group = c.benchmark_group("Proguard Parsing"); 20 | group.bench_function("Proguard Mapper", |b| { 21 | b.iter(|| proguard_mapper(black_box(mapping.clone()))) 22 | }); 23 | 24 | group.bench_function("Proguard Cache creation", |b| { 25 | b.iter(|| { 26 | let mut cache = Vec::new(); 27 | let mapping = ProguardMapping::new(MAPPING); 28 | ProguardCache::write(&mapping, &mut cache).unwrap(); 29 | }) 30 | }); 31 | 32 | group.bench_function("Proguard Cache parsing", |b| { 33 | b.iter(|| proguard_cache(black_box(&cache))) 34 | }); 35 | } 36 | 37 | criterion_group! { 38 | name = benches; 39 | config = Criterion::default().sample_size(25); 40 | targets = criterion_benchmark 41 | } 42 | criterion_main!(benches); 43 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Uncomment and use this with `cargo +nightly fmt`: 2 | # unstable_features = true 3 | # format_code_in_doc_comments = true 4 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | cd $SCRIPT_DIR/.. 6 | 7 | OLD_VERSION="${1}" 8 | NEW_VERSION="${2}" 9 | 10 | echo "Bumping version: ${NEW_VERSION}" 11 | 12 | find . -name Cargo.toml -type f -exec sed -i '' -e "s/^version.*/version = \"$NEW_VERSION\"/" {} \; 13 | -------------------------------------------------------------------------------- /src/cache/debug.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::ProguardCache; 4 | 5 | use super::raw; 6 | 7 | /// A variant of a class entry in a proguard cache file with 8 | /// nice-ish `Debug` and `Display` representations. 9 | pub struct ClassDebug<'a, 'data> { 10 | pub(crate) cache: &'a ProguardCache<'data>, 11 | pub(crate) raw: &'a raw::Class, 12 | } 13 | 14 | impl ClassDebug<'_, '_> { 15 | fn obfuscated_name(&self) -> &str { 16 | self.cache 17 | .read_string(self.raw.obfuscated_name_offset) 18 | .unwrap() 19 | } 20 | 21 | fn original_name(&self) -> &str { 22 | self.cache 23 | .read_string(self.raw.original_name_offset) 24 | .unwrap() 25 | } 26 | 27 | fn file_name(&self) -> Option<&str> { 28 | self.cache.read_string(self.raw.file_name_offset).ok() 29 | } 30 | } 31 | 32 | impl fmt::Debug for ClassDebug<'_, '_> { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | f.debug_struct("Class") 35 | .field("obfuscated_name", &self.obfuscated_name()) 36 | .field("original_name", &self.original_name()) 37 | .field("file_name", &self.file_name()) 38 | .finish() 39 | } 40 | } 41 | 42 | impl fmt::Display for ClassDebug<'_, '_> { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | write!(f, "{} -> {}:", self.original_name(), self.obfuscated_name())?; 45 | if let Some(file_name) = self.file_name() { 46 | writeln!(f)?; 47 | write!(f, r##"# {{"id":"sourceFile","fileName":"{file_name}"}}"##)?; 48 | } 49 | Ok(()) 50 | } 51 | } 52 | 53 | /// A variant of a member entry in a proguard cache file with 54 | /// nice-ish `Debug` and `Display` representations. 55 | pub struct MemberDebug<'a, 'data> { 56 | pub(crate) cache: &'a ProguardCache<'data>, 57 | pub(crate) raw: &'a raw::Member, 58 | } 59 | 60 | impl MemberDebug<'_, '_> { 61 | fn original_class(&self) -> Option<&str> { 62 | self.cache.read_string(self.raw.original_class_offset).ok() 63 | } 64 | 65 | fn original_file(&self) -> Option<&str> { 66 | self.cache.read_string(self.raw.original_file_offset).ok() 67 | } 68 | 69 | fn params(&self) -> &str { 70 | self.cache 71 | .read_string(self.raw.params_offset) 72 | .unwrap_or_default() 73 | } 74 | 75 | fn obfuscated_name(&self) -> &str { 76 | self.cache 77 | .read_string(self.raw.obfuscated_name_offset) 78 | .unwrap() 79 | } 80 | 81 | fn original_name(&self) -> &str { 82 | self.cache 83 | .read_string(self.raw.original_name_offset) 84 | .unwrap() 85 | } 86 | 87 | fn original_endline(&self) -> Option { 88 | if self.raw.original_endline != u32::MAX { 89 | Some(self.raw.original_endline) 90 | } else { 91 | None 92 | } 93 | } 94 | } 95 | 96 | impl fmt::Debug for MemberDebug<'_, '_> { 97 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 98 | f.debug_struct("Member") 99 | .field("obfuscated_name", &self.obfuscated_name()) 100 | .field("startline", &self.raw.startline) 101 | .field("endline", &self.raw.endline) 102 | .field("original_name", &self.original_name()) 103 | .field("original_class", &self.original_class()) 104 | .field("original_file", &self.original_file()) 105 | .field("original_startline", &self.raw.original_startline) 106 | .field("original_endline", &self.original_endline()) 107 | .field("params", &self.params()) 108 | .finish() 109 | } 110 | } 111 | 112 | impl fmt::Display for MemberDebug<'_, '_> { 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | // XXX: We could print the actual return type here if we saved it in the format. 115 | // Wonder if it's worth it, since we'd only use it in this display impl. 116 | write!(f, " {}:{}: ", self.raw.startline, self.raw.endline)?; 117 | if let Some(original_class) = self.original_class() { 118 | write!(f, "{original_class}.")?; 119 | } 120 | write!( 121 | f, 122 | "{}({}):{}", 123 | self.original_name(), 124 | self.params(), 125 | self.raw.original_startline 126 | )?; 127 | if let Some(end) = self.original_endline() { 128 | write!(f, ":{end}")?; 129 | } 130 | write!(f, " -> {}", self.obfuscated_name())?; 131 | Ok(()) 132 | } 133 | } 134 | 135 | pub struct CacheDebug<'a, 'data> { 136 | cache: &'a ProguardCache<'data>, 137 | } 138 | 139 | impl fmt::Display for CacheDebug<'_, '_> { 140 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 141 | for class in self.cache.classes { 142 | writeln!( 143 | f, 144 | "{}", 145 | ClassDebug { 146 | raw: class, 147 | cache: self.cache 148 | } 149 | )?; 150 | let Some(members) = self.cache.get_class_members(class) else { 151 | continue; 152 | }; 153 | 154 | for member in members { 155 | writeln!( 156 | f, 157 | "{}", 158 | MemberDebug { 159 | raw: member, 160 | cache: self.cache 161 | } 162 | )?; 163 | } 164 | } 165 | Ok(()) 166 | } 167 | } 168 | 169 | impl<'data> ProguardCache<'data> { 170 | /// Returns an iterator over class entries in this cache file that can be debug printed. 171 | pub fn debug_classes<'r>(&'r self) -> impl Iterator> { 172 | self.classes.iter().map(move |c| ClassDebug { 173 | cache: self, 174 | raw: c, 175 | }) 176 | } 177 | 178 | /// Returns an iterator over member entries in this cache file that can be debug printed. 179 | pub fn debug_members<'r>(&'r self) -> impl Iterator> { 180 | self.members.iter().map(move |m| MemberDebug { 181 | cache: self, 182 | raw: m, 183 | }) 184 | } 185 | 186 | /// Returns an iterator over by-params member entries in this cache file that can be debug printed. 187 | pub fn debug_members_by_params<'r>(&'r self) -> impl Iterator> { 188 | self.members_by_params.iter().map(move |m| MemberDebug { 189 | cache: self, 190 | raw: m, 191 | }) 192 | } 193 | 194 | /// Creates a view of the cache that implements `Display`. 195 | /// 196 | /// The `Display` impl is very similar to the original proguard format. 197 | pub fn display(&self) -> CacheDebug { 198 | CacheDebug { cache: self } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! A stable on-disk cache format for ProGuard mapping files. 2 | //! 3 | //! # Structure 4 | //! 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; 7 | //! * A [list](ProguardCache::classes) of [`Class`](raw::Class) entries; 8 | //! * 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. 14 | //! 15 | //! ## Class entries 16 | //! A class entry contains an obfuscated and an original name, optionally a file name, 17 | //! and an offset and length for the class's associated records in the `members` 18 | //! and `members_by_params` section, respectively. 19 | //! 20 | //! Class entries are sorted by obfuscated name. 21 | //! 22 | //! ## Member entries 23 | //! A member entry always contains an obfuscated and an original method name, a start 24 | //! and end line (1- based and inclusive), and a params string. 25 | //! It may also contain an original class name, 26 | //! original file name, and original start and end line. 27 | //! 28 | //! Member entries in `members` are sorted by the class they belong to, then by 29 | //! obfuscated method name, and finally by the order in which they were encountered 30 | //! in the original proguard file. 31 | //! 32 | //! Member entries in `members_by_params` are sorted by the class they belong to, 33 | //! then by obfuscated method name, then by params string, and finally 34 | //! by the order in which they were encountered in the original proguard file. 35 | 36 | mod debug; 37 | mod raw; 38 | 39 | use std::cmp::Ordering; 40 | use std::fmt::Write; 41 | 42 | use thiserror::Error; 43 | 44 | use crate::mapper::{format_cause, format_frames, format_throwable}; 45 | use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Throwable}; 46 | 47 | pub use raw::ProguardCache; 48 | 49 | /// Errors returned while loading/parsing a serialized [`ProguardCache`]. 50 | /// 51 | /// After a `ProguardCache` was successfully parsed via [`ProguardCache::parse`], an Error that occurs during 52 | /// access of any data indicates either corruption of the serialized file, or a bug in the 53 | /// converter/serializer. 54 | #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] 55 | #[non_exhaustive] 56 | pub enum CacheErrorKind { 57 | /// The file was generated by a system with different endianness. 58 | #[error("endianness mismatch")] 59 | WrongEndianness, 60 | /// The file magic does not match. 61 | #[error("wrong format magic")] 62 | WrongFormat, 63 | /// The format version in the header is wrong/unknown. 64 | #[error("unknown ProguardCache version")] 65 | WrongVersion, 66 | /// Header could not be parsed from the cache file. 67 | #[error("could not read header")] 68 | InvalidHeader, 69 | /// Class data could not be parsed from the cache file. 70 | #[error("could not read classes")] 71 | InvalidClasses, 72 | /// Member data could not be parsed from the cache file. 73 | #[error("could not read members")] 74 | InvalidMembers, 75 | /// The header claimed an incorrect number of string bytes. 76 | #[error("expected {expected} string bytes, found {found}")] 77 | UnexpectedStringBytes { 78 | /// Expected number of string bytes. 79 | expected: usize, 80 | /// Number of string bytes actually found in the cache file. 81 | found: usize, 82 | }, 83 | } 84 | 85 | /// An error returned when handling a [`ProguardCache`]. 86 | #[derive(Debug, Error)] 87 | #[error("{kind}")] 88 | pub struct CacheError { 89 | pub(crate) kind: CacheErrorKind, 90 | #[source] 91 | pub(crate) source: Option>, 92 | } 93 | 94 | impl CacheError { 95 | /// Returns the corresponding [`ErrorKind`] for this error. 96 | pub fn kind(&self) -> CacheErrorKind { 97 | self.kind 98 | } 99 | } 100 | 101 | impl From for CacheError { 102 | fn from(kind: CacheErrorKind) -> Self { 103 | Self { kind, source: None } 104 | } 105 | } 106 | 107 | impl<'data> ProguardCache<'data> { 108 | fn get_class(&self, name: &str) -> Option<&raw::Class> { 109 | let idx = self 110 | .classes 111 | .binary_search_by(|c| { 112 | let Ok(obfuscated) = self.read_string(c.obfuscated_name_offset) else { 113 | return Ordering::Greater; 114 | }; 115 | obfuscated.cmp(name) 116 | }) 117 | .ok()?; 118 | 119 | self.classes.get(idx) 120 | } 121 | 122 | fn get_class_members(&self, class: &raw::Class) -> Option<&'data [raw::Member]> { 123 | let raw::Class { 124 | members_offset, 125 | members_len, 126 | .. 127 | } = class; 128 | let start = *members_offset as usize; 129 | let end = start.checked_add(*members_len as usize)?; 130 | 131 | self.members.get(start..end) 132 | } 133 | 134 | fn get_class_members_by_params(&self, class: &raw::Class) -> Option<&'data [raw::Member]> { 135 | let raw::Class { 136 | members_by_params_offset, 137 | members_by_params_len, 138 | .. 139 | } = class; 140 | let start = *members_by_params_offset as usize; 141 | let end = start.checked_add(*members_by_params_len as usize)?; 142 | 143 | self.members_by_params.get(start..end) 144 | } 145 | 146 | /// Remaps an obfuscated Class. 147 | /// 148 | /// This works on the fully-qualified name of the class, with its complete 149 | /// module prefix. 150 | /// 151 | /// # Examples 152 | /// 153 | /// ``` 154 | /// use proguard::{ProguardMapping, ProguardCache}; 155 | /// let mapping = ProguardMapping::new(br#"android.arch.core.executor.ArchTaskExecutor -> a.a.a.a.c:"#); 156 | /// let mut cache = Vec::new(); 157 | /// ProguardCache::write(&mapping, &mut cache).unwrap(); 158 | /// let cache = ProguardCache::parse(&cache).unwrap(); 159 | /// 160 | /// let mapped = cache.remap_class("a.a.a.a.c"); 161 | /// assert_eq!(mapped, Some("android.arch.core.executor.ArchTaskExecutor")); 162 | /// ``` 163 | pub fn remap_class(&self, class: &str) -> Option<&'data str> { 164 | let class = self.get_class(class)?; 165 | self.read_string(class.original_name_offset).ok() 166 | } 167 | 168 | /// Remaps an obfuscated Class Method. 169 | /// 170 | /// The `class` argument has to be the fully-qualified obfuscated name of the 171 | /// class, with its complete module prefix. 172 | /// 173 | /// If the `method` can be resolved unambiguously, it will be returned 174 | /// alongside the remapped `class`, otherwise `None` is being returned. 175 | pub fn remap_method(&self, class: &str, method: &str) -> Option<(&'data str, &'data str)> { 176 | let class = self.get_class(class)?; 177 | let members = self.get_class_members(class)?; 178 | 179 | let matching_members = Self::find_range_by_binary_search(members, |m| { 180 | let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { 181 | return Ordering::Greater; 182 | }; 183 | 184 | obfuscated_name.cmp(method) 185 | })?; 186 | let mut iter = matching_members.iter(); 187 | let first = iter.next()?; 188 | 189 | // We conservatively check that all the mappings point to the same method, 190 | // as we don’t have line numbers to disambiguate. 191 | // We could potentially skip inlined functions here, but lets rather be conservative. 192 | let all_matching = 193 | iter.all(|member| member.original_name_offset == first.original_name_offset); 194 | 195 | if !all_matching { 196 | return None; 197 | } 198 | 199 | let original_class = self.read_string(class.original_name_offset).ok()?; 200 | let original_method = self.read_string(first.original_name_offset).ok()?; 201 | 202 | Some((original_class, original_method)) 203 | } 204 | 205 | /// Remaps a single Stackframe. 206 | /// 207 | /// Returns zero or more [`StackFrame`]s, based on the information in 208 | /// the proguard mapping. This can return more than one frame in the case 209 | /// of inlined functions. In that case, frames are sorted top to bottom. 210 | pub fn remap_frame<'r: 'data>( 211 | &'r self, 212 | frame: &StackFrame<'data>, 213 | ) -> RemappedFrameIter<'r, 'data> { 214 | let Some(class) = self.get_class(frame.class) else { 215 | return RemappedFrameIter::empty(); 216 | }; 217 | 218 | let mut frame = frame.clone(); 219 | let Ok(original_class) = self.read_string(class.original_name_offset) else { 220 | return RemappedFrameIter::empty(); 221 | }; 222 | 223 | frame.class = original_class; 224 | 225 | // The following if and else cases are very similar. The only difference 226 | // is that if the frame contains parameter information, we use it in 227 | // our comparisons (in addition to the method name). 228 | if let Some(frame_params) = frame.parameters { 229 | let Some(members) = self.get_class_members_by_params(class) else { 230 | return RemappedFrameIter::empty(); 231 | }; 232 | 233 | // Find the range of members that have the same method name and params 234 | // as the frame. 235 | let Some(members) = Self::find_range_by_binary_search(members, |m| { 236 | let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { 237 | return Ordering::Greater; 238 | }; 239 | 240 | let params = self.read_string(m.params_offset).unwrap_or_default(); 241 | 242 | (obfuscated_name, params).cmp(&(frame.method, frame_params)) 243 | }) else { 244 | return RemappedFrameIter::empty(); 245 | }; 246 | RemappedFrameIter::members(self, frame, members.iter()) 247 | } else { 248 | let Some(members) = self.get_class_members(class) else { 249 | return RemappedFrameIter::empty(); 250 | }; 251 | 252 | // Find the range of members that have the same method name 253 | // as the frame. 254 | let Some(members) = Self::find_range_by_binary_search(members, |m| { 255 | let Ok(obfuscated_name) = self.read_string(m.obfuscated_name_offset) else { 256 | return Ordering::Greater; 257 | }; 258 | 259 | obfuscated_name.cmp(frame.method) 260 | }) else { 261 | return RemappedFrameIter::empty(); 262 | }; 263 | 264 | RemappedFrameIter::members(self, frame, members.iter()) 265 | } 266 | } 267 | 268 | /// Finds the range of elements of `members` for which `f(m) == Ordering::Equal`. 269 | /// 270 | /// This works by first binary searching for any element fitting the criteria 271 | /// and then linearly searching foraward and backward from that one to find 272 | /// the exact range. 273 | /// 274 | /// Obviously this only works if the criteria are consistent with the order 275 | /// of `members`. 276 | fn find_range_by_binary_search(members: &[raw::Member], f: F) -> Option<&[raw::Member]> 277 | where 278 | F: Fn(&raw::Member) -> std::cmp::Ordering, 279 | { 280 | // Find any member fitting the criteria by binary search. 281 | let mid = members.binary_search_by(&f).ok()?; 282 | let matches_not = |m: &raw::Member| f(m).is_ne(); 283 | // Search backwards from `mid` for a member that doesn't match the 284 | // criteria. The one after it must be the first one that does. 285 | let start = members[..mid] 286 | .iter() 287 | .rposition(matches_not) 288 | .map_or(0, |idx| idx + 1); 289 | 290 | // Search forwards from `mid` for a member that doesn't match the 291 | // criteria. The one before it must be the last one that does. 292 | let end = members[mid..] 293 | .iter() 294 | .position(matches_not) 295 | .map_or(members.len(), |idx| idx + mid); 296 | 297 | members.get(start..end) 298 | } 299 | 300 | /// Remaps a throwable which is the first line of a full stacktrace. 301 | /// 302 | /// # Example 303 | /// 304 | /// ``` 305 | /// use proguard::{ProguardMapping, ProguardCache, Throwable}; 306 | /// 307 | /// let mapping = ProguardMapping::new(b"com.example.Mapper -> a.b:"); 308 | /// let mut cache = Vec::new(); 309 | /// ProguardCache::write(&mapping, &mut cache).unwrap(); 310 | /// let cache = ProguardCache::parse(&cache).unwrap(); 311 | /// 312 | /// let throwable = Throwable::try_parse(b"a.b: Crash").unwrap(); 313 | /// let mapped = cache.remap_throwable(&throwable); 314 | /// 315 | /// assert_eq!( 316 | /// Some(Throwable::with_message("com.example.Mapper", "Crash")), 317 | /// mapped 318 | /// ); 319 | /// ``` 320 | pub fn remap_throwable<'a>(&'a self, throwable: &Throwable<'a>) -> Option> { 321 | self.remap_class(throwable.class).map(|class| Throwable { 322 | class, 323 | message: throwable.message, 324 | }) 325 | } 326 | 327 | /// Remaps a complete Java StackTrace, similar to [`Self::remap_stacktrace_typed`] but instead works on 328 | /// strings as input and output. 329 | pub fn remap_stacktrace(&self, input: &str) -> Result { 330 | let mut stacktrace = String::new(); 331 | let mut lines = input.lines(); 332 | 333 | if let Some(line) = lines.next() { 334 | match stacktrace::parse_throwable(line) { 335 | None => match stacktrace::parse_frame(line) { 336 | None => writeln!(&mut stacktrace, "{}", line)?, 337 | Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?, 338 | }, 339 | Some(throwable) => { 340 | format_throwable(&mut stacktrace, line, self.remap_throwable(&throwable))? 341 | } 342 | } 343 | } 344 | 345 | for line in lines { 346 | match stacktrace::parse_frame(line) { 347 | None => match line 348 | .strip_prefix("Caused by: ") 349 | .and_then(stacktrace::parse_throwable) 350 | { 351 | None => writeln!(&mut stacktrace, "{}", line)?, 352 | Some(cause) => { 353 | format_cause(&mut stacktrace, line, self.remap_throwable(&cause))? 354 | } 355 | }, 356 | Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?, 357 | } 358 | } 359 | Ok(stacktrace) 360 | } 361 | 362 | /// Remaps a complete Java StackTrace. 363 | pub fn remap_stacktrace_typed<'a>(&'a self, trace: &StackTrace<'a>) -> StackTrace<'a> { 364 | let exception = trace 365 | .exception 366 | .as_ref() 367 | .and_then(|t| self.remap_throwable(t)); 368 | 369 | let frames = 370 | trace 371 | .frames 372 | .iter() 373 | .fold(Vec::with_capacity(trace.frames.len()), |mut frames, f| { 374 | let mut peek_frames = self.remap_frame(f).peekable(); 375 | if peek_frames.peek().is_some() { 376 | frames.extend(peek_frames); 377 | } else { 378 | frames.push(f.clone()); 379 | } 380 | 381 | frames 382 | }); 383 | 384 | let cause = trace 385 | .cause 386 | .as_ref() 387 | .map(|c| Box::new(self.remap_stacktrace_typed(c))); 388 | 389 | StackTrace { 390 | exception, 391 | frames, 392 | cause, 393 | } 394 | } 395 | 396 | /// returns a tuple where the first element is the list of the function 397 | /// parameters and the second one is the return type 398 | pub fn deobfuscate_signature(&self, signature: &str) -> Option { 399 | java::deobfuscate_bytecode_signature_cache(signature, self).map(DeobfuscatedSignature::new) 400 | } 401 | } 402 | 403 | /// An iterator over remapped stack frames. 404 | /// 405 | /// This is returned by [`ProguardCache::remap_frame`]. 406 | #[derive(Clone, Debug)] 407 | pub struct RemappedFrameIter<'r, 'data> { 408 | inner: Option<( 409 | &'r ProguardCache<'data>, 410 | StackFrame<'data>, 411 | std::slice::Iter<'data, raw::Member>, 412 | )>, 413 | } 414 | 415 | impl<'data> RemappedFrameIter<'_, 'data> { 416 | fn empty() -> Self { 417 | Self { inner: None } 418 | } 419 | 420 | fn members( 421 | cache: &'data ProguardCache<'data>, 422 | frame: StackFrame<'data>, 423 | members: std::slice::Iter<'data, raw::Member>, 424 | ) -> Self { 425 | Self { 426 | inner: Some((cache, frame, members)), 427 | } 428 | } 429 | } 430 | 431 | impl<'data> Iterator for RemappedFrameIter<'_, 'data> { 432 | type Item = StackFrame<'data>; 433 | 434 | fn next(&mut self) -> Option { 435 | let (cache, frame, members) = self.inner.as_mut()?; 436 | if frame.parameters.is_none() { 437 | iterate_with_lines(cache, frame, members) 438 | } else { 439 | iterate_without_lines(cache, frame, members) 440 | } 441 | } 442 | } 443 | 444 | fn iterate_with_lines<'a>( 445 | cache: &ProguardCache<'a>, 446 | frame: &mut StackFrame<'a>, 447 | members: &mut std::slice::Iter<'_, raw::Member>, 448 | ) -> Option> { 449 | for member in members { 450 | // skip any members which do not match our frames line 451 | if member.endline > 0 452 | && (frame.line < member.startline as usize || frame.line > member.endline as usize) 453 | { 454 | continue; 455 | } 456 | // parents of inlined frames don’t have an `endline`, and 457 | // the top inlined frame need to be correctly offset. 458 | let line = if member.original_endline == u32::MAX 459 | || member.original_endline == member.original_startline 460 | { 461 | member.original_startline as usize 462 | } else { 463 | member.original_startline as usize + frame.line - member.startline as usize 464 | }; 465 | 466 | let class = cache 467 | .read_string(member.original_class_offset) 468 | .unwrap_or(frame.class); 469 | 470 | let file = if member.original_file_offset != u32::MAX { 471 | let Ok(file_name) = cache.read_string(member.original_file_offset) else { 472 | continue; 473 | }; 474 | 475 | if file_name == "R8$$SyntheticClass" { 476 | extract_class_name(class) 477 | } else { 478 | Some(file_name) 479 | } 480 | } else if member.original_class_offset != u32::MAX { 481 | // when an inlined function is from a foreign class, we 482 | // don’t know the file it is defined in. 483 | None 484 | } else { 485 | frame.file 486 | }; 487 | 488 | let Ok(method) = cache.read_string(member.original_name_offset) else { 489 | continue; 490 | }; 491 | 492 | return Some(StackFrame { 493 | class, 494 | method, 495 | file, 496 | line, 497 | parameters: frame.parameters, 498 | }); 499 | } 500 | None 501 | } 502 | 503 | fn iterate_without_lines<'a>( 504 | cache: &ProguardCache<'a>, 505 | frame: &mut StackFrame<'a>, 506 | members: &mut std::slice::Iter<'_, raw::Member>, 507 | ) -> Option> { 508 | let member = members.next()?; 509 | 510 | let class = cache 511 | .read_string(member.original_class_offset) 512 | .unwrap_or(frame.class); 513 | 514 | let method = cache.read_string(member.original_name_offset).ok()?; 515 | 516 | Some(StackFrame { 517 | class, 518 | method, 519 | file: None, 520 | line: 0, 521 | parameters: frame.parameters, 522 | }) 523 | } 524 | 525 | fn extract_class_name(full_path: &str) -> Option<&str> { 526 | let after_last_period = full_path.split('.').last()?; 527 | // If the class is an inner class, we need to extract the outer class name 528 | after_last_period.split('$').next() 529 | } 530 | 531 | #[cfg(test)] 532 | mod tests { 533 | use crate::{ProguardMapping, StackFrame, StackTrace, Throwable}; 534 | 535 | use super::raw::ProguardCache; 536 | 537 | #[test] 538 | fn stacktrace() { 539 | let mapping = "\ 540 | com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d: 541 | com.example.MainFragment$RocketException -> com.example.MainFragment$e: 542 | com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: 543 | 1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick 544 | 1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick 545 | 1:1:void onClick(android.view.View):65 -> onClick 546 | 2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick 547 | 2:2:void onClick(android.view.View):65 -> onClick 548 | "; 549 | let stacktrace = StackTrace { 550 | exception: Some(Throwable { 551 | class: "com.example.MainFragment$e", 552 | message: Some("Crash!"), 553 | }), 554 | frames: vec![ 555 | StackFrame { 556 | class: "com.example.MainFragment$g", 557 | method: "onClick", 558 | line: 2, 559 | file: Some("SourceFile"), 560 | parameters: None, 561 | }, 562 | StackFrame { 563 | class: "android.view.View", 564 | method: "performClick", 565 | line: 7393, 566 | file: Some("View.java"), 567 | parameters: None, 568 | }, 569 | ], 570 | cause: Some(Box::new(StackTrace { 571 | exception: Some(Throwable { 572 | class: "com.example.MainFragment$d", 573 | message: Some("Engines overheating"), 574 | }), 575 | frames: vec![StackFrame { 576 | class: "com.example.MainFragment$g", 577 | method: "onClick", 578 | line: 1, 579 | file: Some("SourceFile"), 580 | parameters: None, 581 | }], 582 | cause: None, 583 | })), 584 | }; 585 | let expect = "\ 586 | com.example.MainFragment$RocketException: Crash! 587 | at com.example.MainFragment$Rocket.fly(:85) 588 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65) 589 | at android.view.View.performClick(View.java:7393) 590 | Caused by: com.example.MainFragment$EngineFailureException: Engines overheating 591 | at com.example.MainFragment$Rocket.startEngines(:90) 592 | at com.example.MainFragment$Rocket.fly(:83) 593 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)\n"; 594 | 595 | let mapping = ProguardMapping::new(mapping.as_bytes()); 596 | let mut cache = Vec::new(); 597 | ProguardCache::write(&mapping, &mut cache).unwrap(); 598 | 599 | let cache = ProguardCache::parse(&cache).unwrap(); 600 | 601 | cache.test(); 602 | 603 | assert_eq!( 604 | cache.remap_stacktrace_typed(&stacktrace).to_string(), 605 | expect, 606 | ); 607 | } 608 | 609 | #[test] 610 | fn stacktrace_str() { 611 | let mapping = "\ 612 | com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d: 613 | com.example.MainFragment$RocketException -> com.example.MainFragment$e: 614 | com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: 615 | 1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick 616 | 1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick 617 | 1:1:void onClick(android.view.View):65 -> onClick 618 | 2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick 619 | 2:2:void onClick(android.view.View):65 -> onClick 620 | "; 621 | 622 | let stacktrace = "\ 623 | com.example.MainFragment$e: Crash! 624 | at com.example.MainFragment$g.onClick(SourceFile:2) 625 | at android.view.View.performClick(View.java:7393) 626 | Caused by: com.example.MainFragment$d: Engines overheating 627 | at com.example.MainFragment$g.onClick(SourceFile:1) 628 | ... 13 more"; 629 | 630 | let expect = "\ 631 | com.example.MainFragment$RocketException: Crash! 632 | at com.example.MainFragment$Rocket.fly(:85) 633 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65) 634 | at android.view.View.performClick(View.java:7393) 635 | Caused by: com.example.MainFragment$EngineFailureException: Engines overheating 636 | at com.example.MainFragment$Rocket.startEngines(:90) 637 | at com.example.MainFragment$Rocket.fly(:83) 638 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65) 639 | ... 13 more\n"; 640 | 641 | let mapping = ProguardMapping::new(mapping.as_bytes()); 642 | let mut cache = Vec::new(); 643 | ProguardCache::write(&mapping, &mut cache).unwrap(); 644 | 645 | let cache = ProguardCache::parse(&cache).unwrap(); 646 | 647 | cache.test(); 648 | 649 | assert_eq!(cache.remap_stacktrace(stacktrace).unwrap(), expect); 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /src/cache/raw.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashSet}; 2 | use std::io::Write; 3 | 4 | use watto::{Pod, StringTable}; 5 | 6 | use crate::{ProguardMapping, ProguardRecord}; 7 | 8 | use super::{CacheError, CacheErrorKind}; 9 | 10 | /// The magic file preamble as individual bytes. 11 | const PRGCACHE_MAGIC_BYTES: [u8; 4] = *b"PRGC"; 12 | 13 | /// The magic file preamble to identify ProguardCache files. 14 | /// 15 | /// Serialized as ASCII "PRGC" on little-endian (x64) systems. 16 | pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES); 17 | /// The byte-flipped magic, which indicates an endianness mismatch. 18 | pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes(); 19 | 20 | pub const PRGCACHE_VERSION: u32 = 1; 21 | 22 | /// The header of a proguard cache file. 23 | #[derive(Debug, Clone, PartialEq, Eq)] 24 | #[repr(C)] 25 | pub(crate) struct Header { 26 | /// The file magic representing the file format and endianness. 27 | pub(crate) magic: u32, 28 | /// The ProguardCache Format Version. 29 | pub(crate) version: u32, 30 | /// The number of class entries in this cache. 31 | pub(crate) num_classes: u32, 32 | /// The total number of member entries in this cache. 33 | pub(crate) num_members: u32, 34 | /// The total number of member-by-params entries in this cache. 35 | pub(crate) num_members_by_params: u32, 36 | /// The number of string bytes in this cache. 37 | pub(crate) string_bytes: u32, 38 | } 39 | 40 | /// An entry for a class in a proguard cache file. 41 | #[derive(Debug, Clone, PartialEq, Eq)] 42 | #[repr(C)] 43 | pub(crate) struct Class { 44 | /// The obfuscated class name (offset into the string section). 45 | pub(crate) obfuscated_name_offset: u32, 46 | /// The original class name (offset into the string section). 47 | pub(crate) original_name_offset: u32, 48 | /// The file name (offset into the string section). 49 | pub(crate) file_name_offset: u32, 50 | /// The start of the class's member entries (offset into the member section). 51 | pub(crate) members_offset: u32, 52 | /// The number of member entries for this class. 53 | pub(crate) members_len: u32, 54 | /// The start of the class's member-by-params entries (offset into the member section). 55 | pub(crate) members_by_params_offset: u32, 56 | /// The number of member-by-params entries for this class. 57 | pub(crate) members_by_params_len: u32, 58 | } 59 | 60 | impl Default for Class { 61 | fn default() -> Self { 62 | Self { 63 | obfuscated_name_offset: u32::MAX, 64 | original_name_offset: u32::MAX, 65 | file_name_offset: u32::MAX, 66 | members_offset: u32::MAX, 67 | members_len: 0, 68 | members_by_params_offset: u32::MAX, 69 | members_by_params_len: 0, 70 | } 71 | } 72 | } 73 | 74 | /// An entry corresponding to a method line in a proguard cache file. 75 | #[derive(Debug, Clone, PartialEq, Eq, Default)] 76 | #[repr(C)] 77 | pub(crate) struct Member { 78 | /// The obfuscated method name (offset into the string section). 79 | pub(crate) obfuscated_name_offset: u32, 80 | /// The start of the range covered by this entry (1-based). 81 | pub(crate) startline: u32, 82 | /// The end of the range covered by this entry (inclusive). 83 | pub(crate) endline: u32, 84 | /// The original class name (offset into the string section). 85 | pub(crate) original_class_offset: u32, 86 | /// The original file name (offset into the string section). 87 | pub(crate) original_file_offset: u32, 88 | /// The original method name (offset into the string section). 89 | pub(crate) original_name_offset: u32, 90 | /// The original start line (1-based). 91 | pub(crate) original_startline: u32, 92 | /// The original end line (inclusive). 93 | pub(crate) original_endline: u32, 94 | /// The entry's parameter string (offset into the strings section). 95 | pub(crate) params_offset: u32, 96 | } 97 | 98 | unsafe impl Pod for Header {} 99 | unsafe impl Pod for Class {} 100 | unsafe impl Pod for Member {} 101 | 102 | /// The serialized `ProguardCache` binary format. 103 | #[derive(Clone, PartialEq, Eq)] 104 | pub struct ProguardCache<'data> { 105 | pub(crate) header: &'data Header, 106 | /// A list of class entries. 107 | /// 108 | /// Class entries are sorted by their obfuscated names. 109 | pub(crate) classes: &'data [Class], 110 | /// A list of member entries. 111 | /// 112 | /// Member entries are sorted by class, then 113 | /// obfuscated method name, and finally by the 114 | /// order in which they occurred in the original proguard file. 115 | pub(crate) members: &'data [Member], 116 | /// A list of member entries. 117 | /// 118 | /// These entries are sorted by class, then 119 | /// obfuscated method name, then params string. 120 | pub(crate) members_by_params: &'data [Member], 121 | /// The collection of all strings in the cache file. 122 | pub(crate) string_bytes: &'data [u8], 123 | } 124 | 125 | impl std::fmt::Debug for ProguardCache<'_> { 126 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 | f.debug_struct("ProguardCache") 128 | .field("version", &self.header.version) 129 | .field("classes", &self.header.num_classes) 130 | .field("members", &self.header.num_members) 131 | .field("members_by_params", &self.header.num_members_by_params) 132 | .field("string_bytes", &self.header.string_bytes) 133 | .finish() 134 | } 135 | } 136 | 137 | impl<'data> ProguardCache<'data> { 138 | /// Parses a `ProguardCache` out of bytes. 139 | pub fn parse(buf: &'data [u8]) -> Result { 140 | let (header, rest) = Header::ref_from_prefix(buf).ok_or(CacheErrorKind::InvalidHeader)?; 141 | if header.magic == PRGCACHE_MAGIC_FLIPPED { 142 | return Err(CacheErrorKind::WrongEndianness.into()); 143 | } 144 | if header.magic != PRGCACHE_MAGIC { 145 | return Err(CacheErrorKind::WrongFormat.into()); 146 | } 147 | if header.version != PRGCACHE_VERSION { 148 | return Err(CacheErrorKind::WrongVersion.into()); 149 | } 150 | 151 | let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidClasses)?; 152 | let (classes, rest) = Class::slice_from_prefix(rest, header.num_classes as usize) 153 | .ok_or(CacheErrorKind::InvalidClasses)?; 154 | 155 | let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?; 156 | let (members, rest) = Member::slice_from_prefix(rest, header.num_members as usize) 157 | .ok_or(CacheErrorKind::InvalidMembers)?; 158 | 159 | let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?; 160 | let (members_by_params, rest) = 161 | Member::slice_from_prefix(rest, header.num_members_by_params as usize) 162 | .ok_or(CacheErrorKind::InvalidMembers)?; 163 | 164 | let (_, string_bytes) = 165 | watto::align_to(rest, 8).ok_or(CacheErrorKind::UnexpectedStringBytes { 166 | expected: header.string_bytes as usize, 167 | found: 0, 168 | })?; 169 | 170 | if string_bytes.len() < header.string_bytes as usize { 171 | return Err(CacheErrorKind::UnexpectedStringBytes { 172 | expected: header.string_bytes as usize, 173 | found: string_bytes.len(), 174 | } 175 | .into()); 176 | } 177 | 178 | Ok(Self { 179 | header, 180 | classes, 181 | members, 182 | members_by_params, 183 | string_bytes, 184 | }) 185 | } 186 | 187 | /// Writes a [`ProguardMapping`] into a writer in the proguard cache format. 188 | pub fn write(mapping: &ProguardMapping, writer: &mut W) -> std::io::Result<()> { 189 | let mut string_table = StringTable::new(); 190 | let mut classes: BTreeMap<&str, ClassInProgress> = BTreeMap::new(); 191 | // Create an empty [`ClassInProgress`]; this gets updated as we parse method records. 192 | let mut current_class = ClassInProgress::default(); 193 | 194 | let mut records = mapping.iter().filter_map(Result::ok).peekable(); 195 | while let Some(record) = records.next() { 196 | match record { 197 | ProguardRecord::Header { 198 | key, 199 | value: Some(file_name), 200 | } => { 201 | if key == "sourceFile" { 202 | current_class.class.file_name_offset = 203 | string_table.insert(file_name) as u32; 204 | } 205 | } 206 | ProguardRecord::Class { 207 | original, 208 | obfuscated, 209 | } => { 210 | // Finalize the previous class, but only if it has a name (otherwise it's the dummy class we created at the beginning). 211 | if !current_class.name.is_empty() { 212 | classes.insert(current_class.name, current_class); 213 | } 214 | 215 | let obfuscated_name_offset = string_table.insert(obfuscated) as u32; 216 | let original_name_offset = string_table.insert(original) as u32; 217 | 218 | // Create a new `ClassInProgress` for the record we just encountered. 219 | current_class = ClassInProgress { 220 | name: obfuscated, 221 | class: Class { 222 | original_name_offset, 223 | obfuscated_name_offset, 224 | ..Default::default() 225 | }, 226 | ..Default::default() 227 | }; 228 | } 229 | 230 | ProguardRecord::Method { 231 | original, 232 | obfuscated, 233 | original_class, 234 | line_mapping, 235 | arguments, 236 | .. 237 | } => { 238 | // In case the mapping has no line records, we use `0` here. 239 | let (startline, endline) = line_mapping.map_or((0, 0), |line_mapping| { 240 | (line_mapping.startline as u32, line_mapping.endline as u32) 241 | }); 242 | let (original_startline, original_endline) = 243 | line_mapping.map_or((0, u32::MAX), |line_mapping| { 244 | match line_mapping.original_startline { 245 | Some(original_startline) => ( 246 | original_startline as u32, 247 | line_mapping.original_endline.map_or(u32::MAX, |l| l as u32), 248 | ), 249 | None => { 250 | (line_mapping.startline as u32, line_mapping.endline as u32) 251 | } 252 | } 253 | }); 254 | 255 | let obfuscated_name_offset = string_table.insert(obfuscated) as u32; 256 | let original_name_offset = string_table.insert(original) as u32; 257 | let original_class_offset = original_class.map_or(u32::MAX, |class_name| { 258 | string_table.insert(class_name) as u32 259 | }); 260 | let params_offset = string_table.insert(arguments) as u32; 261 | let original_file_offset = current_class.class.file_name_offset; 262 | let member = Member { 263 | obfuscated_name_offset, 264 | startline, 265 | endline, 266 | original_class_offset, 267 | original_file_offset, 268 | original_name_offset, 269 | original_startline, 270 | original_endline, 271 | params_offset, 272 | }; 273 | 274 | current_class 275 | .members 276 | .entry(obfuscated) 277 | .or_default() 278 | .push(member.clone()); 279 | current_class.class.members_len += 1; 280 | 281 | // If the next line has the same leading line range then this method 282 | // has been inlined by the code minification process, as a result 283 | // it can't show in method traces and can be safely ignored. 284 | if let Some(ProguardRecord::Method { 285 | line_mapping: Some(next_line), 286 | .. 287 | }) = records.peek() 288 | { 289 | if let Some(current_line_mapping) = line_mapping { 290 | if (current_line_mapping.startline == next_line.startline) 291 | && (current_line_mapping.endline == next_line.endline) 292 | { 293 | continue; 294 | } 295 | } 296 | } 297 | 298 | let key = (obfuscated, arguments, original); 299 | if current_class.unique_methods.insert(key) { 300 | current_class 301 | .members_by_params 302 | .entry((obfuscated, arguments)) 303 | .or_default() 304 | .push(member); 305 | current_class.class.members_by_params_len += 1; 306 | } 307 | } 308 | _ => {} 309 | } 310 | } 311 | 312 | // Flush the last constructed class 313 | if !current_class.name.is_empty() { 314 | classes.insert(current_class.name, current_class); 315 | } 316 | 317 | // At this point, we know how many members/members-by-params each class has because we kept count, 318 | // but we don't know where each class's entries start. We'll rectify that below. 319 | 320 | let mut writer = watto::Writer::new(writer); 321 | let string_bytes = string_table.into_bytes(); 322 | 323 | let num_members = classes.values().map(|c| c.class.members_len).sum::(); 324 | let num_members_by_params = classes 325 | .values() 326 | .map(|c| c.class.members_by_params_len) 327 | .sum::(); 328 | 329 | let header = Header { 330 | magic: PRGCACHE_MAGIC, 331 | version: PRGCACHE_VERSION, 332 | num_classes: classes.len() as u32, 333 | num_members, 334 | num_members_by_params, 335 | string_bytes: string_bytes.len() as u32, 336 | }; 337 | 338 | writer.write_all(header.as_bytes())?; 339 | writer.align_to(8)?; 340 | 341 | let mut members = Vec::new(); 342 | let mut members_by_params = Vec::new(); 343 | 344 | for mut c in classes.into_values() { 345 | // We can now set the class's members_offset/members_by_params_offset. 346 | c.class.members_offset = members.len() as u32; 347 | c.class.members_by_params_offset = members.len() as u32; 348 | members.extend(c.members.into_values().flat_map(|m| m.into_iter())); 349 | members_by_params.extend( 350 | c.members_by_params 351 | .into_values() 352 | .flat_map(|m| m.into_iter()), 353 | ); 354 | writer.write_all(c.class.as_bytes())?; 355 | } 356 | writer.align_to(8)?; 357 | 358 | writer.write_all(members.as_bytes())?; 359 | writer.align_to(8)?; 360 | 361 | writer.write_all(members_by_params.as_bytes())?; 362 | writer.align_to(8)?; 363 | 364 | writer.write_all(&string_bytes)?; 365 | 366 | Ok(()) 367 | } 368 | 369 | /// Tests the integrity of this cache. 370 | /// 371 | /// Specifically it checks the following: 372 | /// * All string offsets in class and member entries are either `u32::MAX` or defined. 373 | /// * Member entries are ordered by the class they belong to. 374 | pub fn test(&self) { 375 | let mut prev_end = 0; 376 | for class in self.classes { 377 | assert!(self.read_string(class.obfuscated_name_offset).is_ok()); 378 | assert!(self.read_string(class.original_name_offset).is_ok()); 379 | 380 | if class.file_name_offset != u32::MAX { 381 | assert!(self.read_string(class.file_name_offset).is_ok()); 382 | } 383 | 384 | assert_eq!(class.members_offset, prev_end); 385 | prev_end += class.members_len; 386 | assert!(prev_end as usize <= self.members.len()); 387 | let Some(members) = self.get_class_members(class) else { 388 | continue; 389 | }; 390 | 391 | for member in members { 392 | assert!(self.read_string(member.obfuscated_name_offset).is_ok()); 393 | assert!(self.read_string(member.original_name_offset).is_ok()); 394 | 395 | if member.params_offset != u32::MAX { 396 | assert!(self.read_string(member.params_offset).is_ok()); 397 | } 398 | 399 | if member.original_class_offset != u32::MAX { 400 | assert!(self.read_string(member.original_class_offset).is_ok()); 401 | } 402 | 403 | if member.original_file_offset != u32::MAX { 404 | assert!(self.read_string(member.original_file_offset).is_ok()); 405 | } 406 | } 407 | } 408 | } 409 | 410 | pub(crate) fn read_string(&self, offset: u32) -> Result<&'data str, watto::ReadStringError> { 411 | StringTable::read(self.string_bytes, offset as usize) 412 | } 413 | } 414 | 415 | /// A class that is currently being constructed in the course of writing a [`ProguardCache`]. 416 | #[derive(Debug, Clone, Default)] 417 | struct ClassInProgress<'data> { 418 | /// The name of the class being constructed. 419 | name: &'data str, 420 | /// The class record. 421 | class: Class, 422 | /// The members records for the class, grouped by method name. 423 | members: BTreeMap<&'data str, Vec>, 424 | /// The member records for the class, grouped by method name and parameter string. 425 | members_by_params: BTreeMap<(&'data str, &'data str), Vec>, 426 | /// A map to keep track of which combinations of (obfuscated method name, original method name, parameters) 427 | /// we have already seen for this class. 428 | unique_methods: HashSet<(&'data str, &'data str, &'data str)>, 429 | } 430 | -------------------------------------------------------------------------------- /src/java.rs: -------------------------------------------------------------------------------- 1 | use crate::{mapper::ProguardMapper, ProguardCache}; 2 | 3 | fn java_base_types(encoded_ty: char) -> Option<&'static str> { 4 | match encoded_ty { 5 | 'Z' => Some("boolean"), 6 | 'B' => Some("byte"), 7 | 'C' => Some("char"), 8 | 'S' => Some("short"), 9 | 'I' => Some("int"), 10 | 'J' => Some("long"), 11 | 'F' => Some("float"), 12 | 'D' => Some("double"), 13 | 'V' => Some("void"), 14 | _ => None, 15 | } 16 | } 17 | 18 | fn byte_code_type_to_java_type(byte_code_type: &str, mapper: &ProguardMapper) -> Option { 19 | let mut chrs = byte_code_type.chars(); 20 | let mut suffix = "".to_string(); 21 | while let Some(token) = chrs.next() { 22 | if token == 'L' { 23 | // expect and remove final `;` 24 | if chrs.next_back()? != ';' { 25 | return None; 26 | } 27 | let obfuscated = chrs.as_str().replace('/', "."); 28 | 29 | if let Some(mapped) = mapper.remap_class(&obfuscated) { 30 | return Some(format!("{}{}", mapped, suffix)); 31 | } 32 | 33 | return Some(format!("{}{}", obfuscated, suffix)); 34 | } else if token == '[' { 35 | suffix.push_str("[]"); 36 | continue; 37 | } else if let Some(ty) = java_base_types(token) { 38 | return Some(format!("{}{}", ty, suffix)); 39 | } 40 | } 41 | None 42 | } 43 | 44 | /// Same as [`byte_code_type_to_java_type`], but uses a [`ProguardCache`] for remapping. 45 | fn byte_code_type_to_java_type_cache( 46 | byte_code_type: &str, 47 | cache: &ProguardCache, 48 | ) -> Option { 49 | let mut chrs = byte_code_type.chars(); 50 | let mut suffix = "".to_string(); 51 | while let Some(token) = chrs.next() { 52 | if token == 'L' { 53 | // expect and remove final `;` 54 | if chrs.next_back()? != ';' { 55 | return None; 56 | } 57 | let obfuscated = chrs.as_str().replace('/', "."); 58 | 59 | if let Some(mapped) = cache.remap_class(&obfuscated) { 60 | return Some(format!("{}{}", mapped, suffix)); 61 | } 62 | 63 | return Some(format!("{}{}", obfuscated, suffix)); 64 | } else if token == '[' { 65 | suffix.push_str("[]"); 66 | continue; 67 | } else if let Some(ty) = java_base_types(token) { 68 | return Some(format!("{}{}", ty, suffix)); 69 | } 70 | } 71 | None 72 | } 73 | 74 | // parse_obfuscated_bytecode_signature will parse an obfuscated signatures into parameter 75 | // and return types that can be then deobfuscated 76 | fn parse_obfuscated_bytecode_signature(signature: &str) -> Option<(Vec<&str>, &str)> { 77 | let signature = signature.strip_prefix('(')?; 78 | 79 | let (parameter_types, return_type) = signature.rsplit_once(')')?; 80 | if return_type.is_empty() { 81 | return None; 82 | } 83 | 84 | let mut types: Vec<&str> = Vec::new(); 85 | let mut first_idx = 0; 86 | 87 | let mut param_chrs = parameter_types.char_indices(); 88 | while let Some((idx, token)) = param_chrs.next() { 89 | if token == 'L' { 90 | let mut last_idx = idx; 91 | for (i, c) in param_chrs.by_ref() { 92 | last_idx = i; 93 | if c == ';' { 94 | break; 95 | } 96 | } 97 | let ty = parameter_types.get(first_idx..last_idx + 1)?; 98 | if !ty.ends_with([';']) { 99 | return None; 100 | } 101 | types.push(ty); 102 | first_idx = last_idx + 1; 103 | } else if token == '[' { 104 | continue; 105 | } else if java_base_types(token).is_some() { 106 | let ty = parameter_types.get(first_idx..idx + 1)?; 107 | types.push(ty); 108 | first_idx = idx + 1; 109 | } 110 | } 111 | 112 | Some((types, return_type)) 113 | } 114 | 115 | /// returns a tuple where the first element is the list of the function 116 | /// parameters and the second one is the return type 117 | pub fn deobfuscate_bytecode_signature( 118 | signature: &str, 119 | mapper: &ProguardMapper, 120 | ) -> Option<(Vec, String)> { 121 | let (parameter_types, return_type) = parse_obfuscated_bytecode_signature(signature)?; 122 | let parameter_java_types: Vec = parameter_types 123 | .into_iter() 124 | .filter(|params| !params.is_empty()) 125 | .filter_map(|params| byte_code_type_to_java_type(params, mapper)) 126 | .collect(); 127 | 128 | let return_java_type = byte_code_type_to_java_type(return_type, mapper)?; 129 | 130 | Some((parameter_java_types, return_java_type)) 131 | } 132 | 133 | /// Same as [`deobfuscate_bytecode_signature`], but uses a [`ProguardCache`] for remapping. 134 | pub fn deobfuscate_bytecode_signature_cache( 135 | signature: &str, 136 | cache: &ProguardCache, 137 | ) -> Option<(Vec, String)> { 138 | let (parameter_types, return_type) = parse_obfuscated_bytecode_signature(signature)?; 139 | let parameter_java_types: Vec = parameter_types 140 | .into_iter() 141 | .filter(|params| !params.is_empty()) 142 | .filter_map(|params| byte_code_type_to_java_type_cache(params, cache)) 143 | .collect(); 144 | 145 | let return_java_type = byte_code_type_to_java_type_cache(return_type, cache)?; 146 | 147 | Some((parameter_java_types, return_java_type)) 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use crate::{java::byte_code_type_to_java_type, ProguardMapper, ProguardMapping}; 153 | use std::collections::HashMap; 154 | 155 | #[test] 156 | fn test_byte_code_type_to_java_type() { 157 | let proguard_source = b"org.slf4j.helpers.Util$ClassContextSecurityManager -> org.a.b.g$a: 158 | 65:65:void () -> "; 159 | 160 | let mapping = ProguardMapping::new(proguard_source); 161 | let mapper = ProguardMapper::new(mapping); 162 | 163 | let tests = HashMap::from([ 164 | ("[I", "int[]"), 165 | ("I", "int"), 166 | ("[Ljava/lang/String;", "java.lang.String[]"), 167 | ("[[J", "long[][]"), 168 | ("[B", "byte[]"), 169 | ( 170 | // Obfuscated class type 171 | "Lorg/a/b/g$a;", 172 | "org.slf4j.helpers.Util$ClassContextSecurityManager", 173 | ), 174 | ]); 175 | 176 | // invalid types 177 | let tests_invalid = vec!["", "L", ""]; 178 | 179 | for (ty, expected) in tests { 180 | assert_eq!( 181 | byte_code_type_to_java_type(ty, &mapper).unwrap(), 182 | expected.to_string() 183 | ); 184 | } 185 | 186 | for ty in tests_invalid { 187 | let java_type = byte_code_type_to_java_type(ty, &mapper); 188 | assert!(java_type.is_none()); 189 | } 190 | } 191 | 192 | #[test] 193 | fn test_format_signature() { 194 | let proguard_source = b"org.slf4j.helpers.Util$ClassContextSecurityManager -> org.a.b.g$a: 195 | 65:65:void () -> "; 196 | 197 | let mapping = ProguardMapping::new(proguard_source); 198 | let mapper = ProguardMapper::new(mapping); 199 | 200 | let tests_valid = HashMap::from([ 201 | // valid signatures 202 | ("()V", "()"), 203 | ("([I)V", "(int[])"), 204 | ("(III)V", "(int, int, int)"), 205 | ("([Ljava/lang/String;)V", "(java.lang.String[])"), 206 | ("([[J)V", "(long[][])"), 207 | ("(I)I", "(int): int"), 208 | ("([B)V", "(byte[])"), 209 | ( 210 | "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", 211 | "(java.lang.String, java.lang.String): java.lang.String", 212 | ), 213 | ( 214 | // Obfuscated class type 215 | "(Lorg/a/b/g$a;)V", 216 | "(org.slf4j.helpers.Util$ClassContextSecurityManager)", 217 | ), 218 | ]); 219 | 220 | // invalid signatures 221 | let tests_invalid = vec!["", "()", "(L)"]; 222 | 223 | for (obfuscated, expected) in tests_valid { 224 | let signature = mapper.deobfuscate_signature(obfuscated); 225 | assert_eq!(signature.unwrap().format_signature(), expected.to_string()); 226 | } 227 | 228 | for obfuscated in tests_invalid { 229 | let signature = mapper.deobfuscate_signature(obfuscated); 230 | assert!(signature.is_none()); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate implements handling of proguard mapping files. 2 | //! 3 | //! The main use case is to re-map classes or complete stack frames, but it can 4 | //! also be used to parse a proguard mapping line-by-line. 5 | //! 6 | //! The `uuid` feature also allows getting the UUID of the proguard file. 7 | //! 8 | //! # Examples 9 | //! 10 | //! ``` 11 | //! let mapping = r#" 12 | //! android.arch.core.internal.SafeIterableMap -> a.a.a.b.c: 13 | //! 13:13:java.util.Map$Entry eldest():168:168 -> a 14 | //! "#; 15 | //! let mapper = proguard::ProguardMapper::from(mapping); 16 | //! 17 | //! // re-mapping a classname 18 | //! assert_eq!( 19 | //! mapper.remap_class("a.a.a.b.c"), 20 | //! Some("android.arch.core.internal.SafeIterableMap"), 21 | //! ); 22 | //! 23 | //! // re-map a stack frame 24 | //! assert_eq!( 25 | //! mapper 26 | //! .remap_frame(&proguard::StackFrame::new("a.a.a.b.c", "a", 13)) 27 | //! .collect::>(), 28 | //! vec![proguard::StackFrame::new( 29 | //! "android.arch.core.internal.SafeIterableMap", 30 | //! "eldest", 31 | //! 168 32 | //! )], 33 | //! ); 34 | //! ``` 35 | 36 | #![warn(missing_docs)] 37 | 38 | mod cache; 39 | mod java; 40 | mod mapper; 41 | mod mapping; 42 | mod stacktrace; 43 | 44 | pub use cache::{CacheError, CacheErrorKind, ProguardCache}; 45 | pub use mapper::{DeobfuscatedSignature, ProguardMapper, RemappedFrameIter}; 46 | pub use mapping::{ 47 | LineMapping, MappingSummary, ParseError, ParseErrorKind, ProguardMapping, ProguardRecord, 48 | ProguardRecordIter, 49 | }; 50 | pub use stacktrace::{StackFrame, StackTrace, Throwable}; 51 | -------------------------------------------------------------------------------- /src/mapper.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::collections::HashSet; 3 | use std::fmt; 4 | use std::fmt::{Error as FmtError, Write}; 5 | use std::iter::FusedIterator; 6 | 7 | use crate::java; 8 | use crate::mapping::{ProguardMapping, ProguardRecord}; 9 | use crate::stacktrace::{self, StackFrame, StackTrace, Throwable}; 10 | 11 | /// A deobfuscated method signature. 12 | pub struct DeobfuscatedSignature { 13 | parameters: Vec, 14 | return_type: String, 15 | } 16 | 17 | impl DeobfuscatedSignature { 18 | pub(crate) fn new(signature: (Vec, String)) -> DeobfuscatedSignature { 19 | DeobfuscatedSignature { 20 | parameters: signature.0, 21 | return_type: signature.1, 22 | } 23 | } 24 | 25 | /// Returns the java return type of the method signature 26 | pub fn return_type(&self) -> &str { 27 | self.return_type.as_str() 28 | } 29 | 30 | /// Returns the list of paramater types of the method signature 31 | pub fn parameters_types(&self) -> impl Iterator { 32 | self.parameters.iter().map(|s| s.as_ref()) 33 | } 34 | 35 | /// formats types (param_type list, return_type) into a human-readable signature 36 | pub fn format_signature(&self) -> String { 37 | let mut signature = format!("({})", self.parameters.join(", ")); 38 | if !self.return_type().is_empty() && self.return_type() != "void" { 39 | signature.push_str(": "); 40 | signature.push_str(self.return_type()); 41 | } 42 | 43 | signature 44 | } 45 | } 46 | 47 | impl fmt::Display for DeobfuscatedSignature { 48 | // This trait requires `fmt` with this exact signature. 49 | fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result { 50 | write!(f, "{}", self.format_signature()) 51 | } 52 | } 53 | 54 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 55 | struct MemberMapping<'s> { 56 | startline: usize, 57 | endline: usize, 58 | original_class: Option<&'s str>, 59 | original_file: Option<&'s str>, 60 | original: &'s str, 61 | original_startline: usize, 62 | original_endline: Option, 63 | } 64 | 65 | #[derive(Clone, Debug)] 66 | struct ClassMembers<'s> { 67 | all_mappings: Vec>, 68 | // method_params -> Vec[MemberMapping] 69 | mappings_by_params: HashMap<&'s str, Vec>>, 70 | } 71 | 72 | #[derive(Clone, Debug)] 73 | struct ClassMapping<'s> { 74 | original: &'s str, 75 | obfuscated: &'s str, 76 | file_name: Option<&'s str>, 77 | members: HashMap<&'s str, ClassMembers<'s>>, 78 | } 79 | 80 | type MemberIter<'m> = std::slice::Iter<'m, MemberMapping<'m>>; 81 | 82 | /// An Iterator over remapped StackFrames. 83 | #[derive(Clone, Debug, Default)] 84 | pub struct RemappedFrameIter<'m> { 85 | inner: Option<(StackFrame<'m>, MemberIter<'m>)>, 86 | } 87 | 88 | impl<'m> RemappedFrameIter<'m> { 89 | fn empty() -> Self { 90 | Self { inner: None } 91 | } 92 | fn members(frame: StackFrame<'m>, members: MemberIter<'m>) -> Self { 93 | Self { 94 | inner: Some((frame, members)), 95 | } 96 | } 97 | } 98 | 99 | impl<'m> Iterator for RemappedFrameIter<'m> { 100 | type Item = StackFrame<'m>; 101 | fn next(&mut self) -> Option { 102 | let (frame, ref mut members) = self.inner.as_mut()?; 103 | if frame.parameters.is_none() { 104 | iterate_with_lines(frame, members) 105 | } else { 106 | iterate_without_lines(frame, members) 107 | } 108 | } 109 | } 110 | 111 | fn extract_class_name(full_path: &str) -> Option<&str> { 112 | let after_last_period = full_path.split('.').last()?; 113 | // If the class is an inner class, we need to extract the outer class name 114 | after_last_period.split('$').next() 115 | } 116 | 117 | fn iterate_with_lines<'a>( 118 | frame: &mut StackFrame<'a>, 119 | members: &mut core::slice::Iter<'_, MemberMapping<'a>>, 120 | ) -> Option> { 121 | for member in members { 122 | // skip any members which do not match our frames line 123 | if member.endline > 0 && (frame.line < member.startline || frame.line > member.endline) { 124 | continue; 125 | } 126 | // parents of inlined frames don’t have an `endline`, and 127 | // the top inlined frame need to be correctly offset. 128 | let line = if member.original_endline.is_none() 129 | || member.original_endline == Some(member.original_startline) 130 | { 131 | member.original_startline 132 | } else { 133 | member.original_startline + frame.line - member.startline 134 | }; 135 | let file = if let Some(file_name) = member.original_file { 136 | if file_name == "R8$$SyntheticClass" { 137 | extract_class_name(member.original_class.unwrap_or(frame.class)) 138 | } else { 139 | member.original_file 140 | } 141 | } else if member.original_class.is_some() { 142 | // when an inlined function is from a foreign class, we 143 | // don’t know the file it is defined in. 144 | None 145 | } else { 146 | frame.file 147 | }; 148 | let class = match member.original_class { 149 | Some(class) => class, 150 | _ => frame.class, 151 | }; 152 | return Some(StackFrame { 153 | class, 154 | method: member.original, 155 | file, 156 | line, 157 | parameters: frame.parameters, 158 | }); 159 | } 160 | None 161 | } 162 | 163 | fn iterate_without_lines<'a>( 164 | frame: &mut StackFrame<'a>, 165 | members: &mut core::slice::Iter<'_, MemberMapping<'a>>, 166 | ) -> Option> { 167 | let member = members.next()?; 168 | 169 | let class = match member.original_class { 170 | Some(class) => class, 171 | _ => frame.class, 172 | }; 173 | Some(StackFrame { 174 | class, 175 | method: member.original, 176 | file: None, 177 | line: 0, 178 | parameters: frame.parameters, 179 | }) 180 | } 181 | 182 | impl FusedIterator for RemappedFrameIter<'_> {} 183 | 184 | /// A Proguard Remapper. 185 | /// 186 | /// This can remap class names, stack frames one at a time, or the complete 187 | /// raw stacktrace. 188 | #[derive(Clone, Debug)] 189 | pub struct ProguardMapper<'s> { 190 | classes: HashMap<&'s str, ClassMapping<'s>>, 191 | } 192 | 193 | impl<'s> From<&'s str> for ProguardMapper<'s> { 194 | fn from(s: &'s str) -> Self { 195 | let mapping = ProguardMapping::new(s.as_ref()); 196 | Self::new(mapping) 197 | } 198 | } 199 | 200 | impl<'s> From<(&'s str, bool)> for ProguardMapper<'s> { 201 | fn from(t: (&'s str, bool)) -> Self { 202 | let mapping = ProguardMapping::new(t.0.as_ref()); 203 | Self::new_with_param_mapping(mapping, t.1) 204 | } 205 | } 206 | 207 | impl<'s> ProguardMapper<'s> { 208 | /// Create a new ProguardMapper. 209 | pub fn new(mapping: ProguardMapping<'s>) -> Self { 210 | Self::create_proguard_mapper(mapping, false) 211 | } 212 | 213 | /// Create a new ProguardMapper with the extra mappings_by_params. 214 | /// This is useful when we want to deobfuscate frames with missing 215 | /// line information 216 | pub fn new_with_param_mapping( 217 | mapping: ProguardMapping<'s>, 218 | initialize_param_mapping: bool, 219 | ) -> Self { 220 | Self::create_proguard_mapper(mapping, initialize_param_mapping) 221 | } 222 | 223 | fn create_proguard_mapper( 224 | mapping: ProguardMapping<'s>, 225 | initialize_param_mapping: bool, 226 | ) -> Self { 227 | let mut classes = HashMap::new(); 228 | let mut class = ClassMapping { 229 | original: "", 230 | obfuscated: "", 231 | file_name: None, 232 | members: HashMap::new(), 233 | }; 234 | let mut unique_methods: HashSet<(&str, &str, &str)> = HashSet::new(); 235 | 236 | let mut records = mapping.iter().filter_map(Result::ok).peekable(); 237 | while let Some(record) = records.next() { 238 | match record { 239 | ProguardRecord::Header { key, value } => { 240 | if key == "sourceFile" { 241 | class.file_name = value; 242 | } 243 | } 244 | ProguardRecord::Class { 245 | original, 246 | obfuscated, 247 | } => { 248 | if !class.original.is_empty() { 249 | classes.insert(class.obfuscated, class); 250 | } 251 | class = ClassMapping { 252 | original, 253 | obfuscated, 254 | file_name: None, 255 | members: HashMap::new(), 256 | }; 257 | unique_methods.clear(); 258 | } 259 | ProguardRecord::Method { 260 | original, 261 | obfuscated, 262 | original_class, 263 | line_mapping, 264 | arguments, 265 | .. 266 | } => { 267 | let current_line = if initialize_param_mapping { 268 | line_mapping 269 | } else { 270 | None 271 | }; 272 | // in case the mapping has no line records, we use `0` here. 273 | let (startline, endline) = 274 | line_mapping.as_ref().map_or((0, 0), |line_mapping| { 275 | (line_mapping.startline, line_mapping.endline) 276 | }); 277 | let (original_startline, original_endline) = 278 | line_mapping.map_or((0, None), |line_mapping| { 279 | match line_mapping.original_startline { 280 | Some(original_startline) => { 281 | (original_startline, line_mapping.original_endline) 282 | } 283 | None => (line_mapping.startline, Some(line_mapping.endline)), 284 | } 285 | }); 286 | 287 | let members = class 288 | .members 289 | .entry(obfuscated) 290 | .or_insert_with(|| ClassMembers { 291 | all_mappings: Vec::with_capacity(1), 292 | mappings_by_params: Default::default(), 293 | }); 294 | 295 | let member_mapping = MemberMapping { 296 | startline, 297 | endline, 298 | original_class, 299 | original_file: class.file_name, 300 | original, 301 | original_startline, 302 | original_endline, 303 | }; 304 | members.all_mappings.push(member_mapping.clone()); 305 | 306 | if !initialize_param_mapping { 307 | continue; 308 | } 309 | // If the next line has the same leading line range then this method 310 | // has been inlined by the code minification process, as a result 311 | // it can't show in method traces and can be safely ignored. 312 | if let Some(ProguardRecord::Method { 313 | line_mapping: Some(next_line), 314 | .. 315 | }) = records.peek() 316 | { 317 | if let Some(current_line_mapping) = current_line { 318 | if (current_line_mapping.startline == next_line.startline) 319 | && (current_line_mapping.endline == next_line.endline) 320 | { 321 | continue; 322 | } 323 | } 324 | } 325 | 326 | let key = (obfuscated, arguments, original); 327 | if unique_methods.insert(key) { 328 | members 329 | .mappings_by_params 330 | .entry(arguments) 331 | .or_insert_with(|| Vec::with_capacity(1)) 332 | .push(member_mapping); 333 | } 334 | } // end ProguardRecord::Method 335 | _ => {} 336 | } 337 | } 338 | if !class.original.is_empty() { 339 | classes.insert(class.obfuscated, class); 340 | } 341 | 342 | Self { classes } 343 | } 344 | 345 | /// Remaps an obfuscated Class. 346 | /// 347 | /// This works on the fully-qualified name of the class, with its complete 348 | /// module prefix. 349 | /// 350 | /// # Examples 351 | /// 352 | /// ``` 353 | /// let mapping = r#"android.arch.core.executor.ArchTaskExecutor -> a.a.a.a.c:"#; 354 | /// let mapper = proguard::ProguardMapper::from(mapping); 355 | /// 356 | /// let mapped = mapper.remap_class("a.a.a.a.c"); 357 | /// assert_eq!(mapped, Some("android.arch.core.executor.ArchTaskExecutor")); 358 | /// ``` 359 | pub fn remap_class(&'s self, class: &str) -> Option<&'s str> { 360 | self.classes.get(class).map(|class| class.original) 361 | } 362 | 363 | /// returns a tuple where the first element is the list of the function 364 | /// parameters and the second one is the return type 365 | pub fn deobfuscate_signature(&'s self, signature: &str) -> Option { 366 | java::deobfuscate_bytecode_signature(signature, self).map(DeobfuscatedSignature::new) 367 | } 368 | 369 | /// Remaps an obfuscated Class Method. 370 | /// 371 | /// The `class` argument has to be the fully-qualified obfuscated name of the 372 | /// class, with its complete module prefix. 373 | /// 374 | /// If the `method` can be resolved unambiguously, it will be returned 375 | /// alongside the remapped `class`, otherwise `None` is being returned. 376 | pub fn remap_method(&'s self, class: &str, method: &str) -> Option<(&'s str, &'s str)> { 377 | let class = self.classes.get(class)?; 378 | let mut members = class.members.get(method)?.all_mappings.iter(); 379 | let first = members.next()?; 380 | 381 | // We conservatively check that all the mappings point to the same method, 382 | // as we don’t have line numbers to disambiguate. 383 | // We could potentially skip inlined functions here, but lets rather be conservative. 384 | let all_matching = members.all(|member| member.original == first.original); 385 | 386 | all_matching.then_some((class.original, first.original)) 387 | } 388 | 389 | /// Remaps a single Stackframe. 390 | /// 391 | /// Returns zero or more [`StackFrame`]s, based on the information in 392 | /// the proguard mapping. This can return more than one frame in the case 393 | /// of inlined functions. In that case, frames are sorted top to bottom. 394 | pub fn remap_frame(&'s self, frame: &StackFrame<'s>) -> RemappedFrameIter<'s> { 395 | let Some(class) = self.classes.get(frame.class) else { 396 | return RemappedFrameIter::empty(); 397 | }; 398 | let Some(members) = class.members.get(frame.method) else { 399 | return RemappedFrameIter::empty(); 400 | }; 401 | 402 | let mut frame = frame.clone(); 403 | frame.class = class.original; 404 | 405 | let mappings = if let Some(parameters) = frame.parameters { 406 | if let Some(typed_members) = members.mappings_by_params.get(parameters) { 407 | typed_members.iter() 408 | } else { 409 | return RemappedFrameIter::empty(); 410 | } 411 | } else { 412 | members.all_mappings.iter() 413 | }; 414 | 415 | RemappedFrameIter::members(frame, mappings) 416 | } 417 | 418 | /// Remaps a throwable which is the first line of a full stacktrace. 419 | /// 420 | /// # Example 421 | /// 422 | /// ``` 423 | /// use proguard::{ProguardMapper, Throwable}; 424 | /// 425 | /// let mapping = "com.example.Mapper -> a.b:"; 426 | /// let mapper = ProguardMapper::from(mapping); 427 | /// 428 | /// let throwable = Throwable::try_parse(b"a.b: Crash").unwrap(); 429 | /// let mapped = mapper.remap_throwable(&throwable); 430 | /// 431 | /// assert_eq!( 432 | /// Some(Throwable::with_message("com.example.Mapper", "Crash")), 433 | /// mapped 434 | /// ); 435 | /// ``` 436 | pub fn remap_throwable<'a>(&'a self, throwable: &Throwable<'a>) -> Option> { 437 | self.remap_class(throwable.class).map(|class| Throwable { 438 | class, 439 | message: throwable.message, 440 | }) 441 | } 442 | 443 | /// Remaps a complete Java StackTrace, similar to [`Self::remap_stacktrace_typed`] but instead works on 444 | /// strings as input and output. 445 | pub fn remap_stacktrace(&self, input: &str) -> Result { 446 | let mut stacktrace = String::new(); 447 | let mut lines = input.lines(); 448 | 449 | if let Some(line) = lines.next() { 450 | match stacktrace::parse_throwable(line) { 451 | None => match stacktrace::parse_frame(line) { 452 | None => writeln!(&mut stacktrace, "{}", line)?, 453 | Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?, 454 | }, 455 | Some(throwable) => { 456 | format_throwable(&mut stacktrace, line, self.remap_throwable(&throwable))? 457 | } 458 | } 459 | } 460 | 461 | for line in lines { 462 | match stacktrace::parse_frame(line) { 463 | None => match line 464 | .strip_prefix("Caused by: ") 465 | .and_then(stacktrace::parse_throwable) 466 | { 467 | None => writeln!(&mut stacktrace, "{}", line)?, 468 | Some(cause) => { 469 | format_cause(&mut stacktrace, line, self.remap_throwable(&cause))? 470 | } 471 | }, 472 | Some(frame) => format_frames(&mut stacktrace, line, self.remap_frame(&frame))?, 473 | } 474 | } 475 | Ok(stacktrace) 476 | } 477 | 478 | /// Remaps a complete Java StackTrace. 479 | pub fn remap_stacktrace_typed<'a>(&'a self, trace: &StackTrace<'a>) -> StackTrace<'a> { 480 | let exception = trace 481 | .exception 482 | .as_ref() 483 | .and_then(|t| self.remap_throwable(t)); 484 | 485 | let frames = 486 | trace 487 | .frames 488 | .iter() 489 | .fold(Vec::with_capacity(trace.frames.len()), |mut frames, f| { 490 | let mut peek_frames = self.remap_frame(f).peekable(); 491 | if peek_frames.peek().is_some() { 492 | frames.extend(peek_frames); 493 | } else { 494 | frames.push(f.clone()); 495 | } 496 | 497 | frames 498 | }); 499 | 500 | let cause = trace 501 | .cause 502 | .as_ref() 503 | .map(|c| Box::new(self.remap_stacktrace_typed(c))); 504 | 505 | StackTrace { 506 | exception, 507 | frames, 508 | cause, 509 | } 510 | } 511 | } 512 | 513 | pub(crate) fn format_throwable( 514 | stacktrace: &mut impl Write, 515 | line: &str, 516 | throwable: Option>, 517 | ) -> Result<(), FmtError> { 518 | if let Some(throwable) = throwable { 519 | writeln!(stacktrace, "{}", throwable) 520 | } else { 521 | writeln!(stacktrace, "{}", line) 522 | } 523 | } 524 | 525 | pub(crate) fn format_frames<'s>( 526 | stacktrace: &mut impl Write, 527 | line: &str, 528 | remapped: impl Iterator>, 529 | ) -> Result<(), FmtError> { 530 | let mut remapped = remapped.peekable(); 531 | 532 | if remapped.peek().is_none() { 533 | return writeln!(stacktrace, "{}", line); 534 | } 535 | for line in remapped { 536 | writeln!(stacktrace, " {}", line)?; 537 | } 538 | 539 | Ok(()) 540 | } 541 | 542 | pub(crate) fn format_cause( 543 | stacktrace: &mut impl Write, 544 | line: &str, 545 | cause: Option>, 546 | ) -> Result<(), FmtError> { 547 | if let Some(cause) = cause { 548 | writeln!(stacktrace, "Caused by: {}", cause) 549 | } else { 550 | writeln!(stacktrace, "{}", line) 551 | } 552 | } 553 | 554 | #[cfg(test)] 555 | mod tests { 556 | use super::*; 557 | 558 | #[test] 559 | fn stacktrace() { 560 | let mapping = "\ 561 | com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d: 562 | com.example.MainFragment$RocketException -> com.example.MainFragment$e: 563 | com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: 564 | 1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick 565 | 1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick 566 | 1:1:void onClick(android.view.View):65 -> onClick 567 | 2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick 568 | 2:2:void onClick(android.view.View):65 -> onClick 569 | "; 570 | let stacktrace = StackTrace { 571 | exception: Some(Throwable { 572 | class: "com.example.MainFragment$e", 573 | message: Some("Crash!"), 574 | }), 575 | frames: vec![ 576 | StackFrame { 577 | class: "com.example.MainFragment$g", 578 | method: "onClick", 579 | line: 2, 580 | file: Some("SourceFile"), 581 | parameters: None, 582 | }, 583 | StackFrame { 584 | class: "android.view.View", 585 | method: "performClick", 586 | line: 7393, 587 | file: Some("View.java"), 588 | parameters: None, 589 | }, 590 | ], 591 | cause: Some(Box::new(StackTrace { 592 | exception: Some(Throwable { 593 | class: "com.example.MainFragment$d", 594 | message: Some("Engines overheating"), 595 | }), 596 | frames: vec![StackFrame { 597 | class: "com.example.MainFragment$g", 598 | method: "onClick", 599 | line: 1, 600 | file: Some("SourceFile"), 601 | parameters: None, 602 | }], 603 | cause: None, 604 | })), 605 | }; 606 | let expect = "\ 607 | com.example.MainFragment$RocketException: Crash! 608 | at com.example.MainFragment$Rocket.fly(:85) 609 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65) 610 | at android.view.View.performClick(View.java:7393) 611 | Caused by: com.example.MainFragment$EngineFailureException: Engines overheating 612 | at com.example.MainFragment$Rocket.startEngines(:90) 613 | at com.example.MainFragment$Rocket.fly(:83) 614 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65)\n"; 615 | 616 | let mapper = ProguardMapper::from(mapping); 617 | 618 | assert_eq!( 619 | expect, 620 | mapper.remap_stacktrace_typed(&stacktrace).to_string() 621 | ); 622 | } 623 | 624 | #[test] 625 | fn stacktrace_str() { 626 | let mapping = "\ 627 | com.example.MainFragment$EngineFailureException -> com.example.MainFragment$d: 628 | com.example.MainFragment$RocketException -> com.example.MainFragment$e: 629 | com.example.MainFragment$onActivityCreated$4 -> com.example.MainFragment$g: 630 | 1:1:void com.example.MainFragment$Rocket.startEngines():90:90 -> onClick 631 | 1:1:void com.example.MainFragment$Rocket.fly():83 -> onClick 632 | 1:1:void onClick(android.view.View):65 -> onClick 633 | 2:2:void com.example.MainFragment$Rocket.fly():85:85 -> onClick 634 | 2:2:void onClick(android.view.View):65 -> onClick 635 | "; 636 | let stacktrace = "\ 637 | com.example.MainFragment$e: Crash! 638 | at com.example.MainFragment$g.onClick(SourceFile:2) 639 | at android.view.View.performClick(View.java:7393) 640 | Caused by: com.example.MainFragment$d: Engines overheating 641 | at com.example.MainFragment$g.onClick(SourceFile:1) 642 | ... 13 more"; 643 | let expect = "\ 644 | com.example.MainFragment$RocketException: Crash! 645 | at com.example.MainFragment$Rocket.fly(:85) 646 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65) 647 | at android.view.View.performClick(View.java:7393) 648 | Caused by: com.example.MainFragment$EngineFailureException: Engines overheating 649 | at com.example.MainFragment$Rocket.startEngines(:90) 650 | at com.example.MainFragment$Rocket.fly(:83) 651 | at com.example.MainFragment$onActivityCreated$4.onClick(SourceFile:65) 652 | ... 13 more\n"; 653 | 654 | let mapper = ProguardMapper::from(mapping); 655 | 656 | assert_eq!(expect, mapper.remap_stacktrace(stacktrace).unwrap()); 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /src/stacktrace.rs: -------------------------------------------------------------------------------- 1 | //! A Parser for Java Stacktraces. 2 | 3 | use std::fmt::{Display, Formatter, Result as FmtResult}; 4 | 5 | /// A full Java StackTrace as printed by [`Throwable.printStackTrace()`]. 6 | /// 7 | /// [`Throwable.printStackTrace()`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Throwable.html#printStackTrace() 8 | #[derive(Clone, Debug, PartialEq)] 9 | pub struct StackTrace<'s> { 10 | pub(crate) exception: Option>, 11 | pub(crate) frames: Vec>, 12 | pub(crate) cause: Option>>, 13 | } 14 | 15 | impl<'s> StackTrace<'s> { 16 | /// Create a new StackTrace. 17 | pub fn new(exception: Option>, frames: Vec>) -> Self { 18 | Self { 19 | exception, 20 | frames, 21 | cause: None, 22 | } 23 | } 24 | 25 | /// Create a new StackTrace with cause information. 26 | pub fn with_cause( 27 | exception: Option>, 28 | frames: Vec>, 29 | cause: StackTrace<'s>, 30 | ) -> Self { 31 | Self { 32 | exception, 33 | frames, 34 | cause: Some(Box::new(cause)), 35 | } 36 | } 37 | 38 | /// Parses a StackTrace from a full Java StackTrace. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ```rust 43 | /// use proguard::{StackFrame, StackTrace, Throwable}; 44 | /// 45 | /// let stacktrace = "\ 46 | /// some.CustomException: Crashed! 47 | /// at some.Klass.method(Klass.java:1234) 48 | /// Caused by: some.InnerException 49 | /// at some.Klass2.method2(Klass2.java:5678) 50 | /// "; 51 | /// let parsed = StackTrace::try_parse(stacktrace.as_bytes()); 52 | /// assert_eq!( 53 | /// parsed, 54 | /// Some(StackTrace::with_cause( 55 | /// Some(Throwable::with_message("some.CustomException", "Crashed!")), 56 | /// vec![StackFrame::with_file( 57 | /// "some.Klass", 58 | /// "method", 59 | /// 1234, 60 | /// "Klass.java", 61 | /// )], 62 | /// StackTrace::new( 63 | /// Some(Throwable::new("some.InnerException")), 64 | /// vec![StackFrame::with_file( 65 | /// "some.Klass2", 66 | /// "method2", 67 | /// 5678, 68 | /// "Klass2.java", 69 | /// )] 70 | /// ) 71 | /// )) 72 | /// ); 73 | /// ``` 74 | pub fn try_parse(stacktrace: &'s [u8]) -> Option { 75 | let stacktrace = std::str::from_utf8(stacktrace).ok()?; 76 | parse_stacktrace(stacktrace) 77 | } 78 | 79 | /// The exception at the top of the StackTrace, if present. 80 | pub fn exception(&self) -> Option<&Throwable<'_>> { 81 | self.exception.as_ref() 82 | } 83 | 84 | /// All StackFrames following the exception. 85 | pub fn frames(&self) -> &[StackFrame<'_>] { 86 | &self.frames 87 | } 88 | 89 | /// An optional cause describing the inner exception. 90 | pub fn cause(&self) -> Option<&StackTrace<'_>> { 91 | self.cause.as_deref() 92 | } 93 | } 94 | 95 | impl Display for StackTrace<'_> { 96 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 97 | if let Some(exception) = &self.exception { 98 | writeln!(f, "{}", exception)?; 99 | } 100 | 101 | for frame in &self.frames { 102 | writeln!(f, " {}", frame)?; 103 | } 104 | 105 | if let Some(cause) = &self.cause { 106 | write!(f, "Caused by: {}", cause)?; 107 | } 108 | 109 | Ok(()) 110 | } 111 | } 112 | 113 | fn parse_stacktrace(content: &str) -> Option> { 114 | let mut lines = content.lines().peekable(); 115 | 116 | let exception = lines.peek().and_then(|line| parse_throwable(line)); 117 | if exception.is_some() { 118 | lines.next(); 119 | } 120 | 121 | let mut stacktrace = StackTrace { 122 | exception, 123 | frames: vec![], 124 | cause: None, 125 | }; 126 | let mut current = &mut stacktrace; 127 | 128 | for line in &mut lines { 129 | if let Some(frame) = parse_frame(line) { 130 | current.frames.push(frame); 131 | } else if let Some(line) = line.strip_prefix("Caused by: ") { 132 | current.cause = Some(Box::new(StackTrace { 133 | exception: parse_throwable(line), 134 | frames: vec![], 135 | cause: None, 136 | })); 137 | // We just set the `cause` so it's safe to unwrap here 138 | current = current.cause.as_deref_mut().unwrap(); 139 | } 140 | } 141 | 142 | if stacktrace.exception.is_some() || !stacktrace.frames.is_empty() { 143 | Some(stacktrace) 144 | } else { 145 | None 146 | } 147 | } 148 | 149 | /// A Java StackFrame. 150 | /// 151 | /// Basically a Rust version of the Java [`StackTraceElement`]. 152 | /// 153 | /// [`StackTraceElement`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/StackTraceElement.html 154 | #[derive(Clone, Debug, PartialEq)] 155 | pub struct StackFrame<'s> { 156 | pub(crate) class: &'s str, 157 | pub(crate) method: &'s str, 158 | pub(crate) line: usize, 159 | pub(crate) file: Option<&'s str>, 160 | pub(crate) parameters: Option<&'s str>, 161 | } 162 | 163 | impl<'s> StackFrame<'s> { 164 | /// Create a new StackFrame. 165 | pub fn new(class: &'s str, method: &'s str, line: usize) -> Self { 166 | Self { 167 | class, 168 | method, 169 | line, 170 | file: None, 171 | parameters: None, 172 | } 173 | } 174 | 175 | /// Create a new StackFrame with file information. 176 | pub fn with_file(class: &'s str, method: &'s str, line: usize, file: &'s str) -> Self { 177 | Self { 178 | class, 179 | method, 180 | line, 181 | file: Some(file), 182 | parameters: None, 183 | } 184 | } 185 | 186 | /// Create a new StackFrame with arguments information and no line. 187 | /// This is useful for when we try to do deobfuscation with no line information. 188 | pub fn with_parameters(class: &'s str, method: &'s str, arguments: &'s str) -> Self { 189 | Self { 190 | class, 191 | method, 192 | line: 0, 193 | file: None, 194 | parameters: Some(arguments), 195 | } 196 | } 197 | 198 | /// Parses a StackFrame from a line of a Java StackTrace. 199 | /// 200 | /// # Examples 201 | /// 202 | /// ``` 203 | /// use proguard::StackFrame; 204 | /// 205 | /// let parsed = StackFrame::try_parse(b" at some.Klass.method(Klass.java:1234)"); 206 | /// assert_eq!( 207 | /// parsed, 208 | /// Some(StackFrame::with_file( 209 | /// "some.Klass", 210 | /// "method", 211 | /// 1234, 212 | /// "Klass.java" 213 | /// )) 214 | /// ); 215 | /// ``` 216 | pub fn try_parse(line: &'s [u8]) -> Option { 217 | let line = std::str::from_utf8(line).ok()?; 218 | parse_frame(line) 219 | } 220 | 221 | /// The class of the StackFrame. 222 | pub fn class(&self) -> &str { 223 | self.class 224 | } 225 | 226 | /// The method of the StackFrame. 227 | pub fn method(&self) -> &str { 228 | self.method 229 | } 230 | 231 | /// The fully qualified method name, including the class. 232 | pub fn full_method(&self) -> String { 233 | format!("{}.{}", self.class, self.method) 234 | } 235 | 236 | /// The file of the StackFrame. 237 | pub fn file(&self) -> Option<&str> { 238 | self.file 239 | } 240 | 241 | /// The line of the StackFrame, 1-based. 242 | pub fn line(&self) -> usize { 243 | self.line 244 | } 245 | 246 | /// The parameters of the StackFrame 247 | pub fn parameters(&self) -> Option<&str> { 248 | self.parameters 249 | } 250 | } 251 | 252 | impl Display for StackFrame<'_> { 253 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 254 | write!( 255 | f, 256 | "at {}.{}({}:{})", 257 | self.class, 258 | self.method, 259 | self.file.unwrap_or(""), 260 | self.line 261 | ) 262 | } 263 | } 264 | 265 | /// Parses a single line from a Java StackTrace. 266 | /// 267 | /// Returns `None` if the line could not be parsed. 268 | pub(crate) fn parse_frame(line: &str) -> Option { 269 | let line = line.trim(); 270 | 271 | if !line.starts_with("at ") || !line.ends_with(')') { 272 | return None; 273 | } 274 | 275 | let (method_split, file_split) = line[3..line.len() - 1].split_once('(')?; 276 | let (class, method) = method_split.rsplit_once('.')?; 277 | let (file, line) = file_split.split_once(':')?; 278 | let line = line.parse().ok()?; 279 | 280 | Some(StackFrame { 281 | class, 282 | method, 283 | file: Some(file), 284 | line, 285 | parameters: None, 286 | }) 287 | } 288 | 289 | /// A Java Throwable. 290 | /// 291 | /// This is a Rust version of the first line from a [`Throwable.printStackTrace()`] output in Java. 292 | /// 293 | /// [`Throwable.printStackTrace()`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Throwable.html#printStackTrace() 294 | #[derive(Clone, Debug, PartialEq)] 295 | pub struct Throwable<'s> { 296 | pub(crate) class: &'s str, 297 | pub(crate) message: Option<&'s str>, 298 | } 299 | 300 | impl<'s> Throwable<'s> { 301 | /// Create a new Throwable. 302 | pub fn new(class: &'s str) -> Self { 303 | Self { 304 | class, 305 | message: None, 306 | } 307 | } 308 | 309 | /// Create a new Throwable with message. 310 | pub fn with_message(class: &'s str, message: &'s str) -> Self { 311 | Self { 312 | class, 313 | message: Some(message), 314 | } 315 | } 316 | 317 | /// Parses a Throwable from the a line of a full Java StackTrace. 318 | /// 319 | /// # Example 320 | /// ```rust 321 | /// use proguard::Throwable; 322 | /// 323 | /// let parsed = Throwable::try_parse(b"some.CustomException: Crash!"); 324 | /// assert_eq!( 325 | /// parsed, 326 | /// Some(Throwable::with_message("some.CustomException", "Crash!")), 327 | /// ) 328 | /// ``` 329 | pub fn try_parse(line: &'s [u8]) -> Option { 330 | std::str::from_utf8(line).ok().and_then(parse_throwable) 331 | } 332 | 333 | /// The class of this Throwable. 334 | pub fn class(&self) -> &str { 335 | self.class 336 | } 337 | 338 | /// The optional message of this Throwable. 339 | pub fn message(&self) -> Option<&str> { 340 | self.message 341 | } 342 | } 343 | 344 | impl Display for Throwable<'_> { 345 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 346 | write!(f, "{}", self.class)?; 347 | 348 | if let Some(message) = self.message { 349 | write!(f, ": {}", message)?; 350 | } 351 | 352 | Ok(()) 353 | } 354 | } 355 | 356 | /// Parse the first line of a Java StackTrace which is usually the string version of a 357 | /// [`Throwable`]. 358 | /// 359 | /// Returns `None` if the line could not be parsed. 360 | /// 361 | /// [`Throwable`]: https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Throwable.html 362 | pub(crate) fn parse_throwable(line: &str) -> Option> { 363 | let line = line.trim(); 364 | 365 | let mut class_split = line.splitn(2, ": "); 366 | let class = class_split.next()?; 367 | let message = class_split.next(); 368 | 369 | if class.contains(' ') { 370 | None 371 | } else { 372 | Some(Throwable { class, message }) 373 | } 374 | } 375 | 376 | #[cfg(test)] 377 | mod tests { 378 | use super::*; 379 | 380 | #[test] 381 | fn print_stack_trace() { 382 | let trace = StackTrace { 383 | exception: Some(Throwable { 384 | class: "com.example.MainFragment", 385 | message: Some("Crash"), 386 | }), 387 | frames: vec![StackFrame { 388 | class: "com.example.Util", 389 | method: "show", 390 | line: 5, 391 | file: Some("Util.java"), 392 | parameters: None, 393 | }], 394 | cause: Some(Box::new(StackTrace { 395 | exception: Some(Throwable { 396 | class: "com.example.Other", 397 | message: Some("Invalid data"), 398 | }), 399 | frames: vec![StackFrame { 400 | class: "com.example.Parser", 401 | method: "parse", 402 | line: 115, 403 | file: None, 404 | parameters: None, 405 | }], 406 | cause: None, 407 | })), 408 | }; 409 | let expect = "\ 410 | com.example.MainFragment: Crash 411 | at com.example.Util.show(Util.java:5) 412 | Caused by: com.example.Other: Invalid data 413 | at com.example.Parser.parse(:115)\n"; 414 | 415 | assert_eq!(expect, trace.to_string()); 416 | } 417 | 418 | #[test] 419 | fn stack_frame() { 420 | let line = "at com.example.MainFragment.onClick(SourceFile:1)"; 421 | let stack_frame = parse_frame(line); 422 | let expect = Some(StackFrame { 423 | class: "com.example.MainFragment", 424 | method: "onClick", 425 | line: 1, 426 | file: Some("SourceFile"), 427 | parameters: None, 428 | }); 429 | 430 | assert_eq!(expect, stack_frame); 431 | 432 | let line = " at com.example.MainFragment.onClick(SourceFile:1)"; 433 | let stack_frame = parse_frame(line); 434 | 435 | assert_eq!(expect, stack_frame); 436 | 437 | let line = "\tat com.example.MainFragment.onClick(SourceFile:1)"; 438 | let stack_frame = parse_frame(line); 439 | 440 | assert_eq!(expect, stack_frame); 441 | } 442 | 443 | #[test] 444 | fn print_stack_frame() { 445 | let frame = StackFrame { 446 | class: "com.example.MainFragment", 447 | method: "onClick", 448 | line: 1, 449 | file: None, 450 | parameters: None, 451 | }; 452 | 453 | assert_eq!( 454 | "at com.example.MainFragment.onClick(:1)", 455 | frame.to_string() 456 | ); 457 | 458 | let frame = StackFrame { 459 | class: "com.example.MainFragment", 460 | method: "onClick", 461 | line: 1, 462 | file: Some("SourceFile"), 463 | parameters: None, 464 | }; 465 | 466 | assert_eq!( 467 | "at com.example.MainFragment.onClick(SourceFile:1)", 468 | frame.to_string() 469 | ); 470 | } 471 | 472 | #[test] 473 | fn throwable() { 474 | let line = "com.example.MainFragment: Crash!"; 475 | let throwable = parse_throwable(line); 476 | let expect = Some(Throwable { 477 | class: "com.example.MainFragment", 478 | message: Some("Crash!"), 479 | }); 480 | 481 | assert_eq!(expect, throwable); 482 | } 483 | 484 | #[test] 485 | fn print_throwable() { 486 | let throwable = Throwable { 487 | class: "com.example.MainFragment", 488 | message: None, 489 | }; 490 | 491 | assert_eq!("com.example.MainFragment", throwable.to_string()); 492 | 493 | let throwable = Throwable { 494 | class: "com.example.MainFragment", 495 | message: Some("Crash"), 496 | }; 497 | 498 | assert_eq!("com.example.MainFragment: Crash", throwable.to_string()); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | 3 | use proguard::{ProguardCache, ProguardMapper, ProguardMapping, StackFrame}; 4 | 5 | static MAPPING: &[u8] = include_bytes!("res/mapping.txt"); 6 | lazy_static! { 7 | static ref MAPPING_WIN: Vec = MAPPING 8 | .iter() 9 | .flat_map(|&byte| if byte == b'\n' { 10 | vec![b'\r', b'\n'] 11 | } else { 12 | vec![byte] 13 | }) 14 | .collect(); 15 | } 16 | 17 | #[test] 18 | fn test_basic() { 19 | let mapping = ProguardMapping::new(MAPPING); 20 | assert!(mapping.is_valid()); 21 | assert!(mapping.has_line_info()); 22 | 23 | let mapper = ProguardMapper::new(mapping); 24 | 25 | let class = mapper.remap_class("android.support.constraint.ConstraintLayout$a"); 26 | assert_eq!( 27 | class, 28 | Some("android.support.constraint.ConstraintLayout$LayoutParams") 29 | ); 30 | } 31 | 32 | #[test] 33 | fn test_basic_cache() { 34 | let mapping = ProguardMapping::new(MAPPING); 35 | assert!(mapping.is_valid()); 36 | assert!(mapping.has_line_info()); 37 | 38 | let mut cache = Vec::new(); 39 | ProguardCache::write(&mapping, &mut cache).unwrap(); 40 | let cache = ProguardCache::parse(&cache).unwrap(); 41 | 42 | let class = cache.remap_class("android.support.constraint.ConstraintLayout$a"); 43 | assert_eq!( 44 | class, 45 | Some("android.support.constraint.ConstraintLayout$LayoutParams") 46 | ); 47 | } 48 | 49 | #[test] 50 | fn test_basic_win() { 51 | let mapping = ProguardMapping::new(&MAPPING_WIN[..]); 52 | assert!(mapping.is_valid()); 53 | assert!(mapping.has_line_info()); 54 | 55 | let mapper = ProguardMapper::new(mapping); 56 | 57 | let class = mapper.remap_class("android.support.constraint.ConstraintLayout$a"); 58 | assert_eq!( 59 | class, 60 | Some("android.support.constraint.ConstraintLayout$LayoutParams") 61 | ); 62 | } 63 | 64 | #[test] 65 | fn test_basic_win_cache() { 66 | let mapping = ProguardMapping::new(&MAPPING_WIN[..]); 67 | assert!(mapping.is_valid()); 68 | assert!(mapping.has_line_info()); 69 | 70 | let mut cache = Vec::new(); 71 | ProguardCache::write(&mapping, &mut cache).unwrap(); 72 | let cache = ProguardCache::parse(&cache).unwrap(); 73 | 74 | let class = cache.remap_class("android.support.constraint.ConstraintLayout$a"); 75 | assert_eq!( 76 | class, 77 | Some("android.support.constraint.ConstraintLayout$LayoutParams") 78 | ); 79 | } 80 | 81 | #[test] 82 | fn test_method_matches() { 83 | let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING)); 84 | 85 | let mut mapped = 86 | mapper.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 320)); 87 | 88 | assert_eq!( 89 | mapped.next().unwrap(), 90 | StackFrame::new( 91 | "android.support.constraint.solver.ArrayLinkedVariables", 92 | "remove", 93 | 320 94 | ) 95 | ); 96 | assert_eq!(mapped.next(), None); 97 | 98 | let mut mapped = 99 | mapper.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 200)); 100 | 101 | assert_eq!( 102 | mapped.next().unwrap(), 103 | StackFrame::new( 104 | "android.support.constraint.solver.ArrayLinkedVariables", 105 | "put", 106 | 200 107 | ) 108 | ); 109 | assert_eq!(mapped.next(), None); 110 | } 111 | 112 | #[test] 113 | fn test_method_matches_cache() { 114 | let mapping = ProguardMapping::new(MAPPING); 115 | 116 | let mut cache = Vec::new(); 117 | ProguardCache::write(&mapping, &mut cache).unwrap(); 118 | let cache = ProguardCache::parse(&cache).unwrap(); 119 | 120 | let mut mapped = 121 | cache.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 320)); 122 | 123 | assert_eq!( 124 | mapped.next().unwrap(), 125 | StackFrame::new( 126 | "android.support.constraint.solver.ArrayLinkedVariables", 127 | "remove", 128 | 320 129 | ) 130 | ); 131 | assert_eq!(mapped.next(), None); 132 | 133 | let mut mapped = 134 | cache.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 200)); 135 | 136 | assert_eq!( 137 | mapped.next().unwrap(), 138 | StackFrame::new( 139 | "android.support.constraint.solver.ArrayLinkedVariables", 140 | "put", 141 | 200 142 | ) 143 | ); 144 | assert_eq!(mapped.next(), None); 145 | } 146 | 147 | #[test] 148 | fn test_method_matches_win() { 149 | let mapper = ProguardMapper::new(ProguardMapping::new(&MAPPING_WIN[..])); 150 | 151 | let mut mapped = 152 | mapper.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 320)); 153 | 154 | assert_eq!( 155 | mapped.next().unwrap(), 156 | StackFrame::new( 157 | "android.support.constraint.solver.ArrayLinkedVariables", 158 | "remove", 159 | 320 160 | ) 161 | ); 162 | assert_eq!(mapped.next(), None); 163 | 164 | let mut mapped = 165 | mapper.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 200)); 166 | 167 | assert_eq!( 168 | mapped.next().unwrap(), 169 | StackFrame::new( 170 | "android.support.constraint.solver.ArrayLinkedVariables", 171 | "put", 172 | 200 173 | ) 174 | ); 175 | assert_eq!(mapped.next(), None); 176 | } 177 | 178 | #[test] 179 | fn test_method_matches_win_cache() { 180 | let mapping = ProguardMapping::new(&MAPPING_WIN[..]); 181 | 182 | let mut cache = Vec::new(); 183 | ProguardCache::write(&mapping, &mut cache).unwrap(); 184 | let cache = ProguardCache::parse(&cache).unwrap(); 185 | 186 | let mut mapped = 187 | cache.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 320)); 188 | 189 | assert_eq!( 190 | mapped.next().unwrap(), 191 | StackFrame::new( 192 | "android.support.constraint.solver.ArrayLinkedVariables", 193 | "remove", 194 | 320 195 | ) 196 | ); 197 | assert_eq!(mapped.next(), None); 198 | 199 | let mut mapped = 200 | cache.remap_frame(&StackFrame::new("android.support.constraint.a.a", "a", 200)); 201 | 202 | assert_eq!( 203 | mapped.next().unwrap(), 204 | StackFrame::new( 205 | "android.support.constraint.solver.ArrayLinkedVariables", 206 | "put", 207 | 200 208 | ) 209 | ); 210 | assert_eq!(mapped.next(), None); 211 | } 212 | 213 | #[test] 214 | fn test_inlines() { 215 | let mapping = ProguardMapping::new(include_bytes!("res/mapping-inlines.txt")); 216 | assert!(mapping.is_valid()); 217 | assert!(mapping.has_line_info()); 218 | #[cfg(feature = "uuid")] 219 | { 220 | assert_eq!( 221 | mapping.uuid(), 222 | "3828bd45-950f-5e77-9737-b6b3a1d80299".parse().unwrap() 223 | ); 224 | } 225 | 226 | let mapper = ProguardMapper::new(mapping); 227 | 228 | let raw = r#"java.lang.RuntimeException: Button press caused an exception! 229 | at io.sentry.sample.MainActivity.t(MainActivity.java:1) 230 | at e.a.c.a.onClick 231 | at android.view.View.performClick(View.java:7125) 232 | at android.view.View.performClickInternal(View.java:7102) 233 | at android.view.View.access$3500(View.java:801) 234 | at android.view.View$PerformClick.run(View.java:27336) 235 | at android.os.Handler.handleCallback(Handler.java:883) 236 | at android.os.Handler.dispatchMessage(Handler.java:100) 237 | at android.os.Looper.loop(Looper.java:214) 238 | at android.app.ActivityThread.main(ActivityThread.java:7356) 239 | at java.lang.reflect.Method.invoke(Method.java) 240 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 241 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"#; 242 | let remapped = mapper.remap_stacktrace(raw).unwrap(); 243 | 244 | assert_eq!( 245 | remapped.trim(), 246 | r#"java.lang.RuntimeException: Button press caused an exception! 247 | at io.sentry.sample.MainActivity.bar(MainActivity.java:54) 248 | at io.sentry.sample.MainActivity.foo(MainActivity.java:44) 249 | at io.sentry.sample.MainActivity.onClickHandler(MainActivity.java:40) 250 | at e.a.c.a.onClick 251 | at android.view.View.performClick(View.java:7125) 252 | at android.view.View.performClickInternal(View.java:7102) 253 | at android.view.View.access$3500(View.java:801) 254 | at android.view.View$PerformClick.run(View.java:27336) 255 | at android.os.Handler.handleCallback(Handler.java:883) 256 | at android.os.Handler.dispatchMessage(Handler.java:100) 257 | at android.os.Looper.loop(Looper.java:214) 258 | at android.app.ActivityThread.main(ActivityThread.java:7356) 259 | at java.lang.reflect.Method.invoke(Method.java) 260 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 261 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"# 262 | ); 263 | } 264 | 265 | #[test] 266 | fn test_inlines_cache() { 267 | let mapping = ProguardMapping::new(include_bytes!("res/mapping-inlines.txt")); 268 | assert!(mapping.is_valid()); 269 | assert!(mapping.has_line_info()); 270 | #[cfg(feature = "uuid")] 271 | { 272 | assert_eq!( 273 | mapping.uuid(), 274 | "3828bd45-950f-5e77-9737-b6b3a1d80299".parse().unwrap() 275 | ); 276 | } 277 | 278 | let mut cache = Vec::new(); 279 | ProguardCache::write(&mapping, &mut cache).unwrap(); 280 | let cache = ProguardCache::parse(&cache).unwrap(); 281 | 282 | let raw = r#"java.lang.RuntimeException: Button press caused an exception! 283 | at io.sentry.sample.MainActivity.t(MainActivity.java:1) 284 | at e.a.c.a.onClick 285 | at android.view.View.performClick(View.java:7125) 286 | at android.view.View.performClickInternal(View.java:7102) 287 | at android.view.View.access$3500(View.java:801) 288 | at android.view.View$PerformClick.run(View.java:27336) 289 | at android.os.Handler.handleCallback(Handler.java:883) 290 | at android.os.Handler.dispatchMessage(Handler.java:100) 291 | at android.os.Looper.loop(Looper.java:214) 292 | at android.app.ActivityThread.main(ActivityThread.java:7356) 293 | at java.lang.reflect.Method.invoke(Method.java) 294 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 295 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"#; 296 | let remapped = cache.remap_stacktrace(raw).unwrap(); 297 | 298 | assert_eq!( 299 | remapped.trim(), 300 | r#"java.lang.RuntimeException: Button press caused an exception! 301 | at io.sentry.sample.MainActivity.bar(MainActivity.java:54) 302 | at io.sentry.sample.MainActivity.foo(MainActivity.java:44) 303 | at io.sentry.sample.MainActivity.onClickHandler(MainActivity.java:40) 304 | at e.a.c.a.onClick 305 | at android.view.View.performClick(View.java:7125) 306 | at android.view.View.performClickInternal(View.java:7102) 307 | at android.view.View.access$3500(View.java:801) 308 | at android.view.View$PerformClick.run(View.java:27336) 309 | at android.os.Handler.handleCallback(Handler.java:883) 310 | at android.os.Handler.dispatchMessage(Handler.java:100) 311 | at android.os.Looper.loop(Looper.java:214) 312 | at android.app.ActivityThread.main(ActivityThread.java:7356) 313 | at java.lang.reflect.Method.invoke(Method.java) 314 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 315 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)"# 316 | ); 317 | } 318 | 319 | #[cfg(feature = "uuid")] 320 | #[test] 321 | fn test_uuid() { 322 | assert_eq!( 323 | ProguardMapping::new(MAPPING).uuid(), 324 | "5cd8e873-1127-5276-81b7-8ff25043ecfd".parse().unwrap() 325 | ); 326 | } 327 | 328 | #[cfg(feature = "uuid")] 329 | #[test] 330 | fn test_uuid_win() { 331 | assert_eq!( 332 | ProguardMapping::new(&MAPPING_WIN[..]).uuid(), 333 | "71d468f2-0dc4-5017-9f12-1a81081913ef".parse().unwrap() 334 | ); 335 | } 336 | -------------------------------------------------------------------------------- /tests/callback.rs: -------------------------------------------------------------------------------- 1 | use proguard::{ProguardMapper, ProguardMapping, StackFrame}; 2 | 3 | static MAPPING_CALLBACK: &[u8] = include_bytes!("res/mapping-callback.txt"); 4 | static MAPPING_CALLBACK_EXTRA_CLASS: &[u8] = include_bytes!("res/mapping-callback-extra-class.txt"); 5 | static MAPPING_CALLBACK_INNER_CLASS: &[u8] = include_bytes!("res/mapping-callback-inner-class.txt"); 6 | 7 | #[test] 8 | fn test_method_matches_callback() { 9 | // see the following files for sources used when creating the mapping file: 10 | // - res/mapping-callback_EditActivity.kt 11 | let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_CALLBACK)); 12 | 13 | let mut mapped = mapper.remap_frame(&StackFrame::new( 14 | "io.sentry.samples.instrumentation.ui.g", 15 | "onMenuItemClick", 16 | 28, 17 | )); 18 | 19 | assert_eq!( 20 | mapped.next().unwrap(), 21 | StackFrame::with_file( 22 | "io.sentry.samples.instrumentation.ui.EditActivity", 23 | "onCreate$lambda$1", 24 | 37, 25 | "EditActivity", 26 | ) 27 | ); 28 | assert_eq!( 29 | mapped.next().unwrap(), 30 | StackFrame::with_file( 31 | "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", 32 | "onMenuItemClick", 33 | 0, 34 | "EditActivity", 35 | ) 36 | ); 37 | assert_eq!(mapped.next(), None); 38 | } 39 | 40 | #[test] 41 | fn test_method_matches_callback_extra_class() { 42 | // see the following files for sources used when creating the mapping file: 43 | // - res/mapping-callback-extra-class_EditActivity.kt 44 | // - res/mapping-callback-extra-class_TestSourceContext.kt 45 | let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_CALLBACK_EXTRA_CLASS)); 46 | 47 | let mut mapped = mapper.remap_frame(&StackFrame::new( 48 | "io.sentry.samples.instrumentation.ui.g", 49 | "onMenuItemClick", 50 | 28, 51 | )); 52 | 53 | assert_eq!( 54 | mapped.next().unwrap(), 55 | StackFrame::with_file( 56 | "io.sentry.samples.instrumentation.ui.TestSourceContext", 57 | "test2", 58 | 10, 59 | "TestSourceContext", 60 | ) 61 | ); 62 | assert_eq!( 63 | mapped.next().unwrap(), 64 | StackFrame::with_file( 65 | "io.sentry.samples.instrumentation.ui.TestSourceContext", 66 | "test", 67 | 6, 68 | "TestSourceContext", 69 | ) 70 | ); 71 | assert_eq!( 72 | mapped.next().unwrap(), 73 | StackFrame::with_file( 74 | "io.sentry.samples.instrumentation.ui.EditActivity", 75 | "onCreate$lambda$1", 76 | 38, 77 | "EditActivity", 78 | ) 79 | ); 80 | assert_eq!( 81 | mapped.next().unwrap(), 82 | StackFrame::with_file( 83 | "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", 84 | "onMenuItemClick", 85 | 0, 86 | "EditActivity", 87 | ) 88 | ); 89 | assert_eq!(mapped.next(), None); 90 | } 91 | 92 | #[test] 93 | fn test_method_matches_callback_inner_class() { 94 | // see the following files for sources used when creating the mapping file: 95 | // - res/mapping-callback-inner-class_EditActivity.kt 96 | let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_CALLBACK_INNER_CLASS)); 97 | 98 | let mut mapped = mapper.remap_frame(&StackFrame::new( 99 | "io.sentry.samples.instrumentation.ui.g", 100 | "onMenuItemClick", 101 | 28, 102 | )); 103 | 104 | assert_eq!( 105 | mapped.next().unwrap(), 106 | StackFrame::with_file( 107 | "io.sentry.samples.instrumentation.ui.EditActivity$InnerEditActivityClass", 108 | "testInner", 109 | 19, 110 | "EditActivity", 111 | ) 112 | ); 113 | assert_eq!( 114 | mapped.next().unwrap(), 115 | StackFrame::with_file( 116 | "io.sentry.samples.instrumentation.ui.EditActivity", 117 | "onCreate$lambda$1", 118 | 45, 119 | "EditActivity", 120 | ) 121 | ); 122 | assert_eq!( 123 | mapped.next().unwrap(), 124 | StackFrame::with_file( 125 | "io.sentry.samples.instrumentation.ui.EditActivity$$InternalSyntheticLambda$1$ebaa538726b99bb77e0f5e7c86443911af17d6e5be2b8771952ae0caa4ff2ac7$0", 126 | "onMenuItemClick", 127 | 0, 128 | "EditActivity", 129 | ) 130 | ); 131 | assert_eq!(mapped.next(), None); 132 | } 133 | -------------------------------------------------------------------------------- /tests/r8.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | 3 | use proguard::{ProguardMapper, ProguardMapping, StackFrame}; 4 | 5 | static MAPPING_R8: &[u8] = include_bytes!("res/mapping-r8.txt"); 6 | static MAPPING_R8_SYMBOLICATED_FILE_NAMES: &[u8] = 7 | include_bytes!("res/mapping-r8-symbolicated_file_names.txt"); 8 | 9 | lazy_static! { 10 | static ref MAPPING_WIN_R8: Vec = MAPPING_R8 11 | .iter() 12 | .flat_map(|&byte| if byte == b'\n' { 13 | vec![b'\r', b'\n'] 14 | } else { 15 | vec![byte] 16 | }) 17 | .collect(); 18 | } 19 | 20 | #[test] 21 | fn test_basic_r8() { 22 | let mapping = ProguardMapping::new(MAPPING_R8); 23 | assert!(mapping.is_valid()); 24 | assert!(mapping.has_line_info()); 25 | 26 | let mapper = ProguardMapper::new(mapping); 27 | 28 | let class = mapper.remap_class("a.a.a.a.c"); 29 | assert_eq!(class, Some("android.arch.core.executor.ArchTaskExecutor")); 30 | } 31 | 32 | #[test] 33 | fn test_extra_methods() { 34 | let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_R8)); 35 | 36 | let mut mapped = mapper.remap_frame(&StackFrame::new("a.a.a.b.c$a", "", 1)); 37 | 38 | assert_eq!( 39 | mapped.next().unwrap(), 40 | StackFrame::new( 41 | "android.arch.core.internal.SafeIterableMap$AscendingIterator", 42 | "", 43 | 270 44 | ) 45 | ); 46 | assert_eq!(mapped.next(), None); 47 | } 48 | 49 | #[test] 50 | fn test_method_matches() { 51 | let mapper = ProguardMapper::new(ProguardMapping::new(MAPPING_R8)); 52 | 53 | let mut mapped = mapper.remap_frame(&StackFrame::new("a.a.a.b.c", "a", 1)); 54 | 55 | assert_eq!( 56 | mapped.next().unwrap(), 57 | StackFrame::new( 58 | "android.arch.core.internal.SafeIterableMap", 59 | "access$100", 60 | 35 61 | ) 62 | ); 63 | assert_eq!(mapped.next(), None); 64 | 65 | let mut mapped = mapper.remap_frame(&StackFrame::new("a.a.a.b.c", "a", 13)); 66 | 67 | assert_eq!( 68 | mapped.next().unwrap(), 69 | StackFrame::new("android.arch.core.internal.SafeIterableMap", "eldest", 168) 70 | ); 71 | assert_eq!(mapped.next(), None); 72 | } 73 | 74 | #[test] 75 | fn test_summary() { 76 | let mapping = ProguardMapping::new(MAPPING_R8); 77 | 78 | let summary = mapping.summary(); 79 | assert_eq!(summary.compiler(), Some("R8")); 80 | assert_eq!(summary.compiler_version(), Some("1.3.49")); 81 | assert_eq!(summary.min_api(), Some(15)); 82 | assert_eq!(summary.class_count(), 1167); 83 | assert_eq!(summary.method_count(), 24076); 84 | } 85 | 86 | #[cfg(feature = "uuid")] 87 | #[test] 88 | fn test_uuid() { 89 | assert_eq!( 90 | ProguardMapping::new(MAPPING_R8).uuid(), 91 | "c96fb926-797c-53de-90ee-df2aeaf28340".parse().unwrap() 92 | ); 93 | } 94 | 95 | #[cfg(feature = "uuid")] 96 | #[test] 97 | fn test_uuid_win() { 98 | assert_eq!( 99 | ProguardMapping::new(&MAPPING_WIN_R8[..]).uuid(), 100 | "d8b03b44-58df-5cd7-adc7-aefcfb0e2ade".parse().unwrap() 101 | ); 102 | } 103 | 104 | #[test] 105 | fn test_remap_source_file() { 106 | let mapping = ProguardMapping::new(MAPPING_R8_SYMBOLICATED_FILE_NAMES); 107 | 108 | let mapper = ProguardMapper::new(mapping); 109 | 110 | let test = mapper.remap_stacktrace( 111 | r#" 112 | Caused by: java.lang.Exception: Hello from main! 113 | at a.a.a(SourceFile:12) 114 | at io.wzieba.r8fullmoderenamessources.MainActivity.b(SourceFile:6) 115 | at io.wzieba.r8fullmoderenamessources.MainActivity.a(SourceFile:1) 116 | at a.c.onClick(SourceFile:1) 117 | at android.view.View.performClick(View.java:7659) 118 | at android.view.View.performClickInternal(View.java:7636) 119 | at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)"#, 120 | ); 121 | 122 | assert_eq!(r#" 123 | Caused by: java.lang.Exception: Hello from main! 124 | at io.wzieba.r8fullmoderenamessources.Foobar.foo(Foobar.kt:10) 125 | at io.wzieba.r8fullmoderenamessources.MainActivity.onCreate$lambda$1$lambda$0(MainActivity.kt:14) 126 | at io.wzieba.r8fullmoderenamessources.MainActivity.$r8$lambda$pOQDVg57r6gG0-DzwbGf17BfNbs(MainActivity.kt:0) 127 | at io.wzieba.r8fullmoderenamessources.MainActivity$$ExternalSyntheticLambda0.onClick(MainActivity:0) 128 | at android.view.View.performClick(View.java:7659) 129 | at android.view.View.performClickInternal(View.java:7636) 130 | at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)"#.trim(), test.unwrap().trim()); 131 | } 132 | -------------------------------------------------------------------------------- /tests/res/mapping-callback-extra-class_EditActivity.kt: -------------------------------------------------------------------------------- 1 | package io.sentry.samples.instrumentation.ui 2 | 3 | import android.os.Bundle 4 | import android.widget.EditText 5 | import android.widget.Toast 6 | import androidx.activity.ComponentActivity 7 | import androidx.appcompat.widget.Toolbar 8 | import io.sentry.Sentry 9 | import io.sentry.SpanStatus 10 | import io.sentry.samples.instrumentation.R 11 | import io.sentry.samples.instrumentation.SampleApp 12 | import io.sentry.samples.instrumentation.data.Track 13 | import kotlinx.coroutines.runBlocking 14 | 15 | class EditActivity : ComponentActivity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_edit) 20 | 21 | val nameInput = findViewById(R.id.track_name) 22 | val composerInput = findViewById(R.id.track_composer) 23 | val durationInput = findViewById(R.id.track_duration) 24 | val unitPriceInput = findViewById(R.id.track_unit_price) 25 | 26 | val originalTrack: Track? = intent.getSerializableExtra(TRACK_EXTRA_KEY) as? Track 27 | originalTrack?.run { 28 | nameInput.setText(name) 29 | composerInput.setText(composer) 30 | durationInput.setText(millis.toString()) 31 | unitPriceInput.setText(price.toString()) 32 | } 33 | 34 | findViewById(R.id.toolbar).setOnMenuItemClickListener { 35 | if (it.itemId == R.id.action_save) { 36 | try { 37 | // throw RuntimeException("thrown on purpose to test source context line number mismatch") 38 | TestSourceContext().test() 39 | } catch (t: Throwable) { 40 | Sentry.captureException(t); 41 | } 42 | val transaction = Sentry.startTransaction( 43 | "Track Interaction", 44 | if (originalTrack == null) "ui.action.add" else "ui.action.edit", 45 | true 46 | ) 47 | 48 | val name = nameInput.text.toString() 49 | val composer = composerInput.text.toString() 50 | val duration = durationInput.text.toString() 51 | val unitPrice = unitPriceInput.text.toString() 52 | if (name.isEmpty() || composer.isEmpty() || 53 | duration.isEmpty() || duration.toLongOrNull() == null || 54 | unitPrice.isEmpty() || unitPrice.toFloatOrNull() == null 55 | ) { 56 | Toast.makeText( 57 | this, 58 | "Some of the inputs are empty or have wrong format " + 59 | "(duration/unitprice not a number)", 60 | Toast.LENGTH_LONG 61 | ).show() 62 | } else { 63 | if (originalTrack == null) { 64 | addNewTrack(name, composer, duration.toLong(), unitPrice.toFloat()) 65 | 66 | val createCount = SampleApp.analytics.getInt("create_count", 0) + 1 67 | SampleApp.analytics.edit().putInt("create_count", createCount).apply() 68 | } else { 69 | originalTrack.update(name, composer, duration.toLong(), unitPrice.toFloat()) 70 | 71 | val editCount = SampleApp.analytics.getInt("edit_count", 0) + 1 72 | SampleApp.analytics.edit().putInt("edit_count", editCount).apply() 73 | } 74 | transaction.finish(SpanStatus.OK) 75 | finish() 76 | } 77 | return@setOnMenuItemClickListener true 78 | } 79 | return@setOnMenuItemClickListener false 80 | } 81 | } 82 | 83 | private fun addNewTrack(name: String, composer: String, duration: Long, unitPrice: Float) { 84 | val newTrack = Track( 85 | name = name, 86 | albumId = null, 87 | composer = composer, 88 | mediaTypeId = null, 89 | genreId = null, 90 | millis = duration, 91 | bytes = null, 92 | price = unitPrice 93 | ) 94 | runBlocking { 95 | SampleApp.database.tracksDao().insert(newTrack) 96 | } 97 | } 98 | 99 | private fun Track.update(name: String, composer: String, duration: Long, unitPrice: Float) { 100 | val updatedTrack = copy( 101 | name = name, 102 | composer = composer, 103 | millis = duration, 104 | price = unitPrice 105 | ) 106 | runBlocking { 107 | SampleApp.database.tracksDao().update(updatedTrack) 108 | } 109 | } 110 | 111 | companion object { 112 | const val TRACK_EXTRA_KEY = "EditActivity.Track" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/res/mapping-callback-extra-class_TestSourceContext.kt: -------------------------------------------------------------------------------- 1 | package io.sentry.samples.instrumentation.ui 2 | 3 | class TestSourceContext { 4 | 5 | fun test() { 6 | test2() 7 | } 8 | 9 | fun test2() { 10 | throw IllegalStateException("checking line numbers in source context") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/res/mapping-callback-inner-class_EditActivity.kt: -------------------------------------------------------------------------------- 1 | package io.sentry.samples.instrumentation.ui 2 | 3 | import android.os.Bundle 4 | import android.widget.EditText 5 | import android.widget.Toast 6 | import androidx.activity.ComponentActivity 7 | import androidx.appcompat.widget.Toolbar 8 | import io.sentry.Sentry 9 | import io.sentry.SpanStatus 10 | import io.sentry.samples.instrumentation.R 11 | import io.sentry.samples.instrumentation.SampleApp 12 | import io.sentry.samples.instrumentation.data.Track 13 | import kotlinx.coroutines.runBlocking 14 | 15 | class EditActivity : ComponentActivity() { 16 | 17 | class InnerEditActivityClass { 18 | fun testInner() { 19 | throw RuntimeException("thrown on purpose to test source context line number mismatch") 20 | } 21 | } 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.activity_edit) 26 | 27 | val nameInput = findViewById(R.id.track_name) 28 | val composerInput = findViewById(R.id.track_composer) 29 | val durationInput = findViewById(R.id.track_duration) 30 | val unitPriceInput = findViewById(R.id.track_unit_price) 31 | 32 | val originalTrack: Track? = intent.getSerializableExtra(TRACK_EXTRA_KEY) as? Track 33 | originalTrack?.run { 34 | nameInput.setText(name) 35 | composerInput.setText(composer) 36 | durationInput.setText(millis.toString()) 37 | unitPriceInput.setText(price.toString()) 38 | } 39 | 40 | findViewById(R.id.toolbar).setOnMenuItemClickListener { 41 | if (it.itemId == R.id.action_save) { 42 | try { 43 | // throw RuntimeException("thrown on purpose to test source context line number mismatch") 44 | // TestSourceContext().test() 45 | InnerEditActivityClass().testInner() 46 | } catch (t: Throwable) { 47 | Sentry.captureException(t); 48 | } 49 | val transaction = Sentry.startTransaction( 50 | "Track Interaction", 51 | if (originalTrack == null) "ui.action.add" else "ui.action.edit", 52 | true 53 | ) 54 | 55 | val name = nameInput.text.toString() 56 | val composer = composerInput.text.toString() 57 | val duration = durationInput.text.toString() 58 | val unitPrice = unitPriceInput.text.toString() 59 | if (name.isEmpty() || composer.isEmpty() || 60 | duration.isEmpty() || duration.toLongOrNull() == null || 61 | unitPrice.isEmpty() || unitPrice.toFloatOrNull() == null 62 | ) { 63 | Toast.makeText( 64 | this, 65 | "Some of the inputs are empty or have wrong format " + 66 | "(duration/unitprice not a number)", 67 | Toast.LENGTH_LONG 68 | ).show() 69 | } else { 70 | if (originalTrack == null) { 71 | addNewTrack(name, composer, duration.toLong(), unitPrice.toFloat()) 72 | 73 | val createCount = SampleApp.analytics.getInt("create_count", 0) + 1 74 | SampleApp.analytics.edit().putInt("create_count", createCount).apply() 75 | } else { 76 | originalTrack.update(name, composer, duration.toLong(), unitPrice.toFloat()) 77 | 78 | val editCount = SampleApp.analytics.getInt("edit_count", 0) + 1 79 | SampleApp.analytics.edit().putInt("edit_count", editCount).apply() 80 | } 81 | transaction.finish(SpanStatus.OK) 82 | finish() 83 | } 84 | return@setOnMenuItemClickListener true 85 | } 86 | return@setOnMenuItemClickListener false 87 | } 88 | } 89 | 90 | private fun addNewTrack(name: String, composer: String, duration: Long, unitPrice: Float) { 91 | val newTrack = Track( 92 | name = name, 93 | albumId = null, 94 | composer = composer, 95 | mediaTypeId = null, 96 | genreId = null, 97 | millis = duration, 98 | bytes = null, 99 | price = unitPrice 100 | ) 101 | runBlocking { 102 | SampleApp.database.tracksDao().insert(newTrack) 103 | } 104 | } 105 | 106 | private fun Track.update(name: String, composer: String, duration: Long, unitPrice: Float) { 107 | val updatedTrack = copy( 108 | name = name, 109 | composer = composer, 110 | millis = duration, 111 | price = unitPrice 112 | ) 113 | runBlocking { 114 | SampleApp.database.tracksDao().update(updatedTrack) 115 | } 116 | } 117 | 118 | companion object { 119 | const val TRACK_EXTRA_KEY = "EditActivity.Track" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/res/mapping-callback_EditActivity.kt: -------------------------------------------------------------------------------- 1 | package io.sentry.samples.instrumentation.ui 2 | 3 | import android.os.Bundle 4 | import android.widget.EditText 5 | import android.widget.Toast 6 | import androidx.activity.ComponentActivity 7 | import androidx.appcompat.widget.Toolbar 8 | import io.sentry.Sentry 9 | import io.sentry.SpanStatus 10 | import io.sentry.samples.instrumentation.R 11 | import io.sentry.samples.instrumentation.SampleApp 12 | import io.sentry.samples.instrumentation.data.Track 13 | import kotlinx.coroutines.runBlocking 14 | 15 | class EditActivity : ComponentActivity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_edit) 20 | 21 | val nameInput = findViewById(R.id.track_name) 22 | val composerInput = findViewById(R.id.track_composer) 23 | val durationInput = findViewById(R.id.track_duration) 24 | val unitPriceInput = findViewById(R.id.track_unit_price) 25 | 26 | val originalTrack: Track? = intent.getSerializableExtra(TRACK_EXTRA_KEY) as? Track 27 | originalTrack?.run { 28 | nameInput.setText(name) 29 | composerInput.setText(composer) 30 | durationInput.setText(millis.toString()) 31 | unitPriceInput.setText(price.toString()) 32 | } 33 | 34 | findViewById(R.id.toolbar).setOnMenuItemClickListener { 35 | if (it.itemId == R.id.action_save) { 36 | try { 37 | throw RuntimeException("thrown on purpose to test source context line number mismatch") 38 | } catch (t: Throwable) { 39 | Sentry.captureException(t); 40 | } 41 | val transaction = Sentry.startTransaction( 42 | "Track Interaction", 43 | if (originalTrack == null) "ui.action.add" else "ui.action.edit", 44 | true 45 | ) 46 | 47 | val name = nameInput.text.toString() 48 | val composer = composerInput.text.toString() 49 | val duration = durationInput.text.toString() 50 | val unitPrice = unitPriceInput.text.toString() 51 | if (name.isEmpty() || composer.isEmpty() || 52 | duration.isEmpty() || duration.toLongOrNull() == null || 53 | unitPrice.isEmpty() || unitPrice.toFloatOrNull() == null 54 | ) { 55 | Toast.makeText( 56 | this, 57 | "Some of the inputs are empty or have wrong format " + 58 | "(duration/unitprice not a number)", 59 | Toast.LENGTH_LONG 60 | ).show() 61 | } else { 62 | if (originalTrack == null) { 63 | addNewTrack(name, composer, duration.toLong(), unitPrice.toFloat()) 64 | 65 | val createCount = SampleApp.analytics.getInt("create_count", 0) + 1 66 | SampleApp.analytics.edit().putInt("create_count", createCount).apply() 67 | } else { 68 | originalTrack.update(name, composer, duration.toLong(), unitPrice.toFloat()) 69 | 70 | val editCount = SampleApp.analytics.getInt("edit_count", 0) + 1 71 | SampleApp.analytics.edit().putInt("edit_count", editCount).apply() 72 | } 73 | transaction.finish(SpanStatus.OK) 74 | finish() 75 | } 76 | return@setOnMenuItemClickListener true 77 | } 78 | return@setOnMenuItemClickListener false 79 | } 80 | } 81 | 82 | private fun addNewTrack(name: String, composer: String, duration: Long, unitPrice: Float) { 83 | val newTrack = Track( 84 | name = name, 85 | albumId = null, 86 | composer = composer, 87 | mediaTypeId = null, 88 | genreId = null, 89 | millis = duration, 90 | bytes = null, 91 | price = unitPrice 92 | ) 93 | runBlocking { 94 | SampleApp.database.tracksDao().insert(newTrack) 95 | } 96 | } 97 | 98 | private fun Track.update(name: String, composer: String, duration: Long, unitPrice: Float) { 99 | val updatedTrack = copy( 100 | name = name, 101 | composer = composer, 102 | millis = duration, 103 | price = unitPrice 104 | ) 105 | runBlocking { 106 | SampleApp.database.tracksDao().update(updatedTrack) 107 | } 108 | } 109 | 110 | companion object { 111 | const val TRACK_EXTRA_KEY = "EditActivity.Track" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/res/mapping-inlines.txt: -------------------------------------------------------------------------------- 1 | # compiler: R8 2 | # compiler_version: 2.0.74 3 | # min_api: 16 4 | # pg_map_id: 5b46fdc 5 | # common_typos_disable 6 | $r8$backportedMethods$utility$Objects$2$equals -> a: 7 | boolean equals(java.lang.Object,java.lang.Object) -> a 8 | $r8$twr$utility -> b: 9 | void $closeResource(java.lang.Throwable,java.lang.Object) -> a 10 | android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer: 11 | 1:1:void ():11:11 -> 12 | 1:1:androidx.core.app.RemoteActionCompat read(androidx.versionedparcelable.VersionedParcel):13:13 -> read 13 | 1:1:void write(androidx.core.app.RemoteActionCompat,androidx.versionedparcelable.VersionedParcel):17:17 -> write 14 | android.support.v4.graphics.drawable.IconCompatParcelizer -> android.support.v4.graphics.drawable.IconCompatParcelizer: 15 | 1:1:void ():11:11 -> 16 | 1:1:androidx.core.graphics.drawable.IconCompat read(androidx.versionedparcelable.VersionedParcel):13:13 -> read 17 | 1:1:void write(androidx.core.graphics.drawable.IconCompat,androidx.versionedparcelable.VersionedParcel):17:17 -> write 18 | androidx.activity.Cancellable -> c.a.a: 19 | androidx.activity.ComponentActivity -> androidx.activity.ComponentActivity: 20 | androidx.activity.OnBackPressedDispatcher mOnBackPressedDispatcher -> f 21 | androidx.lifecycle.ViewModelStore mViewModelStore -> e 22 | androidx.lifecycle.LifecycleRegistry mLifecycleRegistry -> c 23 | androidx.savedstate.SavedStateRegistryController mSavedStateRegistryController -> d 24 | 1:1:void ():84:84 -> 25 | 2:2:void ():61:61 -> 26 | 3:3:androidx.savedstate.SavedStateRegistryController androidx.savedstate.SavedStateRegistryController.create(androidx.savedstate.SavedStateRegistryOwner):84:84 -> 27 | 3:3:void ():63 -> 28 | 4:4:void ():63:63 -> 29 | 5:5:void ():68:68 -> 30 | 6:6:androidx.lifecycle.Lifecycle getLifecycle():241:241 -> 31 | 6:6:void ():85 -> 32 | 7:8:void ():93:94 -> 33 | 9:9:androidx.lifecycle.Lifecycle getLifecycle():241:241 -> 34 | 9:9:void ():108 -> 35 | 10:10:void ():108:108 -> 36 | 11:11:void ():120:120 -> 37 | 12:12:androidx.lifecycle.Lifecycle getLifecycle():241:241 -> 38 | 12:12:void ():121 -> 39 | 13:13:void ():121:121 -> 40 | 14:14:void ():88:88 -> 41 | 1:1:androidx.lifecycle.Lifecycle getLifecycle():241:241 -> a 42 | 1:1:androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher():297:297 -> c 43 | 1:1:androidx.savedstate.SavedStateRegistry getSavedStateRegistry():303:303 -> d 44 | 2:2:androidx.savedstate.SavedStateRegistry androidx.savedstate.SavedStateRegistryController.getSavedStateRegistry():46:46 -> d 45 | 2:2:androidx.savedstate.SavedStateRegistry getSavedStateRegistry():303 -> d 46 | 1:1:androidx.lifecycle.ViewModelStore getViewModelStore():257:257 -> e 47 | 2:2:androidx.lifecycle.ViewModelStore getViewModelStore():261:261 -> e 48 | 3:3:androidx.lifecycle.ViewModelStore getViewModelStore():263:263 -> e 49 | 4:4:androidx.lifecycle.ViewModelStore getViewModelStore():266:266 -> e 50 | 5:6:androidx.lifecycle.ViewModelStore getViewModelStore():268:269 -> e 51 | 7:7:androidx.lifecycle.ViewModelStore getViewModelStore():272:272 -> e 52 | 8:8:androidx.lifecycle.ViewModelStore getViewModelStore():258:258 -> e 53 | 1:1:void access$001(androidx.activity.ComponentActivity):50:50 -> j 54 | 1:1:void onBackPressed():286:286 -> onBackPressed 55 | 1:3:void onCreate(android.os.Bundle):149:151 -> onCreate 56 | 1:1:java.lang.Object onRetainNonConfigurationInstance():178:178 -> onRetainNonConfigurationInstance 57 | 2:2:java.lang.Object onRetainNonConfigurationInstance():183:183 -> onRetainNonConfigurationInstance 58 | 3:3:java.lang.Object onRetainNonConfigurationInstance():185:185 -> onRetainNonConfigurationInstance 59 | 4:4:java.lang.Object onRetainNonConfigurationInstance():193:193 -> onRetainNonConfigurationInstance 60 | 5:5:java.lang.Object onRetainNonConfigurationInstance():195:195 -> onRetainNonConfigurationInstance 61 | 1:1:androidx.lifecycle.Lifecycle getLifecycle():241:241 -> onSaveInstanceState 62 | 1:1:void onSaveInstanceState(android.os.Bundle):160 -> onSaveInstanceState 63 | 2:3:void onSaveInstanceState(android.os.Bundle):161:162 -> onSaveInstanceState 64 | 4:4:void androidx.lifecycle.LifecycleRegistry.setCurrentState(androidx.lifecycle.Lifecycle$State):118:118 -> onSaveInstanceState 65 | 4:4:void onSaveInstanceState(android.os.Bundle):162 -> onSaveInstanceState 66 | 5:6:void onSaveInstanceState(android.os.Bundle):164:165 -> onSaveInstanceState 67 | androidx.activity.ComponentActivity$1 -> androidx.activity.ComponentActivity$a: 68 | androidx.activity.ComponentActivity this$0 -> b 69 | 1:1:void (androidx.activity.ComponentActivity):69:69 -> 70 | 1:1:void run():72:72 -> run 71 | androidx.activity.ComponentActivity$2 -> androidx.activity.ComponentActivity$2: 72 | androidx.activity.ComponentActivity this$0 -> a 73 | 1:1:void (androidx.activity.ComponentActivity):94:94 -> 74 | 1:3:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):98:100 -> a 75 | 4:4:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):102:102 -> a 76 | androidx.activity.ComponentActivity$3 -> androidx.activity.ComponentActivity$3: 77 | androidx.activity.ComponentActivity this$0 -> a 78 | 1:1:void (androidx.activity.ComponentActivity):108:108 -> 79 | 1:3:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):112:114 -> a 80 | androidx.activity.ComponentActivity$NonConfigurationInstances -> androidx.activity.ComponentActivity$b: 81 | androidx.lifecycle.ViewModelStore viewModelStore -> a 82 | 1:1:void ():56:56 -> 83 | androidx.activity.ImmLeaksCleaner -> androidx.activity.ImmLeaksCleaner: 84 | java.lang.reflect.Field sServedViewField -> d 85 | java.lang.reflect.Field sNextServedViewField -> e 86 | java.lang.reflect.Field sHField -> c 87 | int sReflectedFieldsInitialized -> b 88 | android.app.Activity mActivity -> a 89 | 1:2:void (android.app.Activity):45:46 -> 90 | 1:1:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):51:51 -> a 91 | 2:2:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):54:54 -> a 92 | 3:10:void initializeReflectiveFields():101:108 -> a 93 | 3:10:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):55 -> a 94 | 11:13:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):57:59 -> a 95 | 14:14:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):62:62 -> a 96 | 15:15:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):69:69 -> a 97 | 16:16:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):72:72 -> a 98 | 17:17:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):79:79 -> a 99 | 18:19:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):81:82 -> a 100 | 20:20:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):87:87 -> a 101 | 21:21:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):91:91 -> a 102 | 22:22:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):94:94 -> a 103 | 23:23:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):89:89 -> a 104 | 24:24:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):76:76 -> a 105 | 25:25:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):74:74 -> a 106 | 26:26:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):91:91 -> a 107 | androidx.activity.OnBackPressedCallback -> c.a.b: 108 | boolean mEnabled -> a 109 | java.util.concurrent.CopyOnWriteArrayList mCancellables -> b 110 | 1:1:void (boolean):54:54 -> 111 | 2:2:void (boolean):46:46 -> 112 | 3:3:void (boolean):55:55 -> 113 | androidx.activity.OnBackPressedDispatcher -> androidx.activity.OnBackPressedDispatcher: 114 | java.lang.Runnable mFallbackOnBackPressed -> a 115 | java.util.ArrayDeque mOnBackPressedCallbacks -> b 116 | 1:1:void (java.lang.Runnable):75:75 -> 117 | 2:2:void (java.lang.Runnable):57:57 -> 118 | 3:3:void (java.lang.Runnable):76:76 -> 119 | 1:4:void onBackPressed():184:187 -> a 120 | 5:5:boolean androidx.activity.OnBackPressedCallback.isEnabled():82:82 -> a 121 | 5:5:void onBackPressed():188 -> a 122 | 6:6:void onBackPressed():189:189 -> a 123 | 7:7:void androidx.fragment.app.FragmentManagerImpl$1.handleOnBackPressed():108:108 -> a 124 | 7:7:void onBackPressed():189 -> a 125 | 8:9:void androidx.fragment.app.FragmentManagerImpl.handleOnBackPressed():230:231 -> a 126 | 8:9:void androidx.fragment.app.FragmentManagerImpl$1.handleOnBackPressed():108 -> a 127 | 8:9:void onBackPressed():189 -> a 128 | 10:10:boolean androidx.activity.OnBackPressedCallback.isEnabled():82:82 -> a 129 | 10:10:void androidx.fragment.app.FragmentManagerImpl.handleOnBackPressed():231 -> a 130 | 10:10:void androidx.fragment.app.FragmentManagerImpl$1.handleOnBackPressed():108 -> a 131 | 10:10:void onBackPressed():189 -> a 132 | 11:11:void androidx.fragment.app.FragmentManagerImpl.handleOnBackPressed():233:233 -> a 133 | 11:11:void androidx.fragment.app.FragmentManagerImpl$1.handleOnBackPressed():108 -> a 134 | 11:11:void onBackPressed():189 -> a 135 | 12:12:void androidx.fragment.app.FragmentManagerImpl.handleOnBackPressed():241:241 -> a 136 | 12:12:void androidx.fragment.app.FragmentManagerImpl$1.handleOnBackPressed():108 -> a 137 | 12:12:void onBackPressed():189 -> a 138 | 13:14:void onBackPressed():193:194 -> a 139 | androidx.activity.OnBackPressedDispatcher$LifecycleOnBackPressedCancellable -> androidx.activity.OnBackPressedDispatcher$LifecycleOnBackPressedCancellable: 140 | androidx.lifecycle.Lifecycle mLifecycle -> a 141 | androidx.activity.OnBackPressedCallback mOnBackPressedCallback -> b 142 | androidx.activity.OnBackPressedDispatcher this$0 -> d 143 | androidx.activity.Cancellable mCurrentCancellable -> c 144 | 1:4:void (androidx.activity.OnBackPressedDispatcher,androidx.lifecycle.Lifecycle,androidx.activity.OnBackPressedCallback):220:223 -> 145 | 1:2:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):229:230 -> a 146 | 3:4:androidx.activity.Cancellable androidx.activity.OnBackPressedDispatcher.addCancellableCallback(androidx.activity.OnBackPressedCallback):112:113 -> a 147 | 3:4:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):230 -> a 148 | 5:5:void androidx.activity.OnBackPressedCallback.addCancellable(androidx.activity.Cancellable):103:103 -> a 149 | 5:5:androidx.activity.Cancellable androidx.activity.OnBackPressedDispatcher.addCancellableCallback(androidx.activity.OnBackPressedCallback):114 -> a 150 | 5:5:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):230 -> a 151 | 6:7:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):230:231 -> a 152 | 8:9:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):233:234 -> a 153 | 10:11:void onStateChanged(androidx.lifecycle.LifecycleOwner,androidx.lifecycle.Lifecycle$Event):236:237 -> a 154 | 1:1:void cancel():243:243 -> cancel 155 | 2:2:void androidx.lifecycle.LifecycleRegistry.removeObserver(androidx.lifecycle.LifecycleObserver):223:223 -> cancel 156 | 2:2:void cancel():243 -> cancel 157 | 3:3:void cancel():244:244 -> cancel 158 | 4:4:void androidx.activity.OnBackPressedCallback.removeCancellable(androidx.activity.Cancellable):107:107 -> cancel 159 | 4:4:void cancel():244 -> cancel 160 | 5:7:void cancel():245:247 -> cancel 161 | io.sentry.sample.-$$Lambda$r3Avcbztes2hicEObh02jjhQqd4 -> e.a.c.a: 162 | io.sentry.sample.MainActivity f$0 -> b 163 | io.sentry.sample.MainActivity -> io.sentry.sample.MainActivity: 164 | 1:1:void ():15:15 -> 165 | 1:1:void onCreate(android.os.Bundle):19:19 -> onCreate 166 | 2:2:void onCreate(android.os.Bundle):22:22 -> onCreate 167 | 3:3:void onCreate(android.os.Bundle):26:26 -> onCreate 168 | 4:5:void onCreate(android.os.Bundle):31:32 -> onCreate 169 | 6:6:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150:150 -> onCreate 170 | 6:6:void onCreate(android.os.Bundle):33 -> onCreate 171 | 7:7:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):414:414 -> onCreate 172 | 7:7:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 173 | 7:7:void onCreate(android.os.Bundle):33 -> onCreate 174 | 8:9:androidx.appcompat.app.ActionBar androidx.appcompat.app.AppCompatDelegateImpl.getSupportActionBar():383:384 -> onCreate 175 | 8:9:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):419 -> onCreate 176 | 8:9:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 177 | 8:9:void onCreate(android.os.Bundle):33 -> onCreate 178 | 10:10:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):420:420 -> onCreate 179 | 10:10:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 180 | 10:10:void onCreate(android.os.Bundle):33 -> onCreate 181 | 11:11:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):428:428 -> onCreate 182 | 11:11:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 183 | 11:11:void onCreate(android.os.Bundle):33 -> onCreate 184 | 12:12:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):432:432 -> onCreate 185 | 12:12:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 186 | 12:12:void onCreate(android.os.Bundle):33 -> onCreate 187 | 13:13:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):436:436 -> onCreate 188 | 13:13:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 189 | 13:13:void onCreate(android.os.Bundle):33 -> onCreate 190 | 14:15:java.lang.CharSequence androidx.appcompat.app.AppCompatDelegateImpl.getTitle():992:993 -> onCreate 191 | 14:15:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):436 -> onCreate 192 | 14:15:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 193 | 14:15:void onCreate(android.os.Bundle):33 -> onCreate 194 | 16:16:java.lang.CharSequence androidx.appcompat.app.AppCompatDelegateImpl.getTitle():996:996 -> onCreate 195 | 16:16:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):436 -> onCreate 196 | 16:16:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 197 | 16:16:void onCreate(android.os.Bundle):33 -> onCreate 198 | 17:17:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):436:436 -> onCreate 199 | 17:17:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 200 | 17:17:void onCreate(android.os.Bundle):33 -> onCreate 201 | 18:19:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):438:439 -> onCreate 202 | 18:19:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 203 | 18:19:void onCreate(android.os.Bundle):33 -> onCreate 204 | 20:20:android.view.Window$Callback androidx.appcompat.app.ToolbarActionBar.getWrappedWindowCallback():77:77 -> onCreate 205 | 20:20:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):439 -> onCreate 206 | 20:20:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 207 | 20:20:void onCreate(android.os.Bundle):33 -> onCreate 208 | 21:21:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):439:439 -> onCreate 209 | 21:21:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 210 | 21:21:void onCreate(android.os.Bundle):33 -> onCreate 211 | 22:22:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):441:441 -> onCreate 212 | 22:22:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 213 | 22:22:void onCreate(android.os.Bundle):33 -> onCreate 214 | 23:23:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):443:443 -> onCreate 215 | 23:23:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 216 | 23:23:void onCreate(android.os.Bundle):33 -> onCreate 217 | 24:24:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):446:446 -> onCreate 218 | 24:24:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 219 | 24:24:void onCreate(android.os.Bundle):33 -> onCreate 220 | 25:26:void onCreate(android.os.Bundle):35:36 -> onCreate 221 | 27:27:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):421:421 -> onCreate 222 | 27:27:void androidx.appcompat.app.AppCompatActivity.setSupportActionBar(androidx.appcompat.widget.Toolbar):150 -> onCreate 223 | 27:27:void onCreate(android.os.Bundle):33 -> onCreate 224 | 1:1:boolean onCreateOptionsMenu(android.view.Menu):60:60 -> onCreateOptionsMenu 225 | 1:1:boolean onOptionsItemSelected(android.view.MenuItem):69:69 -> onOptionsItemSelected 226 | 2:2:boolean onOptionsItemSelected(android.view.MenuItem):76:76 -> onOptionsItemSelected 227 | 1:1:void bar():54:54 -> t 228 | 1:1:void foo():44 -> t 229 | 1:1:void onClickHandler(android.view.View):40 -> t 230 | -------------------------------------------------------------------------------- /tests/res/mapping-r8-symbolicated_file_names.txt: -------------------------------------------------------------------------------- 1 | # compiler: R8 2 | # compiler_version: 8.3.36 3 | # min_api: 24 4 | # common_typos_disable 5 | # {"id":"com.android.tools.r8.mapping","version":"2.2"} 6 | # pg_map_id: 48ffd94 7 | # pg_map_hash: SHA-256 48ffd9478fda293e1c713db4cc7c449781a9e799fa504e389ee32ed19775a3ba 8 | io.wzieba.r8fullmoderenamessources.Foobar -> a.a: 9 | # {"id":"sourceFile","fileName":"Foobar.kt"} 10 | 1:3:void ():3:3 -> 11 | 4:11:void ():5:5 -> 12 | 1:7:void foo():9:9 -> a 13 | 8:15:void foo():10:10 -> a 14 | io.wzieba.r8fullmoderenamessources.FoobarKt -> a.b: 15 | # {"id":"sourceFile","fileName":"Foobar.kt"} 16 | 1:5:void main():15:15 -> a 17 | 6:9:void main():16:16 -> a 18 | 1:4:void main(java.lang.String[]):0:0 -> b 19 | io.wzieba.r8fullmoderenamessources.MainActivity -> io.wzieba.r8fullmoderenamessources.MainActivity: 20 | # {"id":"sourceFile","fileName":"MainActivity.kt"} 21 | 1:4:void ():7:7 -> 22 | 1:1:void $r8$lambda$pOQDVg57r6gG0-DzwbGf17BfNbs(android.view.View):0:0 -> a 23 | # {"id":"com.android.tools.r8.synthesized"} 24 | 1:9:void onCreate$lambda$1$lambda$0(android.view.View):14:14 -> b 25 | 1:3:void onCreate(android.os.Bundle):10:10 -> onCreate 26 | 4:8:void onCreate(android.os.Bundle):12:12 -> onCreate 27 | 9:16:void onCreate(android.os.Bundle):13:13 -> onCreate 28 | 17:20:void onCreate(android.os.Bundle):12:12 -> onCreate 29 | io.wzieba.r8fullmoderenamessources.MainActivity$$ExternalSyntheticLambda0 -> a.c: 30 | # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} 31 | # {"id":"com.android.tools.r8.synthesized"} 32 | 1:4:void onClick(android.view.View):0:0 -> onClick 33 | # {"id":"com.android.tools.r8.synthesized"} 34 | io.wzieba.r8fullmoderenamessources.R -> a.d: 35 | void () -> 36 | # {"id":"com.android.tools.r8.synthesized"} 37 | -------------------------------------------------------------------------------- /tests/retrace.rs: -------------------------------------------------------------------------------- 1 | use proguard::{ProguardMapper, StackFrame}; 2 | 3 | #[test] 4 | fn test_remap() { 5 | // https://github.com/getsentry/rust-proguard/issues/5#issue-410310382 6 | let mapper = ProguardMapper::from( 7 | r#"some.Class -> obfuscated: 8 | 7:8:void method3(long):78:79 -> main 9 | 7:8:void method2(int):87 -> main 10 | 7:8:void method1(java.lang.String):95 -> main 11 | 7:8:void main(java.lang.String[]):101 -> main"#, 12 | ); 13 | let stacktrace = " at obfuscated.main(Foo.java:8)"; 14 | let mapped = mapper.remap_stacktrace(stacktrace).unwrap(); 15 | assert_eq!( 16 | mapped, 17 | " at some.Class.method3(Foo.java:79) 18 | at some.Class.method2(Foo.java:87) 19 | at some.Class.method1(Foo.java:95) 20 | at some.Class.main(Foo.java:101)\n" 21 | ); 22 | 23 | // https://github.com/getsentry/rust-proguard/issues/6#issuecomment-605610326 24 | let mapper = ProguardMapper::from( 25 | r#"com.exmaple.app.MainActivity -> com.exmaple.app.MainActivity: 26 | com.example1.domain.MyBean myBean -> p 27 | 1:1:void ():11:11 -> 28 | 1:1:void buttonClicked(android.view.View):29:29 -> buttonClicked 29 | 2:2:void com.example1.domain.MyBean.doWork():16:16 -> buttonClicked 30 | 2:2:void buttonClicked(android.view.View):29 -> buttonClicked 31 | 1:1:void onCreate(android.os.Bundle):17:17 -> onCreate 32 | 2:5:void onCreate(android.os.Bundle):22:25 -> onCreate"#, 33 | ); 34 | let stacktrace = " at com.exmaple.app.MainActivity.buttonClicked(MainActivity.java:2)"; 35 | let mapped = mapper.remap_stacktrace(stacktrace).unwrap(); 36 | assert_eq!( 37 | mapped, 38 | " at com.example1.domain.MyBean.doWork(:16) 39 | at com.exmaple.app.MainActivity.buttonClicked(MainActivity.java:29)\n" 40 | ); 41 | 42 | // https://github.com/getsentry/rust-proguard/issues/6#issuecomment-605613412 43 | let mapper = ProguardMapper::from( 44 | r#"com.exmaple.app.MainActivity -> com.exmaple.app.MainActivity: 45 | com.example1.domain.MyBean myBean -> k 46 | 11:11:void () -> 47 | 17:26:void onCreate(android.os.Bundle) -> onCreate 48 | 29:30:void buttonClicked(android.view.View) -> buttonClicked 49 | 1016:1016:void com.example1.domain.MyBean.doWork():16:16 -> buttonClicked 50 | 1016:1016:void buttonClicked(android.view.View):29 -> buttonClicked"#, 51 | ); 52 | let stacktrace = " at com.exmaple.app.MainActivity.buttonClicked(MainActivity.java:1016)"; 53 | let mapped = mapper.remap_stacktrace(stacktrace).unwrap(); 54 | assert_eq!( 55 | mapped, 56 | " at com.example1.domain.MyBean.doWork(:16) 57 | at com.exmaple.app.MainActivity.buttonClicked(MainActivity.java:29)\n" 58 | ); 59 | } 60 | 61 | #[test] 62 | fn test_remap_no_lines() { 63 | let mapper = ProguardMapper::from( 64 | r#"original.class.name -> a: 65 | void originalMethodName() -> b"#, 66 | ); 67 | 68 | let mapped = mapper.remap_class("a"); 69 | assert_eq!(mapped, Some("original.class.name")); 70 | 71 | let mut mapped = mapper.remap_frame(&StackFrame::new("a", "b", 10)); 72 | assert_eq!( 73 | mapped.next().unwrap(), 74 | StackFrame::new("original.class.name", "originalMethodName", 0) 75 | ); 76 | assert_eq!(mapped.next(), None); 77 | } 78 | 79 | #[test] 80 | fn test_remap_kotlin() { 81 | let mapper = ProguardMapper::from( 82 | r#"io.sentry.sample.-$$Lambda$r3Avcbztes2hicEObh02jjhQqd4 -> e.a.c.a: 83 | io.sentry.sample.MainActivity f$0 -> b 84 | 1:1:void io.sentry.sample.KotlinSampleKt.fun3(io.sentry.sample.KotlinSample):16:16 -> onClick 85 | 1:1:void io.sentry.sample.KotlinSample.fun2():11 -> onClick 86 | 1:1:void io.sentry.sample.KotlinSample.fun1():7 -> onClick 87 | 1:1:void io.sentry.sample.MainActivity.bar():56 -> onClick 88 | 1:1:void io.sentry.sample.MainActivity.foo():44 -> onClick 89 | 1:1:void io.sentry.sample.MainActivity.onClickHandler(android.view.View):40 -> onClick 90 | 1:1:void onClick(android.view.View):0 -> onClick"#, 91 | ); 92 | 93 | let mapped = mapper 94 | .remap_stacktrace(" at e.a.c.a.onClick(lambda:1)") 95 | .unwrap(); 96 | 97 | assert_eq!( 98 | mapped.trim(), 99 | r#" at io.sentry.sample.KotlinSampleKt.fun3(:16) 100 | at io.sentry.sample.KotlinSample.fun2(:11) 101 | at io.sentry.sample.KotlinSample.fun1(:7) 102 | at io.sentry.sample.MainActivity.bar(:56) 103 | at io.sentry.sample.MainActivity.foo(:44) 104 | at io.sentry.sample.MainActivity.onClickHandler(:40) 105 | at io.sentry.sample.-$$Lambda$r3Avcbztes2hicEObh02jjhQqd4.onClick(lambda:0)"# 106 | .trim() 107 | ); 108 | } 109 | 110 | #[test] 111 | fn test_remap_just_method() { 112 | let mapper = ProguardMapper::from( 113 | r#"com.exmaple.app.MainActivity -> a.b.c.d: 114 | com.example1.domain.MyBean myBean -> p 115 | 1:1:void ():11:11 -> 116 | 1:1:void buttonClicked(android.view.View):29:29 -> buttonClicked 117 | 2:2:void com.example1.domain.MyBean.doWork():16:16 -> buttonClicked 118 | 2:2:void buttonClicked(android.view.View):29 -> buttonClicked 119 | 1:1:void onCreate(android.os.Bundle):17:17 -> onCreate 120 | 2:5:void onCreate(android.os.Bundle):22:25 -> onCreate"#, 121 | ); 122 | 123 | let unambiguous = mapper.remap_method("a.b.c.d", "onCreate"); 124 | assert_eq!( 125 | unambiguous, 126 | Some(("com.exmaple.app.MainActivity", "onCreate")) 127 | ); 128 | 129 | let ambiguous = mapper.remap_method("a.b.c.d", "buttonClicked"); 130 | assert_eq!(ambiguous, None); 131 | } 132 | --------------------------------------------------------------------------------