├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── COPYING ├── COPYING.LESSER ├── README.md ├── build.gradle.kts ├── common ├── build.gradle.kts └── src │ └── main │ ├── java │ └── dev │ │ └── booky │ │ └── stackdeobf │ │ ├── http │ │ ├── FailedHttpRequestException.java │ │ ├── HttpResponseContainer.java │ │ ├── HttpUtil.java │ │ └── VerifiableUrl.java │ │ ├── mappings │ │ ├── CachedMappings.java │ │ ├── MappingCacheVisitor.java │ │ ├── RemappedThrowable.java │ │ ├── RemappingUtil.java │ │ └── providers │ │ │ ├── AbstractMappingProvider.java │ │ │ ├── BuildBasedMappingProvider.java │ │ │ ├── CustomMappingProvider.java │ │ │ ├── IntermediaryMappingProvider.java │ │ │ ├── MojangMappingProvider.java │ │ │ ├── QuiltMappingProvider.java │ │ │ └── YarnMappingProvider.java │ │ └── util │ │ ├── Log4jRemapUtil.java │ │ ├── MavenArtifactInfo.java │ │ ├── RemappingRewritePolicy.java │ │ ├── VersionConstants.java │ │ └── VersionData.java │ └── resources │ ├── assets │ └── stackdeobf │ │ └── icon.png │ └── fabric.mod.json ├── fabric ├── build.gradle.kts └── src │ └── main │ ├── java │ └── dev │ │ └── booky │ │ └── stackdeobf │ │ ├── StackDeobfMod.java │ │ ├── config │ │ ├── MappingProviderSerializer.java │ │ └── StackDeobfConfig.java │ │ └── mixin │ │ ├── CrashReportCategoryMixin.java │ │ ├── CrashReportMixin.java │ │ ├── StackDeobfMixinPlugin.java │ │ └── ThreadingDetectorMixin.java │ └── resources │ ├── assets │ └── stackdeobf │ │ └── icon.png │ ├── fabric.mod.json │ └── stackdeobf.mixins.json ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── web ├── build.gradle.kts └── src └── main ├── java └── dev │ └── booky │ └── stackdeobf │ └── web │ ├── ApiRoutes.java │ ├── StackDeobfMain.java │ └── StackDeobfService.java └── resources ├── log4j2.xml └── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── mc_versions.json ├── robots.txt ├── script.js ├── site.webmanifest └── style.css /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Minecraft Version:** 1.x.x 27 | **Fabric Loader Version:** 0.x.x 28 | **Mod version:** 1.x.x 29 | 30 | **Additional info** 31 | Add any other info about the problem here, if necessary. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional info** 20 | Add any other info or screenshots about the feature request here, if necessary. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [ pull_request, push ] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | java: [ 17 ] 9 | os: [ ubuntu-24.04 ] 10 | 11 | runs-on: ${{ matrix.os }} 12 | if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')" 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Validate gradle wrapper 19 | uses: gradle/actions/wrapper-validation@v3 20 | 21 | - name: Setup java ${{ matrix.java }} 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: ${{ matrix.java }} 25 | distribution: temurin 26 | cache: gradle 27 | 28 | - name: Build 29 | run: ./gradlew build 30 | 31 | - name: capture fabric build artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: StackDeobfuscator-Fabric-Artifacts 35 | path: build/libs/StackDeobfuscatorFabric-* 36 | if-no-files-found: error 37 | 38 | - name: capture web build artifacts 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: StackDeobfuscator-Web-Artifacts 42 | path: build/libs/StackDeobfuscatorWeb-* 43 | if-no-files-found: error 44 | deploy_web: 45 | needs: build 46 | strategy: 47 | matrix: 48 | os: [ ubuntu-24.04 ] 49 | runs-on: ${{ matrix.os }} 50 | if: ${{ github.ref == 'refs/heads/master' }} 51 | steps: 52 | - name: Download web artifact 53 | uses: actions/download-artifact@v4 54 | with: 55 | name: StackDeobfuscator-Web-Artifacts 56 | - name: Extract web artifact file name 57 | id: name_finder 58 | run: | 59 | echo -n 'file_name=' >> "$GITHUB_OUTPUT" && ls . | grep all.jar >> "$GITHUB_OUTPUT" 60 | - name: Deploy web 61 | uses: burnett01/rsync-deployments@7.0.1 62 | env: 63 | INPUT_CENSOR_HOSTNAME: "${{ secrets.DEPLOY_SERVER_CENSOR }}" 64 | with: 65 | switches: -av --delete 66 | path: "${{ steps.name_finder.outputs.file_name }}" 67 | remote_user: "${{ secrets.DEPLOY_USER }}" 68 | remote_host: "${{ secrets.DEPLOY_SERVER }}" 69 | remote_key: "${{ secrets.DEPLOY_PRIVATE_KEY }}" 70 | remote_path: "${{ secrets.DEPLOY_PATH }}" 71 | - name: Trigger deploy hook 72 | env: 73 | INPUT_REMOTE_KEY: "${{ secrets.DEPLOY_PRIVATE_KEY }}" 74 | INPUT_REMOTE_USER: "${{ secrets.DEPLOY_USER }}" 75 | INPUT_REMOTE_HOST: "${{ secrets.DEPLOY_SERVER }}" 76 | INPUT_REMOTE_TRIGGER_SCRIPT: "${{ secrets.DEPLOY_SCRIPT }}" 77 | INPUT_PATH: "${{ steps.name_finder.outputs.file_name }}" 78 | run: | 79 | echo "$INPUT_REMOTE_KEY" > privkey 80 | chmod 700 privkey 81 | ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -i privkey -- "$INPUT_REMOTE_USER"@"$INPUT_REMOTE_HOST" "$INPUT_REMOTE_TRIGGER_SCRIPT" "$INPUT_PATH" 82 | rm -f privkey 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Created by https://www.toptal.com/developers/gitignore/api/linux,windows,macos,eclipse,visualstudiocode,jetbrains+all,maven,gradle,node,rust 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,windows,macos,eclipse,visualstudiocode,jetbrains+all,maven,gradle,node,rust 3 | 4 | ### Eclipse ### 5 | .metadata 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | .recommenders 16 | 17 | # External tool builders 18 | .externalToolBuilders/ 19 | 20 | # Locally stored "Eclipse launch configurations" 21 | *.launch 22 | 23 | # PyDev specific (Python IDE for Eclipse) 24 | *.pydevproject 25 | 26 | # CDT-specific (C/C++ Development Tooling) 27 | .cproject 28 | 29 | # CDT- autotools 30 | .autotools 31 | 32 | # Java annotation processor (APT) 33 | .factorypath 34 | 35 | # PDT-specific (PHP Development Tools) 36 | .buildpath 37 | 38 | # sbteclipse plugin 39 | .target 40 | 41 | # Tern plugin 42 | .tern-project 43 | 44 | # TeXlipse plugin 45 | .texlipse 46 | 47 | # STS (Spring Tool Suite) 48 | .springBeans 49 | 50 | # Code Recommenders 51 | .recommenders/ 52 | 53 | # Annotation Processing 54 | .apt_generated/ 55 | .apt_generated_test/ 56 | 57 | # Scala IDE specific (Scala & Java development for Eclipse) 58 | .cache-main 59 | .scala_dependencies 60 | .worksheet 61 | 62 | # Uncomment this line if you wish to ignore the project description file. 63 | # Typically, this file would be tracked if it contains build/dependency configurations: 64 | #.project 65 | 66 | ### Eclipse Patch ### 67 | # Spring Boot Tooling 68 | .sts4-cache/ 69 | 70 | ### JetBrains+all ### 71 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 72 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 73 | 74 | # User-specific stuff 75 | .idea/**/workspace.xml 76 | .idea/**/tasks.xml 77 | .idea/**/usage.statistics.xml 78 | .idea/**/dictionaries 79 | .idea/**/shelf 80 | 81 | # AWS User-specific 82 | .idea/**/aws.xml 83 | 84 | # Generated files 85 | .idea/**/contentModel.xml 86 | 87 | # Sensitive or high-churn files 88 | .idea/**/dataSources/ 89 | .idea/**/dataSources.ids 90 | .idea/**/dataSources.local.xml 91 | .idea/**/sqlDataSources.xml 92 | .idea/**/dynamic.xml 93 | .idea/**/uiDesigner.xml 94 | .idea/**/dbnavigator.xml 95 | 96 | # Gradle 97 | .idea/**/gradle.xml 98 | .idea/**/libraries 99 | 100 | # Gradle and Maven with auto-import 101 | # When using Gradle or Maven with auto-import, you should exclude module files, 102 | # since they will be recreated, and may cause churn. Uncomment if using 103 | # auto-import. 104 | # .idea/artifacts 105 | # .idea/compiler.xml 106 | # .idea/jarRepositories.xml 107 | # .idea/modules.xml 108 | # .idea/*.iml 109 | # .idea/modules 110 | # *.iml 111 | # *.ipr 112 | 113 | # CMake 114 | cmake-build-*/ 115 | 116 | # Mongo Explorer plugin 117 | .idea/**/mongoSettings.xml 118 | 119 | # File-based project format 120 | *.iws 121 | 122 | # IntelliJ 123 | out/ 124 | 125 | # mpeltonen/sbt-idea plugin 126 | .idea_modules/ 127 | 128 | # JIRA plugin 129 | atlassian-ide-plugin.xml 130 | 131 | # Cursive Clojure plugin 132 | .idea/replstate.xml 133 | 134 | # SonarLint plugin 135 | .idea/sonarlint/ 136 | 137 | # Crashlytics plugin (for Android Studio and IntelliJ) 138 | com_crashlytics_export_strings.xml 139 | crashlytics.properties 140 | crashlytics-build.properties 141 | fabric.properties 142 | 143 | # Editor-based Rest Client 144 | .idea/httpRequests 145 | 146 | # Android studio 3.1+ serialized cache file 147 | .idea/caches/build_file_checksums.ser 148 | 149 | ### JetBrains+all Patch ### 150 | # Ignore everything but code style settings and run configurations 151 | # that are supposed to be shared within teams. 152 | 153 | .idea/* 154 | 155 | !.idea/codeStyles 156 | !.idea/runConfigurations 157 | 158 | ### Linux ### 159 | *~ 160 | 161 | # temporary files which can be created if a process still has a handle open of a deleted file 162 | .fuse_hidden* 163 | 164 | # KDE directory preferences 165 | .directory 166 | 167 | # Linux trash folder which might appear on any partition or disk 168 | .Trash-* 169 | 170 | # .nfs files are created when an open file is removed but is still being accessed 171 | .nfs* 172 | 173 | ### macOS ### 174 | # General 175 | .DS_Store 176 | .AppleDouble 177 | .LSOverride 178 | 179 | # Icon must end with two \r 180 | Icon 181 | 182 | # Thumbnails 183 | ._* 184 | 185 | # Files that might appear in the root of a volume 186 | .DocumentRevisions-V100 187 | .fseventsd 188 | .Spotlight-V100 189 | .TemporaryItems 190 | .Trashes 191 | .VolumeIcon.icns 192 | .com.apple.timemachine.donotpresent 193 | 194 | # Directories potentially created on remote AFP share 195 | .AppleDB 196 | .AppleDesktop 197 | Network Trash Folder 198 | Temporary Items 199 | .apdisk 200 | 201 | ### macOS Patch ### 202 | # iCloud generated files 203 | *.icloud 204 | 205 | ### Maven ### 206 | target/ 207 | pom.xml.tag 208 | pom.xml.releaseBackup 209 | pom.xml.versionsBackup 210 | pom.xml.next 211 | release.properties 212 | dependency-reduced-pom.xml 213 | buildNumber.properties 214 | .mvn/timing.properties 215 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 216 | .mvn/wrapper/maven-wrapper.jar 217 | 218 | # Eclipse m2e generated files 219 | # Eclipse Core 220 | .project 221 | # JDT-specific (Eclipse Java Development Tools) 222 | .classpath 223 | 224 | ### Node ### 225 | # Logs 226 | logs 227 | *.log 228 | npm-debug.log* 229 | yarn-debug.log* 230 | yarn-error.log* 231 | lerna-debug.log* 232 | .pnpm-debug.log* 233 | 234 | # Diagnostic reports (https://nodejs.org/api/report.html) 235 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 236 | 237 | # Runtime data 238 | pids 239 | *.pid 240 | *.seed 241 | *.pid.lock 242 | 243 | # Directory for instrumented libs generated by jscoverage/JSCover 244 | lib-cov 245 | 246 | # Coverage directory used by tools like istanbul 247 | coverage 248 | *.lcov 249 | 250 | # nyc test coverage 251 | .nyc_output 252 | 253 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 254 | .grunt 255 | 256 | # Bower dependency directory (https://bower.io/) 257 | bower_components 258 | 259 | # node-waf configuration 260 | .lock-wscript 261 | 262 | # Compiled binary addons (https://nodejs.org/api/addons.html) 263 | build/Release 264 | 265 | # Dependency directories 266 | node_modules/ 267 | jspm_packages/ 268 | 269 | # Snowpack dependency directory (https://snowpack.dev/) 270 | web_modules/ 271 | 272 | # TypeScript cache 273 | *.tsbuildinfo 274 | 275 | # Optional npm cache directory 276 | .npm 277 | 278 | # Optional eslint cache 279 | .eslintcache 280 | 281 | # Optional stylelint cache 282 | .stylelintcache 283 | 284 | # Microbundle cache 285 | .rpt2_cache/ 286 | .rts2_cache_cjs/ 287 | .rts2_cache_es/ 288 | .rts2_cache_umd/ 289 | 290 | # Optional REPL history 291 | .node_repl_history 292 | 293 | # Output of 'npm pack' 294 | *.tgz 295 | 296 | # Yarn Integrity file 297 | .yarn-integrity 298 | 299 | # dotenv environment variable files 300 | .env 301 | .env.development.local 302 | .env.test.local 303 | .env.production.local 304 | .env.local 305 | 306 | # parcel-bundler cache (https://parceljs.org/) 307 | .cache 308 | .parcel-cache 309 | 310 | # Next.js build output 311 | .next 312 | out 313 | 314 | # Nuxt.js build / generate output 315 | .nuxt 316 | dist 317 | 318 | # Gatsby files 319 | .cache/ 320 | # Comment in the public line in if your project uses Gatsby and not Next.js 321 | # https://nextjs.org/blog/next-9-1#public-directory-support 322 | # public 323 | 324 | # vuepress build output 325 | .vuepress/dist 326 | 327 | # vuepress v2.x temp and cache directory 328 | .temp 329 | 330 | # Docusaurus cache and generated files 331 | .docusaurus 332 | 333 | # Serverless directories 334 | .serverless/ 335 | 336 | # FuseBox cache 337 | .fusebox/ 338 | 339 | # DynamoDB Local files 340 | .dynamodb/ 341 | 342 | # TernJS port file 343 | .tern-port 344 | 345 | # Stores VSCode versions used for testing VSCode extensions 346 | .vscode-test 347 | 348 | # yarn v2 349 | .yarn/cache 350 | .yarn/unplugged 351 | .yarn/build-state.yml 352 | .yarn/install-state.gz 353 | .pnp.* 354 | 355 | ### Node Patch ### 356 | # Serverless Webpack directories 357 | .webpack/ 358 | 359 | # Optional stylelint cache 360 | 361 | # SvelteKit build / generate output 362 | .svelte-kit 363 | 364 | ### Rust ### 365 | # Generated by Cargo 366 | # will have compiled files and executables 367 | debug/ 368 | 369 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 370 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 371 | Cargo.lock 372 | 373 | # These are backup files generated by rustfmt 374 | **/*.rs.bk 375 | 376 | # MSVC Windows builds of rustc generate these, which store debugging information 377 | *.pdb 378 | 379 | ### VisualStudioCode ### 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | !.vscode/*.code-snippets 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Built Visual Studio Code Extensions 391 | *.vsix 392 | 393 | ### VisualStudioCode Patch ### 394 | # Ignore all local history of files 395 | .history 396 | .ionide 397 | 398 | ### Windows ### 399 | # Windows thumbnail cache files 400 | Thumbs.db 401 | Thumbs.db:encryptable 402 | ehthumbs.db 403 | ehthumbs_vista.db 404 | 405 | # Dump file 406 | *.stackdump 407 | 408 | # Folder config file 409 | [Dd]esktop.ini 410 | 411 | # Recycle Bin used on file shares 412 | $RECYCLE.BIN/ 413 | 414 | # Windows Installer files 415 | *.cab 416 | *.msi 417 | *.msix 418 | *.msm 419 | *.msp 420 | 421 | # Windows shortcuts 422 | *.lnk 423 | 424 | ### Gradle ### 425 | .gradle 426 | **/build/ 427 | !src/**/build/ 428 | 429 | # Ignore Gradle GUI config 430 | gradle-app.setting 431 | 432 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 433 | !gradle-wrapper.jar 434 | 435 | # Avoid ignore Gradle wrappper properties 436 | !gradle-wrapper.properties 437 | 438 | # Cache of project 439 | .gradletasknamecache 440 | 441 | # Eclipse Gradle plugin generated files 442 | # Eclipse Core 443 | # JDT-specific (Eclipse Java Development Tools) 444 | 445 | ### Gradle Patch ### 446 | # Java heap dump 447 | *.hprof 448 | 449 | # End of https://www.toptal.com/developers/gitignore/api/linux,windows,macos,eclipse,visualstudiocode,jetbrains+all,maven,gradle,node,rust 450 | 451 | # Manually added exclusions 452 | run/ 453 | .idea/ 454 | next-env.d.ts 455 | *.iml 456 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.4.3 2 | 3 | - Add more version data to [web viewer](https://stackdeobf.booky.dev/) (1.20.4-rc1 to 1.21) 4 | - Don't verify checksums for yarn maven metadata at all ([#10](https://github.com/booky10/StackDeobfuscator/issues/10)) 5 | - This can't be reliably checked, as fabric's maven cache returns outdated files for a while after an update 6 | - Improve styling of web viewer 7 | - Update library to prevent issues with other mods updating mapping-io library 8 | - Caused by old [breaking changes](https://github.com/FabricMC/mapping-io/commit/6a06f2a86ffad9f5615688e17e33a119fe682b70#diff-734687dc5c161203c349415385c620d1c6278456f66466e1956d1a65ab943b51) in fabric's mapping-io library 9 | 10 | ## v1.4.2 11 | 12 | - Add more version data to web viewer (versions 23w33a to 1.20.3) 13 | - Use sha256 instead of sha512 checksums for yarn (works around sha512 checksum being wrong sometimes) 14 | - Fix mixin refmap filename looking weird 15 | - Add static mappings getter to fabric mod class 16 | 17 | ## v1.4.1 18 | 19 | - Add 23w31a and 23w32a version data to web viewer 20 | - Select latest stable version by default in web viewer 21 | - Correctly set mixin refmap name (resolves crash when Noxesium is installed) 22 | 23 | ## v1.4.0 24 | 25 | ### Fixes 26 | 27 | - Handle `/` being used instead of `.` as package separator 28 | - E.g. `net/minecraft/class_5272` is now remapped correctly 29 | to `net/minecraft/client/item/ModelPredicateProviderRegistry` 30 | - Fix some classes causing errors when trying to remap ( 31 | Fixes [#7](https://github.com/booky10/StackDeobfuscator/issues/7)) 32 | 33 | ### New 34 | 35 | - Separate fabric integration and common remapping code 36 | - Adds web subproject for remapping text to different mappings/versions 37 | - The checksum of all files is now verified on download 38 | - Intermediary/Quilt mappings always use sha512 39 | - Mojang mappings always use md5/sha1 40 | - Yarn uses sha512 on modern version, but falls back to sha1 for 19w02a, Combat Test 4 and all versions older than 41 | 1.15 (except 1.14.4) 42 | - Some fixes to support more versions 43 | - Mapping/Metadata downloads will be retried 3x before being cancelled, if: 44 | - The server returns a non 2xx response code 45 | - Checksum verification fails 46 | 47 | ## v1.3.2 48 | 49 | - Fix exceptions when remapping lambda methods 50 | - Added optional remapping of every log message 51 | - Rewrote internal log injection handling 52 | - Now uses Log4j's exception rendering 53 | - Removed "MC//" prefix in stacktrace, minecraft classes are now suffixed with "`~[client-intermediary.jar:?]`" 54 | 55 | ## v1.3.1 56 | 57 | - Added support for quilt mappings below 1.19.2 58 | - Now goes down to 1.18.2 59 | 60 | ## v1.3.0 61 | 62 | - Mappings are now loaded asynchronously, removing startup time impact 63 | - Added more log messages (e.g. time tracking and detailed http requests) 64 | - Added support for more minecraft versions 65 | - Yarn: 18w49a (1.14 snapshot) or higher 66 | - Quilt: 1.19.2 or higher 67 | - Mojang: 1.14.4 and 19w36a (1.15 snapshot) or higher 68 | - Yarn/Quilt versions are now cached for 48 hours before being refreshed 69 | - Added note in stacktraces when something has been remapped (`MC//` prefix before classname) 70 | - Custom mappings now support the in-jar format used by intermediary, yarn and quilt 71 | - They also support GZIP (without TAR) and normal ZIP (just one file in a zip) compression 72 | → Auto-detected by file name extension 73 | - All cached mappings (yarn, quilt, intermediary and mojang) are now saved GZIP compressed 74 | 75 | ## v1.2.1 76 | 77 | - Fixed some issues with remapping of inner classes 78 | - Added support for quilt mappings 79 | 80 | ## v1.2.0 81 | 82 | - Added support for yarn and custom mappings 83 | - Yarn mappings are now selected by default 84 | - See [wiki](https://github.com/booky10/StackDeobfuscator/wiki) on how to configure other mappings 85 | 86 | ## v1.1.0 87 | 88 | - Initial public release 89 | -------------------------------------------------------------------------------- /COPYING.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack Deobfuscator 2 | 3 | ## Downloads 4 | 5 | - Modrinth: https://modrinth.com/mod/stackdeobf 6 | - Curseforge: https://curseforge.com/minecraft/mc-mods/stackdeobf 7 | 8 | The configuration is documented on the [wiki](https://github.com/booky10/StackDeobfuscator/wiki/Configuration). 9 | 10 | A hosted web-version is available at [stackdeobf.net](https://stackdeobf.net/). 11 | See [wiki](https://github.com/booky10/StackDeobfuscator/wiki/Web-Remapping#installation-self-hosting) for self-hosting 12 | this page. 13 | 14 | ## What does this mod do? 15 | 16 | All errors displayed in the console and all crash reports will be remapped from unreadable production names (e.g. 17 | `net.minecraft.class_310`) to readable mapped names (e.g. `net.minecraft.client.MinecraftClient`). 18 | 19 | This allows mod developers to more easily identify issues in a non-development environment, as the errors are instantly 20 | human-readable. 21 | 22 | ### Comparison 23 | 24 | 25 | Before 26 | 27 | > ``` 28 | > [23:13:08] [Render thread/ERROR]: Reported exception thrown! 29 | > net.minecraft.class_148: Manually triggered debug crash 30 | > at net.minecraft.class_309.method_1474(class_309.java:509) ~[client-intermediary.jar:?] 31 | > at net.minecraft.class_310.method_1574(class_310.java:1955) ~[client-intermediary.jar:?] 32 | > at net.minecraft.class_310.method_1523(class_310.java:1180) ~[client-intermediary.jar:?] 33 | > at net.minecraft.class_310.method_1514(class_310.java:801) ~[client-intermediary.jar:?] 34 | > at net.minecraft.client.main.Main.main(Main.java:237) ~[minecraft-1.19.4-client.jar:?] 35 | > at net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider.launch(MinecraftGameProvider.java:462) ~[fabric-loader-0.14.18.jar:?] 36 | > at net.fabricmc.loader.impl.launch.knot.Knot.launch(Knot.java:74) ~[fabric-loader-0.14.18.jar:?] 37 | > at net.fabricmc.loader.impl.launch.knot.KnotClient.main(KnotClient.java:23) ~[fabric-loader-0.14.18.jar:?] 38 | > at org.prismlauncher.launcher.impl.StandardLauncher.launch(StandardLauncher.java:88) ~[NewLaunch.jar:?] 39 | > at org.prismlauncher.EntryPoint.listen(EntryPoint.java:126) ~[NewLaunch.jar:?] 40 | > at org.prismlauncher.EntryPoint.main(EntryPoint.java:71) ~[NewLaunch.jar:?] 41 | > Caused by: java.lang.Throwable: Manually triggered debug crash 42 | > at net.minecraft.class_309.method_1474(class_309.java:506) ~[client-intermediary.jar:?] 43 | > ... 10 more 44 | > ``` 45 | 46 | 47 | 48 | After (yarn/quilt mappings) 49 | 50 | > ``` 51 | > [23:11:25] [Render thread/ERROR]: Reported exception thrown! 52 | > net.minecraft.util.crash.CrashException: Manually triggered debug crash 53 | > at net.minecraft.client.Keyboard.pollDebugCrash(Keyboard.java:509) ~[client-intermediary.jar:?] 54 | > at net.minecraft.client.MinecraftClient.tick(MinecraftClient.java:1955) ~[client-intermediary.jar:?] 55 | > at net.minecraft.client.MinecraftClient.render(MinecraftClient.java:1180) ~[client-intermediary.jar:?] 56 | > at net.minecraft.client.MinecraftClient.run(MinecraftClient.java:801) ~[client-intermediary.jar:?] 57 | > at net.minecraft.client.main.Main.main(Main.java:237) ~[minecraft-1.19.4-client.jar:?] 58 | > at net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider.launch(MinecraftGameProvider.java:462) ~[fabric-loader-0.14.18.jar:?] 59 | > at net.fabricmc.loader.impl.launch.knot.Knot.launch(Knot.java:74) ~[fabric-loader-0.14.18.jar:?] 60 | > at net.fabricmc.loader.impl.launch.knot.KnotClient.main(KnotClient.java:23) ~[fabric-loader-0.14.18.jar:?] 61 | > at org.prismlauncher.launcher.impl.StandardLauncher.launch(StandardLauncher.java:88) ~[NewLaunch.jar:?] 62 | > at org.prismlauncher.EntryPoint.listen(EntryPoint.java:126) ~[NewLaunch.jar:?] 63 | > at org.prismlauncher.EntryPoint.main(EntryPoint.java:71) ~[NewLaunch.jar:?] 64 | > Caused by: java.lang.Throwable: Manually triggered debug crash 65 | > at net.minecraft.client.Keyboard.pollDebugCrash(Keyboard.java:506) ~[client-intermediary.jar:?] 66 | > ... 10 more 67 | > ``` 68 | 69 | 70 | 71 | After (mojang mappings) 72 | 73 | > ``` 74 | > [23:04:12] [Render thread/ERROR]: Reported exception thrown! 75 | > net.minecraft.ReportedException: Manually triggered debug crash 76 | > at net.minecraft.client.KeyboardHandler.tick(KeyboardHandler.java:509) ~[client-intermediary.jar:?] 77 | > at net.minecraft.client.Minecraft.tick(Minecraft.java:1955) ~[client-intermediary.jar:?] 78 | > at net.minecraft.client.Minecraft.runTick(Minecraft.java:1180) ~[client-intermediary.jar:?] 79 | > at net.minecraft.client.Minecraft.run(Minecraft.java:801) ~[client-intermediary.jar:?] 80 | > at net.minecraft.client.main.Main.main(Main.java:237) ~[minecraft-1.19.4-client.jar:?] 81 | > at net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider.launch(MinecraftGameProvider.java:462) ~[fabric-loader-0.14.18.jar:?] 82 | > at net.fabricmc.loader.impl.launch.knot.Knot.launch(Knot.java:74) ~[fabric-loader-0.14.18.jar:?] 83 | > at net.fabricmc.loader.impl.launch.knot.KnotClient.main(KnotClient.java:23) ~[fabric-loader-0.14.18.jar:?] 84 | > at org.prismlauncher.launcher.impl.StandardLauncher.launch(StandardLauncher.java:88) ~[NewLaunch.jar:?] 85 | > at org.prismlauncher.EntryPoint.listen(EntryPoint.java:126) ~[NewLaunch.jar:?] 86 | > at org.prismlauncher.EntryPoint.main(EntryPoint.java:71) ~[NewLaunch.jar:?] 87 | > Caused by: java.lang.Throwable: Manually triggered debug crash 88 | > at net.minecraft.client.KeyboardHandler.tick(KeyboardHandler.java:506) ~[client-intermediary.jar:?] 89 | > ... 10 more 90 | > ``` 91 | 92 | 93 | 94 | ## Mappings Overview 95 | 96 | Mappings are downloaded and parsed asynchronously. They are downloaded only once per version. 97 | Yarn and Quilt refresh their version every 48 hours to check for updates. 98 | 99 | | Mappings | Compatible Minecraft Versions | Download Size (zipped¹)² | Cached Size (gzipped)² | 100 | |----------|---------------------------------------------|----------------------------------------|-------------------------------------| 101 | | Yarn | 18w49a (1.14 snapshot) or higher | `1.4 MiB` | `1.4 MiB` | 102 | | Quilt | 1.18.2 or higher | `1.4 MiB` (+`1.1 MiB` below 1.19.2) | `1.4 MiB` (+`1.1 MiB` below 1.19.2) | 103 | | Mojang | 1.14.4 and 19w36a (1.15 snapshot) or higher | `9.0 MiB` (uncompressed) + `559.1 KiB` | `1.3 MiB` + `558.6 KiB` | 104 | 105 | ¹: Mojang mappings are not compressed 106 | ²: Sizes as of 12th May 2024 (1.20.6 is latest) 107 | 108 | ## Building 109 | 110 | ```shell 111 | ./gradlew build # remove "./" on windows 112 | ``` 113 | 114 | The output jar can be found in `build` → `libs`. 115 | 116 | ## License 117 | 118 | This project is licensed under [**LGPL-3.0-only**](./COPYING.LESSER) unless specified otherwise. 119 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.ByteArrayOutputStream 2 | 3 | plugins { 4 | id("java-library") 5 | id("maven-publish") 6 | } 7 | 8 | fun getGitCommit(): String { 9 | val stdout = ByteArrayOutputStream() 10 | exec { 11 | commandLine("git", "rev-parse", "--short", "HEAD") 12 | standardOutput = stdout 13 | } 14 | return stdout.toString().trim() 15 | } 16 | 17 | tasks["jar"].enabled = false 18 | 19 | subprojects { 20 | apply() 21 | apply() 22 | 23 | version = "1.4.3+${getGitCommit()}" 24 | group = "dev.booky" 25 | 26 | repositories { 27 | mavenCentral() 28 | maven("https://maven.fabricmc.net/") 29 | maven("https://libraries.minecraft.net/") 30 | } 31 | 32 | java { 33 | withSourcesJar() 34 | toolchain { 35 | languageVersion = JavaLanguageVersion.of(17) 36 | vendor = JvmVendorSpec.ADOPTIUM 37 | } 38 | } 39 | 40 | publishing { 41 | publications.create("maven") { 42 | artifactId = project.name.lowercase() 43 | from(components["java"]) 44 | } 45 | 46 | if (rootProject.ext.has("publishingRepo")) { 47 | val publishingRepo = rootProject.ext.get("publishingRepo") as String 48 | repositories.maven(publishingRepo) { 49 | name = url.host.replace(".", "") 50 | authentication { create("basic") } 51 | credentials(PasswordCredentials::class) 52 | } 53 | } 54 | } 55 | 56 | tasks { 57 | withType { 58 | options.encoding = Charsets.UTF_8.name() 59 | options.release = 17 60 | } 61 | 62 | withType().configureEach { 63 | destinationDirectory = rootProject.layout.buildDirectory.dir("libs") 64 | archiveBaseName = project.name.replace("-", "") 65 | 66 | sequenceOf("COPYING", "COPYING.LESSER") 67 | .map { rootProject.layout.projectDirectory.file(it) } 68 | .forEach { sourceFile -> 69 | from(sourceFile) { 70 | rename { return@rename "${it}_${rootProject.name.lowercase()}" } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api(libs.fabric.mappingio) 3 | 4 | // only int -> object maps required for fastutil 5 | compileOnly(libs.bundles.builtin) 6 | } 7 | 8 | tasks.processResources { 9 | inputs.property("version", project.version) 10 | filesMatching("fabric.mod.json") { 11 | expand("version" to project.version) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/http/FailedHttpRequestException.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.http; 2 | // Created by booky10 in StackDeobfuscator (18:16 29.03.23) 3 | 4 | import org.jetbrains.annotations.ApiStatus; 5 | 6 | public class FailedHttpRequestException extends RuntimeException { 7 | 8 | @ApiStatus.Internal 9 | public FailedHttpRequestException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/http/HttpResponseContainer.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.http; 2 | // Created by booky10 in StackDeobfuscator (21:47 30.07.23) 3 | 4 | import java.net.http.HttpRequest; 5 | import java.net.http.HttpResponse; 6 | import java.time.Duration; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.function.Supplier; 9 | 10 | public final class HttpResponseContainer { 11 | 12 | private final HttpRequest request; 13 | private final HttpResponse response; 14 | private final Duration duration; 15 | private final Supplier> retry; 16 | 17 | HttpResponseContainer(HttpRequest request, HttpResponse response, Duration duration, 18 | Supplier> retry) { 19 | this.request = request; 20 | this.response = response; 21 | this.duration = duration; 22 | this.retry = retry; 23 | } 24 | 25 | public CompletableFuture retry() { 26 | return this.retry.get(); 27 | } 28 | 29 | public byte[] getBody() { 30 | return this.response.body(); 31 | } 32 | 33 | public int getStatuscode() { 34 | return this.response.statusCode(); 35 | } 36 | 37 | public HttpRequest getRequest() { 38 | return this.request; 39 | } 40 | 41 | public HttpResponse getResponse() { 42 | return this.response; 43 | } 44 | 45 | public Duration getDuration() { 46 | return this.duration; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/http/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.http; 2 | // Created by booky10 in StackDeobfuscator (18:10 29.03.23) 3 | 4 | import org.apache.commons.io.FileUtils; 5 | import org.apache.logging.log4j.Level; 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.Logger; 8 | 9 | import java.net.URI; 10 | import java.net.http.HttpClient; 11 | import java.net.http.HttpRequest; 12 | import java.net.http.HttpResponse.BodyHandlers; 13 | import java.time.Duration; 14 | import java.time.Instant; 15 | import java.util.Map; 16 | import java.util.WeakHashMap; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.Executor; 19 | import java.util.function.Supplier; 20 | 21 | public final class HttpUtil { 22 | 23 | private static final Logger LOGGER = LogManager.getLogger("StackDeobfuscator"); 24 | private static final int RETRY_LIMIT = Integer.getInteger("stackdeobf.http-retry-count", 3); 25 | 26 | private static final Map HTTP = new WeakHashMap<>(); 27 | 28 | private HttpUtil() { 29 | } 30 | 31 | private static boolean isSuccess(int code) { 32 | return code / 100 == 2; 33 | } 34 | 35 | private static HttpClient getHttpClient(Executor executor) { 36 | synchronized (HTTP) { 37 | return HTTP.computeIfAbsent(executor, $ -> HttpClient.newBuilder().executor(executor).build()); 38 | } 39 | } 40 | 41 | public static CompletableFuture getAsync(URI url, Executor executor) { 42 | return getAsync(url, executor, 0); 43 | } 44 | 45 | public static CompletableFuture getAsync(HttpRequest request, Executor executor) { 46 | return getAsync(request, executor, 0); 47 | } 48 | 49 | public static CompletableFuture getAsync(URI url, Executor executor, int depth) { 50 | HttpRequest request = HttpRequest.newBuilder(url).build(); 51 | return getAsync(request, executor, depth); 52 | } 53 | 54 | public static CompletableFuture getAsync(HttpRequest request, Executor executor, int depth) { 55 | if (depth > RETRY_LIMIT) { 56 | throw new FailedHttpRequestException("Retry depth exceeded retry limit (" + RETRY_LIMIT + ") – " 57 | + "cancelling request to " + request.method() + " " + request.uri()); 58 | } 59 | 60 | LOGGER.info("Trying to request {} {}... (try #{})", 61 | request.method(), request.uri(), depth); 62 | 63 | Instant start = Instant.now(); 64 | return getHttpClient(executor) 65 | .sendAsync(request, BodyHandlers.ofByteArray()) 66 | .thenComposeAsync(resp -> { 67 | Duration duration = Duration.between(start, Instant.now()); 68 | 69 | boolean success = isSuccess(resp.statusCode()); 70 | int bodyByteCount = resp.body().length; 71 | 72 | LOGGER.log(success ? Level.INFO : Level.ERROR, 73 | "Received {} bytes ({}) with status {} from {} {} in {}ms", 74 | bodyByteCount, FileUtils.byteCountToDisplaySize(bodyByteCount), 75 | resp.statusCode(), request.method(), request.uri(), duration.toMillis()); 76 | 77 | Supplier> retry = 78 | () -> getAsync(request, executor, depth + 1); 79 | if (!success) { 80 | return retry.get(); 81 | } 82 | 83 | HttpResponseContainer respContainer = new HttpResponseContainer(request, resp, duration, retry); 84 | return CompletableFuture.completedFuture(respContainer); 85 | }, executor); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/http/VerifiableUrl.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.http; 2 | // Created by booky10 in StackDeobfuscator (01:57 10.06.23) 3 | 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.net.URI; 9 | import java.net.http.HttpRequest; 10 | import java.security.MessageDigest; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.util.Arrays; 13 | import java.util.Base64; 14 | import java.util.Optional; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.Executor; 17 | 18 | public final class VerifiableUrl { 19 | 20 | private static final Logger LOGGER = LogManager.getLogger("StackDeobfuscator"); 21 | 22 | private final URI uri; 23 | private final HashType hashType; 24 | private final HashResult hashResult; 25 | 26 | public VerifiableUrl(URI uri, HashType hashType, String hash) { 27 | this(uri, hashType, new HashResult(hash)); 28 | } 29 | 30 | public VerifiableUrl(URI uri, HashType hashType, byte[] hash) { 31 | this(uri, hashType, new HashResult(hash)); 32 | } 33 | 34 | public VerifiableUrl(URI uri, HashType hashType, HashResult hashResult) { 35 | this.uri = uri; 36 | this.hashType = hashType; 37 | this.hashResult = hashResult; 38 | } 39 | 40 | public static CompletableFuture resolve(URI url, HashType hashType, Executor executor) { 41 | return resolve(url, hashType, executor, 0); 42 | } 43 | 44 | public static CompletableFuture resolve(URI url, HashType hashType, Executor executor, int depth) { 45 | if (hashType == HashType.DUMMY) { 46 | VerifiableUrl dummyUrl = new VerifiableUrl(url, HashType.DUMMY, HashResult.emptyResult()); 47 | return CompletableFuture.completedFuture(dummyUrl); 48 | } 49 | 50 | LOGGER.info("Downloading {} hash for {}...", hashType, url); 51 | URI hashUrl = URI.create(url + hashType.getExtension()); 52 | return HttpUtil.getAsync(hashUrl, executor, depth) 53 | .thenApply(resp -> { 54 | // the hash file contains the hash bytes as hex, so this has to be 55 | // converted to a string and then parsed to bytes again 56 | String hashStr = new String(resp.getBody()); 57 | return new VerifiableUrl(url, hashType, hashStr); 58 | }); 59 | } 60 | 61 | public static CompletableFuture resolveByMd5Header(URI url, Executor executor) { 62 | return resolveByMd5Header(url, executor, 0); 63 | } 64 | 65 | public static CompletableFuture resolveByMd5Header(URI url, Executor executor, int depth) { 66 | HttpRequest request = HttpRequest.newBuilder(url) 67 | .method("HEAD", HttpRequest.BodyPublishers.noBody()) 68 | .build(); 69 | HashType hashType = HashType.MD5; 70 | 71 | LOGGER.info("Downloading {} hash for {}... (try #{})", hashType, url, depth); 72 | return HttpUtil.getAsync(request, executor, depth).thenCompose(resp -> { 73 | // sent by piston-meta server, base64-encoded md5 hash bytes of the response body 74 | Optional optMd5Hash = resp.getResponse().headers().firstValue("content-md5"); 75 | if (optMd5Hash.isEmpty()) { 76 | LOGGER.error("No expected 'content-md5' header found for {}; retrying...", url); 77 | return resolveByMd5Header(url, executor, depth + 1); 78 | } 79 | 80 | byte[] md5Bytes = Base64.getDecoder().decode(optMd5Hash.get()); 81 | return CompletableFuture.completedFuture(new VerifiableUrl(url, hashType, md5Bytes)); 82 | }); 83 | } 84 | 85 | public CompletableFuture get(Executor executor) { 86 | return get(executor, 0); 87 | } 88 | 89 | public CompletableFuture get(Executor executor, int depth) { 90 | return HttpUtil.getAsync(this.uri, executor, depth).thenCompose(resp -> { 91 | if (this.hashType == HashType.DUMMY) { 92 | return CompletableFuture.completedFuture(resp); 93 | } 94 | 95 | HashResult hashResult = this.hashType.hash(resp.getBody()); 96 | LOGGER.info("Verifying {} hash {} for {}...", 97 | this.hashType, hashResult, this.uri); 98 | 99 | if (!hashResult.equals(this.hashResult)) { 100 | LOGGER.error("{} hash {} doesn't match {} for {}; retrying...", 101 | this.hashType, hashResult, this.hashResult, this.uri); 102 | return get(executor, depth + 1); 103 | } 104 | return CompletableFuture.completedFuture(resp); 105 | }); 106 | } 107 | 108 | public URI getUrl() { 109 | return this.uri; 110 | } 111 | 112 | public enum HashType { 113 | 114 | MD5(".md5", "MD5"), 115 | SHA1(".sha1", "SHA-1"), 116 | SHA256(".sha256", "SHA-256"), 117 | SHA512(".sha512", "SHA-512"), 118 | DUMMY(null, null); 119 | 120 | private final @Nullable String extension; 121 | private final @Nullable MessageDigest digester; 122 | 123 | HashType(@Nullable String extension, @Nullable String algoName) { 124 | if ((extension == null) != (algoName == null)) { 125 | throw new IllegalArgumentException("Can't create half-valid hash type"); 126 | } 127 | this.extension = extension; 128 | 129 | if (algoName != null) { 130 | try { 131 | this.digester = MessageDigest.getInstance(algoName); 132 | } catch (NoSuchAlgorithmException exception) { 133 | throw new IllegalStateException("Illegal algorithm provided: " + algoName, exception); 134 | } 135 | } else { 136 | this.digester = null; 137 | } 138 | } 139 | 140 | public HashResult hash(byte[] bytes) { 141 | if (this.digester == null) { 142 | throw new UnsupportedOperationException(); 143 | } 144 | byte[] resultBytes; 145 | synchronized (this.digester) { 146 | resultBytes = this.digester.digest(bytes); 147 | } 148 | return new HashResult(resultBytes); 149 | } 150 | 151 | public String getExtension() { 152 | if (this.extension == null) { 153 | throw new UnsupportedOperationException(); 154 | } 155 | return this.extension; 156 | } 157 | } 158 | 159 | public record HashResult(byte[] bytes) { 160 | 161 | private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); 162 | 163 | public HashResult(String string) { 164 | this(decode(string)); 165 | } 166 | 167 | public static HashResult emptyResult() { 168 | return new HashResult(new byte[0]); 169 | } 170 | 171 | private static byte[] decode(String string) { 172 | if (string.length() % 2 != 0) { 173 | throw new IllegalArgumentException("Illegal hash string: " + string); 174 | } 175 | 176 | byte[] bytes = new byte[string.length() / 2]; 177 | for (int i = 0; i < string.length(); i += 2) { 178 | int b = Character.digit(string.charAt(i), 16) << 4; 179 | b |= Character.digit(string.charAt(i + 1), 16); 180 | bytes[i / 2] = (byte) b; 181 | } 182 | return bytes; 183 | } 184 | 185 | public boolean isEmpty() { 186 | return this.bytes.length == 0; 187 | } 188 | 189 | @Override 190 | public boolean equals(Object obj) { 191 | if (this == obj) return true; 192 | if (!(obj instanceof HashResult that)) return false; 193 | return Arrays.equals(this.bytes, that.bytes); 194 | } 195 | 196 | @Override 197 | public int hashCode() { 198 | return Arrays.hashCode(this.bytes); 199 | } 200 | 201 | @Override 202 | public String toString() { 203 | StringBuilder builder = new StringBuilder(2 * this.bytes.length); 204 | for (byte eightBits : this.bytes) { 205 | builder.append(HEX_DIGITS[(eightBits >> 4) & 0xF]); 206 | builder.append(HEX_DIGITS[eightBits & 0xF]); 207 | } 208 | return builder.toString(); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/CachedMappings.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings; 2 | // Created by booky10 in StackDeobfuscator (17:04 20.03.23) 3 | 4 | import dev.booky.stackdeobf.mappings.providers.AbstractMappingProvider; 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 6 | import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; 7 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 8 | import org.apache.commons.lang3.concurrent.BasicThreadFactory; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.Logger; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import java.nio.file.Path; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.Executor; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | public final class CachedMappings { 20 | 21 | private static final Logger LOGGER = LogManager.getLogger("StackDeobfuscator"); 22 | 23 | // "CLASSES" name has package prefixed (separated by '.') 24 | private final Int2ObjectMap classes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); 25 | private final Int2ObjectMap methods = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); 26 | private final Int2ObjectMap fields = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); 27 | 28 | private CachedMappings() { 29 | } 30 | 31 | public static CompletableFuture create(Path cacheDir, AbstractMappingProvider provider) { 32 | LOGGER.info("Creating asynchronous mapping cache executor..."); 33 | ExecutorService executor = Executors.newSingleThreadExecutor( 34 | new BasicThreadFactory.Builder().namingPattern("Mappings Cache Thread #%d").daemon(true).build()); 35 | 36 | return create(cacheDir, provider, executor) 37 | // needs to be executed asynchronously, otherwise the 38 | // executor of the current thread would be shut down 39 | .whenCompleteAsync(($, throwable) -> { 40 | LOGGER.info("Shutting down asynchronous mapping cache executor..."); 41 | executor.shutdown(); 42 | 43 | if (throwable != null) { 44 | LOGGER.error("An error occurred while creating mappings cache", throwable); 45 | } 46 | }); 47 | } 48 | 49 | public static CompletableFuture create(Path cacheDir, AbstractMappingProvider provider, Executor executor) { 50 | long start = System.currentTimeMillis(); 51 | CachedMappings mappings = new CachedMappings(); 52 | 53 | // visitor expects mappings to be intermediary -> named 54 | MappingCacheVisitor visitor = new MappingCacheVisitor(mappings.classes, mappings.methods, mappings.fields); 55 | return provider.cacheMappings(cacheDir, visitor, executor).thenApply($ -> { 56 | long timeDiff = System.currentTimeMillis() - start; 57 | LOGGER.info("Cached mappings have been built (took {}ms)", timeDiff); 58 | 59 | LOGGER.info(" Classes: " + mappings.classes.size()); 60 | LOGGER.info(" Methods: " + mappings.methods.size()); 61 | LOGGER.info(" Fields: " + mappings.fields.size()); 62 | 63 | return mappings; 64 | }); 65 | } 66 | 67 | public @Nullable String remapClass(int id) { 68 | return this.classes.get(id); 69 | } 70 | 71 | public @Nullable String remapMethod(int id) { 72 | return this.methods.get(id); 73 | } 74 | 75 | public @Nullable String remapField(int id) { 76 | return this.fields.get(id); 77 | } 78 | 79 | public String remapClasses(String string) { 80 | return RemappingUtil.remapClasses(this, string); 81 | } 82 | 83 | public String remapMethods(String string) { 84 | return RemappingUtil.remapMethods(this, string); 85 | } 86 | 87 | public String remapFields(String string) { 88 | return RemappingUtil.remapFields(this, string); 89 | } 90 | 91 | public String remapString(String string) { 92 | return RemappingUtil.remapString(this, string); 93 | } 94 | 95 | public Throwable remapThrowable(Throwable throwable) { 96 | return RemappingUtil.remapThrowable(this, throwable); 97 | } 98 | 99 | public void remapStackTrace(StackTraceElement[] elements) { 100 | RemappingUtil.remapStackTrace(this, elements); 101 | } 102 | 103 | public StackTraceElement remapStackTrace(StackTraceElement element) { 104 | return RemappingUtil.remapStackTrace(this, element); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/MappingCacheVisitor.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings; 2 | // Created by booky10 in StackDeobfuscator (15:08 23.03.23) 3 | 4 | import net.fabricmc.mappingio.MappedElementKind; 5 | import net.fabricmc.mappingio.MappingVisitor; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class MappingCacheVisitor implements MappingVisitor { 12 | 13 | private final Map classes, methods, fields; 14 | private int dstNamespaceId = 0; 15 | 16 | private String srcClassName; 17 | private String srcMethodName; 18 | private String srcFieldName; 19 | 20 | public MappingCacheVisitor(Map classes, Map methods, Map fields) { 21 | this.classes = classes; 22 | this.methods = methods; 23 | this.fields = fields; 24 | } 25 | 26 | @Override 27 | public void visitNamespaces(String srcNamespace, List dstNamespaces) { 28 | int namedIndex = dstNamespaces.indexOf("named"); 29 | if (namedIndex != -1) { 30 | this.dstNamespaceId = namedIndex; 31 | } 32 | } 33 | 34 | @Override 35 | public boolean visitClass(String srcName) { 36 | this.srcClassName = srcName; 37 | return true; 38 | } 39 | 40 | @Override 41 | public boolean visitField(String srcName, String srcDesc) { 42 | this.srcFieldName = srcName; 43 | return true; 44 | } 45 | 46 | @Override 47 | public boolean visitMethod(String srcName, String srcDesc) { 48 | this.srcMethodName = srcName; 49 | return true; 50 | } 51 | 52 | @Override 53 | public boolean visitMethodArg(int argPosition, int lvIndex, String srcName) { 54 | return false; 55 | } 56 | 57 | @Override 58 | public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, int endOpIdx, @Nullable String srcName) { 59 | return false; 60 | } 61 | 62 | @Override 63 | public void visitDstName(MappedElementKind targetKind, int namespace, String name) { 64 | if (this.dstNamespaceId != namespace) { 65 | return; 66 | } 67 | 68 | switch (targetKind) { 69 | case CLASS -> { 70 | String srcName = this.srcClassName; 71 | if (srcName.equals(name)) { 72 | return; 73 | } 74 | if (!srcName.startsWith("net/minecraft/class_")) { 75 | // I don't know why, but in some versions (tested in 1.14.2) the 76 | // names are just switched, without the namespaces being switched 77 | if (name.startsWith("net/minecraft/class_")) { 78 | String ogName = name; 79 | name = srcName; 80 | srcName = ogName; 81 | } else { 82 | return; 83 | } 84 | } 85 | 86 | String classIdStr; 87 | int innerClassSeparatorIndex = srcName.lastIndexOf('$'); 88 | if (innerClassSeparatorIndex != -1) { 89 | classIdStr = srcName.substring(innerClassSeparatorIndex + 1); 90 | if (!classIdStr.startsWith("class_")) { 91 | return; // don't save it if it is a lambda 92 | } 93 | classIdStr = classIdStr.substring("class_".length()); 94 | } else { 95 | classIdStr = srcName.substring("net/minecraft/class_".length()); 96 | } 97 | 98 | innerClassSeparatorIndex = name.indexOf('$'); 99 | if (innerClassSeparatorIndex != -1) { 100 | name = name.substring(innerClassSeparatorIndex + 1); 101 | } else { 102 | name = name.replace('/', '.'); 103 | } 104 | this.classes.put(Integer.parseInt(classIdStr), name); 105 | } 106 | case METHOD -> { 107 | String srcName = this.srcMethodName; 108 | if (srcName.equals(name)) { 109 | return; 110 | } 111 | if (!srcName.startsWith("method_")) { 112 | if (name.startsWith("method_")) { 113 | String ogName = name; 114 | name = srcName; 115 | srcName = ogName; 116 | } else { 117 | return; 118 | } 119 | } 120 | 121 | int methodId = Integer.parseInt(srcName.substring("method_".length())); 122 | this.methods.put(methodId, name); 123 | } 124 | case FIELD -> { 125 | String srcName = this.srcFieldName; 126 | if (srcName.equals(name)) { 127 | return; 128 | } 129 | if (!srcName.startsWith("field_")) { 130 | if (name.startsWith("field_")) { 131 | String ogName = name; 132 | name = srcName; 133 | srcName = ogName; 134 | } else { 135 | return; 136 | } 137 | } 138 | 139 | int fieldId = Integer.parseInt(srcName.substring("field_".length())); 140 | this.fields.put(fieldId, name); 141 | } 142 | } 143 | } 144 | 145 | @Override 146 | public void visitComment(MappedElementKind targetKind, String comment) { 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/RemappedThrowable.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings; 2 | // Created by booky10 in StackDeobfuscator (18:03 20.03.23) 3 | 4 | /** 5 | * Internal wrapper class. 6 | */ 7 | public class RemappedThrowable extends Throwable { 8 | 9 | private final Throwable original; 10 | private final String className; 11 | 12 | public RemappedThrowable(String message, Throwable cause, 13 | Throwable original, String className) { 14 | super(message, cause); 15 | this.original = original; 16 | this.className = className; 17 | } 18 | 19 | public Throwable getOriginal() { 20 | return this.original; 21 | } 22 | 23 | public String getClassName() { 24 | return this.className; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | String message = this.getLocalizedMessage(); 30 | if (message == null) { 31 | return this.className; 32 | } 33 | return this.className + ": " + message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/RemappingUtil.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings; 2 | // Created by booky10 in StackDeobfuscator (17:43 17.12.22) 3 | 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | final class RemappingUtil { 8 | 9 | private static final Pattern CLASS_PATTERN = Pattern.compile("(net\\.minecraft\\.|net/minecraft/)?class_(\\d+)"); 10 | private static final Pattern METHOD_PATTERN = Pattern.compile("method_(\\d+)"); 11 | private static final Pattern FIELD_PATTERN = Pattern.compile("field_(\\d+)"); 12 | 13 | private RemappingUtil() { 14 | } 15 | 16 | public static String remapClasses(CachedMappings mappings, String string) { 17 | return CLASS_PATTERN.matcher(string).replaceAll(result -> { 18 | int classId = Integer.parseInt(result.group(2)); 19 | String className = mappings.remapClass(classId); 20 | if (className == null) { 21 | return Matcher.quoteReplacement(result.group()); 22 | } 23 | 24 | String packageName = result.group(1); 25 | if (packageName != null) { 26 | // a package has been specified, don't remove it 27 | if (packageName.indexOf('.') == -1) { 28 | // original package name contains "/" as package separator instead of "." 29 | className = className.replace('.', '/'); 30 | } 31 | } else { 32 | // no package in original string, remove it 33 | int packageIndex = className.lastIndexOf('.'); 34 | if (packageIndex != -1) { 35 | className = className.substring(packageIndex + 1); 36 | } 37 | } 38 | return Matcher.quoteReplacement(className); 39 | }); 40 | } 41 | 42 | public static String remapMethods(CachedMappings mappings, String string) { 43 | return METHOD_PATTERN.matcher(string).replaceAll(result -> { 44 | int methodId = Integer.parseInt(result.group(1)); 45 | String methodName = mappings.remapMethod(methodId); 46 | return Matcher.quoteReplacement(methodName == null ? result.group() : methodName); 47 | }); 48 | } 49 | 50 | public static String remapFields(CachedMappings mappings, String string) { 51 | return FIELD_PATTERN.matcher(string).replaceAll(result -> { 52 | int fieldId = Integer.parseInt(result.group(1)); 53 | String fieldName = mappings.remapField(fieldId); 54 | return Matcher.quoteReplacement(fieldName == null ? result.group() : fieldName); 55 | }); 56 | } 57 | 58 | public static String remapString(CachedMappings mappings, String string) { 59 | if (string.contains("class_")) { 60 | string = remapClasses(mappings, string); 61 | } 62 | 63 | if (string.contains("method_")) { 64 | string = remapMethods(mappings, string); 65 | } 66 | 67 | if (string.contains("field_")) { 68 | string = remapFields(mappings, string); 69 | } 70 | 71 | return string; 72 | } 73 | 74 | public static Throwable remapThrowable(CachedMappings mappings, Throwable throwable) { 75 | if (throwable instanceof RemappedThrowable) { 76 | return throwable; 77 | } 78 | 79 | StackTraceElement[] stackTrace = throwable.getStackTrace(); 80 | remapStackTrace(mappings, stackTrace); 81 | 82 | Throwable cause = throwable.getCause(); 83 | if (cause != null) { 84 | cause = remapThrowable(mappings, cause); 85 | } 86 | 87 | String message = throwable.getMessage(); 88 | if (message != null) { 89 | message = remapString(mappings, message); 90 | } 91 | 92 | String throwableName = throwable.getClass().getName(); 93 | if (throwableName.startsWith("net.minecraft.class_")) { 94 | throwableName = remapClasses(mappings, throwableName); 95 | } 96 | 97 | Throwable remapped = new RemappedThrowable(message, cause, throwable, throwableName); 98 | remapped.setStackTrace(stackTrace); 99 | for (Throwable suppressed : throwable.getSuppressed()) { 100 | remapped.addSuppressed(remapThrowable(mappings, suppressed)); 101 | } 102 | return remapped; 103 | } 104 | 105 | public static void remapStackTrace(CachedMappings mappings, StackTraceElement[] elements) { 106 | for (int i = 0; i < elements.length; i++) { 107 | elements[i] = remapStackTrace(mappings, elements[i]); 108 | } 109 | } 110 | 111 | public static StackTraceElement remapStackTrace(CachedMappings mappings, StackTraceElement element) { 112 | String className = element.getClassName(); 113 | boolean remappedClass = false; 114 | if (className.startsWith("net.minecraft.class_")) { 115 | className = remapClasses(mappings, className); 116 | remappedClass = true; 117 | } 118 | 119 | String fileName = element.getFileName(); 120 | if (fileName != null && fileName.startsWith("class_")) { 121 | fileName = remapClasses(mappings, fileName); 122 | } 123 | 124 | String methodName = element.getMethodName(); 125 | if (methodName.startsWith("method_")) { 126 | methodName = remapMethods(mappings, methodName); 127 | } 128 | 129 | String classLoaderName = element.getClassLoaderName(); 130 | if (remappedClass) { 131 | if (classLoaderName == null) { 132 | classLoaderName = "MC"; 133 | } else { 134 | classLoaderName += "//MC"; 135 | } 136 | } 137 | 138 | return new StackTraceElement(classLoaderName, element.getModuleName(), element.getModuleVersion(), 139 | className, methodName, fileName, element.getLineNumber()); 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/providers/AbstractMappingProvider.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings.providers; 2 | // Created by booky10 in StackDeobfuscator (14:35 23.03.23) 3 | 4 | import dev.booky.stackdeobf.util.VersionData; 5 | import net.fabricmc.mappingio.MappingVisitor; 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.Logger; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.FileSystem; 11 | import java.nio.file.FileSystems; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.Executor; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | import static dev.booky.stackdeobf.util.VersionConstants.V1_14_2; 20 | import static dev.booky.stackdeobf.util.VersionConstants.V21W39A; 21 | 22 | public abstract class AbstractMappingProvider { 23 | 24 | protected static final Logger LOGGER = LogManager.getLogger("StackDeobfuscator"); 25 | private static final Pattern ID_COMMIT_HASH_PATTERN = Pattern.compile("^(.+) / [0-9a-f]{32}$"); 26 | 27 | protected final VersionData versionData; 28 | protected final String name; 29 | 30 | protected AbstractMappingProvider(VersionData versionData, String name) { 31 | this.versionData = versionData; 32 | this.name = name; 33 | } 34 | 35 | protected static String getFabricatedVersion(VersionData versionData) { 36 | String version; 37 | // after 1.14.2, fabric switched to using the version id instead of the name for yarn versions 38 | if (versionData.getWorldVersion() >= V1_14_2 39 | // WHAT THE FUCK DID HAPPEN HERE? HOW? 40 | // "Minecraft.Server / f7d695aa1ba843f2aa0cbc2ece6aea49" 41 | // HOW IS THIS A VERSION ID??? HOW DID THIS GET DEPLOYED ANYWHERE? 42 | && versionData.getWorldVersion() != V21W39A) { 43 | version = versionData.getId(); 44 | 45 | // versions before 1.14.3-pre1 (and some combat tests) include the current 46 | // commit hash in the version.json id; just remove it using a regex everywhere 47 | Matcher matcher = ID_COMMIT_HASH_PATTERN.matcher(version); 48 | if (matcher.matches()) { 49 | version = matcher.group(1); 50 | } 51 | } else { 52 | version = versionData.getName(); 53 | } 54 | 55 | // the first combat test used a very obscure "id", just replace it manually 56 | if ("1.14.3 - Combat Test".equals(version)) { 57 | version = "1.14_combat-212796"; 58 | } 59 | 60 | // these just randomly have dots replaced with underscores, I don't want 61 | // to look into why, just do this manually for now 62 | // 63 | // 1_15_combat-1+build.1 also exists, but is completely gone from maven metadata? 64 | return switch (version) { 65 | // @formatter:off // what are you doing? 66 | case "1.15_combat-6", "1.16_combat-0" 67 | -> version.replace('.', '_'); 68 | // @formatter:on 69 | default -> version; 70 | }; 71 | } 72 | 73 | private static Path getCacheDir(Path fallbackDir) { 74 | Path cacheDir; 75 | if (System.getProperties().containsKey("stackdeobf.mappings-cache-dir")) { 76 | cacheDir = Path.of(System.getProperty("stackdeobf.mappings-cache-dir")); 77 | } else { 78 | cacheDir = fallbackDir; 79 | } 80 | 81 | if (Files.notExists(cacheDir)) { 82 | try { 83 | Files.createDirectories(cacheDir); 84 | } catch (IOException exception) { 85 | throw new RuntimeException(exception); 86 | } 87 | } 88 | 89 | if (!Files.isDirectory(cacheDir)) { 90 | throw new IllegalMonitorStateException(cacheDir + " has to be a directory"); 91 | } 92 | return cacheDir; 93 | } 94 | 95 | public CompletableFuture cacheMappings(Path fallbackCacheDir, MappingVisitor visitor, Executor executor) { 96 | Path cacheDir = getCacheDir(fallbackCacheDir); 97 | 98 | return CompletableFuture.completedFuture(null) 99 | .thenComposeAsync($ -> this.downloadMappings(cacheDir, executor), executor) 100 | .thenCompose($ -> this.parseMappings(executor)) 101 | .thenCompose($ -> this.visitMappings(visitor, executor)); 102 | } 103 | 104 | protected byte[] extractPackagedMappings(byte[] jarBytes) { 105 | try { 106 | Path jarPath = Files.createTempFile(null, ".jar"); 107 | try { 108 | Files.write(jarPath, jarBytes); 109 | try (FileSystem jar = FileSystems.newFileSystem(jarPath)) { 110 | return Files.readAllBytes(jar.getPath("mappings", "mappings.tiny")); 111 | } 112 | } finally { 113 | Files.delete(jarPath); 114 | } 115 | } catch (IOException exception) { 116 | throw new RuntimeException(exception); 117 | } 118 | } 119 | 120 | private CompletableFuture trackTime(CompletableFuture future) { 121 | long start = System.currentTimeMillis(); 122 | return future.thenApply($ -> System.currentTimeMillis() - start); 123 | } 124 | 125 | private CompletableFuture downloadMappings(Path cacheDir, Executor executor) { 126 | LOGGER.info("Verifying cache of {} mappings...", this.name); 127 | return this.trackTime(this.downloadMappings0(cacheDir, executor)).thenAccept(timeDiff -> 128 | LOGGER.info("Verified cache of {} mappings (took {}ms)", this.name, timeDiff)); 129 | } 130 | 131 | private CompletableFuture parseMappings(Executor executor) { 132 | LOGGER.info("Parsing {} mappings...", this.name); 133 | return this.trackTime(this.parseMappings0(executor)).thenAccept(timeDiff -> 134 | LOGGER.info("Parsed {} mappings (took {}ms)", this.name, timeDiff)); 135 | } 136 | 137 | private CompletableFuture visitMappings(MappingVisitor visitor, Executor executor) { 138 | LOGGER.info("Caching {} mappings...", this.name); 139 | return this.trackTime(this.visitMappings0(visitor, executor)).thenAccept(timeDiff -> 140 | LOGGER.info("Cached {} mappings (took {}ms)", this.name, timeDiff)); 141 | } 142 | 143 | protected abstract CompletableFuture downloadMappings0(Path cacheDir, Executor executor); 144 | 145 | protected abstract CompletableFuture parseMappings0(Executor executor); 146 | 147 | protected abstract CompletableFuture visitMappings0(MappingVisitor visitor, Executor executor); 148 | 149 | public String getName() { 150 | return this.name; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/providers/BuildBasedMappingProvider.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings.providers; 2 | // Created by booky10 in StackDeobfuscator (22:08 23.03.23) 3 | 4 | import dev.booky.stackdeobf.http.VerifiableUrl; 5 | import dev.booky.stackdeobf.util.MavenArtifactInfo; 6 | import dev.booky.stackdeobf.util.VersionData; 7 | import net.fabricmc.mappingio.MappingReader; 8 | import net.fabricmc.mappingio.MappingVisitor; 9 | import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; 10 | import net.fabricmc.mappingio.format.MappingFormat; 11 | import net.fabricmc.mappingio.tree.MemoryMappingTree; 12 | import org.w3c.dom.Document; 13 | import org.w3c.dom.NodeList; 14 | import org.xml.sax.SAXException; 15 | 16 | import javax.xml.parsers.DocumentBuilderFactory; 17 | import javax.xml.parsers.ParserConfigurationException; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | import java.io.OutputStream; 23 | import java.io.Reader; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.util.concurrent.CompletableFuture; 27 | import java.util.concurrent.Executor; 28 | import java.util.zip.GZIPInputStream; 29 | import java.util.zip.GZIPOutputStream; 30 | 31 | public class BuildBasedMappingProvider extends AbstractMappingProvider { 32 | 33 | protected final MavenArtifactInfo artifactInfo; 34 | protected final VerifiableUrl.HashType hashType; 35 | 36 | protected Path path; 37 | protected MemoryMappingTree mappings; 38 | 39 | public BuildBasedMappingProvider(VersionData versionData, String name, MavenArtifactInfo artifactInfo, VerifiableUrl.HashType hashType) { 40 | super(versionData, name); 41 | this.artifactInfo = artifactInfo; 42 | this.hashType = hashType; 43 | } 44 | 45 | protected MappingFormat getMappingFormat() { 46 | return MappingFormat.TINY_2_FILE; 47 | } 48 | 49 | public VerifiableUrl.HashType getMetaHashType() { 50 | return this.hashType; 51 | } 52 | 53 | public VerifiableUrl.HashType getJarHashType() { 54 | return this.hashType; 55 | } 56 | 57 | @Override 58 | protected CompletableFuture downloadMappings0(Path cacheDir, Executor executor) { 59 | String version = getFabricatedVersion(this.versionData); 60 | return this.fetchLatestVersion(cacheDir, version, executor) 61 | .thenCompose(build -> { 62 | this.path = cacheDir.resolve(this.name + "_" + build + ".gz"); 63 | 64 | // already cached, don't download anything 65 | if (Files.exists(this.path)) { 66 | LOGGER.info("Mappings for {} build {} are already downloaded", this.name, build); 67 | return CompletableFuture.completedFuture(null); 68 | } 69 | 70 | return this.artifactInfo.buildVerifiableUrl(build, "jar", this.getJarHashType(), executor) 71 | .thenCompose(verifiableUrl -> { 72 | LOGGER.info("Downloading {} mappings jar for build {}...", this.name, build); 73 | return verifiableUrl.get(executor); 74 | }) 75 | .thenAccept(resp -> { 76 | byte[] mappingBytes = this.extractPackagedMappings(resp.getBody()); 77 | try (OutputStream fileOutput = Files.newOutputStream(this.path); 78 | GZIPOutputStream gzipOutput = new GZIPOutputStream(fileOutput)) { 79 | gzipOutput.write(mappingBytes); 80 | } catch (IOException exception) { 81 | throw new RuntimeException(exception); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | private CompletableFuture fetchLatestVersion(Path cacheDir, String mcVersion, Executor executor) { 88 | return CompletableFuture.completedFuture(null).thenComposeAsync($ -> { 89 | Path versionCachePath = cacheDir.resolve(this.name + "_" + mcVersion + "_latest.txt"); 90 | if (Files.exists(versionCachePath)) { 91 | try { 92 | long lastVersionFetch = Files.getLastModifiedTime(versionCachePath).toMillis(); 93 | long timeDiff = (System.currentTimeMillis() - lastVersionFetch) / 1000 / 60; 94 | 95 | long maxTimeDiff = Long.getLong("stackdeobf.build-refresh-cooldown", 2 * 24 * 60 /* specified in minutes */); 96 | if (timeDiff <= maxTimeDiff) { 97 | // latest build has already been fetched in the last x minutes (default: 2 days) 98 | LOGGER.info("Latest build for {} is already cached ({} hour(s) ago, refresh in {} hour(s))", 99 | this.name, (long) Math.floor(timeDiff / 60d), (long) Math.ceil((maxTimeDiff - timeDiff) / 60d)); 100 | return CompletableFuture.completedFuture(Files.readString(versionCachePath).trim()); 101 | } else { 102 | LOGGER.info("Refreshing latest {} build (last refresh was {} hour(s) ago)...", 103 | this.name, (long) Math.ceil(timeDiff / 60d)); 104 | } 105 | } catch (IOException exception) { 106 | throw new RuntimeException(exception); 107 | } 108 | } 109 | 110 | return this.artifactInfo.buildVerifiableMetaUrl(this.getMetaHashType(), executor).thenCompose(verifiableUrl -> { 111 | LOGGER.info("Fetching latest {} build...", this.name); 112 | return verifiableUrl.get(executor).thenApply(resp -> { 113 | Document document; 114 | try (InputStream input = new ByteArrayInputStream(resp.getBody())) { 115 | // https://stackoverflow.com/a/14968272 116 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 117 | document = factory.newDocumentBuilder().parse(input); 118 | } catch (IOException | ParserConfigurationException | SAXException exception) { 119 | throw new RuntimeException("Can't parse response from " + verifiableUrl.getUrl() + " for " + mcVersion, exception); 120 | } 121 | 122 | NodeList versions = document.getElementsByTagName("version"); 123 | for (int i = versions.getLength() - 1; i >= 0; i--) { 124 | String version = versions.item(i).getTextContent(); 125 | if (!version.startsWith(mcVersion + "+")) { 126 | // 19w14b and before have this formatting 127 | if (!version.startsWith(mcVersion + ".")) { 128 | continue; 129 | } 130 | 131 | if (version.substring((mcVersion + ".").length()).indexOf('.') != -1) { 132 | // mcVersion is something like "1.19" and version is something like "1.19.4+build.1" 133 | // this prevents this being recognized as a valid mapping 134 | continue; 135 | } 136 | } 137 | 138 | try { 139 | Files.writeString(versionCachePath, version); 140 | } catch (IOException exception) { 141 | throw new RuntimeException("Can't write cache file for version " + version, exception); 142 | } 143 | LOGGER.info("Cached latest {} build version: {}", this.name, version); 144 | 145 | return version; 146 | } 147 | throw new IllegalArgumentException("Can't find " + this.name + " mappings for minecraft version " + mcVersion); 148 | }); 149 | }); 150 | }, executor); 151 | } 152 | 153 | @Override 154 | protected CompletableFuture parseMappings0(Executor executor) { 155 | return CompletableFuture.supplyAsync(() -> { 156 | MemoryMappingTree mappings = new MemoryMappingTree(); 157 | 158 | try (InputStream fileInput = Files.newInputStream(this.path); 159 | GZIPInputStream gzipInput = new GZIPInputStream(fileInput); 160 | Reader reader = new InputStreamReader(gzipInput)) { 161 | MappingReader.read(reader, this.getMappingFormat(), mappings); 162 | } catch (IOException exception) { 163 | throw new RuntimeException(exception); 164 | } 165 | 166 | // tiny v1 mappings format is official -> intermediary/named 167 | // this has to be switched, so it is intermediary -> official/named 168 | MemoryMappingTree reorderedMappings; 169 | if (!"intermediary".equals(mappings.getSrcNamespace()) 170 | && mappings.getDstNamespaces().contains("intermediary")) { 171 | try { 172 | reorderedMappings = new MemoryMappingTree(); 173 | mappings.accept(new MappingSourceNsSwitch(reorderedMappings, "intermediary")); 174 | } catch (IOException exception) { 175 | throw new RuntimeException("Error while switching namespaces", exception); 176 | } 177 | } else { 178 | reorderedMappings = mappings; 179 | } 180 | 181 | this.mappings = reorderedMappings; 182 | return null; 183 | }, executor); 184 | } 185 | 186 | @Override 187 | protected CompletableFuture visitMappings0(MappingVisitor visitor, Executor executor) { 188 | return CompletableFuture.supplyAsync(() -> { 189 | try { 190 | this.mappings.accept(visitor); 191 | } catch (IOException exception) { 192 | throw new RuntimeException(exception); 193 | } 194 | return null; 195 | }, executor); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/providers/CustomMappingProvider.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings.providers; 2 | // Created by booky10 in StackDeobfuscator (17:42 23.03.23) 3 | 4 | import dev.booky.stackdeobf.util.VersionData; 5 | import net.fabricmc.mappingio.MappingReader; 6 | import net.fabricmc.mappingio.MappingVisitor; 7 | import net.fabricmc.mappingio.adapter.MappingDstNsReorder; 8 | import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; 9 | import net.fabricmc.mappingio.format.MappingFormat; 10 | import net.fabricmc.mappingio.tree.MemoryMappingTree; 11 | 12 | import java.io.FileNotFoundException; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.io.Reader; 17 | import java.nio.file.FileSystem; 18 | import java.nio.file.FileSystems; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Objects; 24 | import java.util.concurrent.CompletableFuture; 25 | import java.util.concurrent.Executor; 26 | import java.util.stream.Stream; 27 | import java.util.zip.GZIPInputStream; 28 | 29 | public class CustomMappingProvider extends AbstractMappingProvider { 30 | 31 | private final Path path; 32 | private final MappingFormat format; 33 | private MemoryMappingTree mappings; 34 | 35 | public CustomMappingProvider(VersionData versionData, Path path, MappingFormat format) { 36 | super(versionData, "custom"); 37 | this.path = path; 38 | this.format = format; 39 | } 40 | 41 | @Override 42 | protected CompletableFuture downloadMappings0(Path cacheDir, Executor executor) { 43 | try { 44 | // this one check does not need to be done asynchronously 45 | if (Files.notExists(this.path)) { 46 | throw new FileNotFoundException("Custom mappings file at " + this.path + " doesn't exist"); 47 | } 48 | } catch (IOException exception) { 49 | throw new RuntimeException(exception); 50 | } 51 | 52 | LOGGER.info("Skipping mappings download, custom mappings are selected"); 53 | return CompletableFuture.completedFuture(null); 54 | } 55 | 56 | @Override 57 | protected CompletableFuture parseMappings0(Executor executor) { 58 | return CompletableFuture.supplyAsync(() -> { 59 | try { 60 | MemoryMappingTree mappings = new MemoryMappingTree(); 61 | this.readPath(this.path, mappings, this.format); 62 | 63 | if (!"intermediary".equals(mappings.getSrcNamespace())) { 64 | if (!mappings.getDstNamespaces().contains("intermediary")) { 65 | throw new IllegalStateException("Custom mappings don't map from 'intermediary'"); 66 | } 67 | 68 | // try to switch to have intermediary as source 69 | MemoryMappingTree switchedMappings = new MemoryMappingTree(); 70 | mappings.accept(new MappingSourceNsSwitch(switchedMappings, "intermediary")); 71 | mappings = switchedMappings; 72 | } 73 | 74 | if (!mappings.getDstNamespaces().contains("named")) { 75 | throw new IllegalStateException("Custom mappings don't map to 'named'"); 76 | } 77 | 78 | // 'named' needs to be the first destination namespace 79 | if (mappings.getDstNamespaces().indexOf("named") != 0) { 80 | List orderedNamespaces = new ArrayList<>(mappings.getDstNamespaces()); 81 | orderedNamespaces.remove("named"); 82 | orderedNamespaces.add(0, "named"); 83 | 84 | MemoryMappingTree reorderedMappings = new MemoryMappingTree(); 85 | mappings.accept(new MappingDstNsReorder(reorderedMappings, orderedNamespaces)); 86 | mappings = reorderedMappings; 87 | } 88 | 89 | this.mappings = mappings; 90 | } catch (IOException exception) { 91 | throw new RuntimeException(exception); 92 | } 93 | return null; 94 | }, executor); 95 | } 96 | 97 | @Override 98 | protected CompletableFuture visitMappings0(MappingVisitor visitor, Executor executor) { 99 | return CompletableFuture.supplyAsync(() -> { 100 | try { 101 | this.mappings.accept(visitor); 102 | } catch (IOException exception) { 103 | throw new RuntimeException(exception); 104 | } 105 | return null; 106 | }, executor); 107 | } 108 | 109 | private void readPath(Path path, MappingVisitor visitor, MappingFormat format) throws IOException { 110 | String contentType = Files.probeContentType(path); 111 | if ("application/gzip".equals(contentType)) { 112 | try (InputStream fileInput = Files.newInputStream(path); 113 | GZIPInputStream gzipInput = new GZIPInputStream(fileInput); 114 | Reader reader = new InputStreamReader(gzipInput)) { 115 | MappingReader.read(reader, format, visitor); 116 | } 117 | } 118 | 119 | if ("application/java-archive".equals(contentType) || "application/zip".equals(contentType)) { 120 | try (FileSystem archive = FileSystems.newFileSystem(path)) { 121 | Path innerPath = archive.getPath("mappings", "mappings.tiny"); 122 | if (Files.notExists(innerPath)) { 123 | Path singlePath = null; 124 | for (Path directory : archive.getRootDirectories()) { 125 | try (Stream files = Files.list(directory)) { 126 | for (Path subPath : files.toList()) { 127 | if (singlePath != null) { 128 | throw new IllegalStateException("More than one file found in " + path.toAbsolutePath()); 129 | } 130 | singlePath = subPath; 131 | } 132 | } 133 | } 134 | innerPath = singlePath; 135 | } 136 | Objects.requireNonNull(innerPath, "No mappings file found in " + path.toAbsolutePath()); 137 | 138 | this.readPath(innerPath, visitor, format); 139 | return; 140 | } 141 | } 142 | 143 | if (contentType != null && !"text/plain".equals(contentType)) { 144 | LOGGER.warn("Can't recognize content type of {}, assuming it's plain text", 145 | path.toAbsolutePath()); 146 | } 147 | 148 | MappingReader.read(path, format, visitor); 149 | } 150 | 151 | public Path getPath() { 152 | return this.path; 153 | } 154 | 155 | public MappingFormat getFormat() { 156 | return this.format; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/providers/IntermediaryMappingProvider.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings.providers; 2 | // Created by booky10 in StackDeobfuscator (20:56 30.03.23) 3 | 4 | import dev.booky.stackdeobf.http.VerifiableUrl; 5 | import dev.booky.stackdeobf.util.MavenArtifactInfo; 6 | import dev.booky.stackdeobf.util.VersionData; 7 | import net.fabricmc.mappingio.MappingReader; 8 | import net.fabricmc.mappingio.MappingVisitor; 9 | import net.fabricmc.mappingio.format.MappingFormat; 10 | import net.fabricmc.mappingio.tree.MemoryMappingTree; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.InputStreamReader; 15 | import java.io.OutputStream; 16 | import java.io.Reader; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.Executor; 21 | import java.util.zip.GZIPInputStream; 22 | import java.util.zip.GZIPOutputStream; 23 | 24 | public class IntermediaryMappingProvider extends AbstractMappingProvider { 25 | 26 | private static final String REPO_URL = System.getProperty("stackdeobf.intermediary.repo-url", "https://maven.fabricmc.net"); 27 | private static final MavenArtifactInfo MAPPINGS_ARTIFACT = MavenArtifactInfo.parse(REPO_URL, 28 | System.getProperty("stackdeobf.intermediary.mappings-artifact", "net.fabricmc:intermediary:v2")); 29 | 30 | // even though yarn didn't have sha512 at the time, intermediary did 31 | private static final VerifiableUrl.HashType HASH_TYPE = VerifiableUrl.HashType.SHA512; 32 | 33 | private Path path; 34 | private MemoryMappingTree mappings; 35 | 36 | // only used as a conversion step (mojang + hashed quilt) 37 | IntermediaryMappingProvider(VersionData versionData) { 38 | super(versionData, "intermediary"); 39 | } 40 | 41 | @Override 42 | protected CompletableFuture downloadMappings0(Path cacheDir, Executor executor) { 43 | String fabricatedVersion = getFabricatedVersion(this.versionData); 44 | 45 | this.path = cacheDir.resolve("intermediary_" + fabricatedVersion + ".gz"); 46 | if (Files.exists(this.path)) { 47 | return CompletableFuture.completedFuture(null); 48 | } 49 | 50 | return MAPPINGS_ARTIFACT.buildVerifiableUrl(fabricatedVersion, "jar", HASH_TYPE, executor) 51 | .thenCompose(verifiableUrl -> { 52 | LOGGER.info("Downloading intermediary mappings for {}...", fabricatedVersion); 53 | return verifiableUrl.get(executor); 54 | }) 55 | .thenAccept(resp -> { 56 | byte[] mappingBytes = this.extractPackagedMappings(resp.getBody()); 57 | try (OutputStream fileOutput = Files.newOutputStream(this.path); 58 | GZIPOutputStream gzipOutput = new GZIPOutputStream(fileOutput)) { 59 | gzipOutput.write(mappingBytes); 60 | } catch (IOException exception) { 61 | throw new RuntimeException(exception); 62 | } 63 | }); 64 | } 65 | 66 | @Override 67 | protected CompletableFuture parseMappings0(Executor executor) { 68 | return CompletableFuture.supplyAsync(() -> { 69 | MemoryMappingTree mappings = new MemoryMappingTree(); 70 | 71 | try (InputStream fileInput = Files.newInputStream(this.path); 72 | GZIPInputStream gzipInput = new GZIPInputStream(fileInput); 73 | Reader reader = new InputStreamReader(gzipInput)) { 74 | MappingReader.read(reader, MappingFormat.TINY_2_FILE, mappings); 75 | } catch (IOException exception) { 76 | throw new RuntimeException(exception); 77 | } 78 | 79 | this.mappings = mappings; 80 | return null; 81 | }, executor); 82 | } 83 | 84 | @Override 85 | protected CompletableFuture visitMappings0(MappingVisitor visitor, Executor executor) { 86 | throw new UnsupportedOperationException(); 87 | } 88 | 89 | public Path getPath() { 90 | return this.path; 91 | } 92 | 93 | public MemoryMappingTree getMappings() { 94 | return this.mappings; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/providers/QuiltMappingProvider.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings.providers; 2 | // Created by booky10 in StackDeobfuscator (22:06 23.03.23) 3 | 4 | import dev.booky.stackdeobf.http.VerifiableUrl; 5 | import dev.booky.stackdeobf.util.MavenArtifactInfo; 6 | import dev.booky.stackdeobf.util.VersionData; 7 | import net.fabricmc.mappingio.MappingReader; 8 | import net.fabricmc.mappingio.MappingVisitor; 9 | import net.fabricmc.mappingio.adapter.ForwardingMappingVisitor; 10 | import net.fabricmc.mappingio.format.MappingFormat; 11 | import net.fabricmc.mappingio.tree.MappingTree; 12 | import net.fabricmc.mappingio.tree.MemoryMappingTree; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.InputStreamReader; 17 | import java.io.OutputStream; 18 | import java.io.Reader; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.Executor; 23 | import java.util.zip.GZIPInputStream; 24 | import java.util.zip.GZIPOutputStream; 25 | 26 | import static dev.booky.stackdeobf.util.VersionConstants.V1_18_2; 27 | import static dev.booky.stackdeobf.util.VersionConstants.V1_19_2; 28 | import static dev.booky.stackdeobf.util.VersionConstants.V1_19_DEEP_DARK_EXPERIMENTAL_SNAPSHOT_1; 29 | import static dev.booky.stackdeobf.util.VersionConstants.V22W13A; 30 | import static dev.booky.stackdeobf.util.VersionConstants.V23W13A_OR_B; 31 | 32 | public final class QuiltMappingProvider extends BuildBasedMappingProvider { 33 | 34 | private static final String REPO_URL = System.getProperty("stackdeobf.quilt.repo-url", "https://maven.quiltmc.org/repository/release"); 35 | 36 | private static final MavenArtifactInfo MAPPINGS_ARTIFACT_PRE_1192 = MavenArtifactInfo.parse(REPO_URL, 37 | System.getProperty("stackdeobf.quilt.mappings-artifact.pre-1192", 38 | "org.quiltmc:quilt-mappings:v2")); 39 | private static final MavenArtifactInfo MAPPINGS_ARTIFACT_POST_1192 = MavenArtifactInfo.parse(REPO_URL, 40 | System.getProperty("stackdeobf.quilt.mappings-artifact.post-1192", 41 | "org.quiltmc:quilt-mappings:intermediary-v2")); 42 | private static final MavenArtifactInfo HASHED_MAPPINGS_ARTIFACT = MavenArtifactInfo.parse(REPO_URL, 43 | System.getProperty("stackdeobf.quilt.mappings-artifact", "org.quiltmc:hashed")); 44 | 45 | // quilt mappings below 1.19.2 require special handling 46 | // 47 | // they are using their hashed inbetween step and didn't publish 48 | // intermediary mapped versions below 1.19.2, so more steps are 49 | // needed for converting hashed mappings to intermediary mappings 50 | private final IntermediaryMappingProvider intermediary; 51 | 52 | private Path hashedPath; 53 | private MemoryMappingTree hashedMappings; 54 | 55 | public QuiltMappingProvider(VersionData versionData) { 56 | super(versionData, "quilt", versionData.getWorldVersion() >= V1_19_2 57 | ? MAPPINGS_ARTIFACT_POST_1192 : MAPPINGS_ARTIFACT_PRE_1192, 58 | VerifiableUrl.HashType.SHA512); 59 | if (this.versionData.getWorldVersion() < V1_18_2) { 60 | throw new IllegalStateException("Quilt mappings are only supported for 1.18.2 and higher"); 61 | } 62 | if ((this.versionData.getWorldVersion() >= V1_19_DEEP_DARK_EXPERIMENTAL_SNAPSHOT_1 && this.versionData.getWorldVersion() <= V22W13A) 63 | || this.versionData.getWorldVersion() == V23W13A_OR_B) { 64 | throw new IllegalStateException("Quilt mappings are not supported for " 65 | + versionData.getName() + " (" + versionData.getWorldVersion() + ")"); 66 | } 67 | this.intermediary = new IntermediaryMappingProvider(versionData); 68 | } 69 | 70 | @Override 71 | protected CompletableFuture downloadMappings0(Path cacheDir, Executor executor) { 72 | CompletableFuture future = super.downloadMappings0(cacheDir, executor); 73 | 74 | if (this.versionData.getWorldVersion() >= V1_19_2) { 75 | // quilt already provides intermediary -> quilt mappings starting with 1.19.2, 76 | // don't make things too complex if not necessary 77 | return future; 78 | } 79 | 80 | // see comment at field declaration for reason 81 | future = future.thenCompose($ -> this.intermediary.downloadMappings0(cacheDir, executor)); 82 | 83 | this.hashedPath = cacheDir.resolve(this.name + "_" + this.versionData.getId() + "_hashed.gz"); 84 | if (Files.exists(this.hashedPath)) { 85 | LOGGER.info("Hashed {} mappings for {} are already downloaded", this.name, this.versionData.getId()); 86 | return future; 87 | } 88 | 89 | return future 90 | .thenCompose($ -> HASHED_MAPPINGS_ARTIFACT.buildVerifiableUrl(this.versionData.getId(), "jar", this.getJarHashType(), executor)) 91 | .thenCompose(verifiableUrl -> { 92 | LOGGER.info("Downloading hashed {} mappings for {}...", this.name, this.versionData.getId()); 93 | return verifiableUrl.get(executor); 94 | }) 95 | .thenAccept(resp -> { 96 | byte[] mappingBytes = this.extractPackagedMappings(resp.getBody()); 97 | try (OutputStream fileOutput = Files.newOutputStream(this.hashedPath); 98 | GZIPOutputStream gzipOutput = new GZIPOutputStream(fileOutput)) { 99 | gzipOutput.write(mappingBytes); 100 | } catch (IOException exception) { 101 | throw new RuntimeException(exception); 102 | } 103 | }); 104 | } 105 | 106 | @Override 107 | protected CompletableFuture parseMappings0(Executor executor) { 108 | CompletableFuture future = super.parseMappings0(executor); 109 | 110 | if (this.versionData.getWorldVersion() >= V1_19_2) { 111 | return future; // see comment at method above 112 | } 113 | 114 | return future 115 | .thenCompose($ -> this.intermediary.parseMappings0(executor)) 116 | .thenRun(() -> { 117 | MemoryMappingTree mappings = new MemoryMappingTree(); 118 | 119 | try (InputStream fileInput = Files.newInputStream(this.hashedPath); 120 | GZIPInputStream gzipInput = new GZIPInputStream(fileInput); 121 | Reader reader = new InputStreamReader(gzipInput)) { 122 | MappingReader.read(reader, MappingFormat.TINY_2_FILE 123 | , mappings); 124 | } catch (IOException exception) { 125 | throw new RuntimeException(exception); 126 | } 127 | 128 | this.hashedMappings = mappings; 129 | }); 130 | } 131 | 132 | @Override 133 | protected CompletableFuture visitMappings0(MappingVisitor visitor, Executor executor) { 134 | if (this.versionData.getWorldVersion() >= V1_19_2) { 135 | return super.visitMappings0(visitor, executor); // see comment at method above the above method 136 | } 137 | 138 | return CompletableFuture.supplyAsync(() -> { 139 | try { 140 | // source names need to be mapped back through hashed mappings to obfuscated names, 141 | // these then will get mapped to intermediary 142 | this.mappings.accept(new HashedVisitor(visitor)); 143 | } catch (IOException exception) { 144 | throw new RuntimeException(exception); 145 | } 146 | return null; 147 | }, executor); 148 | } 149 | 150 | // basically the same as MojangMappingProvider$Visitor, but with one extra step 151 | private final class HashedVisitor extends ForwardingMappingVisitor { 152 | 153 | private MappingTree.ClassMapping hashedClass; 154 | private MappingTree.ClassMapping intermediaryClass; 155 | 156 | private HashedVisitor(MappingVisitor next) { 157 | super(next); 158 | } 159 | 160 | @Override 161 | public boolean visitClass(String srcName /* hashed */) throws IOException { 162 | // lookup hashed class mapping by inputting the source name into the destination 163 | this.hashedClass = QuiltMappingProvider.this.hashedMappings.getClass(srcName, 0); 164 | if (this.hashedClass == null) { 165 | return false; 166 | } 167 | 168 | // lookup intermediary class mapping by inputting the source name of the hashedClass into the source name of intermediary 169 | this.intermediaryClass = QuiltMappingProvider.this.intermediary.getMappings().getClass(this.hashedClass.getSrcName()); 170 | if (this.intermediaryClass == null) { 171 | return false; 172 | } 173 | 174 | return super.visitClass(this.intermediaryClass.getDstName(0)); 175 | } 176 | 177 | @Override 178 | public boolean visitMethod(String srcName, String srcDesc) throws IOException { 179 | // do the same as above again 180 | MappingTree.MethodMapping hashedMethod = this.hashedClass.getMethod(srcName, srcDesc, 0); 181 | if (hashedMethod == null) { 182 | return false; 183 | } 184 | 185 | MappingTree.MethodMapping intermediaryMethod = this.intermediaryClass.getMethod(hashedMethod.getSrcName(), hashedMethod.getSrcDesc()); 186 | if (intermediaryMethod == null) { 187 | return false; 188 | } 189 | 190 | return super.visitMethod(intermediaryMethod.getDstName(0), intermediaryMethod.getDstDesc(0)); 191 | } 192 | 193 | @Override 194 | public boolean visitField(String srcName, String srcDesc) throws IOException { 195 | // and again 196 | MappingTree.FieldMapping hashedField = this.hashedClass.getField(srcName, srcDesc, 0); 197 | if (hashedField == null) { 198 | return false; 199 | } 200 | 201 | MappingTree.FieldMapping intermediaryField = this.intermediaryClass.getField(hashedField.getSrcName(), hashedField.getSrcDesc()); 202 | if (intermediaryField == null) { 203 | return false; 204 | } 205 | 206 | return super.visitField(intermediaryField.getDstName(0), intermediaryField.getDstDesc(0)); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/mappings/providers/YarnMappingProvider.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mappings.providers; 2 | // Created by booky10 in StackDeobfuscator (16:59 23.03.23) 3 | 4 | import dev.booky.stackdeobf.http.VerifiableUrl; 5 | import dev.booky.stackdeobf.util.MavenArtifactInfo; 6 | import dev.booky.stackdeobf.util.VersionData; 7 | import net.fabricmc.mappingio.format.MappingFormat; 8 | 9 | import java.util.EnumSet; 10 | import java.util.Set; 11 | 12 | import static dev.booky.stackdeobf.util.VersionConstants.V19W02A; 13 | import static dev.booky.stackdeobf.util.VersionConstants.V19W42A; 14 | import static dev.booky.stackdeobf.util.VersionConstants.V1_14_4; 15 | import static dev.booky.stackdeobf.util.VersionConstants.V1_15; 16 | import static dev.booky.stackdeobf.util.VersionConstants.V1_15_COMBAT_1; 17 | 18 | public class YarnMappingProvider extends BuildBasedMappingProvider { 19 | 20 | private static final String REPO_URL = System.getProperty("stackdeobf.yarn.repo-url", "https://maven.fabricmc.net"); 21 | 22 | // default to false as fabric's maven repository sometimes 23 | // returns invalid checksums after an update for maven-metadata.xml files; 24 | // this should work around https://github.com/booky10/StackDeobfuscator/issues/10 25 | private static final boolean VERIFY_YARN_METADATA_CHECKSUMS = 26 | Boolean.getBoolean("stackdeobf.yarn.verify-metadata-checksums"); 27 | 28 | private static final MavenArtifactInfo MAPPINGS_ARTIFACT_V1 = MavenArtifactInfo.parse(REPO_URL, 29 | System.getProperty("stackdeobf.yarn.mappings-artifact.v1", "net.fabricmc:yarn")); 30 | private static final MavenArtifactInfo MAPPINGS_ARTIFACT_V2 = MavenArtifactInfo.parse(REPO_URL, 31 | System.getProperty("stackdeobf.yarn.mappings-artifact.v2", "net.fabricmc:yarn:v2")); 32 | 33 | public YarnMappingProvider(VersionData versionData) { 34 | super(versionData, "yarn", getArtifact(versionData), getHashType(versionData)); 35 | } 36 | 37 | private static Set getVersionFlags(VersionData versionData) { 38 | if (versionData.getWorldVersion() == V19W02A) { 39 | // I don't understand this... 40 | return EnumSet.allOf(VersionFlag.class); 41 | } 42 | 43 | // I gave up writing comments explaining this mess... please don't look at the file history 44 | Set flags = EnumSet.noneOf(VersionFlag.class); 45 | if (versionData.getWorldVersion() < V1_15 46 | && versionData.getWorldVersion() != V1_14_4) { 47 | flags.add(VersionFlag.NO_SHA256); 48 | 49 | if (versionData.getWorldVersion() <= V19W42A) { 50 | flags.add(VersionFlag.NO_V2); 51 | } 52 | } 53 | 54 | if (versionData.getWorldVersion() == V1_15_COMBAT_1) { 55 | flags.add(VersionFlag.NO_SHA256); 56 | } 57 | 58 | return flags; 59 | } 60 | 61 | private static MavenArtifactInfo getArtifact(VersionData versionData) { 62 | return getVersionFlags(versionData).contains(VersionFlag.NO_V2) 63 | ? MAPPINGS_ARTIFACT_V1 : MAPPINGS_ARTIFACT_V2; 64 | } 65 | 66 | private static VerifiableUrl.HashType getHashType(VersionData versionData) { 67 | return getVersionFlags(versionData).contains(VersionFlag.NO_SHA256) 68 | ? VerifiableUrl.HashType.SHA1 : VerifiableUrl.HashType.SHA256; 69 | } 70 | 71 | @Override 72 | public VerifiableUrl.HashType getMetaHashType() { 73 | if (!VERIFY_YARN_METADATA_CHECKSUMS) { 74 | return VerifiableUrl.HashType.DUMMY; 75 | } 76 | return super.getMetaHashType(); 77 | } 78 | 79 | @Override 80 | protected MappingFormat getMappingFormat() { 81 | return getVersionFlags(this.versionData).contains(VersionFlag.NO_V2) 82 | ? MappingFormat.TINY_FILE : MappingFormat.TINY_2_FILE; 83 | } 84 | 85 | private enum VersionFlag { 86 | NO_V2, // or just no checksums for it 87 | NO_SHA256, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/util/Log4jRemapUtil.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.util; 2 | // Created by booky10 in StackDeobfuscator (22:33 14.04.23) 3 | 4 | import dev.booky.stackdeobf.mappings.CachedMappings; 5 | import org.apache.logging.log4j.core.impl.ExtendedStackTraceElement; 6 | import org.apache.logging.log4j.core.impl.ThrowableProxy; 7 | 8 | import java.lang.reflect.Field; 9 | 10 | final class Log4jRemapUtil { 11 | 12 | private static final Field PROXY_NAME = getField(ThrowableProxy.class, "name"); 13 | private static final Field PROXY_MESSAGE = getField(ThrowableProxy.class, "message"); 14 | private static final Field PROXY_LOCALIZED_MESSAGE = getField(ThrowableProxy.class, "localizedMessage"); 15 | private static final Field EXT_STACK_ELEMENT = getField(ExtendedStackTraceElement.class, "stackTraceElement"); 16 | 17 | private static Field getField(Class> clazz, String name) { 18 | try { 19 | Field field = clazz.getDeclaredField(name); 20 | field.setAccessible(true); 21 | return field; 22 | } catch (ReflectiveOperationException exception) { 23 | throw new RuntimeException(exception); 24 | } 25 | } 26 | 27 | static void remapThrowableProxy(CachedMappings mappings, ThrowableProxy proxy) throws IllegalAccessException { 28 | // remap throwable classname 29 | if (proxy.getName() != null && proxy.getName().startsWith("net.minecraft.class_")) { 30 | PROXY_NAME.set(proxy, mappings.remapClasses(proxy.getName())); 31 | } 32 | 33 | // remap throwable message 34 | if (proxy.getMessage() != null) { 35 | PROXY_MESSAGE.set(proxy, mappings.remapString(proxy.getMessage())); 36 | } 37 | if (proxy.getLocalizedMessage() != null) { 38 | PROXY_LOCALIZED_MESSAGE.set(proxy, mappings.remapString(proxy.getLocalizedMessage())); 39 | } 40 | 41 | // remap throwable stack trace 42 | for (ExtendedStackTraceElement extElement : proxy.getExtendedStackTrace()) { 43 | StackTraceElement element = extElement.getStackTraceElement(); 44 | element = mappings.remapStackTrace(element); 45 | EXT_STACK_ELEMENT.set(extElement, element); 46 | } 47 | 48 | // remap cause + suppressed throwables 49 | if (proxy.getCauseProxy() != null) { 50 | remapThrowableProxy(mappings, proxy.getCauseProxy()); 51 | } 52 | for (ThrowableProxy suppressed : proxy.getSuppressedProxies()) { 53 | remapThrowableProxy(mappings, suppressed); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/util/MavenArtifactInfo.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.util; 2 | // Created by booky10 in StackDeobfuscator (20:21 30.03.23) 3 | 4 | import dev.booky.stackdeobf.http.VerifiableUrl; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.net.URI; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Objects; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.Executor; 13 | 14 | public class MavenArtifactInfo { 15 | 16 | private final String repoUrl; 17 | private final String groupId; 18 | private final String artifactId; 19 | private final String classifier; 20 | 21 | public MavenArtifactInfo(String repoUrl, String groupId, String artifactId, @Nullable String classifier) { 22 | this.repoUrl = repoUrl.endsWith("/") ? repoUrl : repoUrl + "/"; 23 | this.groupId = groupId; 24 | this.artifactId = artifactId; 25 | this.classifier = classifier; 26 | } 27 | 28 | // based on https://stackoverflow.com/a/4605816 + comments 29 | private static String encodePath(String input) { 30 | byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); 31 | String processedInput = new String(inputBytes, StandardCharsets.ISO_8859_1); 32 | 33 | StringBuilder resultStr = new StringBuilder(); 34 | for (char ch : processedInput.toCharArray()) { 35 | if (ch == '\0') { 36 | continue; 37 | } 38 | 39 | if (isUnsafe(ch)) { 40 | resultStr.append('%'); 41 | resultStr.append(toHex(ch / 16)); 42 | resultStr.append(toHex(ch % 16)); 43 | } else { 44 | resultStr.append(ch); 45 | } 46 | } 47 | return resultStr.toString(); 48 | } 49 | 50 | private static char toHex(int ch) { 51 | return (char) (ch < 10 ? '0' + ch : 'A' + ch - 10); 52 | } 53 | 54 | private static boolean isUnsafe(char ch) { 55 | return ch > 128 || " %$&,/:;=?@<>#%".indexOf(ch) >= 0; 56 | } 57 | 58 | public static MavenArtifactInfo parse(String repoUrl, String info) { 59 | String[] split = StringUtils.split(info, ':'); 60 | if (split.length != 2 && split.length != 3) { 61 | throw new IllegalArgumentException("Artifact info is invalid: " + info); 62 | } 63 | 64 | String groupId = split[0], artifactId = split[1]; 65 | String classifier = split.length > 2 ? split[2] : null; 66 | 67 | return new MavenArtifactInfo(repoUrl, groupId, artifactId, classifier); 68 | } 69 | 70 | public CompletableFuture buildVerifiableMetaUrl(VerifiableUrl.HashType hashType, Executor executor) { 71 | return VerifiableUrl.resolve(this.buildMetaUrl(), hashType, executor); 72 | } 73 | 74 | public URI buildMetaUrl() { 75 | return URI.create(this.repoUrl + this.groupId.replace('.', '/') + 76 | "/" + this.artifactId + "/maven-metadata.xml"); 77 | } 78 | 79 | public CompletableFuture buildVerifiableUrl(String version, String extension, 80 | VerifiableUrl.HashType hashType, Executor executor) { 81 | return VerifiableUrl.resolve(this.buildUrl(version, extension), hashType, executor); 82 | } 83 | 84 | public URI buildUrl(String version, String extension) { 85 | // url encode the build, some april fools snapshots have spaces in their names 86 | String encodedVersion = encodePath(version); 87 | 88 | String fileName = this.artifactId + "-" + encodedVersion + 89 | (this.classifier != null ? "-" + this.classifier : "") + "." + extension; 90 | return URI.create(this.repoUrl + this.groupId.replace('.', '/') + 91 | "/" + this.artifactId + "/" + encodedVersion + "/" + fileName); 92 | } 93 | 94 | public String getRepoUrl() { 95 | return this.repoUrl; 96 | } 97 | 98 | public String getGroupId() { 99 | return this.groupId; 100 | } 101 | 102 | public String getArtifactId() { 103 | return this.artifactId; 104 | } 105 | 106 | public @Nullable String getClassifier() { 107 | return this.classifier; 108 | } 109 | 110 | @Override 111 | public boolean equals(Object obj) { 112 | if (this == obj) return true; 113 | if (!(obj instanceof MavenArtifactInfo that)) return false; 114 | if (!this.repoUrl.equals(that.repoUrl)) return false; 115 | if (!this.groupId.equals(that.groupId)) return false; 116 | if (!this.artifactId.equals(that.artifactId)) return false; 117 | return Objects.equals(this.classifier, that.classifier); 118 | } 119 | 120 | @Override 121 | public int hashCode() { 122 | int result = this.repoUrl.hashCode(); 123 | result = 31 * result + this.groupId.hashCode(); 124 | result = 31 * result + this.artifactId.hashCode(); 125 | result = 31 * result + (this.classifier != null ? this.classifier.hashCode() : 0); 126 | return result; 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return "MavenArtifactInfo{repoUrl='" + this.repoUrl + '\'' + ", groupId='" + this.groupId + '\'' + ", artifactId='" + this.artifactId + '\'' + ", classifier='" + this.classifier + '\'' + '}'; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/util/RemappingRewritePolicy.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.util; 2 | // Created by booky10 in StackDeobfuscator (15:17 14.04.23) 3 | 4 | import dev.booky.stackdeobf.mappings.CachedMappings; 5 | import org.apache.logging.log4j.core.Appender; 6 | import org.apache.logging.log4j.core.LogEvent; 7 | import org.apache.logging.log4j.core.Logger; 8 | import org.apache.logging.log4j.core.appender.rewrite.RewriteAppender; 9 | import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy; 10 | import org.apache.logging.log4j.core.config.AppenderRef; 11 | import org.apache.logging.log4j.core.config.Configuration; 12 | import org.apache.logging.log4j.core.config.LoggerConfig; 13 | import org.apache.logging.log4j.core.impl.Log4jLogEvent; 14 | import org.apache.logging.log4j.core.impl.ThrowableProxy; 15 | import org.apache.logging.log4j.message.SimpleMessage; 16 | 17 | import java.util.Set; 18 | 19 | public final class RemappingRewritePolicy implements RewritePolicy { 20 | 21 | private final CachedMappings mappings; 22 | private final boolean rewriteMessages; 23 | 24 | public RemappingRewritePolicy(CachedMappings mappings, boolean rewriteMessages) { 25 | this.mappings = mappings; 26 | this.rewriteMessages = rewriteMessages; 27 | } 28 | 29 | public void inject(Logger logger) { 30 | // code mostly based on ChatGPT 31 | 32 | Set appenders = Set.copyOf(logger.getAppenders().values()); 33 | for (Appender appender : appenders) { 34 | logger.removeAppender(appender); 35 | } 36 | 37 | Configuration config = logger.getContext().getConfiguration(); 38 | LoggerConfig logCfg = config.getLoggerConfig(logger.getName()); 39 | AppenderRef[] refs = logCfg.getAppenderRefs().toArray(AppenderRef[]::new); 40 | 41 | RewriteAppender appender = RewriteAppender.createAppender("StackDeobfAppender", 42 | null, refs, config, this, null); 43 | appender.start(); 44 | logger.addAppender(appender); 45 | } 46 | 47 | @Override 48 | public LogEvent rewrite(LogEvent source) { 49 | if (!this.rewriteMessages && source.getThrown() == null) { 50 | return source; 51 | } 52 | 53 | Log4jLogEvent.Builder builder = new Log4jLogEvent.Builder(source); 54 | if (this.rewriteMessages) { 55 | String message = source.getMessage().getFormattedMessage(); 56 | message = this.mappings.remapString(message); 57 | builder.setMessage(new SimpleMessage(message)); 58 | } 59 | if (source.getThrown() != null) { 60 | // the remapping part of the logger needs to be done in the ThrowableProxy, 61 | // otherwise the ExtendedClassInfo would be lost 62 | 63 | try { 64 | ThrowableProxy proxy = new ThrowableProxy(source.getThrown()); 65 | Log4jRemapUtil.remapThrowableProxy(this.mappings, proxy); 66 | builder.setThrownProxy(proxy); 67 | } catch (IllegalAccessException exception) { 68 | throw new RuntimeException(exception); 69 | } 70 | } 71 | return builder.build(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/util/VersionConstants.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.util; 2 | 3 | // automatically generated, I'm not insane 4 | @SuppressWarnings("unused") 5 | public final class VersionConstants { 6 | 7 | public static final int V18W49A = 1916; 8 | public static final int V18W50A = 1919; 9 | public static final int V19W02A = 1921; 10 | public static final int V19W03A = 1922; 11 | public static final int V19W03B = 1923; 12 | public static final int V19W03C = 1924; 13 | public static final int V19W04A = 1926; 14 | public static final int V19W04B = 1927; 15 | public static final int V19W05A = 1930; 16 | public static final int V19W06A = 1931; 17 | public static final int V19W07A = 1932; 18 | public static final int V19W08A = 1933; 19 | public static final int V19W08B = 1934; 20 | public static final int V19W09A = 1935; 21 | public static final int V19W11A = 1937; 22 | public static final int V19W11B = 1938; 23 | public static final int V19W12A = 1940; 24 | public static final int V19W12B = 1941; 25 | public static final int V19W13A = 1942; 26 | public static final int V19W13B = 1943; 27 | public static final int V3D_SHAREWARE_V1_34 = 1943; 28 | public static final int V19W14A = 1944; 29 | public static final int V19W14B = 1945; 30 | public static final int V1_14_PRE_RELEASE_1 = 1947; 31 | public static final int V1_14_PRE_RELEASE_2 = 1948; 32 | public static final int V1_14_PRE_RELEASE_3 = 1949; 33 | public static final int V1_14_PRE_RELEASE_4 = 1950; 34 | public static final int V1_14_PRE_RELEASE_5 = 1951; 35 | public static final int V1_14 = 1952; 36 | public static final int V1_14_1_PRE_RELEASE_1 = 1955; 37 | public static final int V1_14_1_PRE_RELEASE_2 = 1956; 38 | public static final int V1_14_1 = 1957; 39 | public static final int V1_14_2_PRE_RELEASE_2 = 1959; 40 | public static final int V1_14_2_PRE_RELEASE_3 = 1960; 41 | public static final int V1_14_2_PRE_RELEASE_4 = 1962; 42 | public static final int V1_14_2 = 1963; 43 | public static final int V1_14_3_PRE1 = 1964; 44 | public static final int V1_14_3_PRE2 = 1965; 45 | public static final int V1_14_3_PRE3 = 1966; 46 | public static final int V1_14_3_PRE4 = 1967; 47 | public static final int V1_14_COMBAT_212796 = 2067; 48 | public static final int V1_14_3 = 1968; 49 | public static final int V1_14_4_PRE1 = 1969; 50 | public static final int V1_14_4_PRE2 = 1970; 51 | public static final int V1_14_4_PRE3 = 1971; 52 | public static final int V1_14_4_PRE4 = 1972; 53 | public static final int V1_14_4_PRE5 = 1973; 54 | public static final int V1_14_4_PRE6 = 1974; 55 | public static final int V1_14_4_PRE7 = 1975; 56 | public static final int V1_14_4 = 1976; 57 | public static final int V1_14_COMBAT_0 = 2068; 58 | public static final int V19W34A = 2200; 59 | public static final int V19W35A = 2201; 60 | public static final int V19W36A = 2203; 61 | public static final int V19W37A = 2204; 62 | public static final int V19W38A = 2205; 63 | public static final int V19W38B = 2206; 64 | public static final int V19W39A = 2207; 65 | public static final int V19W40A = 2208; 66 | public static final int V19W41A = 2210; 67 | public static final int V19W42A = 2212; 68 | public static final int V19W44A = 2213; 69 | public static final int V1_14_COMBAT_3 = 2069; 70 | public static final int V19W45A = 2214; 71 | public static final int V19W45B = 2215; 72 | public static final int V19W46A = 2216; 73 | public static final int V19W46B = 2217; 74 | public static final int V1_15_PRE1 = 2218; 75 | public static final int V1_15_PRE2 = 2219; 76 | public static final int V1_15_PRE3 = 2220; 77 | public static final int V1_15_COMBAT_1 = 2320; 78 | public static final int V1_15_PRE4 = 2221; 79 | public static final int V1_15_PRE5 = 2222; 80 | public static final int V1_15_PRE6 = 2223; 81 | public static final int V1_15_PRE7 = 2224; 82 | public static final int V1_15 = 2225; 83 | public static final int V1_15_1_PRE1 = 2226; 84 | public static final int V1_15_1 = 2227; 85 | public static final int V1_15_2_PRE1 = 2228; 86 | public static final int V1_15_COMBAT_6 = 2321; 87 | public static final int V1_15_2_PRE2 = 2229; 88 | public static final int V1_15_2 = 2230; 89 | public static final int V20W06A = 2504; 90 | public static final int V20W07A = 2506; 91 | public static final int V20W08A = 2507; 92 | public static final int V20W09A = 2510; 93 | public static final int V20W10A = 2512; 94 | public static final int V20W11A = 2513; 95 | public static final int V20W12A = 2515; 96 | public static final int V20W13A = 2520; 97 | public static final int V20W13B = 2521; 98 | public static final int V20W14INFINITE = 2522; 99 | public static final int V20W14A = 2524; 100 | public static final int V20W15A = 2525; 101 | public static final int V20W16A = 2526; 102 | public static final int V20W17A = 2529; 103 | public static final int V20W18A = 2532; 104 | public static final int V20W19A = 2534; 105 | public static final int V20W20A = 2536; 106 | public static final int V20W20B = 2537; 107 | public static final int V20W21A = 2554; 108 | public static final int V20W22A = 2555; 109 | public static final int V1_16_PRE1 = 2556; 110 | public static final int V1_16_PRE2 = 2557; 111 | public static final int V1_16_PRE3 = 2559; 112 | public static final int V1_16_PRE4 = 2560; 113 | public static final int V1_16_PRE5 = 2561; 114 | public static final int V1_16_PRE6 = 2562; 115 | public static final int V1_16_PRE7 = 2563; 116 | public static final int V1_16_PRE8 = 2564; 117 | public static final int V1_16_RC1 = 2565; 118 | public static final int V1_16 = 2566; 119 | public static final int V1_16_1 = 2567; 120 | public static final int V20W27A = 2569; 121 | public static final int V20W28A = 2570; 122 | public static final int V20W29A = 2571; 123 | public static final int V20W30A = 2572; 124 | public static final int V1_16_2_PRE1 = 2573; 125 | public static final int V1_16_2_PRE2 = 2574; 126 | public static final int V1_16_2_PRE3 = 2575; 127 | public static final int V1_16_COMBAT_0 = 2701; 128 | public static final int V1_16_2_RC1 = 2576; 129 | public static final int V1_16_2_RC2 = 2577; 130 | public static final int V1_16_2 = 2578; 131 | public static final int V1_16_COMBAT_3 = 2704; 132 | public static final int V1_16_3_RC1 = 2579; 133 | public static final int V1_16_3 = 2580; 134 | public static final int V1_16_4_PRE1 = 2581; 135 | public static final int V1_16_4_PRE2 = 2582; 136 | public static final int V1_16_4_RC1 = 2583; 137 | public static final int V1_16_4 = 2584; 138 | public static final int V20W45A = 2681; 139 | public static final int V20W46A = 2682; 140 | public static final int V20W48A = 2683; 141 | public static final int V20W49A = 2685; 142 | public static final int V20W51A = 2687; 143 | public static final int V1_16_5_RC1 = 2585; 144 | public static final int V1_16_5 = 2586; 145 | public static final int V21W03A = 2689; 146 | public static final int V21W05A = 2690; 147 | public static final int V21W05B = 2692; 148 | public static final int V21W06A = 2694; 149 | public static final int V21W07A = 2695; 150 | public static final int V21W08A = 2697; 151 | public static final int V21W08B = 2698; 152 | public static final int V21W10A = 2699; 153 | public static final int V21W11A = 2703; 154 | public static final int V21W13A = 2705; 155 | public static final int V21W14A = 2706; 156 | public static final int V21W15A = 2709; 157 | public static final int V21W16A = 2711; 158 | public static final int V21W17A = 2712; 159 | public static final int V21W18A = 2713; 160 | public static final int V21W19A = 2714; 161 | public static final int V21W20A = 2715; 162 | public static final int V1_17_PRE1 = 2716; 163 | public static final int V1_17_PRE2 = 2718; 164 | public static final int V1_17_PRE3 = 2719; 165 | public static final int V1_17_PRE4 = 2720; 166 | public static final int V1_17_PRE5 = 2721; 167 | public static final int V1_17_RC1 = 2722; 168 | public static final int V1_17_RC2 = 2723; 169 | public static final int V1_17 = 2724; 170 | public static final int V1_17_1_PRE1 = 2725; 171 | public static final int V1_17_1_PRE2 = 2726; 172 | public static final int V1_17_1_PRE3 = 2727; 173 | public static final int V1_17_1_RC1 = 2728; 174 | public static final int V1_17_1_RC2 = 2729; 175 | public static final int V1_17_1 = 2730; 176 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_1 = 2825; 177 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_2 = 2826; 178 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_3 = 2827; 179 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_4 = 2828; 180 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_5 = 2829; 181 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_6 = 2830; 182 | public static final int V1_18_EXPERIMENTAL_SNAPSHOT_7 = 2831; 183 | public static final int V21W37A = 2834; 184 | public static final int V21W38A = 2835; 185 | public static final int V21W39A = 2836; 186 | public static final int V21W40A = 2838; 187 | public static final int V21W41A = 2839; 188 | public static final int V21W42A = 2840; 189 | public static final int V21W43A = 2844; 190 | public static final int V21W44A = 2845; 191 | public static final int V1_18_PRE1 = 2847; 192 | public static final int V1_18_PRE2 = 2848; 193 | public static final int V1_18_PRE3 = 2849; 194 | public static final int V1_18_PRE4 = 2850; 195 | public static final int V1_18_PRE5 = 2851; 196 | public static final int V1_18_PRE6 = 2853; 197 | public static final int V1_18_PRE7 = 2854; 198 | public static final int V1_18_PRE8 = 2855; 199 | public static final int V1_18_RC1 = 2856; 200 | public static final int V1_18_RC2 = 2857; 201 | public static final int V1_18_RC3 = 2858; 202 | public static final int V1_18_RC4 = 2859; 203 | public static final int V1_18 = 2860; 204 | public static final int V1_18_1_PRE1 = 2861; 205 | public static final int V1_18_1_RC1 = 2862; 206 | public static final int V1_18_1_RC2 = 2863; 207 | public static final int V1_18_1_RC3 = 2864; 208 | public static final int V1_18_1 = 2865; 209 | public static final int V22W03A = 2966; 210 | public static final int V22W05A = 2967; 211 | public static final int V22W06A = 2968; 212 | public static final int V22W07A = 2969; 213 | public static final int V1_19_DEEP_DARK_EXPERIMENTAL_SNAPSHOT_1 = 3066; 214 | public static final int V1_18_2_PRE1 = 2971; 215 | public static final int V1_18_2_PRE2 = 2972; 216 | public static final int V1_18_2_PRE3 = 2973; 217 | public static final int V1_18_2_RC1 = 2974; 218 | public static final int V1_18_2 = 2975; 219 | public static final int V22W11A = 3080; 220 | public static final int V22W12A = 3082; 221 | public static final int V22W13A = 3085; 222 | public static final int V22W13ONEBLOCKATATIME = 3076; 223 | public static final int V22W14A = 3088; 224 | public static final int V22W15A = 3089; 225 | public static final int V22W16A = 3091; 226 | public static final int V22W16B = 3092; 227 | public static final int V22W17A = 3093; 228 | public static final int V22W18A = 3095; 229 | public static final int V22W19A = 3096; 230 | public static final int V1_19_PRE1 = 3098; 231 | public static final int V1_19_PRE2 = 3099; 232 | public static final int V1_19_PRE3 = 3100; 233 | public static final int V1_19_PRE4 = 3101; 234 | public static final int V1_19_PRE5 = 3102; 235 | public static final int V1_19_RC1 = 3103; 236 | public static final int V1_19_RC2 = 3104; 237 | public static final int V1_19 = 3105; 238 | public static final int V22W24A = 3106; 239 | public static final int V1_19_1_PRE1 = 3107; 240 | public static final int V1_19_1_RC1 = 3109; 241 | public static final int V1_19_1_PRE2 = 3110; 242 | public static final int V1_19_1_PRE3 = 3111; 243 | public static final int V1_19_1_PRE4 = 3112; 244 | public static final int V1_19_1_PRE5 = 3113; 245 | public static final int V1_19_1_PRE6 = 3114; 246 | public static final int V1_19_1_RC2 = 3115; 247 | public static final int V1_19_1_RC3 = 3116; 248 | public static final int V1_19_1 = 3117; 249 | public static final int V1_19_2_RC1 = 3118; 250 | public static final int V1_19_2_RC2 = 3119; 251 | public static final int V1_19_2 = 3120; 252 | public static final int V22W42A = 3205; 253 | public static final int V22W43A = 3206; 254 | public static final int V22W44A = 3207; 255 | public static final int V22W45A = 3208; 256 | public static final int V22W46A = 3210; 257 | public static final int V1_19_3_PRE1 = 3211; 258 | public static final int V1_19_3_PRE2 = 3212; 259 | public static final int V1_19_3_PRE3 = 3213; 260 | public static final int V1_19_3_RC1 = 3215; 261 | public static final int V1_19_3_RC2 = 3216; 262 | public static final int V1_19_3_RC3 = 3217; 263 | public static final int V1_19_3 = 3218; 264 | public static final int V23W03A = 3320; 265 | public static final int V23W04A = 3321; 266 | public static final int V23W05A = 3323; 267 | public static final int V23W06A = 3326; 268 | public static final int V23W07A = 3329; 269 | public static final int V1_19_4_PRE1 = 3330; 270 | public static final int V1_19_4_PRE2 = 3331; 271 | public static final int V1_19_4_PRE3 = 3332; 272 | public static final int V1_19_4_PRE4 = 3333; 273 | public static final int V1_19_4_RC1 = 3334; 274 | public static final int V1_19_4_RC2 = 3335; 275 | public static final int V1_19_4_RC3 = 3336; 276 | public static final int V1_19_4 = 3337; 277 | public static final int V23W12A = 3442; 278 | public static final int V23W13A = 3443; 279 | public static final int V23W13A_OR_B = 3444; 280 | public static final int V23W14A = 3445; 281 | public static final int V23W16A = 3449; 282 | public static final int V23W17A = 3452; 283 | public static final int V23W18A = 3453; 284 | public static final int V1_20_PRE1 = 3454; 285 | public static final int V1_20_PRE2 = 3455; 286 | public static final int V1_20_PRE3 = 3456; 287 | public static final int V1_20_PRE4 = 3457; 288 | public static final int V1_20_PRE5 = 3458; 289 | public static final int V1_20_PRE6 = 3460; 290 | public static final int V1_20_PRE7 = 3461; 291 | public static final int V1_20_RC1 = 3462; 292 | public static final int V1_20 = 3463; 293 | public static final int V1_20_1_RC1 = 3464; 294 | public static final int V1_20_1 = 3465; 295 | 296 | private VersionConstants() { 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /common/src/main/java/dev/booky/stackdeobf/util/VersionData.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.util; 2 | // Created by booky10 in StackDeobfuscator (19:41 06.07.23) 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.JsonObject; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.Reader; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.time.OffsetDateTime; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.Objects; 17 | 18 | public final class VersionData { 19 | 20 | private static final Gson GSON = new Gson(); 21 | 22 | private final String id; 23 | private final String name; 24 | private final int worldVersion; 25 | private final int protocolVersion; 26 | private final OffsetDateTime buildTime; 27 | 28 | private VersionData(String id, String name, int worldVersion, int protocolVersion, OffsetDateTime buildTime) { 29 | this.id = id; 30 | this.name = name; 31 | this.worldVersion = worldVersion; 32 | this.protocolVersion = protocolVersion; 33 | this.buildTime = buildTime; 34 | } 35 | 36 | public static VersionData fromClasspath() { 37 | JsonObject object; 38 | try (InputStream input = VersionData.class.getResourceAsStream("/version.json"); 39 | Reader reader = new InputStreamReader(Objects.requireNonNull(input, "version.json not found"))) { 40 | object = GSON.fromJson(reader, JsonObject.class); 41 | } catch (Throwable throwable) { 42 | throw new RuntimeException("Error while reading version data from classpath", throwable); 43 | } 44 | return fromJson(object); 45 | } 46 | 47 | public static VersionData fromJson(Path path) throws IOException { 48 | JsonObject object; 49 | try (BufferedReader reader = Files.newBufferedReader(path)) { 50 | object = GSON.fromJson(reader, JsonObject.class); 51 | } 52 | return fromJson(object); 53 | } 54 | 55 | public static VersionData fromJson(JsonObject object) { 56 | String id = object.get("id").getAsString(); 57 | String name = object.get("name").getAsString(); 58 | int worldVersion = object.get("world_version").getAsInt(); 59 | int protocolVersion = object.get("protocol_version").getAsInt(); 60 | 61 | String buildTimeStr = object.get("build_time").getAsString(); 62 | DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; 63 | OffsetDateTime buildTime = OffsetDateTime.parse(buildTimeStr, formatter); 64 | 65 | return new VersionData(id, name, worldVersion, protocolVersion, buildTime); 66 | } 67 | 68 | public String getId() { 69 | return this.id; 70 | } 71 | 72 | public String getName() { 73 | return this.name; 74 | } 75 | 76 | public int getWorldVersion() { 77 | return this.worldVersion; 78 | } 79 | 80 | public int getProtocolVersion() { 81 | return this.protocolVersion; 82 | } 83 | 84 | public OffsetDateTime getBuildTime() { 85 | return this.buildTime; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return "VersionData{id='" + this.id + '\'' + ", name='" + this.name + '\'' + ", worldVersion=" + this.worldVersion + ", protocolVersion=" + this.protocolVersion + ", buildTime=" + this.buildTime + '}'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/stackdeobf/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/common/src/main/resources/assets/stackdeobf/icon.png -------------------------------------------------------------------------------- /common/src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "stackdeobfuscator-common", 4 | "version": "${version}", 5 | 6 | "name": "StackDeobfuscator Common", 7 | "description": "Common library for remapping stacktraces to different mappings", 8 | 9 | "authors": [ 10 | "booky10" 11 | ], 12 | 13 | "contact": { 14 | "issues": "https://github.com/booky10/StackDeobfuscator/issues", 15 | "sources": "https://github.com/booky10/StackDeobfuscator" 16 | }, 17 | 18 | "icon": "assets/stackdeobf/icon.png", 19 | "license": "LGPL-3.0-only", 20 | "environment": "*", 21 | 22 | "depends": { 23 | "java": ">=17" 24 | }, 25 | 26 | "custom": { 27 | "modmenu": { 28 | "badges": [ 29 | "library" 30 | ], 31 | "links": { 32 | "modmenu.wiki": "https://github.com/booky10/StackDeobfuscator/wiki", 33 | "modmenu.discord": "https://discord.gg/XSB7jn9", 34 | "modmenu.github_releases": "https://github.com/booky10/StackDeobfuscator/releases", 35 | "modmenu.modrinth": "https://modrinth.com/mod/stackdeobf", 36 | "modmenu.curseforge": "https://www.curseforge.com/minecraft/mc-mods/stackdeobf" 37 | }, 38 | "parent": "stackdeobfuscator", 39 | "update_checker": false 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /fabric/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.loom) 3 | } 4 | 5 | dependencies { 6 | minecraft(libs.minecraft) 7 | mappings(loom.officialMojangMappings()) 8 | 9 | modImplementation(libs.fabric.loader) 10 | 11 | // include common project 12 | include(implementation(projects.stackDeobfuscatorCommon)!!) 13 | 14 | // "include" doesn't include jars transitively 15 | include(libs.fabric.mappingio) 16 | } 17 | 18 | tasks { 19 | processResources { 20 | inputs.property("version", project.version) 21 | filesMatching("fabric.mod.json") { 22 | expand("version" to project.version) 23 | } 24 | } 25 | 26 | sequenceOf(remapJar, remapSourcesJar).forEach { task -> 27 | task { 28 | archiveBaseName = jar.get().archiveBaseName 29 | // this somehow errors when trying to just reuse 30 | // the destination directory set in normal jar task 31 | destinationDirectory = rootProject.layout.buildDirectory.dir("libs") 32 | } 33 | } 34 | } 35 | 36 | loom { 37 | mixin { 38 | defaultRefmapName.set("${rootProject.name.lowercase()}-refmap.json") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/StackDeobfMod.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf; 2 | // Created by booky10 in StackDeobfuscator (17:38 18.12.22) 3 | 4 | import dev.booky.stackdeobf.config.StackDeobfConfig; 5 | import dev.booky.stackdeobf.mappings.CachedMappings; 6 | import dev.booky.stackdeobf.util.RemappingRewritePolicy; 7 | import dev.booky.stackdeobf.util.VersionData; 8 | import net.fabricmc.loader.api.FabricLoader; 9 | import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.core.Logger; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.nio.file.Path; 15 | import java.util.concurrent.CompletableFuture; 16 | 17 | public class StackDeobfMod implements PreLaunchEntrypoint { 18 | 19 | private static final org.apache.logging.log4j.Logger LOGGER = LogManager.getLogger("StackDeobfuscator"); 20 | 21 | private static final String CONFIG_FILE_NAME = "stackdeobf.json"; 22 | private static final VersionData VERSION_DATA = VersionData.fromClasspath(); 23 | 24 | private static CachedMappings mappings; 25 | 26 | public static Throwable remap(Throwable throwable) { 27 | if (mappings != null) { 28 | return mappings.remapThrowable(throwable); 29 | } 30 | return throwable; 31 | } 32 | 33 | public static void remap(StackTraceElement[] elements) { 34 | if (mappings != null) { 35 | mappings.remapStackTrace(elements); 36 | } 37 | } 38 | 39 | public static @Nullable CachedMappings getMappings() { 40 | return mappings; 41 | } 42 | 43 | public static VersionData getVersionData() { 44 | return VERSION_DATA; 45 | } 46 | 47 | @Override 48 | public void onPreLaunch() { 49 | StackDeobfConfig config = this.loadConfig(); 50 | 51 | Path gameDir = FabricLoader.getInstance().getGameDir(); 52 | Path cacheDir = gameDir.resolve("stackdeobf_mappings"); 53 | 54 | // don't need to print errors, already done after loading 55 | CompletableFuture loadFuture = CachedMappings.create(cacheDir, config.getMappingProvider()) 56 | .thenAccept(mappings -> { 57 | StackDeobfMod.mappings = mappings; 58 | 59 | if (config.hasLogInjectEnabled()) { 60 | LOGGER.info("Injecting into root logger..."); 61 | 62 | RemappingRewritePolicy policy = new RemappingRewritePolicy( 63 | mappings, config.shouldRewriteEveryLogMessage()); 64 | policy.inject((Logger) LogManager.getRootLogger()); 65 | } 66 | }); 67 | 68 | if (config.isSyncLoading()) { 69 | LOGGER.warn("Waiting for mapping loading to finish, this may take some time"); 70 | LOGGER.warn("To disable synchronized mapping loading, disable 'synchronized-loading' in " + CONFIG_FILE_NAME); 71 | loadFuture.join(); 72 | } 73 | } 74 | 75 | private StackDeobfConfig loadConfig() { 76 | try { 77 | Path configPath = FabricLoader.getInstance().getConfigDir().resolve(CONFIG_FILE_NAME); 78 | return StackDeobfConfig.load(VERSION_DATA, configPath); 79 | } catch (Throwable throwable) { 80 | throw new RuntimeException("Exception occurred while loading stack deobfuscator configuration", throwable); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/config/MappingProviderSerializer.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.config; 2 | // Created by booky10 in StackDeobfuscator (17:15 23.03.23) 3 | 4 | import com.google.gson.JsonDeserializationContext; 5 | import com.google.gson.JsonDeserializer; 6 | import com.google.gson.JsonElement; 7 | import com.google.gson.JsonObject; 8 | import com.google.gson.JsonParseException; 9 | import com.google.gson.JsonPrimitive; 10 | import com.google.gson.JsonSerializationContext; 11 | import com.google.gson.JsonSerializer; 12 | import dev.booky.stackdeobf.mappings.providers.AbstractMappingProvider; 13 | import dev.booky.stackdeobf.mappings.providers.CustomMappingProvider; 14 | import dev.booky.stackdeobf.mappings.providers.MojangMappingProvider; 15 | import dev.booky.stackdeobf.mappings.providers.QuiltMappingProvider; 16 | import dev.booky.stackdeobf.mappings.providers.YarnMappingProvider; 17 | import dev.booky.stackdeobf.util.VersionData; 18 | import net.fabricmc.api.EnvType; 19 | import net.fabricmc.loader.api.FabricLoader; 20 | import net.fabricmc.mappingio.format.MappingFormat; 21 | 22 | import java.lang.reflect.Type; 23 | import java.nio.file.Path; 24 | import java.util.Locale; 25 | 26 | public final class MappingProviderSerializer implements JsonSerializer, JsonDeserializer { 27 | 28 | private final VersionData versionData; 29 | 30 | public MappingProviderSerializer(VersionData versionData) { 31 | this.versionData = versionData; 32 | } 33 | 34 | @Override 35 | public AbstractMappingProvider deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx) throws JsonParseException { 36 | if (json.isJsonObject()) { 37 | JsonObject obj = json.getAsJsonObject(); 38 | Path path = Path.of(obj.get("path").getAsString()); 39 | MappingFormat format = ctx.deserialize(obj.get("mapping-format"), MappingFormat.class); 40 | return new CustomMappingProvider(this.versionData, path, format); 41 | } 42 | 43 | EnvType env = FabricLoader.getInstance().getEnvironmentType(); 44 | String envName = env.name().toLowerCase(Locale.ROOT); 45 | 46 | String id = json.getAsString().trim().toLowerCase(Locale.ROOT); 47 | return switch (id) { 48 | case "mojang" -> new MojangMappingProvider(this.versionData, envName); 49 | case "yarn" -> new YarnMappingProvider(this.versionData); 50 | case "quilt" -> new QuiltMappingProvider(this.versionData); 51 | default -> throw new JsonParseException("Invalid mappings id: " + id); 52 | }; 53 | } 54 | 55 | @Override 56 | public JsonElement serialize(AbstractMappingProvider src, Type typeOfSrc, JsonSerializationContext context) { 57 | if (src instanceof MojangMappingProvider) { 58 | return new JsonPrimitive("mojang"); 59 | } 60 | if (src instanceof YarnMappingProvider) { 61 | return new JsonPrimitive("yarn"); 62 | } 63 | if (src instanceof QuiltMappingProvider) { 64 | return new JsonPrimitive("quilt"); 65 | } 66 | if (src instanceof CustomMappingProvider custom) { 67 | JsonObject obj = new JsonObject(); 68 | obj.addProperty("path", custom.getPath().toString()); 69 | obj.addProperty("mapping-format", custom.getFormat().name()); 70 | return obj; 71 | } 72 | throw new UnsupportedOperationException("Unsupported mapping provider: " + src.getName()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/config/StackDeobfConfig.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.config; 2 | // Created by booky10 in StackDeobfuscator (17:12 23.03.23) 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.annotations.SerializedName; 7 | import dev.booky.stackdeobf.mappings.providers.AbstractMappingProvider; 8 | import dev.booky.stackdeobf.mappings.providers.YarnMappingProvider; 9 | import dev.booky.stackdeobf.util.VersionData; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.BufferedWriter; 13 | import java.io.IOException; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | 17 | public final class StackDeobfConfig { 18 | 19 | private static final int CURRENT_VERSION = 3; 20 | 21 | @SerializedName("config-version-dont-touch-this") 22 | private int version; 23 | 24 | @SerializedName("inject-logger") 25 | private boolean logInject = true; 26 | 27 | @SerializedName("rewrite-every-log-message") 28 | private boolean rewriteEveryLogMessage = false; 29 | 30 | @SerializedName("mapping-type") 31 | private AbstractMappingProvider mappingProvider; 32 | 33 | @SerializedName("synchronized-loading") 34 | private boolean syncLoading = false; 35 | 36 | private StackDeobfConfig() { 37 | } 38 | 39 | public static StackDeobfConfig load(VersionData versionData, Path configPath) throws IOException { 40 | Gson gson = new GsonBuilder() 41 | .disableHtmlEscaping().setPrettyPrinting() 42 | .registerTypeHierarchyAdapter(AbstractMappingProvider.class, new MappingProviderSerializer(versionData)) 43 | .create(); 44 | 45 | StackDeobfConfig config; 46 | if (Files.exists(configPath)) { 47 | try (BufferedReader reader = Files.newBufferedReader(configPath)) { 48 | config = gson.fromJson(reader, StackDeobfConfig.class); 49 | } 50 | 51 | if (config.version == CURRENT_VERSION) { 52 | // don't need to save the config again, it is 53 | // already up-to-date 54 | return config; 55 | } 56 | 57 | // migrate the config format to the newer version 58 | 59 | if (config.version < 3) { 60 | config.syncLoading = false; 61 | } 62 | if (config.version < 2) { 63 | config.rewriteEveryLogMessage = false; 64 | } 65 | } else { 66 | // create default config 67 | config = new StackDeobfConfig(); 68 | config.mappingProvider = new YarnMappingProvider(versionData); 69 | } 70 | 71 | // either the config didn't exist before, or the config version was too old 72 | config.version = CURRENT_VERSION; 73 | try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { 74 | gson.toJson(config, writer); 75 | } 76 | return config; 77 | } 78 | 79 | public boolean hasLogInjectEnabled() { 80 | return this.logInject; 81 | } 82 | 83 | public boolean shouldRewriteEveryLogMessage() { 84 | return this.rewriteEveryLogMessage; 85 | } 86 | 87 | public AbstractMappingProvider getMappingProvider() { 88 | return this.mappingProvider; 89 | } 90 | 91 | public boolean isSyncLoading() { 92 | return this.syncLoading; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/mixin/CrashReportCategoryMixin.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mixin; 2 | // Created by booky10 in StackDeobfuscator (18:46 20.03.23) 3 | 4 | import dev.booky.stackdeobf.StackDeobfMod; 5 | import net.minecraft.CrashReportCategory; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 11 | 12 | @Mixin(CrashReportCategory.class) 13 | public class CrashReportCategoryMixin { 14 | 15 | @Shadow 16 | private StackTraceElement[] stackTrace; 17 | 18 | @Inject( 19 | method = "fillInStackTrace", 20 | at = @At( 21 | value = "INVOKE", 22 | target = "Ljava/lang/System;arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V", 23 | shift = At.Shift.AFTER 24 | ) 25 | ) 26 | public void postStackTraceFill(int i, CallbackInfoReturnable cir) { 27 | StackDeobfMod.remap(this.stackTrace); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/mixin/CrashReportMixin.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mixin; 2 | // Created by booky10 in StackDeobfuscator (18:35 20.03.23) 3 | 4 | import dev.booky.stackdeobf.StackDeobfMod; 5 | import dev.booky.stackdeobf.mappings.RemappedThrowable; 6 | import net.minecraft.CrashReport; 7 | import org.spongepowered.asm.mixin.Final; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Mutable; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 15 | 16 | @Mixin(CrashReport.class) 17 | public class CrashReportMixin { 18 | 19 | @Mutable 20 | @Shadow 21 | @Final 22 | private Throwable exception; 23 | 24 | @Inject( 25 | method = "", 26 | at = @At("TAIL") 27 | ) 28 | public void postInit(String title, Throwable throwable, CallbackInfo ci) { 29 | this.exception = StackDeobfMod.remap(throwable); 30 | } 31 | 32 | @Inject( 33 | method = "getException", 34 | at = @At("HEAD"), 35 | cancellable = true 36 | ) 37 | public void preExceptionGet(CallbackInfoReturnable cir) { 38 | // redirect calls to getException to the original, unmapped Throwable 39 | // 40 | // this method is called in the ReportedException, which 41 | // caused the "RemappedThrowable" name to show up in the logger 42 | 43 | if (this.exception instanceof RemappedThrowable remapped) { 44 | cir.setReturnValue(remapped.getOriginal()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/mixin/StackDeobfMixinPlugin.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mixin; 2 | // Created by booky10 in StackDeobfuscator (21:17 29.03.23) 3 | 4 | import dev.booky.stackdeobf.StackDeobfMod; 5 | import org.objectweb.asm.tree.ClassNode; 6 | import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; 7 | import org.spongepowered.asm.mixin.extensibility.IMixinInfo; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import static dev.booky.stackdeobf.util.VersionConstants.V1_18_PRE7; 13 | 14 | public class StackDeobfMixinPlugin implements IMixinConfigPlugin { 15 | 16 | @Override 17 | public void onLoad(String mixinPackage) { 18 | } 19 | 20 | @Override 21 | public String getRefMapperConfig() { 22 | return null; 23 | } 24 | 25 | @Override 26 | public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { 27 | if (mixinClassName.endsWith("ThreadingDetectorMixin")) { 28 | return StackDeobfMod.getVersionData().getWorldVersion() >= V1_18_PRE7; 29 | } 30 | return true; 31 | } 32 | 33 | @Override 34 | public void acceptTargets(Set myTargets, Set otherTargets) { 35 | } 36 | 37 | @Override 38 | public List getMixins() { 39 | return null; 40 | } 41 | 42 | @Override 43 | public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 44 | } 45 | 46 | @Override 47 | public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fabric/src/main/java/dev/booky/stackdeobf/mixin/ThreadingDetectorMixin.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.mixin; 2 | // Created by booky10 in StackDeobfuscator (18:50 20.03.23) 3 | 4 | import dev.booky.stackdeobf.StackDeobfMod; 5 | import net.minecraft.util.ThreadingDetector; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Redirect; 9 | 10 | @Mixin(ThreadingDetector.class) 11 | public class ThreadingDetectorMixin { 12 | 13 | @Redirect( 14 | method = "stackTrace", 15 | at = @At( 16 | value = "INVOKE", 17 | target = "Ljava/lang/Thread;getStackTrace()[Ljava/lang/StackTraceElement;" 18 | ) 19 | ) 20 | private static StackTraceElement[] redirStackTrace(Thread thread) { 21 | StackTraceElement[] stackTrace = thread.getStackTrace(); 22 | StackDeobfMod.remap(stackTrace); 23 | return stackTrace; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /fabric/src/main/resources/assets/stackdeobf/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/fabric/src/main/resources/assets/stackdeobf/icon.png -------------------------------------------------------------------------------- /fabric/src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "stackdeobfuscator", 4 | "version": "${version}", 5 | 6 | "name": "StackDeobfuscator", 7 | "description": "Deobfuscates stacktraces to yarn/quilt/mojang mappings", 8 | 9 | "authors": [ 10 | "booky10" 11 | ], 12 | 13 | "contact": { 14 | "issues": "https://github.com/booky10/StackDeobfuscator/issues", 15 | "sources": "https://github.com/booky10/StackDeobfuscator" 16 | }, 17 | 18 | "icon": "assets/stackdeobf/icon.png", 19 | "license": "LGPL-3.0-only", 20 | "environment": "*", 21 | 22 | "entrypoints": { 23 | "preLaunch": [ 24 | "dev.booky.stackdeobf.StackDeobfMod" 25 | ] 26 | }, 27 | 28 | "mixins": [ 29 | "stackdeobf.mixins.json" 30 | ], 31 | 32 | "depends": { 33 | "fabricloader": ">=0.14.11", 34 | "minecraft": ">=1.14-alpha.18.49.a", 35 | "java": ">=17" 36 | }, 37 | 38 | "custom": { 39 | "modmenu": { 40 | "links": { 41 | "modmenu.wiki": "https://github.com/booky10/StackDeobfuscator/wiki", 42 | "modmenu.discord": "https://discord.gg/XSB7jn9", 43 | "modmenu.github_releases": "https://github.com/booky10/StackDeobfuscator/releases", 44 | "modmenu.modrinth": "https://modrinth.com/mod/stackdeobf", 45 | "modmenu.curseforge": "https://www.curseforge.com/minecraft/mc-mods/stackdeobf" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fabric/src/main/resources/stackdeobf.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "dev.booky.stackdeobf.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "plugin": "dev.booky.stackdeobf.mixin.StackDeobfMixinPlugin", 7 | "mixins": [ 8 | "CrashReportCategoryMixin", 9 | "CrashReportMixin", 10 | "ThreadingDetectorMixin" 11 | ], 12 | "injectors": { 13 | "defaultRequire": 1, 14 | "maxShiftBy": 5 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs = -Xmx1G 2 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | loom = "1.8-SNAPSHOT" 3 | shadow = "8.3.3" 4 | 5 | fabric-loader = "0.16.7" 6 | fabric-mappingio = "0.6.1" 7 | minecraft = "1.20.1" 8 | 9 | # built in minecraft, required for compiling 10 | builtin-log4j = "2.19.0" 11 | builtin-commons-io = "2.11.0" 12 | builtin-commons-lang3 = "3.12.0" 13 | builtin-google-gson = "2.10" 14 | builtin-fastutil = "8.5.9" 15 | builtin-jetbrains-annotations = "24.0.1" 16 | 17 | # web project 18 | javalin = "6.3.0" 19 | caffeine = "3.1.8" 20 | 21 | [plugins] 22 | loom = { id = "fabric-loom", version.ref = "loom" } 23 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 24 | 25 | [libraries] 26 | fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } 27 | fabric-mappingio = { module = "net.fabricmc:mapping-io", version.ref = "fabric-mappingio" } 28 | minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } 29 | 30 | # built in minecraft, required for compiling 31 | builtin-log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "builtin-log4j" } 32 | builtin-log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "builtin-log4j" } 33 | builtin-log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "builtin-log4j" } 34 | builtin-commons-io = { module = "commons-io:commons-io", version.ref = "builtin-commons-io" } 35 | builtin-commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "builtin-commons-lang3" } 36 | builtin-google-gson = { module = "com.google.code.gson:gson", version.ref = "builtin-google-gson" } 37 | builtin-fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "builtin-fastutil" } 38 | builtin-jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "builtin-jetbrains-annotations" } 39 | 40 | # web 41 | javalin = { module = "io.javalin:javalin", version.ref = "javalin" } 42 | caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } 43 | log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "builtin-log4j" } 44 | 45 | [bundles] 46 | # built in minecraft, required for compiling 47 | builtin = ["builtin-log4j-api", "builtin-log4j-core", 48 | "builtin-commons-io", "builtin-commons-lang3", 49 | "builtin-google-gson", "builtin-fastutil", 50 | "builtin-jetbrains-annotations"] 51 | 52 | # web 53 | log4j = ["builtin-log4j-api", "builtin-log4j-core", "builtin-log4j-slf4j2-impl", "log4j-iostreams"] 54 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | rootProject.name = "StackDeobfuscator" 4 | 5 | pluginManagement { 6 | repositories { 7 | maven("https://maven.fabricmc.net/") 8 | gradlePluginPortal() 9 | } 10 | } 11 | 12 | plugins { 13 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 14 | } 15 | 16 | setupSubProject("common") 17 | setupSubProject("fabric") 18 | setupSubProject("web") 19 | 20 | fun setupSubProject(name: String) { 21 | val projectName = "${rootProject.name}-${name[0].titlecase() + name.substring(1)}" 22 | include(projectName) 23 | project(":$projectName").projectDir = file(name) 24 | } 25 | -------------------------------------------------------------------------------- /web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer 2 | 3 | plugins { 4 | id("application") 5 | alias(libs.plugins.shadow) 6 | } 7 | 8 | val bootClass = "$group.stackdeobf.web.StackDeobfMain" 9 | 10 | dependencies { 11 | implementation(projects.stackDeobfuscatorCommon) 12 | 13 | // required mc libraries 14 | implementation(libs.bundles.builtin) 15 | 16 | implementation(libs.javalin) 17 | implementation(libs.caffeine) 18 | implementation(libs.bundles.log4j) 19 | } 20 | 21 | application { 22 | mainClass.set(bootClass) 23 | } 24 | 25 | tasks { 26 | withType { 27 | workingDir = projectDir.resolve("run") 28 | workingDir.mkdirs() 29 | } 30 | 31 | jar { 32 | manifest.attributes( 33 | "Implementation-Title" to rootProject.name, 34 | "Implementation-Version" to project.version, 35 | "Implementation-Vendor" to "booky10", 36 | 37 | "Main-Class" to bootClass, 38 | "Multi-Release" to "true", // prevents some log4j warnings 39 | ) 40 | } 41 | 42 | shadowJar { 43 | // this may seem like some unnecessary exclusions, but this saves about 24MiB of disk space... 44 | // fastutil is very big, and this project only requires the Int2Object maps 45 | val fastutilPkg = "it/unimi/dsi/fastutil" 46 | sequenceOf("booleans", "bytes", "chars", "doubles", "floats", "io", "longs", "shorts") 47 | .forEach { exclude("$fastutilPkg/$it/*") } 48 | sequenceOf("A", "B", "I", "P", "S") 49 | .forEach { exclude("$fastutilPkg/$it*") } 50 | sequenceOf( 51 | "Abstract", "ObjectA", "ObjectB", "ObjectCh", "ObjectCom", "Object2", 52 | "ObjectD", "ObjectF", "ObjectH", "ObjectIm", "ObjectIn", "ObjectIterat", "ObjectL", 53 | "ObjectO", "ObjectR", "ObjectR", "ObjectSem", "ObjectSh", "ObjectSo", "ObjectSp", "Ref" 54 | ).forEach { exclude("$fastutilPkg/objects/$it*") } 55 | sequenceOf( 56 | "AbstractInt2B", "AbstractInt2C", "AbstractInt2D", "AbstractInt2F", "AbstractInt2I", 57 | "AbstractInt2L", "AbstractInt2ObjectS", "AbstractInt2R", "AbstractInt2S", "AbstractIntB", 58 | "AbstractIntC", "AbstractIntI", "AbstractIntL", "AbstractIntP", "AbstractIntS", "Int2B", 59 | "Int2C", "Int2D", "Int2F", "Int2I", "Int2L", "Int2ObjectA", "Int2ObjectL", "Int2ObjectOpenC", 60 | "Int2ObjectR", "Int2ObjectS", "Int2R", "Int2S", "IntA", "IntB", "IntCh", "IntCom", "IntCon", 61 | "IntD", "IntF", "IntH", "IntIm", "IntIn", "IntIterator", "IntL", "IntM", "IntO", "IntP", 62 | "IntR", "IntSem", "IntSh", "IntSo", "IntSp", "IntSt", "IntU" 63 | ).forEach { exclude("$fastutilPkg/ints/$it*") } 64 | 65 | // rebuild shadowjar when exclusions change 66 | inputs.properties("excludes" to excludes) 67 | 68 | // merges log4j plugin files 69 | transform(Log4j2PluginsCacheFileTransformer::class.java) 70 | } 71 | 72 | assemble { 73 | dependsOn(shadowJar) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /web/src/main/java/dev/booky/stackdeobf/web/ApiRoutes.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.web; 2 | // Created by booky10 in StackDeobfuscator (17:06 06.07.23) 3 | 4 | import com.github.benmanes.caffeine.cache.AsyncLoadingCache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import dev.booky.stackdeobf.http.HttpResponseContainer; 7 | import dev.booky.stackdeobf.http.HttpUtil; 8 | import dev.booky.stackdeobf.mappings.CachedMappings; 9 | import dev.booky.stackdeobf.mappings.providers.AbstractMappingProvider; 10 | import dev.booky.stackdeobf.mappings.providers.MojangMappingProvider; 11 | import dev.booky.stackdeobf.mappings.providers.QuiltMappingProvider; 12 | import dev.booky.stackdeobf.mappings.providers.YarnMappingProvider; 13 | import dev.booky.stackdeobf.util.VersionData; 14 | import io.javalin.Javalin; 15 | import io.javalin.http.BadRequestResponse; 16 | import io.javalin.http.Context; 17 | import io.javalin.http.util.NaiveRateLimit; 18 | 19 | import java.net.URI; 20 | import java.net.http.HttpRequest; 21 | import java.nio.file.Path; 22 | import java.util.Locale; 23 | import java.util.Map; 24 | import java.util.Objects; 25 | import java.util.concurrent.CompletableFuture; 26 | import java.util.concurrent.Executor; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import static dev.booky.stackdeobf.util.VersionConstants.V18W49A; 30 | import static dev.booky.stackdeobf.util.VersionConstants.V19W36A; 31 | import static dev.booky.stackdeobf.util.VersionConstants.V1_14_4; 32 | import static dev.booky.stackdeobf.util.VersionConstants.V1_18_2; 33 | import static dev.booky.stackdeobf.util.VersionConstants.V1_19_DEEP_DARK_EXPERIMENTAL_SNAPSHOT_1; 34 | import static dev.booky.stackdeobf.util.VersionConstants.V22W13A; 35 | import static dev.booky.stackdeobf.util.VersionConstants.V23W13A_OR_B; 36 | 37 | public final class ApiRoutes { 38 | 39 | private static final String HASTEBIN_API_TOKEN = System.getProperty("web.hastebin-api-token"); 40 | 41 | private static final Path CACHE_DIR = Path.of(System.getProperty("mappings.cachedir", "mappings")); 42 | private static final String DEFAULT_MAPPINGS_PROVIDER = "yarn"; 43 | private static final String DEFAULT_MAPPINGS_VERSION = "3465"; 44 | private static final String DEFAULT_ENVIRONMENT = "client"; 45 | 46 | private static final String PREFIX = "/api/v1"; 47 | 48 | private final AsyncLoadingCache mappingsCache; 49 | private final AsyncLoadingCache urlCache; 50 | 51 | private final Javalin javalin; 52 | private final Map versionData; 53 | 54 | private ApiRoutes(Javalin javalin, Map versionData) { 55 | this.javalin = javalin; 56 | this.versionData = versionData; 57 | 58 | this.mappingsCache = Caffeine.newBuilder() 59 | .expireAfterAccess(1, TimeUnit.HOURS) 60 | .buildAsync(this::loadMapping); 61 | this.urlCache = Caffeine.newBuilder() 62 | .expireAfterAccess(1, TimeUnit.HOURS) 63 | .buildAsync(this::loadUrl); 64 | } 65 | 66 | public static void register(Javalin javalin, Map versionData) { 67 | ApiRoutes routes = new ApiRoutes(javalin, versionData); 68 | routes.register(); 69 | } 70 | 71 | private CompletableFuture loadMapping(CacheKey key, Executor executor) { 72 | String mappings = key.mappings(); 73 | int version = key.version(); 74 | String environment = key.environment(); 75 | 76 | VersionData versionData = this.versionData.get(version); 77 | if (versionData == null) { 78 | throw new BadRequestResponse("Unsupported version specified: " + version); 79 | } 80 | 81 | AbstractMappingProvider provider = switch (mappings) { 82 | case "yarn" -> { 83 | if (versionData.getWorldVersion() < V18W49A) { 84 | throw new BadRequestResponse("Unsupported version for yarn mappings specified: " + version); 85 | } 86 | yield new YarnMappingProvider(versionData); 87 | } 88 | case "quilt" -> { 89 | if (versionData.getWorldVersion() < V1_18_2 || versionData.getWorldVersion() == V23W13A_OR_B 90 | || (versionData.getWorldVersion() >= V1_19_DEEP_DARK_EXPERIMENTAL_SNAPSHOT_1 && versionData.getWorldVersion() <= V22W13A)) { 91 | throw new BadRequestResponse("Unsupported version for quilt mappings specified: " + version); 92 | } 93 | yield new QuiltMappingProvider(versionData); 94 | } 95 | case "mojang" -> { 96 | if (versionData.getWorldVersion() < V19W36A && versionData.getWorldVersion() != V1_14_4) { 97 | throw new BadRequestResponse("Unsupported version for mojang mappings specified: " + version); 98 | } 99 | yield new MojangMappingProvider(versionData, environment); 100 | } 101 | default -> throw new BadRequestResponse("Unsupported mappings specified: " + mappings); 102 | }; 103 | 104 | return CachedMappings.create(CACHE_DIR, provider, executor); 105 | } 106 | 107 | private CompletableFuture loadUrl(URI uri, Executor executor) { 108 | HttpRequest.Builder request = HttpRequest.newBuilder(uri); 109 | if ("hastebin.com".equals(uri.getHost()) && uri.getPath().startsWith("/raw")) { 110 | if (HASTEBIN_API_TOKEN != null) { 111 | request.header("Authorization", "Bearer " + HASTEBIN_API_TOKEN); 112 | } 113 | } 114 | return HttpUtil.getAsync(request.build(), executor) 115 | .thenApply(HttpResponseContainer::getBody); 116 | } 117 | 118 | private void register() { 119 | this.javalin.get(PREFIX + "/deobf/url", ctx -> ctx.future(() -> this.handleDeobfUrlReq(ctx))); 120 | this.javalin.post(PREFIX + "/deobf/body", ctx -> ctx.future(() -> this.handleDeobfBodyReq(ctx))); 121 | } 122 | 123 | private CompletableFuture getMappings(Context ctx) { 124 | String mappings = Objects.requireNonNullElse(ctx.queryParam("mappings"), DEFAULT_MAPPINGS_PROVIDER); 125 | String version = Objects.requireNonNullElse(ctx.queryParam("version"), DEFAULT_MAPPINGS_VERSION); 126 | String environment = Objects.requireNonNullElse(ctx.queryParam("environment"), DEFAULT_ENVIRONMENT); 127 | 128 | CacheKey cacheKey; 129 | try { 130 | cacheKey = new CacheKey(mappings, version, environment); 131 | } catch (NumberFormatException exception) { 132 | throw new BadRequestResponse("Illegal world version specified: " + version); 133 | } 134 | 135 | return this.mappingsCache.get(cacheKey); 136 | } 137 | 138 | private CompletableFuture> handleDeobfUrlReq(Context ctx) { 139 | // one request per 10 seconds, should be enough 140 | NaiveRateLimit.requestPerTimeUnit(ctx, 6, TimeUnit.MINUTES); 141 | 142 | URI uri; 143 | try { 144 | uri = URI.create(Objects.requireNonNull(ctx.queryParam("url"))); 145 | } catch (IllegalArgumentException exception) { 146 | throw new BadRequestResponse("Illegal url specified: " + exception); 147 | } 148 | 149 | long startStep = System.nanoTime(); 150 | return this.getMappings(ctx).thenCompose(mappings -> { 151 | long urlStep = System.nanoTime(); 152 | ctx.header("Mappings-Time", Long.toString(urlStep - startStep)); 153 | 154 | return this.urlCache.get(uri).thenAccept(bytes -> { 155 | long remapStep = System.nanoTime(); 156 | ctx.header("Url-Time", Long.toString(remapStep - urlStep)); 157 | 158 | String str = new String(bytes); 159 | String remappedStr = mappings.remapString(str); 160 | ctx.result(remappedStr); 161 | 162 | long resultStep = System.nanoTime(); 163 | ctx.header("Remap-Time", Long.toString(resultStep - remapStep)); 164 | ctx.header("Total-Time", Long.toString(resultStep - startStep)); 165 | }); 166 | }); 167 | } 168 | 169 | private CompletableFuture> handleDeobfBodyReq(Context ctx) { 170 | // one request per 6 seconds, should be enough 171 | NaiveRateLimit.requestPerTimeUnit(ctx, 10, TimeUnit.MINUTES); 172 | 173 | long startStep = System.nanoTime(); 174 | return this.getMappings(ctx).thenAccept(mappings -> { 175 | long remapStep = System.nanoTime(); 176 | ctx.header("Mappings-Time", Long.toString(remapStep - startStep)); 177 | 178 | String remappedStr = mappings.remapString(ctx.body()); 179 | ctx.result(remappedStr); 180 | 181 | long resultStep = System.nanoTime(); 182 | ctx.header("Remap-Time", Long.toString(resultStep - remapStep)); 183 | ctx.header("Total-Time", Long.toString(resultStep - startStep)); 184 | }); 185 | } 186 | 187 | private record CacheKey(String mappings, int version, String environment) { 188 | 189 | public CacheKey(String mappings, int version, String environment) { 190 | if (!"client".equalsIgnoreCase(environment) && !"server".equalsIgnoreCase(environment)) { 191 | throw new BadRequestResponse("Illegal environment specified: " + environment); 192 | } 193 | 194 | this.mappings = mappings.toLowerCase(Locale.ROOT); 195 | this.version = version; 196 | this.environment = environment.toLowerCase(Locale.ROOT); 197 | } 198 | 199 | public CacheKey(String mappings, String versionStr, String environment) throws NumberFormatException { 200 | this(mappings, Integer.parseInt(versionStr), environment); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /web/src/main/java/dev/booky/stackdeobf/web/StackDeobfMain.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.web; 2 | // Created by booky10 in StackDeobfuscator (16:34 06.07.23) 3 | 4 | import org.apache.logging.log4j.Level; 5 | import org.apache.logging.log4j.io.IoBuilder; 6 | 7 | public final class StackDeobfMain { 8 | 9 | private static final String HTTP_BIND = System.getProperty("web.bind", "localhost"); 10 | private static final int HTTP_PORT = Integer.getInteger("web.port", 8082); 11 | 12 | static { 13 | System.setProperty("java.awt.headless", "true"); 14 | System.setOut(IoBuilder.forLogger("STDOUT").setLevel(Level.INFO).buildPrintStream()); 15 | System.setErr(IoBuilder.forLogger("STDERR").setLevel(Level.ERROR).buildPrintStream()); 16 | } 17 | 18 | private StackDeobfMain() { 19 | } 20 | 21 | public static void main(String[] args) { 22 | long startTime = System.currentTimeMillis(); 23 | Thread.currentThread().setName("Startup Thread"); 24 | 25 | StackDeobfService service = new StackDeobfService(HTTP_BIND, HTTP_PORT); 26 | service.start(startTime); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/src/main/java/dev/booky/stackdeobf/web/StackDeobfService.java: -------------------------------------------------------------------------------- 1 | package dev.booky.stackdeobf.web; 2 | // Created by booky10 in StackDeobfuscator (17:02 06.07.23) 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.JsonArray; 6 | import com.google.gson.JsonElement; 7 | import dev.booky.stackdeobf.util.VersionData; 8 | import io.javalin.Javalin; 9 | import io.javalin.plugin.bundled.CorsPluginConfig.CorsRule; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.InputStreamReader; 16 | import java.text.DecimalFormat; 17 | import java.util.Collections; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.Objects; 21 | 22 | public final class StackDeobfService { 23 | 24 | private static final Logger LOGGER = LogManager.getLogger("StackDeobfuscator"); 25 | 26 | private final String bindAddress; 27 | private final int port; 28 | 29 | private Javalin javalin; 30 | private boolean running = true; 31 | 32 | public StackDeobfService(String bindAddress, int port) { 33 | this.bindAddress = bindAddress; 34 | this.port = port; 35 | } 36 | 37 | private static Map parseVersionData() throws IOException { 38 | JsonArray array; 39 | try (InputStream input = StackDeobfService.class.getResourceAsStream("/public/mc_versions.json")) { 40 | Objects.requireNonNull(input, "No minecraft version data file found in classpath"); 41 | try (InputStreamReader reader = new InputStreamReader(input)) { 42 | array = new Gson().fromJson(reader, JsonArray.class); 43 | } 44 | } 45 | 46 | Map versionDataMap = new HashMap<>(); 47 | for (JsonElement element : array) { 48 | VersionData versionData = VersionData.fromJson(element.getAsJsonObject()); 49 | int key = versionData.getWorldVersion(); 50 | versionDataMap.put(key, versionData); 51 | } 52 | return Collections.unmodifiableMap(versionDataMap); 53 | } 54 | 55 | public void start(long startTime) { 56 | Runtime.getRuntime().addShutdownHook(new Thread(() -> stop0(null), "Shutdown Handling Thread")); 57 | 58 | LOGGER.info("Parsing minecraft version data..."); 59 | Map versionData; 60 | try { 61 | versionData = parseVersionData(); 62 | } catch (IOException exception) { 63 | throw new RuntimeException("Error while trying to parse minecraft version data", exception); 64 | } 65 | 66 | LOGGER.info("Creating javalin service..."); 67 | this.javalin = Javalin.create(config -> { 68 | config.showJavalinBanner = false; 69 | config.bundledPlugins.enableCors(cors -> cors.addRule(CorsRule::anyHost)); 70 | config.bundledPlugins.enableRouteOverview("/api"); 71 | config.staticFiles.add(files -> { /**/ }); 72 | }); 73 | 74 | LOGGER.info("Configuring http routes..."); 75 | ApiRoutes.register(this.javalin, versionData); 76 | 77 | LOGGER.info("Launching javalin service on {}...", this.bindAddress); 78 | this.javalin.start(this.bindAddress, this.port); 79 | 80 | double bootTime = (System.currentTimeMillis() - startTime) / 1000d; 81 | String bootTimeStr = new DecimalFormat("#.##").format(bootTime); 82 | LOGGER.info("Done ({}s)! To shutdown press CTRL+C", bootTimeStr); 83 | } 84 | 85 | public void stop(boolean cleanExit) { 86 | this.stop0(cleanExit); 87 | } 88 | 89 | private void stop0(Boolean cleanExit) { 90 | if (!this.running) { 91 | return; 92 | } 93 | this.running = false; 94 | 95 | LOGGER.info("Closing javalin server..."); 96 | if (this.javalin != null) { 97 | this.javalin.stop(); 98 | } 99 | 100 | LOGGER.info("Shutting down... Goodbye (°_°)"); 101 | LogManager.shutdown(); 102 | 103 | if (cleanExit != null) { 104 | System.exit(cleanExit ? 0 : 1); 105 | } 106 | } 107 | 108 | public String getBindAddress() { 109 | return this.bindAddress; 110 | } 111 | 112 | public int getPort() { 113 | return this.port; 114 | } 115 | 116 | public boolean isRunning() { 117 | return this.running; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /web/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/src/main/resources/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/web/src/main/resources/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/src/main/resources/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/web/src/main/resources/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/src/main/resources/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/web/src/main/resources/public/apple-touch-icon.png -------------------------------------------------------------------------------- /web/src/main/resources/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/web/src/main/resources/public/favicon-16x16.png -------------------------------------------------------------------------------- /web/src/main/resources/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/web/src/main/resources/public/favicon-32x32.png -------------------------------------------------------------------------------- /web/src/main/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booky10/StackDeobfuscator/80dd38e91d1ecbbdf34a7dd8d93fbfd300454335/web/src/main/resources/public/favicon.ico -------------------------------------------------------------------------------- /web/src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | StackDeobfuscator Web 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | StackDeobfuscator 26 | 27 | Enter Stacktrace or other text containing 28 | fabric intermediary 29 | names, select the target version and click "Deobfuscate" 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | Mappings: 51 | 52 | Mojang 53 | Yarn 54 | Quilt 55 | 56 | 57 | 58 | 59 | Version: 60 | 61 | Loading... 62 | 63 | 64 | 65 | 66 | Only stable versions 67 | 68 | 69 | 70 | Environment: 71 | 72 | Client 73 | Server 74 | 75 | 76 | 77 | 78 | Deobfuscate 79 | Copy 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /web/src/main/resources/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | Disallow: /api 4 | -------------------------------------------------------------------------------- /web/src/main/resources/public/script.js: -------------------------------------------------------------------------------- 1 | const input = document.getElementById("input"); 2 | const urlInput = document.getElementById("urlinput"); 3 | 4 | const status = document.getElementById("status"); 5 | const message = document.getElementById("message"); 6 | 7 | const copyButton = document.getElementById("copy"); 8 | const deobfButton = document.getElementById("deobfuscate"); 9 | 10 | const versionSelect = document.getElementById("version"); 11 | const stableVersionToggle = document.getElementById("stableversions"); 12 | 13 | const mappingsSelect = document.getElementById("mappings"); 14 | const environmentSelect = document.getElementById("environment"); 15 | 16 | // fixes the input url by trying to get the raw content instead of the html webpage 17 | function tryFixInputUrl() { 18 | if (urlInput.value.length == 0) { 19 | return; 20 | } 21 | 22 | const url = new URL(urlInput.value); 23 | if (url.host.endsWith("mclo.gs") && url.host !== "api.mclo.gs") { 24 | url.host = "api.mclo.gs"; 25 | url.pathname = "/1/raw" + url.pathname; 26 | } else if (url.host.endsWith("pastes.dev") && url.host !== "api.pastes.dev") { 27 | // can't just remap to raw pathname, like all other paste sites 28 | // swapping hosts is just as easy though 29 | url.host = "api.pastes.dev"; 30 | } else if (url.host.endsWith("paste.ee")) { 31 | // getting the raw text from paste.ee is as simple as swapping "/p/" for "/d/" 32 | if (!url.pathname.startsWith("/d") && url.pathname.startsWith("/p")) { 33 | url.pathname = "/d" + url.pathname.substring("/p".length); 34 | } 35 | } else if (url.host.endsWith("hastebin.com")) { 36 | // after toptal bought hastebin.com they completely destroyed the ability to 37 | // change the pathname to get the raw file content without authentication... thanks? 38 | if (url.pathname.startsWith("/share")) { 39 | url.pathname = "/raw" + url.pathname.substring("/share".length); 40 | } 41 | } else if ( 42 | // all haste-servers (before being bought by toptal) and pastebin 43 | // support simply adding "/raw" at the front of the path 44 | (url.host.startsWith("haste") || url.host.startsWith("paste")) 45 | // paste.gg allows storing multiple files, so just ignore this 46 | && !url.host.endsWith("paste.gg")) { 47 | if (!url.pathname.startsWith("/raw")) { 48 | url.pathname = "/raw" + url.pathname; 49 | } 50 | } 51 | // don't want to add support for github gist, too complicated with multiple files 52 | 53 | urlInput.value = url.toString(); 54 | } 55 | 56 | let onlyStableVersions; 57 | 58 | function updateVersions() { 59 | onlyStableVersions = stableVersionToggle.checked; 60 | for (versionOpt of versionSelect.children) { 61 | if (versionOpt.value == "-1") { 62 | continue; 63 | } 64 | 65 | if (!versionOpt.stable) { 66 | versionOpt.style.display = onlyStableVersions ? "none" : null; 67 | } 68 | } 69 | } 70 | this.updateVersions(); 71 | 72 | function loadVersions() { 73 | const req = new XMLHttpRequest(); 74 | req.open("GET", `mc_versions.json?t=${Date.now()}`, true); 75 | req.onreadystatechange = () => { 76 | if (req.readyState != 4) { 77 | return; 78 | } 79 | 80 | const versions = JSON.parse(req.responseText); 81 | versions.forEach((version, index) => { 82 | const option = document.createElement("option"); 83 | option.value = version.world_version; 84 | option.innerText = version.name; 85 | 86 | option.stable = version.stable; 87 | if (onlyStableVersions && !version.stable) { 88 | option.style.display = "none"; 89 | } 90 | versionSelect.appendChild(option); 91 | 92 | if (versionSelect.value == -1 && version.stable) { 93 | versionSelect.value = option.value; 94 | } 95 | }); 96 | versionSelect.remove(0); 97 | console.log(`Added ${versions.length} possible minecraft versions`) 98 | }; 99 | 100 | req.setRequestHeader("Accept", "application/json"); 101 | req.send(); 102 | } 103 | 104 | function deobfuscate() { 105 | tryFixInputUrl(); 106 | 107 | const mappings = mappingsSelect.options[mappingsSelect.selectedIndex].value; 108 | const version = versionSelect.options[versionSelect.selectedIndex].value; 109 | const environment = environmentSelect.options[environmentSelect.selectedIndex].value; 110 | 111 | status.innerText = "Deobfuscating..."; 112 | message.innerText = ""; 113 | 114 | input.disabled = true; 115 | urlInput.disabled = true; 116 | copyButton.disabled = true; 117 | deobfButton.disabled = true; 118 | mappingsSelect.disabled = true; 119 | versionSelect.disabled = true; 120 | environmentSelect.disabled = true; 121 | 122 | const req = new XMLHttpRequest(); 123 | req.onreadystatechange = () => { 124 | if (req.readyState != 4) { 125 | return; 126 | } 127 | 128 | input.disabled = false; 129 | urlInput.disabled = false; 130 | copyButton.disabled = false; 131 | deobfButton.disabled = false; 132 | mappingsSelect.disabled = false; 133 | versionSelect.disabled = false; 134 | environmentSelect.disabled = false; 135 | 136 | const totalTime = (req.getResponseHeader("Total-Time") / 1000000).toFixed(2); 137 | 138 | if (req.status != 200) { 139 | status.innerText = `Error ${req.status} while deobfuscating${totalTime > 0 ? ` (took ${totalTime}ms)` : ""}:`; 140 | message.innerText = req.responseText; 141 | return; 142 | } 143 | 144 | let urlTime = req.getResponseHeader("Url-Time"); 145 | let urlTimeStr = ""; 146 | if (urlTime) { 147 | urlTime = (urlTime / 1000000).toFixed(2); 148 | urlTimeStr = `${urlTime}ms getting url, `; 149 | } 150 | 151 | const mappingsTime = (req.getResponseHeader("Mappings-Time") / 1000000).toFixed(2); 152 | const remapTime = (req.getResponseHeader("Remap-Time") / 1000000).toFixed(2); 153 | 154 | status.innerText = `Deobfuscating took ${totalTime}ms (${mappingsTime}ms creating mappings, ${urlTimeStr}${remapTime}ms remapping)`; 155 | input.value = req.responseText; 156 | }; 157 | 158 | const setHeaders = () => { 159 | // these require an open request, so just do this 160 | req.setRequestHeader("Content-Type", "text/plain"); 161 | req.setRequestHeader("Accept", "text/plain"); 162 | }; 163 | 164 | const reqParams = `mappings=${mappings}&version=${version}&environment=${environment}`; 165 | if (urlInput.value.length == 0) { 166 | req.open("POST", `api/v1/deobf/body?${reqParams}`, true); 167 | setHeaders(); 168 | req.send(input.value); 169 | } else { 170 | const url = encodeURIComponent(urlInput.value); 171 | req.open("GET", `api/v1/deobf/url?${reqParams}&url=${url}`, true); 172 | setHeaders(); 173 | req.send(); 174 | } 175 | } 176 | 177 | copyButton.onclick = () => { 178 | input.focus(); 179 | input.select(); 180 | 181 | if (navigator.clipboard) { 182 | navigator.clipboard.writeText(input.value); 183 | } else { 184 | // fallback 185 | document.execCommand("copy"); 186 | } 187 | }; 188 | 189 | loadVersions(); 190 | deobfButton.onclick = (event) => deobfuscate(); 191 | -------------------------------------------------------------------------------- /web/src/main/resources/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /web/src/main/resources/public/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #161819; 3 | color: white; 4 | margin: 1em; 5 | color-scheme: dark !important; 6 | font-family: 'Liberation Sans', 'Open Sans', 'Arial', 'Verdana', 'Helvetica', 'sans-serif'; 7 | } 8 | 9 | .flex { 10 | display: flex; 11 | gap: 0.42em; 12 | align-items: stretch; 13 | } 14 | 15 | .flex-vert { 16 | flex-direction: column; 17 | } 18 | 19 | .flex-root { 20 | min-height: 95vh; 21 | align-items: stretch; 22 | } 23 | 24 | .big-btn { 25 | padding: 0.75em; 26 | font-weight: bold; 27 | font-size: 1.125em; 28 | } 29 | 30 | .border-box { 31 | /* https://stackoverflow.com/a/39068185 */ 32 | -webkit-box-sizing: border-box; 33 | -moz-box-sizing: border-box; 34 | box-sizing: border-box; 35 | } 36 | 37 | #input { 38 | width: 100%; 39 | 40 | white-space: pre; 41 | overflow-wrap: normal; 42 | overflow-x: auto; 43 | resize: none; 44 | } 45 | 46 | header.flex { 47 | align-items: center; 48 | } 49 | 50 | footer { 51 | margin-top: 3em; 52 | } 53 | 54 | footer.flex { 55 | justify-content: center; 56 | } 57 | 58 | #urlinput { 59 | width: 30em; 60 | max-width: 35em; 61 | } 62 | 63 | @media screen and (max-width: 518px) { 64 | .flex-long { 65 | flex-direction: column; 66 | } 67 | 68 | #urlinput { 69 | width: 25em; 70 | } 71 | } 72 | --------------------------------------------------------------------------------