├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── node.js.yml │ └── publishnpm.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── SECURITY.md ├── media └── decompileIcon.png ├── package.json ├── patches ├── @babel+types+7.12.11.patch ├── @types+babel__traverse+7.11.0.patch ├── @types+command-line-args+5.0.0.patch └── @types+fs-extra+9.0.6.patch ├── src ├── cacheParse.ts ├── decompilers │ ├── babel │ │ ├── class │ │ │ └── babelClassEvaluator.ts │ │ └── cleaners │ │ │ └── toConsumableArrayCleaner.ts │ ├── cleaners │ │ ├── assignmentIfElseToTernary.ts │ │ ├── cleanReturns.ts │ │ ├── powCleaner.ts │ │ └── uselessCommaOperatorCleaner.ts │ ├── decompilerList.ts │ ├── es6 │ │ ├── exportsToEs6.ts │ │ └── importsToEs6.ts │ ├── evaluators │ │ ├── arrayDestructureEvaluator.ts │ │ ├── defaultInteropEvaluator.ts │ │ └── spreadifier.ts │ ├── longhanders │ │ ├── hangingIfElseWrapper.ts │ │ ├── longBooleans.ts │ │ └── voidZeroToUndefined.ts │ ├── mappers │ │ └── requireMapper.ts │ └── react │ │ ├── jsxConverter.ts │ │ └── setStateRenamer.ts ├── editors │ ├── cleaners │ │ └── esModuleCleaner.ts │ ├── converters │ │ └── babelInlineConverters.ts │ ├── editorList.ts │ ├── unwrappers │ │ └── commaOperatorUnwrapper.ts │ └── variables │ │ └── noUndefinedExport.ts ├── eslintConfig.ts ├── fileParsers │ ├── cacheParser.ts │ ├── fileParser.ts │ ├── fileParserRouter.ts │ ├── reactNativeFolderParser.ts │ ├── reactNativeSingleParser.ts │ ├── webpackFolderParser.ts │ ├── webpackParser.ts │ └── webpackSingleParser.ts ├── interfaces │ ├── cachedFile.ts │ ├── cmdArgs.ts │ └── paramMappings.ts ├── main.ts ├── module.ts ├── plugin.ts ├── router.ts ├── taggers │ ├── npmModuleFinders │ │ ├── babelModuleFinder.ts │ │ ├── moduleFinder.ts │ │ ├── polyfillModuleFinder.ts │ │ └── simpleModuleFinder.ts │ ├── remappers │ │ └── passthroughModuleRemapper.ts │ ├── static │ │ └── cssFinder.ts │ ├── taggerList.ts │ └── vanilla │ │ └── emptyIgnorer.ts └── util │ ├── arrayMap.ts │ ├── bindingArrayMap.ts │ ├── performanceTracker.ts │ └── progressBar.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint nyc coverage output 4 | coverage -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | module.exports = { 17 | root: true, 18 | parser: '@typescript-eslint/parser', 19 | parserOptions: { 20 | tsconfigRootDir: __dirname, 21 | project: ['./tsconfig.json'], 22 | }, 23 | plugins: [ 24 | '@typescript-eslint', 25 | ], 26 | extends: [ 27 | 'eslint:recommended', 28 | 'plugin:@typescript-eslint/recommended', 29 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 30 | 'airbnb-typescript/base', 31 | ], 32 | rules: { 33 | "max-len": ["warn", { code: 180, ignoreComments: true }], 34 | "no-empty": ["error", { "allowEmptyCatch": true }], 35 | "no-console": "off", 36 | "@typescript-eslint/lines-between-class-members": "off", 37 | "class-methods-use-this": "off", 38 | "prefer-destructuring": "off", 39 | "no-param-reassign": ["error", { props: false }], 40 | "@typescript-eslint/no-empty-function": ['error', { 41 | allow: [ 42 | 'arrowFunctions', 43 | 'functions', 44 | 'methods', 45 | 'private-constructors', 46 | 'protected-constructors', 47 | ] 48 | }], 49 | "@typescript-eslint/no-unused-vars": "off", 50 | }, 51 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 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 | **Actual behavior** 24 | A clear and concise description of what actually happened. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 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 context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 13 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | - run: npm ci 19 | - run: npm run build 20 | 21 | lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | - run: npm ci 27 | - run: npm run lint 28 | -------------------------------------------------------------------------------- /.github/workflows/publishnpm.yml: -------------------------------------------------------------------------------- 1 | name: publish package to npm 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | Environment: 7 | description: 'Enter Anything' 8 | required: false 9 | default: '' 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 12 19 | - run: yarn install 20 | - name: "Automated Version Bump" 21 | uses: "phips28/gh-action-bump-version@master" 22 | with: 23 | tag-prefix: '' 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | - uses: JS-DevTools/npm-publish@v1 27 | with: 28 | token: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output* 2 | .vscode 3 | node_modules 4 | *.bundle* 5 | run*.* 6 | out 7 | js-modules 8 | nodemon.json 9 | yarn.lock 10 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .github -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Would you like to support me? 2 | 3 |
4 | 5 | 6 | 7 | 8 |
9 |
Buy Me A Coffee 10 |
11 | 12 | 13 | # React Native Decompiler [ALPHA] 14 | A CLI for React Native that allows you to decompile JS code of Android and IOS. 15 | 16 | 17 |

18 | icon 19 |

20 | 21 | **DOES NOT SUPPORT ENCRYPTED/BINARY (FACEBOOK, INSTAGRAM) BUNDLES** 22 | 23 | Decompiles React Native `index.android.bundle` JS files. Webpack files too! 24 | 25 | Also tries to remove some compilation artifacts (via internal plugins, ESLint, and Prettier) to make it easier to read. 26 | 27 | 28 | 29 | ### using npx 30 | ```sh 31 | npx react-native-decompiler 32 | ``` 33 | 34 | 35 | ### using npm 36 | ```sh 37 | npm i -g react-native-decompiler 38 | ``` 39 | 40 | ```sh 41 | react-native-decompiler 42 | 43 | # or you can write blow command 44 | 45 | rnd 46 | ``` 47 | 48 | # Usage 49 | Example 1: `npx react-native-decompiler -i ./index.android.bundle -o ./output` 50 | 51 | Example 2: `npx react-native-decompiler -i ./main.jsbundle -o ./output` 52 | 53 | Example 3: `react-native-decompiler -i ./index.android.bundle -o ./output` 54 | 55 | Example 4: `rnd -i ./index.android.bundle -o ./output` 56 | 57 | Command params: 58 | - `-i` (required) - input file/folder 59 | - `-o` (required) - the path to the output folder 60 | - `-e` - a module ID, if specified will only decompile that module & it's dependencies. 61 | - `-p` - performance monitoring flag, will print out runtime for each decompiler plugin 62 | - `-v` - verbose flag, does not include debug logging (use `DEBUG=react-native-decompiler:*` env flag for that) 63 | - `--es6` - attempts to decompile to ES6 module syntax. 64 | - `--noEslint` - does not run ESLint after doing decompilation 65 | - `--prettier` - does not run Prettier after doing decompilation 66 | - `--unpackOnly` - only unpacks the app with no other adjustments 67 | - `--decompileIgnored` - decompile ignored modules (modules are generally ignored if they are flagged as an NPM module) 68 | - `--agressiveCache` - skips some cache checks at the expense of possible cache desync 69 | - `--noProgress` - don't show progress bar 70 | - `--debug` - when also given a module ID, will print out that modules code after any plugin handles the app. 71 | 72 | # Android 73 | ## Extract index.android.bundle from APK 74 | 75 | ### installation of apktool 76 | 77 | #### For Macbook 78 | ``` 79 | brew install apktool 80 | ``` 81 | 82 | #### For Linux 83 | ``` 84 | apt-get install -y apktool 85 | ``` 86 | 87 | #### For Window 88 | 89 | you can read installion step for window [DOCS](https://ibotpeaches.github.io/Apktool/install/ "DOCS") 90 | 91 | after install `apktool`, unzip apk file by run this command on terminal like this: 92 | 93 | ``` 94 | apktool d /pathOfApkFile.apk 95 | ``` 96 | 97 | After that you will get `index.android.bundle` file at `pathOfApkFile/assets/index.android.bundle` 98 | 99 | than you can use `react-native-decompiler` for decompile `index.android.bundle` file 100 | # IOS 101 | ## Extract main.jsbundle from IPA 102 | you can unzip `ipa` by unzip command on terminal 103 | ```sh 104 | $ unzip AppName.ipa 105 | ``` 106 | after unzip, you will get `Playload` folder, then you have to copy `main.jsbundle` file. 107 | there are two ways to copy this file as follow below 108 | 109 | 1. run this command `cp ./Payload/AppName.app/main.jsbundle ./` to get get `main.jsbundle` file 110 | 111 | 2. Go to `Payload` folder and right click on `AppName.app` and choose `Show Package Contents` then you will find `main.jsbundle` file at root. you can copy this file to any location 112 | 113 | after getting `main.jsbundle` you can use `react-native-decompiler` for decompile `main.jsbundle` file 114 | 115 | ## Valid inputs 116 | 117 | The following input formats are currently supported: 118 | - A single `index.android.bundle`/`main.jsbundle` file that contains all modules (most cases for React Native) 119 | - A folder containing React Native modules (usually called `js-modules`) in "unbundled" apps 120 | - A single Webpack entrypoint bundle file (entrypoint bundles begin with `!function(e)`, chunked bundles start with `window.webpackJsonp`) 121 | - A folder containg Webpack chunks, where at least one file is the entrypoint 122 | 123 | # Extending 124 | 125 | The decompiler operates on a tagger -> editor -> decompiler system. 126 | 127 | * Taggers - Manipulates the module metadata 128 | * Editors - Manipulates the module lines (add, move, or remove). 129 | * Decompilers - Manipulates the module code. 130 | 131 | To add a new plugin, add it into the represpective list. 132 | 133 | The plugins are initialized per module, so any data you store in your plugins will only persist for the current module. 134 | 135 | If your plugin needs to be run before or after other plugins, adjust the ordering in the list, or modify it's pass position. 136 | 137 | Guidelines: 138 | 139 | * When doing any modifications to the AST, use the NodePath methods. 140 | * When you are only doing reading, directly reading from `.node` is acceptable. 141 | 142 | 143 | 144 | ## Contributors ✨ 145 | 146 | 147 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 148 | 149 | 150 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |

Richard

💻

Numan

💻
161 | 162 | 163 | 164 | 165 | 166 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 167 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you find a security issue in this application, please create an issue. 4 | 5 | **DO NOT** disclose any information about the vulnerability publicly. You will get a reply on how to privately disclose details. 6 | 7 | Please make sure that any vulnerabilities you report are not from a dependency. Please report dependency vulnerabilities directly to them. 8 | -------------------------------------------------------------------------------- /media/decompileIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/numandev1/react-native-decompiler/77c7b94e986c67b95c9dad5e619b118ef17bbe17/media/decompileIcon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-decompiler", 3 | "version": "0.2.4", 4 | "description": "react native decompile apk and ipa(soon)", 5 | "main": "./out/index.js", 6 | "author": "richardfuca", 7 | "license": "AGPL-3.0-or-later", 8 | "bin": { 9 | "react-native-decompiler": "./out/main.js", 10 | "rnd": "./out/main.js" 11 | }, 12 | "files": [ 13 | "out" 14 | ], 15 | "scripts": { 16 | "build": "tsc", 17 | "start": "npm i && ts-node ./src/main.ts", 18 | "lint": "eslint ./src", 19 | "postinstall_": "patch-package" 20 | }, 21 | "dependencies": { 22 | "@babel/generator": "7.12.11", 23 | "@babel/parser": "7.12.11", 24 | "@babel/traverse": "7.12.12", 25 | "@babel/types": "7.12.11", 26 | "chalk": "4.1.0", 27 | "cli-progress": "3.8.2", 28 | "command-line-args": "5.0.2", 29 | "debug": "4.3.1", 30 | "eslint": "7.18.0", 31 | "eslint-plugin-react": "7.21.5", 32 | "fs-extra": "9.0.1", 33 | "prettier": "2.2.1" 34 | }, 35 | "devDependencies": { 36 | "@types/babel__generator": "7.6.2", 37 | "@types/babel__traverse": "7.11.0", 38 | "@types/cli-progress": "3.8.0", 39 | "@types/command-line-args": "5.0.0", 40 | "@types/debug": "4.1.5", 41 | "@types/eslint": "7.2.6", 42 | "@types/fs-extra": "9.0.6", 43 | "@types/prettier": "2.1.6", 44 | "@typescript-eslint/eslint-plugin": "4.11.0", 45 | "@typescript-eslint/parser": "4.11.0", 46 | "eslint-config-airbnb-typescript": "12.0.0", 47 | "eslint-plugin-import": "2.22.1", 48 | "eslint-plugin-jsx-a11y": "6.4.1", 49 | "eslint-plugin-node": "11.1.0", 50 | "eslint-plugin-react-hooks": "4.2.0", 51 | "jest": "26.6.3", 52 | "patch-package": "^6.4.7", 53 | "ts-node": "10.9.1", 54 | "typescript": "4.1.3" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/nomi9995/react-native-decompiler/issues" 58 | }, 59 | "homepage": "https://github.com/nomi9995/react-native-decompiler#readme", 60 | "contributors": [ 61 | "Numan (https://github.com/nomi9995)", 62 | "Richard (https://github.com/richardfuca)" 63 | ], 64 | "keywords": [ 65 | "react-native", 66 | "android", 67 | "decompile", 68 | "decompiler", 69 | "index.android.bundle", 70 | "apk" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /patches/@babel+types+7.12.11.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@babel/types/lib/index.d.ts b/node_modules/@babel/types/lib/index.d.ts 2 | index ab4efc3..a6ec1d2 100644 3 | --- a/node_modules/@babel/types/lib/index.d.ts 4 | +++ b/node_modules/@babel/types/lib/index.d.ts 5 | @@ -1395,6 +1395,7 @@ declare type TSBaseType = TSAnyKeyword | TSBooleanKeyword | TSBigIntKeyword | TS 6 | interface Aliases { 7 | Expression: Expression; 8 | Binary: Binary; 9 | + Scope: Node; 10 | Scopable: Scopable; 11 | BlockParent: BlockParent; 12 | Block: Block; 13 | -------------------------------------------------------------------------------- /patches/@types+babel__traverse+7.11.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@types/babel__traverse/index.d.ts b/node_modules/@types/babel__traverse/index.d.ts 2 | index ab63f9d..11bed48 100644 3 | --- a/node_modules/@types/babel__traverse/index.d.ts 4 | +++ b/node_modules/@types/babel__traverse/index.d.ts 5 | @@ -108,6 +108,8 @@ export class Scope { 6 | 7 | checkBlockScopedCollisions(local: Node, kind: string, name: string, id: object): void; 8 | 9 | + crawl(): void; 10 | + 11 | rename(oldName: string, newName?: string, block?: Node): void; 12 | 13 | dump(): void; 14 | @@ -120,7 +122,7 @@ export class Scope { 15 | 16 | registerConstantViolation(path: NodePath): void; 17 | 18 | - registerBinding(kind: string, path: NodePath, bindingPath?: NodePath): void; 19 | + registerBinding(kind: string, path: NodePath, bindingPath?: NodePath): void; 20 | 21 | addGlobal(node: Node): void; 22 | 23 | @@ -538,6 +540,8 @@ export class NodePath { 24 | getCompletionRecords(): NodePath[]; 25 | 26 | getSibling(key: string | number): NodePath; 27 | + getPrevSibling(): NodePath; 28 | + getNextSibling(): NodePath; 29 | getAllPrevSiblings(): NodePath[]; 30 | getAllNextSiblings(): NodePath[]; 31 | 32 | @@ -549,7 +553,7 @@ export class NodePath { 33 | : T[K] extends Node | null | undefined 34 | ? NodePath 35 | : never; 36 | - get(key: string, context?: boolean | TraversalContext): NodePath | NodePath[]; 37 | + // get(key: string, context?: boolean | TraversalContext): NodePath | NodePath[]; 38 | 39 | getBindingIdentifiers(duplicates?: boolean): Node[]; 40 | 41 | -------------------------------------------------------------------------------- /patches/@types+command-line-args+5.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@types/command-line-args/index.d.ts b/node_modules/@types/command-line-args/index.d.ts 2 | index f6a718c..03b9e9b 100644 3 | --- a/node_modules/@types/command-line-args/index.d.ts 4 | +++ b/node_modules/@types/command-line-args/index.d.ts 5 | @@ -8,16 +8,15 @@ 6 | * Returns an object containing option values parsed from the command line. By default it parses the global `process.argv` array. 7 | * Parsing is strict by default. To be more permissive, enable `partial` or `stopAtFirstUnknown` modes. 8 | */ 9 | -declare function commandLineArgs(optionDefinitions: commandLineArgs.OptionDefinition[], options?: commandLineArgs.ParseOptions): commandLineArgs.CommandLineOptions; 10 | +declare function commandLineArgs(optionDefinitions: commandLineArgs.OptionDefinition[], options?: commandLineArgs.ParseOptions): commandLineArgs.CommandLineOptions; 11 | 12 | declare namespace commandLineArgs { 13 | - interface CommandLineOptions { 14 | + type CommandLineOptions = { 15 | /** 16 | * Command-line arguments not parsed by `commandLineArgs`. 17 | */ 18 | _unknown?: string[]; 19 | - [propName: string]: any; 20 | - } 21 | + } & T; 22 | 23 | interface ParseOptions { 24 | /** 25 | -------------------------------------------------------------------------------- /patches/@types+fs-extra+9.0.6.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@types/fs-extra/LICENSE b/node_modules/@types/fs-extra/LICENSE 2 | deleted file mode 100644 3 | index 9e841e7..0000000 4 | --- a/node_modules/@types/fs-extra/LICENSE 5 | +++ /dev/null 6 | @@ -1,21 +0,0 @@ 7 | - MIT License 8 | - 9 | - Copyright (c) Microsoft Corporation. 10 | - 11 | - Permission is hereby granted, free of charge, to any person obtaining a copy 12 | - of this software and associated documentation files (the "Software"), to deal 13 | - in the Software without restriction, including without limitation the rights 14 | - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | - copies of the Software, and to permit persons to whom the Software is 16 | - furnished to do so, subject to the following conditions: 17 | - 18 | - The above copyright notice and this permission notice shall be included in all 19 | - copies or substantial portions of the Software. 20 | - 21 | - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | - SOFTWARE 28 | diff --git a/node_modules/@types/fs-extra/README.md b/node_modules/@types/fs-extra/README.md 29 | deleted file mode 100644 30 | index 8dc3274..0000000 31 | --- a/node_modules/@types/fs-extra/README.md 32 | +++ /dev/null 33 | @@ -1,16 +0,0 @@ 34 | -# Installation 35 | -> `npm install --save @types/fs-extra` 36 | - 37 | -# Summary 38 | -This package contains type definitions for fs-extra (https://github.com/jprichardson/node-fs-extra). 39 | - 40 | -# Details 41 | -Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra. 42 | - 43 | -### Additional Details 44 | - * Last updated: Wed, 23 Dec 2020 21:09:59 GMT 45 | - * Dependencies: [@types/node](https://npmjs.com/package/@types/node) 46 | - * Global values: none 47 | - 48 | -# Credits 49 | -These definitions were written by [Alan Agius](https://github.com/alan-agius4), [midknight41](https://github.com/midknight41), [Brendan Forster](https://github.com/shiftkey), [Mees van Dijk](https://github.com/mees-), [Justin Rockwood](https://github.com/jrockwood), [Sang Dang](https://github.com/sangdth), [Florian Keller](https://github.com/ffflorian), [Piotr Błażejewicz](https://github.com/peterblazejewicz), and [Tiger Oakes](https://github.com/NotWoods). 50 | diff --git a/node_modules/@types/fs-extra/index.d.ts b/node_modules/@types/fs-extra/index.d.ts 51 | index e253b7f..878ec3f 100644 52 | --- a/node_modules/@types/fs-extra/index.d.ts 53 | +++ b/node_modules/@types/fs-extra/index.d.ts 54 | @@ -58,15 +58,15 @@ export function outputFile(file: string, data: any, callback: (err: Error) => vo 55 | export function outputFile(file: string, data: any, options: WriteFileOptions | string, callback: (err: Error) => void): void; 56 | export function outputFileSync(file: string, data: any, options?: WriteFileOptions | string): void; 57 | 58 | -export function readJson(file: string, options?: ReadOptions): Promise; 59 | -export function readJson(file: string, callback: (err: Error, jsonObject: any) => void): void; 60 | -export function readJson(file: string, options: ReadOptions, callback: (err: Error, jsonObject: any) => void): void; 61 | -export function readJSON(file: string, options?: ReadOptions): Promise; 62 | -export function readJSON(file: string, callback: (err: Error, jsonObject: any) => void): void; 63 | -export function readJSON(file: string, options: ReadOptions, callback: (err: Error, jsonObject: any) => void): void; 64 | +export function readJson(file: string, options?: ReadOptions): Promise; 65 | +export function readJson(file: string, callback: (err: Error, jsonObject: T) => void): void; 66 | +export function readJson(file: string, options: ReadOptions, callback: (err: Error, jsonObject: T) => void): void; 67 | +export function readJSON(file: string, options?: ReadOptions): Promise; 68 | +export function readJSON(file: string, callback: (err: Error, jsonObject: T) => void): void; 69 | +export function readJSON(file: string, options: ReadOptions, callback: (err: Error, jsonObject: T) => void): void; 70 | 71 | -export function readJsonSync(file: string, options?: ReadOptions): any; 72 | -export function readJSONSync(file: string, options?: ReadOptions): any; 73 | +export function readJsonSync(file: string, options?: ReadOptions): T; 74 | +export function readJSONSync(file: string, options?: ReadOptions): T; 75 | 76 | export function remove(dir: string): Promise; 77 | export function remove(dir: string, callback: (err: Error) => void): void; 78 | @@ -81,15 +81,15 @@ export function outputJson(file: string, data: any, callback: (err: Error) => vo 79 | export function outputJsonSync(file: string, data: any, options?: WriteOptions): void; 80 | export function outputJSONSync(file: string, data: any, options?: WriteOptions): void; 81 | 82 | -export function writeJSON(file: string, object: any, options?: WriteOptions): Promise; 83 | -export function writeJSON(file: string, object: any, callback: (err: Error) => void): void; 84 | -export function writeJSON(file: string, object: any, options: WriteOptions, callback: (err: Error) => void): void; 85 | -export function writeJson(file: string, object: any, options?: WriteOptions): Promise; 86 | -export function writeJson(file: string, object: any, callback: (err: Error) => void): void; 87 | -export function writeJson(file: string, object: any, options: WriteOptions, callback: (err: Error) => void): void; 88 | +export function writeJSON(file: string, object: T, options?: WriteOptions): Promise; 89 | +export function writeJSON(file: string, object: T, callback: (err: Error) => void): void; 90 | +export function writeJSON(file: string, object: T, options: WriteOptions, callback: (err: Error) => void): void; 91 | +export function writeJson(file: string, object: T, options?: WriteOptions): Promise; 92 | +export function writeJson(file: string, object: T, callback: (err: Error) => void): void; 93 | +export function writeJson(file: string, object: T, options: WriteOptions, callback: (err: Error) => void): void; 94 | 95 | -export function writeJsonSync(file: string, object: any, options?: WriteOptions): void; 96 | -export function writeJSONSync(file: string, object: any, options?: WriteOptions): void; 97 | +export function writeJsonSync(file: string, object: T, options?: WriteOptions): void; 98 | +export function writeJSONSync(file: string, object: T, options?: WriteOptions): void; 99 | 100 | export function ensureFile(path: string): Promise; 101 | export function ensureFile(path: string, callback: (err: Error) => void): void; 102 | -------------------------------------------------------------------------------- /src/cacheParse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import crypto from 'crypto'; 17 | import fs from 'fs-extra'; 18 | import path from 'path'; 19 | import Module from './module'; 20 | import { CachedFile } from './interfaces/cachedFile'; 21 | import CmdArgs from './interfaces/cmdArgs'; 22 | 23 | export default class CacheParse { 24 | cmdArgs: CmdArgs; 25 | 26 | constructor(cmdArgs: CmdArgs) { 27 | this.cmdArgs = cmdArgs; 28 | } 29 | 30 | async writeCache(filename: string, moduleList: Module[]): Promise { 31 | return fs.writeJSON(filename, { 32 | inputChecksum: await this.generateInputChecksums(this.cmdArgs.in), 33 | modules: moduleList.filter((ele) => ele != null).map((e) => e.toCache()), 34 | }); 35 | } 36 | 37 | private async generateInputChecksums(input: string): Promise { 38 | if ((await fs.lstat(input)).isDirectory()) { 39 | return fs.readdir(input) 40 | .then((fileNames) => Promise.all(fileNames.map((file) => fs.readFile(path.join(input, file))))) 41 | .then((files) => files.map((file) => crypto.createHash('md5').update(file).digest('hex'))); 42 | } 43 | 44 | return [crypto.createHash('md5').update(await fs.readFile(input)).digest('hex')]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/decompilers/babel/class/babelClassEvaluator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import * as t from '@babel/types'; 17 | import { NodePath, Visitor } from '@babel/traverse'; 18 | import { Plugin } from '../../../plugin'; 19 | import ArrayMap from '../../../util/arrayMap'; 20 | 21 | /** 22 | * Evaluates Babel class structures 23 | */ 24 | export default class BabelClassEvaluator extends Plugin { 25 | readonly pass = 3; 26 | private classCreateName?: string; 27 | private classCreatePath?: NodePath; 28 | private callExpressions: ArrayMap> = new ArrayMap(); 29 | private assignmentExpressions: ArrayMap> = new ArrayMap(); 30 | 31 | getVisitor(): Visitor { 32 | return { 33 | VariableDeclarator: (path) => { 34 | if (!t.isIdentifier(path.node.id)) return; 35 | if (this.variableIsForDependency(path, '@babel/runtime/helpers/createClass')) { 36 | this.classCreateName = path.node.id.name; 37 | path.remove(); 38 | } 39 | }, 40 | ImportDeclaration: (path) => { 41 | if (!t.isImportDefaultSpecifier(path.node.specifiers[0]) || !t.isIdentifier(path.node.specifiers[0].local)) return; 42 | if (this.variableIsForDependency(path, '@babel/runtime/helpers/createClass')) { 43 | this.classCreateName = path.node.specifiers[0].local.name; 44 | path.remove(); 45 | } 46 | }, 47 | CallExpression: (path) => { 48 | if (!t.isIdentifier(path.node.callee) || path.node.arguments.length > 2) return; 49 | 50 | this.callExpressions.push(path.node.callee.name, path); 51 | }, 52 | AssignmentExpression: (path) => { 53 | if (!t.isBlockStatement(path.parentPath.parent) || !t.isMemberExpression(path.node.left) || !t.isIdentifier(path.node.left.object)) return; 54 | 55 | this.assignmentExpressions.push(path.node.left.object.name, path); 56 | }, 57 | }; 58 | } 59 | 60 | afterPass(): void { 61 | if (!this.classCreateName || !this.callExpressions.has(this.classCreateName)) return; 62 | 63 | this.callExpressions.forEachElement(this.classCreateName, (path) => { 64 | if (path.removed) return; 65 | 66 | const varDeclar = path.find((e) => e.isVariableDeclarator()); 67 | if (!varDeclar?.isVariableDeclarator() || !t.isIdentifier(varDeclar.node.id) || !t.isVariableDeclaration(varDeclar.parent)) return; 68 | const className = varDeclar.node.id.name; 69 | 70 | const parentBody = path.find((e) => e.isBlockStatement()); 71 | if (!parentBody?.isBlockStatement()) return; 72 | 73 | const extendsId = parentBody.get('body').map((line) => { 74 | if (!line.isExpressionStatement() || !t.isCallExpression(line.node.expression)) return null; 75 | const exp = line.node.expression; 76 | if (!t.isFunctionExpression(exp.callee) || !t.isIdentifier(exp.arguments[0]) || !t.isExpression(exp.arguments[1])) return null; 77 | 78 | let hasSuperExpression = false; 79 | 80 | line.traverse({ 81 | StringLiteral: (p) => { 82 | if (p.node.value.includes('Super expression must either be null or a function')) { 83 | hasSuperExpression = true; 84 | } 85 | }, 86 | }); 87 | 88 | return hasSuperExpression ? exp.arguments[1] : null; 89 | }).find((line) => line != null); 90 | 91 | const methods = []; 92 | 93 | const constructor = this.createConstructor(path); 94 | if (constructor) { 95 | methods.push(constructor); 96 | } 97 | 98 | methods.push(...this.createStatic(className, varDeclar.scope.bindings[className].identifier)); 99 | methods.push(...this.createMethods(path)); 100 | 101 | if (varDeclar.parent.declarations.length === 1) { 102 | varDeclar.parentPath.replaceWith(t.classDeclaration(t.identifier(className), extendsId, t.classBody(methods))); 103 | } else { 104 | varDeclar.parentPath.insertAfter(t.classDeclaration(t.identifier(className), extendsId, t.classBody(methods))); 105 | varDeclar.remove(); 106 | } 107 | }); 108 | 109 | this.classCreatePath?.remove(); 110 | } 111 | 112 | private createConstructor(path: NodePath): t.ClassMethod | null { 113 | const firstParam = path.get('arguments')[0]; 114 | if (!firstParam?.isIdentifier()) return null; 115 | 116 | const constructorFunction = firstParam.scope.getBinding(firstParam.node.name)?.path; 117 | if (!constructorFunction?.isFunctionDeclaration()) return null; 118 | 119 | if (constructorFunction.node.body.body.length === 0) return null; 120 | 121 | return t.classMethod('constructor', t.identifier('constructor'), constructorFunction.node.params, constructorFunction.node.body); 122 | } 123 | 124 | private createStatic(varName: string, bindingIdentifier: t.Identifier): (t.ClassProperty | t.ClassMethod)[] { 125 | const methods: (t.ClassProperty | t.ClassMethod)[] = []; 126 | 127 | this.assignmentExpressions.forEachElement(varName, (path) => { 128 | if (path.removed || !path.scope.bindingIdentifierEquals(varName, bindingIdentifier)) return; 129 | if (!t.isMemberExpression(path.node.left) || !t.isIdentifier(path.node.left.property)) return; 130 | 131 | if (t.isFunctionExpression(path.node.right)) { 132 | methods.push(t.classMethod('method', t.identifier(path.node.left.property.name), path.node.right.params, path.node.right.body, undefined, true)); 133 | path.remove(); 134 | } else { 135 | // methods.push(t.classProperty(path.node.left.property, path.node.right, undefined, undefined, undefined, true)); 136 | } 137 | }); 138 | return methods; 139 | } 140 | 141 | private createMethods(path: NodePath): t.ClassMethod[] { 142 | const secondParam = path.get('arguments')[1]; 143 | if (!secondParam?.isArrayExpression()) return []; 144 | 145 | const methods: t.ClassMethod[] = []; 146 | secondParam.node.elements.forEach((e) => { 147 | if (!t.isObjectExpression(e) || !t.isObjectProperty(e.properties[0]) || !t.isObjectProperty(e.properties[1])) return; 148 | if (!t.isIdentifier(e.properties[0].key) || !t.isStringLiteral(e.properties[0].value) || !t.isFunctionExpression(e.properties[1].value)) return; 149 | 150 | methods.push(t.classMethod('method', t.identifier(e.properties[0].value.value), e.properties[1].value.params, e.properties[1].value.body)); 151 | }); 152 | 153 | return methods; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/decompilers/babel/cleaners/toConsumableArrayCleaner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | import { Visitor, NodePath } from '@babel/traverse'; 16 | import { 17 | CallExpression, 18 | isIdentifier, 19 | isCallExpression, 20 | isVariableDeclarator, 21 | VariableDeclarator, 22 | } from '@babel/types'; 23 | import { Plugin } from '../../../plugin'; 24 | import Module from '../../../module'; 25 | import ArrayMap from '../../../util/arrayMap'; 26 | import CmdArgs from '../../../interfaces/cmdArgs'; 27 | 28 | /** 29 | * Cleans up `@babel/runtime/helpers/toConsumableArray` usage 30 | */ 31 | export default class ToConsumableArrayCleaner extends Plugin { 32 | readonly pass = 3; 33 | 34 | private moduleUsed: boolean; 35 | private moduleVarPath?: NodePath; 36 | private moduleBindingLocation?: number; 37 | private callExpressions: ArrayMap> = new ArrayMap(); 38 | 39 | constructor(args: CmdArgs, module: Module, moduleList: Module[]) { 40 | super(args, module, moduleList); 41 | 42 | const destructureDependency = moduleList.find((mod) => mod?.moduleName === '@babel/runtime/helpers/toConsumableArray'); 43 | this.moduleUsed = destructureDependency?.moduleId != null && module.dependencies.includes(destructureDependency?.moduleId); 44 | } 45 | 46 | getVisitor(): Visitor { 47 | if (!this.moduleUsed) return {}; 48 | 49 | return { 50 | CallExpression: (path) => { 51 | if (!isIdentifier(path.node.callee)) return; 52 | 53 | const bindingLocation = path.scope.getBindingIdentifier(path.node.callee.name)?.start; 54 | if (bindingLocation == null) return; 55 | 56 | this.callExpressions.push(bindingLocation, path); 57 | }, 58 | VariableDeclarator: (path) => { 59 | if (this.moduleVarPath || !isIdentifier(path.node.id) || !isCallExpression(path.node.init)) return; 60 | 61 | const init = path.get('init'); 62 | if (!init.isCallExpression()) return; 63 | const moduleDependency = this.getModuleDependency(init); 64 | if (moduleDependency?.moduleName !== '@babel/runtime/helpers/toConsumableArray') return; 65 | 66 | this.moduleVarPath = path; 67 | this.moduleBindingLocation = path.scope.getBindingIdentifier(path.node.id.name)?.start ?? undefined; 68 | }, 69 | }; 70 | } 71 | 72 | afterPass(): void { 73 | if (this.moduleBindingLocation != null && this.moduleVarPath) { 74 | if (!this.moduleVarPath.removed) { 75 | this.moduleVarPath.remove(); 76 | } 77 | 78 | this.callExpressions.forEachElement(this.moduleBindingLocation, (exp) => { 79 | if (isVariableDeclarator(exp.parent) && isIdentifier(exp.parent.id) && isIdentifier(exp.node.arguments[0])) { 80 | exp.scope.rename(exp.parent.id.name, exp.node.arguments[0].name); 81 | if (!exp.parentPath.removed) { 82 | exp.parentPath.remove(); 83 | } 84 | } else { 85 | exp.replaceWith(exp.node.arguments[0]); 86 | } 87 | }); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/decompilers/cleaners/assignmentIfElseToTernary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { isLogicalExpression, isReturnStatement, conditionalExpression } from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Converts `return condition && a || b;` to `return condition ? a : b;` 22 | */ 23 | export default class AssignmentIfElseToTernary extends Plugin { 24 | readonly pass = 1; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | LogicalExpression(path) { 29 | if (!isReturnStatement(path.parent) || !isLogicalExpression(path.node.left)) return; 30 | if (path.node.operator !== '||' || path.node.left.operator !== '&&') return; 31 | 32 | path.replaceWith(conditionalExpression(path.node.left.left, path.node.left.right, path.node.right)); 33 | }, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/decompilers/cleaners/cleanReturns.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { isAssignmentExpression, isIdentifier } from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Keeps returns clean with no weird things like AssignmentExpressions 22 | */ 23 | export default class CleanReturns extends Plugin { 24 | readonly pass = 1; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | ReturnStatement: (path) => { 29 | if (isAssignmentExpression(path.node.argument) && isIdentifier(path.node.argument.left)) { 30 | path.insertBefore(path.node.argument); 31 | path.get('argument').replaceWith(path.node.argument.left); 32 | } 33 | }, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/decompilers/cleaners/powCleaner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Converts Math.pow to "a **" b" 22 | */ 23 | export default class PowCleaner extends Plugin { 24 | readonly pass = 1; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | CallExpression: (path) => { 29 | if (!t.isMemberExpression(path.node.callee) || !t.isIdentifier(path.node.callee.object) || !t.isIdentifier(path.node.callee.property)) return; 30 | if (path.node.callee.object.name !== 'Math' && path.node.callee.property.name !== 'pow') return; 31 | if (!t.isExpression(path.node.arguments[0]) || !t.isExpression(path.node.arguments[1])) return; 32 | 33 | path.replaceWith(t.binaryExpression('**', path.node.arguments[0], path.node.arguments[1])); 34 | }, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/decompilers/cleaners/uselessCommaOperatorCleaner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { isNumericLiteral } from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Cleans up any useless comma operations, for example `(0, function)` 22 | */ 23 | export default class UselessCommaOperatorCleaner extends Plugin { 24 | readonly pass = 1; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | SequenceExpression(path) { 29 | if (path.node.expressions.length !== 2 || !isNumericLiteral(path.node.expressions[0])) return; 30 | path.replaceWith(path.node.expressions[1]); 31 | }, 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/decompilers/decompilerList.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { PluginConstructor } from '../plugin'; 17 | import VoidZeroToUndefined from './longhanders/voidZeroToUndefined'; 18 | import LongBooleans from './longhanders/longBooleans'; 19 | import RequireMapper from './mappers/requireMapper'; 20 | import UselessCommaOperatorCleaner from './cleaners/uselessCommaOperatorCleaner'; 21 | import AssignmentIfElseToTernary from './cleaners/assignmentIfElseToTernary'; 22 | import HangingIfElseWrapper from './longhanders/hangingIfElseWrapper'; 23 | import DefaultInteropEvaluator from './evaluators/defaultInteropEvaluator'; 24 | import ArrayDestructureEvaluator from './evaluators/arrayDestructureEvaluator'; 25 | import SetStateRenamer from './react/setStateRenamer'; 26 | import ToConsumableArrayCleaner from './babel/cleaners/toConsumableArrayCleaner'; 27 | import Spreadifier from './evaluators/spreadifier'; 28 | import JSXConverter from './react/jsxConverter'; 29 | import ExportsToEs6 from './es6/exportsToEs6'; 30 | import CleanReturns from './cleaners/cleanReturns'; 31 | import BabelClassEvaluator from './babel/class/babelClassEvaluator'; 32 | import PowCleaner from './cleaners/powCleaner'; 33 | import ImportsToEs6 from './es6/importsToEs6'; 34 | 35 | const decompilerList: PluginConstructor[] = [ 36 | VoidZeroToUndefined, 37 | LongBooleans, 38 | RequireMapper, 39 | AssignmentIfElseToTernary, 40 | HangingIfElseWrapper, 41 | CleanReturns, 42 | PowCleaner, 43 | DefaultInteropEvaluator, 44 | ArrayDestructureEvaluator, 45 | Spreadifier, 46 | UselessCommaOperatorCleaner, 47 | // pass 2 48 | ExportsToEs6, 49 | ImportsToEs6, 50 | // pass 3 51 | ToConsumableArrayCleaner, 52 | BabelClassEvaluator, 53 | JSXConverter, 54 | // pass 4 55 | SetStateRenamer, 56 | ]; 57 | 58 | export default decompilerList; 59 | -------------------------------------------------------------------------------- /src/decompilers/es6/exportsToEs6.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Convertes exports.? into ES6 named exports 22 | */ 23 | export default class ExportsToEs6 extends Plugin { 24 | readonly pass = 2; 25 | 26 | getVisitor(): Visitor { 27 | if (!this.cmdArgs.es6 || !this.module.tags.includes('__esModule')) return {}; 28 | return { 29 | AssignmentExpression: (path) => { 30 | if (!t.isMemberExpression(path.node.left) || !t.isIdentifier(path.node.left.object) || !t.isIdentifier(path.node.left.property)) return; 31 | if (path.node.left.object.name !== 'exports') return; 32 | const isDefault = path.node.left.property.name === 'default'; 33 | const generatedExport = this.generateEs6Export(path.node, isDefault); 34 | if (!generatedExport) return; 35 | 36 | if (t.isExpressionStatement(path.parent)) { 37 | path.parentPath.replaceWith(generatedExport); 38 | } else if (t.isFunctionExpression(path.node.right) && path.parentPath.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id)) { 39 | path.scope.rename(path.parentPath.node.id.name, path.node.left.property.name); 40 | if (path.parentPath.parentPath.isVariableDeclaration() && path.parentPath.parentPath.node.declarations.length === 1) { 41 | path.parentPath.parentPath.replaceWith(generatedExport); 42 | } else { 43 | path.parentPath.parentPath.insertAfter(generatedExport); 44 | path.parentPath.remove(); 45 | } 46 | } 47 | }, 48 | }; 49 | } 50 | 51 | private generateEs6Export(node: t.AssignmentExpression, isDefault: boolean): t.ExportNamedDeclaration | t.ExportDefaultDeclaration | null { 52 | if (!t.isMemberExpression(node.left) || !t.isIdentifier(node.left.property)) throw new Error('Failed assertion'); 53 | 54 | const exportType = isDefault ? t.exportDefaultDeclaration : t.exportNamedDeclaration; 55 | if (t.isObjectExpression(node.right) && !isDefault) { 56 | return t.exportNamedDeclaration(t.variableDeclaration('const', [t.variableDeclarator(node.left.property, node.right)])); 57 | } 58 | if (t.isFunctionExpression(node.right)) { 59 | return exportType(t.functionDeclaration(node.left.property, node.right.params, node.right.body)); 60 | } 61 | if (t.isIdentifier(node.right) && isDefault) { 62 | return t.exportDefaultDeclaration(node.right); 63 | } 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/decompilers/es6/importsToEs6.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { NodePath, Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Convertes requires from ES6 modules into ES6 imports 22 | */ 23 | export default class ImportsToEs6 extends Plugin { 24 | readonly pass = 2; 25 | 26 | getVisitor(): Visitor { 27 | if (!this.cmdArgs.es6) return {}; 28 | return { 29 | CallExpression: (path) => { 30 | if (!t.isIdentifier(path.node.callee) || !t.isStringLiteral(path.node.arguments[0])) return; 31 | 32 | const moduleDependency = this.getModuleDependency(path); 33 | if (moduleDependency == null) return; 34 | 35 | const varDeclar = path.find((e) => e.isVariableDeclarator()); 36 | if (varDeclar == null || !varDeclar.isVariableDeclarator() || !t.isIdentifier(varDeclar.node.id)) return; 37 | const varIdentifier = varDeclar.node.id; 38 | const varName = varIdentifier.name; 39 | 40 | if (moduleDependency.tags.includes('defaultExportOnly') || !moduleDependency.tags.includes('__esModule')) { 41 | this.bindingTraverse(varDeclar.scope.bindings[varName], varName, { 42 | MemberExpression: (bPath) => { 43 | if (t.isIdentifier(bPath.node.object) && t.isIdentifier(bPath.node.property) && bPath.node.object.name === varName && bPath.node.property.name === 'default') { 44 | bPath.replaceWith(bPath.node.object); 45 | } 46 | }, 47 | }); 48 | const [newPath] = varDeclar.parentPath.insertBefore(t.importDeclaration([t.importDefaultSpecifier(varIdentifier)], path.node.arguments[0])); 49 | newPath.scope.registerBinding('module', newPath); 50 | varDeclar.remove(); 51 | return; 52 | } 53 | 54 | const binding = varDeclar.scope.bindings[varName]; 55 | const memberExpressions: NodePath[] = []; 56 | const imports: Set = new Set(); 57 | this.bindingTraverse(binding, varName, { 58 | MemberExpression: (bPath) => { 59 | if (t.isIdentifier(bPath.node.object) && t.isIdentifier(bPath.node.property) && bPath.node.object.name === varName) { 60 | memberExpressions.push(bPath); 61 | imports.add(bPath.node.property.name); 62 | } 63 | }, 64 | }); 65 | if (imports.size < 1) return; 66 | 67 | memberExpressions.forEach((bPath) => { 68 | if (t.isIdentifier(bPath.node.property) && bPath.node.property.name === 'default') { 69 | bPath.replaceWith(bPath.node.object); 70 | } else { 71 | bPath.replaceWith(bPath.node.property); 72 | } 73 | }); 74 | 75 | const importSpecifiers = [...imports].map((i) => (i === 'default' ? t.importDefaultSpecifier(varIdentifier) : t.importSpecifier(t.identifier(i), t.identifier(i)))); 76 | varDeclar.parentPath.insertBefore(t.importDeclaration(importSpecifiers, path.node.arguments[0])); 77 | varDeclar.remove(); 78 | }, 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/decompilers/evaluators/arrayDestructureEvaluator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor, NodePath } from '@babel/traverse'; 17 | import { 18 | VariableDeclarator, 19 | isCallExpression, 20 | isIdentifier, 21 | isMemberExpression, 22 | isNumericLiteral, 23 | arrayPattern, 24 | identifier, 25 | Identifier, 26 | } from '@babel/types'; 27 | import { Plugin } from '../../plugin'; 28 | import Module from '../../module'; 29 | import CmdArgs from '../../interfaces/cmdArgs'; 30 | 31 | interface VariableDeclaratorData { 32 | path: NodePath; 33 | varName: string; 34 | varStart: number; 35 | couldBeDestructure: boolean; 36 | destructureBindingStart?: number; 37 | destructureArrayBindingStart?: number; 38 | couldBeArrayAccess: boolean; 39 | arrayAccessBindingStart?: number; 40 | arrayAccessVal?: number; 41 | } 42 | 43 | /** 44 | * Converts Babel array destructuring to the native one 45 | */ 46 | export default class ArrayDestructureEvaluator extends Plugin { 47 | readonly pass = 2; 48 | 49 | private readonly destructureUsed: boolean; 50 | private readonly variableDeclarators: VariableDeclaratorData[] = []; 51 | private destructureFunction?: NodePath; 52 | private destructureFunctionStart?: number; 53 | 54 | constructor(args: CmdArgs, module: Module, moduleList: Module[]) { 55 | super(args, module, moduleList); 56 | 57 | const destructureDependency = moduleList.find((mod) => mod?.moduleName === '@babel/runtime/helpers/slicedToArray'); 58 | this.destructureUsed = destructureDependency?.moduleId != null && module.dependencies.includes(destructureDependency?.moduleId); 59 | } 60 | 61 | getVisitor(): Visitor { 62 | if (!this.destructureUsed) return {}; 63 | 64 | return { 65 | VariableDeclarator: (path) => { 66 | if (!isIdentifier(path.node.id) || path.node.id.start == null) return; 67 | 68 | const variableDeclaratorData: VariableDeclaratorData = { 69 | path, 70 | couldBeDestructure: false, 71 | couldBeArrayAccess: false, 72 | varName: path.node.id.name, 73 | varStart: path.node.id.start, 74 | }; 75 | 76 | if (isCallExpression(path.node.init) && isIdentifier(path.node.init.callee) 77 | && path.node.init.arguments.length === 2 && isIdentifier(path.node.init.arguments[0]) && isNumericLiteral(path.node.init.arguments[1])) { 78 | variableDeclaratorData.couldBeDestructure = true; 79 | variableDeclaratorData.destructureBindingStart = path.scope.getBindingIdentifier(path.node.init.callee.name)?.start ?? undefined; 80 | variableDeclaratorData.destructureArrayBindingStart = path.scope.getBindingIdentifier(path.node.init.arguments[0].name)?.start ?? undefined; 81 | } 82 | if (isMemberExpression(path.node.init) && isIdentifier(path.node.init.object) && isNumericLiteral(path.node.init.property)) { 83 | variableDeclaratorData.couldBeArrayAccess = true; 84 | variableDeclaratorData.arrayAccessBindingStart = path.scope.getBindingIdentifier(path.node.init.object.name)?.start ?? undefined; 85 | variableDeclaratorData.arrayAccessVal = path.node.init.property.value; 86 | } 87 | 88 | this.variableDeclarators.push(variableDeclaratorData); 89 | 90 | const callExpression = path.get('init'); 91 | if (!callExpression.isCallExpression()) return; 92 | 93 | const moduleDependency = this.getModuleDependency(callExpression); 94 | if (moduleDependency?.moduleName === '@babel/runtime/helpers/slicedToArray') { 95 | this.destructureFunction = path; 96 | this.destructureFunctionStart = path.node.id.start; 97 | } 98 | }, 99 | }; 100 | } 101 | 102 | afterPass(): void { 103 | if (this.destructureFunctionStart == null) return; 104 | 105 | this.variableDeclarators.forEach((data) => { 106 | if (!data.couldBeDestructure) return; 107 | if (data.destructureBindingStart !== this.destructureFunctionStart) return; 108 | 109 | const sourceArray = this.variableDeclarators.find((srcData) => srcData.varStart === data.destructureArrayBindingStart); 110 | const arrayUsages = this.variableDeclarators.filter((arrData) => arrData.arrayAccessBindingStart === data.varStart); 111 | if (!sourceArray || !arrayUsages.length) return; 112 | 113 | const arrayPatternElements: (Identifier | null)[] = []; 114 | arrayUsages.forEach((usage) => { 115 | if (usage.arrayAccessVal == null) throw new Error(); 116 | arrayPatternElements[usage.arrayAccessVal] = identifier(usage.varName); 117 | }); 118 | for (let i = 0; i < arrayPatternElements.length; i += 1) { 119 | if (arrayPatternElements[i] === undefined) { 120 | arrayPatternElements[i] = null; 121 | } 122 | } 123 | 124 | sourceArray.path.node.id = arrayPattern(arrayPatternElements); 125 | 126 | if (!this.destructureFunction?.removed) { 127 | this.destructureFunction?.remove(); 128 | } 129 | if (!data.path.removed) { 130 | data.path.remove(); 131 | } 132 | arrayUsages.forEach((usageData) => (usageData.path.removed ? null : usageData.path.remove())); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/decompilers/evaluators/defaultInteropEvaluator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { 18 | isCallExpression, 19 | isIdentifier, 20 | } from '@babel/types'; 21 | import { Plugin } from '../../plugin'; 22 | 23 | /** 24 | * Evaluates babel default interops 25 | */ 26 | export default class DefaultInteropEvaluator extends Plugin { 27 | readonly pass = 1; 28 | 29 | getVisitor(): Visitor { 30 | return { 31 | VariableDeclarator: (path) => { 32 | if (!isIdentifier(path.node.id)) return; 33 | if (this.variableIsForDependency(path, ['@babel/runtime/helpers/interopRequireDefault', '@babel/runtime/helpers/interopRequireWildcard'])) { 34 | const interopVarName = path.node.id.name; 35 | this.bindingTraverse(path.scope.bindings[interopVarName], interopVarName, { 36 | CallExpression: (bindingPath) => { 37 | if (!isIdentifier(bindingPath.node.callee) || bindingPath.node.callee.name !== interopVarName) return; 38 | if (isCallExpression(bindingPath.node.arguments[0])) { 39 | bindingPath.replaceWith(bindingPath.node.arguments[0]); 40 | } else if (isIdentifier(bindingPath.node.arguments[0])) { 41 | const parent = bindingPath.find((p) => p.isVariableDeclarator()); 42 | if (!parent?.isVariableDeclarator() || !isIdentifier(parent.node.id)) throw new Error('Failed assertion'); 43 | this.mergeBindings(parent, parent.node.id.name, bindingPath.node.arguments[0].name); 44 | } 45 | }, 46 | }); 47 | path.remove(); 48 | } 49 | }, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/decompilers/evaluators/spreadifier.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | /** 20 | * Coverts x.apply(x, [...]) into spreads) 21 | */ 22 | export default class Spreadifier extends Plugin { 23 | readonly pass = 1; 24 | readonly name = 'Spreadifer'; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | CallExpression: (path) => { 29 | const callee = path.get('callee'); 30 | if (!callee.isMemberExpression()) return; 31 | if (!t.isIdentifier(callee.node.property) || callee.node.property.name !== 'apply') return; 32 | const args = path.get('arguments'); 33 | if (!t.isIdentifier(args[0].node) || !t.isCallExpression(args[1].node) || !t.isMemberExpression(args[1].node.callee)) return; 34 | if (!t.isArrayExpression(args[1].node.callee.object) || !t.isIdentifier(args[1].node.callee.property)) return; 35 | if (!t.isExpression(args[1].node.arguments[0]) || args[1].node.callee.property.name !== 'concat') return; 36 | 37 | let expectedThis: t.Node = path.node.callee; 38 | while (t.isMemberExpression(expectedThis)) { 39 | expectedThis = expectedThis.object; 40 | } 41 | if (!t.isIdentifier(expectedThis) || args[0].node.name !== expectedThis.name) return; 42 | 43 | callee.replaceWith(callee.node.object); 44 | 45 | const newAugments = [...args[1].node.callee.object.elements, t.spreadElement(args[1].node.arguments[0])]; 46 | path.node.arguments = newAugments; 47 | }, 48 | ForStatement: (path) => { 49 | if (!t.isExpressionStatement(path.node.body) || !t.isAssignmentExpression(path.node.body.expression)) return; 50 | const forBody = path.node.body.expression; 51 | if (!t.isMemberExpression(forBody.left) || !t.isMemberExpression(forBody.right)) return; 52 | if (!t.isIdentifier(forBody.left.object) || !t.isIdentifier(forBody.left.property)) return; 53 | if (!t.isIdentifier(forBody.right.object) || !t.isIdentifier(forBody.right.property) || forBody.right.object.name !== 'arguments') return; 54 | 55 | let hasArgumentsLength = false; 56 | path.traverse({ 57 | MemberExpression: (p) => { 58 | if (!t.isIdentifier(p.node.object) || !t.isIdentifier(p.node.property) || p.node.object.name !== 'arguments' || p.node.property.name !== 'length') return; 59 | hasArgumentsLength = true; 60 | }, 61 | }); 62 | if (!hasArgumentsLength) return; 63 | 64 | const func = path.find((p) => p.isFunctionExpression()); 65 | if (!func?.isFunctionExpression()) return; 66 | 67 | path.scope.rename(forBody.left.object.name, 'args'); 68 | path.remove(); 69 | func.pushContainer('params', t.restElement(t.identifier('args'))); 70 | }, 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/decompilers/longhanders/hangingIfElseWrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { NodePath, Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Converts `cond && statement` to `if (cond) statement` 22 | */ 23 | export default class HangingIfElseWrapper extends Plugin { 24 | readonly pass = 1; 25 | readonly name = 'HangingIfElseWrapper'; 26 | 27 | getVisitor(): Visitor { 28 | return { 29 | ExpressionStatement: (path) => { 30 | if (!t.isBlockStatement(path.parent) && !t.isSwitchCase(path.parent)) return; 31 | 32 | if (t.isLogicalExpression(path.node.expression) && path.node.expression.operator === '&&') { 33 | this.convertShorthandIf(path, path.node.expression, true); 34 | } else if (t.isLogicalExpression(path.node.expression) && path.node.expression.operator === '||') { 35 | this.convertShorthandIf(path, path.node.expression, false); 36 | } else if (t.isConditionalExpression(path.node.expression)) { 37 | this.convertShorthandIfElse(path, path.node.expression); 38 | } 39 | }, 40 | ReturnStatement: (path) => { 41 | if (!t.isConditionalExpression(path.node.argument)) return; 42 | const eitherIsSeqExp = t.isSequenceExpression(path.node.argument.consequent) || t.isSequenceExpression(path.node.argument.alternate); 43 | const bothAreCondExp = t.isConditionalExpression(path.node.argument.consequent) && t.isConditionalExpression(path.node.argument.alternate); 44 | if (!eitherIsSeqExp && !bothAreCondExp) return; 45 | 46 | path.replaceWith(t.ifStatement(path.node.argument.test, this.convertToReturnBody(path.node.argument.consequent), this.convertToReturnBody(path.node.argument.alternate))); 47 | }, 48 | }; 49 | } 50 | 51 | private convertShorthandIf(path: NodePath, expression: t.LogicalExpression, condition: boolean): void { 52 | this.debugLog(this.debugPathToCode(path)); 53 | path.replaceWith(this.parseConditionalIf(expression, condition)); 54 | } 55 | 56 | private parseConditionalIf(exp: t.LogicalExpression, condition: boolean): t.IfStatement { 57 | const body = t.isSequenceExpression(exp.right) ? t.blockStatement(exp.right.expressions.map((e) => t.expressionStatement(e))) : t.expressionStatement(exp.right); 58 | return t.ifStatement(condition ? exp.left : t.unaryExpression('!', exp.left), body); 59 | } 60 | 61 | private convertShorthandIfElse(path: NodePath, cond: t.ConditionalExpression): void { 62 | this.debugLog(this.debugPathToCode(path)); 63 | path.replaceWith(this.parseConditionalToIfElse(cond)); 64 | } 65 | 66 | private parseConditionalToIfElse(cond: t.ConditionalExpression): t.IfStatement { 67 | const elseBlock = t.isConditionalExpression(cond.alternate) ? this.parseConditionalToIfElse(cond.alternate) : this.convertToBody(cond.alternate); 68 | return t.ifStatement(cond.test, this.convertToBody(cond.consequent), elseBlock); 69 | } 70 | 71 | private convertToBody(e: t.Node): t.Statement { 72 | if (t.isSequenceExpression(e)) { 73 | return t.blockStatement(e.expressions.map((exp) => t.expressionStatement(exp))); 74 | } 75 | if (t.isLogicalExpression(e)) { 76 | return this.parseConditionalIf(e, e.operator === '&&'); 77 | } 78 | if (t.isExpression(e)) { 79 | return t.expressionStatement(e); 80 | } 81 | if (t.isStatement(e)) { 82 | return e; 83 | } 84 | throw new Error(`Unexpected conversion of ${e.type} to statement body`); 85 | } 86 | 87 | private convertToReturnBody(e: t.Node): t.Statement { 88 | if (t.isSequenceExpression(e)) { 89 | return t.blockStatement(e.expressions.map((exp, i) => (i + 1 === e.expressions.length ? t.returnStatement(exp) : t.expressionStatement(exp)))); 90 | } 91 | if (t.isExpression(e)) { 92 | return t.returnStatement(e); 93 | } 94 | throw new Error(`Unexpected conversion of ${e.type} to return body`); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/decompilers/longhanders/longBooleans.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { isNumericLiteral, booleanLiteral } from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Converts `!1` and `!0` to `false` and `true` respectively 22 | */ 23 | export default class LongBooleans extends Plugin { 24 | readonly pass = 1; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | UnaryExpression(path) { 29 | const node = path.node; 30 | if (node.operator !== '!' || !isNumericLiteral(node.argument) || (node.argument.value !== 0 && node.argument.value !== 1)) return; 31 | path.replaceWith(booleanLiteral(!node.argument.value)); 32 | }, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/decompilers/longhanders/voidZeroToUndefined.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { identifier, isNumericLiteral } from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Converts `void 0` to `undefined` 22 | */ 23 | export default class VoidZeroToUndefined extends Plugin { 24 | readonly pass = 1; 25 | 26 | getVisitor(): Visitor { 27 | return { 28 | UnaryExpression(path) { 29 | if (path.node.operator !== 'void' || !isNumericLiteral(path.node.argument) || path.node.argument.value !== 0) return; 30 | path.replaceWith(identifier('undefined')); 31 | }, 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/decompilers/mappers/requireMapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import Module from '../../module'; 19 | import { Plugin } from '../../plugin'; 20 | 21 | /** 22 | * Maps the webpack requires to their file/NPM counterparts (that we generate) 23 | */ 24 | export default class RequireMapper extends Plugin { 25 | readonly pass = 1; 26 | 27 | getVisitor(): Visitor { 28 | return { 29 | CallExpression: (path) => { 30 | if (!t.isIdentifier(path.node.callee)) return; 31 | 32 | const moduleDependency = this.getModuleDependency(path); 33 | if (moduleDependency == null) return; 34 | 35 | const varDeclar = path.find((p) => p.isVariableDeclarator()); 36 | const varName = varDeclar?.isVariableDeclarator() && t.isIdentifier(varDeclar.node.id) ? varDeclar.node.id.name : null; 37 | 38 | if (moduleDependency.isPolyfill && varName) { 39 | this.bindingTraverse(path.scope.bindings[varName], varName, { 40 | MemberExpression: (bPath) => { 41 | if (!t.isIdentifier(bPath.node.object) || !t.isIdentifier(bPath.node.property)) return; 42 | if (bPath.node.object.name !== varName || bPath.node.property.name !== 'default') return; 43 | 44 | bPath.replaceWith(bPath.node.object); 45 | }, 46 | }); 47 | path.scope.rename(varName, moduleDependency.npmModuleVarName); 48 | varDeclar?.remove(); 49 | return; 50 | } 51 | 52 | path.get('arguments')[0].replaceWith(t.stringLiteral(this.generateModuleName(moduleDependency))); 53 | if (!varDeclar?.isVariableDeclarator()) return; 54 | if (!t.isIdentifier(varDeclar.node.id)) return; 55 | path.scope.rename(varDeclar.node.id.name, moduleDependency.npmModuleVarName || `module${moduleDependency.moduleId}`); 56 | }, 57 | }; 58 | } 59 | 60 | private generateModuleName(module: Module) { 61 | if (module.isNpmModule) { 62 | return module.moduleName; 63 | } 64 | if (module.isStatic && module) { 65 | return `./${module.moduleName}.css`; 66 | } 67 | return `./${module.moduleName}`; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/decompilers/react/jsxConverter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { 18 | isMemberExpression, 19 | isIdentifier, 20 | jsxElement, 21 | isObjectProperty, 22 | isStringLiteral, 23 | jsxAttribute, 24 | jsxIdentifier, 25 | isBooleanLiteral, 26 | jsxExpressionContainer, 27 | isExpression, 28 | JSXAttribute, 29 | jsxOpeningElement, 30 | jsxMemberExpression, 31 | JSXIdentifier, 32 | JSXMemberExpression, 33 | isObjectExpression, 34 | JSXElement, 35 | isCallExpression, 36 | jsxClosingElement, 37 | JSXExpressionContainer, 38 | JSXFragment, 39 | JSXSpreadChild, 40 | JSXText, 41 | Expression, 42 | jsxText, 43 | } from '@babel/types'; 44 | import { Plugin } from '../../plugin'; 45 | 46 | /** 47 | * Converts React.createElement to JSX 48 | */ 49 | export default class JSXConverter extends Plugin { 50 | readonly pass = 3; 51 | name = 'JSXConverter'; 52 | 53 | getVisitor(): Visitor { 54 | return { 55 | CallExpression: (path) => { 56 | if (!isMemberExpression(path.node.callee) || !isIdentifier(path.node.callee.object) || !isIdentifier(path.node.callee.property)) return; 57 | if (path.node.callee.object.name !== 'React' || path.node.callee.property.name !== 'createElement') return; 58 | 59 | path.replaceWith(this.parseJsx(path.node)); 60 | this.module.tags.push('jsx'); 61 | }, 62 | }; 63 | } 64 | 65 | private parseJsx(node: Expression): JSXText | JSXExpressionContainer | JSXSpreadChild | JSXElement | JSXFragment { 66 | if (isStringLiteral(node)) { 67 | return jsxText(node.value); 68 | } 69 | if (isCallExpression(node)) { 70 | const args = node.arguments; 71 | 72 | let name: JSXIdentifier | JSXMemberExpression | undefined; 73 | if (isIdentifier(args[0]) || isStringLiteral(args[0])) { 74 | name = jsxIdentifier(isIdentifier(args[0]) ? args[0].name : args[0].value); 75 | } else if (isMemberExpression(args[0]) && isIdentifier(args[0].object) && isIdentifier(args[0].property)) { 76 | name = jsxMemberExpression(jsxIdentifier(args[0].object.name), jsxIdentifier(args[0].property.name)); 77 | } else { 78 | this.debugLog(`fail to parse component ${args[0].type} inside callExpression`); 79 | return jsxExpressionContainer(node); 80 | } 81 | 82 | let props: JSXAttribute[] = []; 83 | if (isObjectExpression(args[1])) { 84 | props = args[1].properties.map((prop) => { 85 | if (!isObjectProperty(prop) || !isIdentifier(prop.key)) return null; 86 | if (isStringLiteral(prop.value)) { 87 | return jsxAttribute(jsxIdentifier(prop.key.name), prop.value); 88 | } 89 | if (isBooleanLiteral(prop.value) && prop.value.value) { 90 | return jsxAttribute(jsxIdentifier(prop.key.name), null); 91 | } 92 | if (isExpression(prop.value)) { 93 | return jsxAttribute(jsxIdentifier(prop.key.name), jsxExpressionContainer(prop.value)); 94 | } 95 | return null; 96 | }).filter((e): e is JSXAttribute => e != null); 97 | } 98 | 99 | const children = args.slice(2).map((e) => (isExpression(e) ? this.parseJsx(e) : null)).filter((e): e is JSXElement => e != null); 100 | 101 | if (children.length) { 102 | return jsxElement(jsxOpeningElement(name, props), jsxClosingElement(name), children); 103 | } 104 | 105 | return jsxElement(jsxOpeningElement(name, props, true), null, []); 106 | } 107 | 108 | this.debugLog(`fail to parse component ${node.type}`); 109 | return jsxExpressionContainer(node); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/decompilers/react/setStateRenamer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { 18 | isArrayPattern, 19 | isCallExpression, 20 | isMemberExpression, 21 | isIdentifier, 22 | } from '@babel/types'; 23 | import { Plugin } from '../../plugin'; 24 | 25 | /** 26 | * Does some renaming on Reach Hook setStates for clarity (changes setter function to `setX`) 27 | */ 28 | export default class SetStateRenamer extends Plugin { 29 | // has to run after array destructuring 30 | readonly pass = 4; 31 | 32 | getVisitor(): Visitor { 33 | return { 34 | VariableDeclaration: (path) => { 35 | path.node.declarations.forEach((varNode) => { 36 | // is it array destructure with 2 elements? 37 | if (!isArrayPattern(varNode.id) || varNode.id.elements.length !== 2 || !isIdentifier(varNode.id.elements[0]) || !isIdentifier(varNode.id.elements[1])) return; 38 | 39 | // is it defined by React.useState? 40 | if (!isCallExpression(varNode.init) || !isMemberExpression(varNode.init.callee)) return; 41 | if (!isIdentifier(varNode.init.callee.object) || !isIdentifier(varNode.init.callee.property)) return; 42 | if (varNode.init.callee.object.name !== 'React' || varNode.init.callee.property.name !== 'useState') return; 43 | path.parentPath.scope.crawl(); 44 | path.parentPath.scope.rename(varNode.id.elements[1].name, `set${varNode.id.elements[0].name[0].toUpperCase()}${varNode.id.elements[0].name.slice(1)}`); 45 | }); 46 | }, 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/editors/cleaners/esModuleCleaner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { 17 | isExpressionStatement, 18 | isCallExpression, 19 | isMemberExpression, 20 | isIdentifier, 21 | isStringLiteral, 22 | FunctionExpression, 23 | } from '@babel/types'; 24 | import { NodePath } from '@babel/traverse'; 25 | import { Plugin } from '../../plugin'; 26 | 27 | /** 28 | * Removes the ```Object.defineProperty(exports, '__esModule', { value: true });``` 29 | */ 30 | export default class EsModuleCleaner extends Plugin { 31 | readonly pass = 2; 32 | 33 | evaluate(path: NodePath): void { 34 | const bodyPath = this.navigateToModuleBody(path); 35 | 36 | bodyPath.node.body = bodyPath.node.body.filter((line) => { 37 | const callExpression = isExpressionStatement(line) ? line.expression : line; 38 | if (!isCallExpression(callExpression)) return true; 39 | if (!isMemberExpression(callExpression.callee)) return true; 40 | if (!isIdentifier(callExpression.callee.object) || !isIdentifier(callExpression.callee.property)) return true; 41 | if (callExpression.callee.object.name !== 'Object' || callExpression.callee.property.name !== 'defineProperty') return true; 42 | if (!isIdentifier(callExpression.arguments[0]) || !isStringLiteral(callExpression.arguments[1])) return true; 43 | if (bodyPath.scope.getBindingIdentifier(callExpression.arguments[0].name)?.start !== this.module.exportsParam?.start) return true; 44 | if (callExpression.arguments[1].value !== '__esModule') return true; 45 | 46 | this.module.tags.push('__esModule'); 47 | return false; 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/editors/converters/babelInlineConverters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { NodePath, Visitor } from '@babel/traverse'; 17 | import * as t from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Converts inlines to requires for decompilers 22 | */ 23 | export default class BabelInlineConverters extends Plugin { 24 | readonly pass = 2; 25 | name = 'BabelInlineConverters'; 26 | 27 | private interopRequireName?: string; 28 | private createClassName?: string; 29 | 30 | getVisitor(): Visitor { 31 | return { 32 | FunctionDeclaration: (nodePath) => { 33 | this.interopRequireDefaultFunction(nodePath); 34 | }, 35 | CallExpression: (nodePath) => { 36 | this.createClassFunctionInline(nodePath); 37 | this.classCallCheckInline(nodePath); 38 | this.assertThisInitalizedInline(nodePath); 39 | }, 40 | VariableDeclarator: (nodePath) => { 41 | this.interopRequireDefaultVarInline(nodePath); 42 | this.slicedToArrayFunction(nodePath); 43 | }, 44 | }; 45 | } 46 | 47 | private generateRequireDeclaration(name: t.Identifier, requireModule: string): t.VariableDeclaration { 48 | return t.variableDeclaration('const', [ 49 | t.variableDeclarator(name, t.callExpression(t.identifier('require'), [t.stringLiteral(requireModule)])), 50 | ]); 51 | } 52 | 53 | private generateRequireDeclarator(name: t.Identifier, requireModule: string): t.VariableDeclarator { 54 | return t.variableDeclarator(name, t.callExpression(t.identifier('require'), [t.stringLiteral(requireModule)])); 55 | } 56 | 57 | private interopRequireDefaultFunction(path: NodePath) { 58 | const body = path.node.body.body; 59 | if (path.node.params.length !== 1 || body.length !== 1 || !t.isIdentifier(path.node.id) || !t.isReturnStatement(body[0])) return; 60 | if (!t.isConditionalExpression(body[0].argument) || !t.isLogicalExpression(body[0].argument.test)) return; 61 | if (!t.isIdentifier(body[0].argument.test.left) || body[0].argument.test.operator !== '&&' || !t.isMemberExpression(body[0].argument.test.right)) return; 62 | const esModuleExpression = body[0].argument.test.right; 63 | if (!t.isIdentifier(esModuleExpression.object) || !t.isIdentifier(esModuleExpression.property) || body[0].argument.test.left.name !== esModuleExpression.object.name) return; 64 | if (esModuleExpression.property.name !== '__esModule') return; 65 | 66 | this.debugLog('removed inline babel interopRequireDefault function:'); 67 | this.debugLog(this.debugPathToCode(path)); 68 | 69 | if (this.interopRequireName) { 70 | path.scope.rename(path.node.id.name, this.interopRequireName); 71 | path.remove(); 72 | } else { 73 | this.interopRequireName = path.node.id.name; 74 | path.replaceWith(this.generateRequireDeclaration(path.node.id, '@babel/runtime/helpers/interopRequireDefault')); 75 | } 76 | this.addTag('babel-interop'); 77 | } 78 | 79 | private interopRequireDefaultVarInline(path: NodePath) { 80 | const node = path.node; 81 | const init = node.init; 82 | if (!t.isIdentifier(node.id)) return; 83 | if (!t.isConditionalExpression(init) || !t.isLogicalExpression(init.test) || !t.isIdentifier(init.consequent) || !t.isObjectExpression(init.alternate)) return; 84 | const test = init.test; 85 | if (!t.isAssignmentExpression(test.left) || !t.isIdentifier(test.left.left) || !t.isIdentifier(test.left.right)) return; 86 | if (!t.isMemberExpression(test.right) || !t.isIdentifier(test.right.object) || !t.isIdentifier(test.right.property)) return; 87 | if (test.left.left.name !== test.right.object.name || test.right.property.name !== '__esModule') return; 88 | 89 | const moduleSource = path.scope.getBinding(test.left.right.name); 90 | if (!moduleSource) return; 91 | const moduleSourcePath = moduleSource.path.find((p) => p.isVariableDeclarator()); 92 | if (moduleSourcePath == null || !moduleSourcePath.isVariableDeclarator() || !t.isIdentifier(moduleSourcePath.node.id)) return; 93 | 94 | this.debugLog('removed inline babel interopRequireDefault inline:'); 95 | this.debugLog(this.debugPathToCode(path)); 96 | 97 | this.mergeBindings(path, node.id.name, moduleSourcePath.node.id.name); 98 | 99 | path.scope.bindings[test.left.left.name].path.remove(); 100 | } 101 | 102 | private createClassFunctionInline(path: NodePath) { 103 | if (!t.isFunctionExpression(path.node.callee)) return; 104 | const body = path.node.callee.body.body; 105 | const lastLine = body[body.length - 1]; 106 | if (!t.isReturnStatement(lastLine) || !t.isFunctionExpression(lastLine.argument)) return; 107 | const returnBody = lastLine.argument.body.body; 108 | if (!t.isExpressionStatement(returnBody[0]) || !t.isExpressionStatement(returnBody[1]) || !t.isReturnStatement(returnBody[2])) return; 109 | if (!t.isLogicalExpression(returnBody[0].expression) || !t.isIdentifier(returnBody[0].expression.left) || !t.isCallExpression(returnBody[0].expression.right)) return; 110 | const testArgs = returnBody[0].expression.right.arguments; 111 | if (!t.isMemberExpression(testArgs[0]) || !t.isIdentifier(testArgs[1]) || !t.isIdentifier(testArgs[0].property) || testArgs[0].property.name !== 'prototype') return; 112 | 113 | this.debugLog('removed inline babel createClass function:'); 114 | this.debugLog(this.debugPathToCode(path)); 115 | 116 | const varDeclar = path.find((e) => e.isVariableDeclarator()); 117 | if (varDeclar == null || !varDeclar.isVariableDeclarator() || !t.isIdentifier(varDeclar.node.id)) return; 118 | 119 | if (this.createClassName) { 120 | path.scope.rename(varDeclar.node.id.name, this.createClassName); 121 | path.remove(); 122 | } else { 123 | this.createClassName = varDeclar.node.id.name; 124 | varDeclar.replaceWith(this.generateRequireDeclarator(varDeclar.node.id, '@babel/runtime/helpers/createClass')); 125 | } 126 | this.addTag('babel-createClass'); 127 | } 128 | 129 | private classCallCheckInline(path: NodePath) { 130 | if (!t.isFunctionExpression(path.node.callee) || path.node.arguments.length !== 2) return; 131 | if (!t.isThisExpression(path.node.arguments[0]) || !t.isIdentifier(path.node.arguments[1])) return; 132 | 133 | let hasErrorString = false; 134 | path.traverse({ 135 | StringLiteral: (stringPath) => { 136 | if (stringPath.node.value === 'Cannot call a class as a function') { 137 | hasErrorString = true; 138 | } 139 | }, 140 | }); 141 | if (!hasErrorString) return; 142 | 143 | const parent = path.find((p) => p.isExpressionStatement()); 144 | if (!parent) return; 145 | 146 | this.debugLog('removed inline babel classCallCheck function:'); 147 | this.debugLog(this.debugPathToCode(path)); 148 | 149 | parent.remove(); 150 | } 151 | 152 | private assertThisInitalizedInline(path: NodePath) { 153 | if (!t.isFunctionExpression(path.node.callee) || path.node.arguments.length !== 2) return; 154 | if (!t.isThisExpression(path.node.arguments[0]) || !t.isCallExpression(path.node.arguments[1])) return; 155 | 156 | let hasErrorString = false; 157 | path.traverse({ 158 | StringLiteral: (stringPath) => { 159 | if (stringPath.node.value === 'this hasn\'t been initialised - super() hasn\'t been called') { 160 | hasErrorString = true; 161 | } 162 | }, 163 | }); 164 | if (!hasErrorString) return; 165 | 166 | const parent = t.isReturnStatement(path.parent) ? path.parentPath : path.find((p) => p.isExpressionStatement()); 167 | if (!parent) return; 168 | 169 | this.debugLog('removed inline babel assertThisInitalized function:'); 170 | this.debugLog(this.debugPathToCode(path)); 171 | 172 | parent.remove(); 173 | } 174 | 175 | private slicedToArrayFunction(path: NodePath) { 176 | if (path.removed || !t.isCallExpression(path.node.init) || path.node.init.arguments.length !== 0) return; 177 | 178 | let hasErrorString = false; 179 | path.traverse({ 180 | StringLiteral: (stringPath) => { 181 | if (stringPath.node.value === 'Invalid attempt to destructure non-iterable instance') { 182 | hasErrorString = true; 183 | } 184 | }, 185 | }); 186 | if (!hasErrorString) return; 187 | 188 | this.debugLog('replace var incline babel slicedToArray function:'); 189 | this.debugLog(this.debugPathToCode(path)); 190 | 191 | path.get('init').replaceWith(t.callExpression(t.identifier('require'), [t.stringLiteral('@babel/runtime/helpers/slicedToArray')])); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/editors/editorList.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import CommaOperatorUnwrapper from './unwrappers/commaOperatorUnwrapper'; 17 | import EsModuleCleaner from './cleaners/esModuleCleaner'; 18 | import { PluginConstructor } from '../plugin'; 19 | import BabelInlineConverters from './converters/babelInlineConverters'; 20 | import NoUndefinedExport from './variables/noUndefinedExport'; 21 | 22 | const editorList: PluginConstructor[] = [ 23 | CommaOperatorUnwrapper, 24 | // pass 2 25 | BabelInlineConverters, 26 | EsModuleCleaner, 27 | NoUndefinedExport, 28 | ]; 29 | 30 | export default editorList; 31 | -------------------------------------------------------------------------------- /src/editors/unwrappers/commaOperatorUnwrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import * as t from '@babel/types'; 17 | import { Visitor } from '@babel/traverse'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Seperates statements wrapped in comma operations `(a, b)` into seperate lines 22 | */ 23 | export default class CommaOperatorUnwrapper extends Plugin { 24 | readonly pass = 1; 25 | readonly name = 'CommaOperatorUnwrapper'; 26 | 27 | getVisitor(): Visitor { 28 | return { 29 | ReturnStatement: (path) => { 30 | const argument = path.get('argument'); 31 | if (!argument.isSequenceExpression() || argument.get('expressions').length <= 1) return; 32 | const expressions = argument.get('expressions'); 33 | 34 | this.debugLog('ReturnStatement:'); 35 | this.debugLog(this.debugPathToCode(path)); 36 | 37 | path.insertBefore(this.sequenceExpressionToStatements(expressions.slice(0, -1).map((e) => e.node))); 38 | for (let i = 0; i < expressions.length - 1; i += 1) { 39 | expressions[i].remove(); 40 | } 41 | path.get('argument').replaceWith(expressions[expressions.length - 1]); 42 | }, 43 | VariableDeclaration: (path) => { 44 | const declarations = path.get('declarations'); 45 | declarations.forEach((declarator) => { 46 | const init = declarator.get('init'); 47 | if (!init.isSequenceExpression()) return; 48 | 49 | const validExpressions = init.get('expressions').filter((expression) => { 50 | if (!expression.isAssignmentExpression()) return true; 51 | if (!t.isIdentifier(expression.node.left)) return true; 52 | 53 | const matchingDeclaration = declarations.find((declar) => t.isIdentifier(declar.node.id) && declar.node.id.name === (expression.node.left).name); 54 | if (!matchingDeclaration) return true; 55 | 56 | matchingDeclaration.get('init').replaceWith(expression.get('right').node); 57 | expression.remove(); 58 | return false; 59 | }); 60 | 61 | path.insertBefore(this.sequenceExpressionToStatements(validExpressions.slice(0, -1).map((e) => e.node))); 62 | for (let i = 0; i < validExpressions.length - 1; i += 1) { 63 | validExpressions[i].remove(); 64 | } 65 | declarator.get('init').replaceWith(validExpressions[validExpressions.length - 1]); 66 | }); 67 | }, 68 | ExpressionStatement: (path) => { 69 | const expression = path.get('expression'); 70 | if (!expression.isSequenceExpression() || expression.get('expressions').length <= 1) return; 71 | 72 | this.debugLog('ExpressionStatement:'); 73 | this.debugLog(this.debugPathToCode(path)); 74 | 75 | path.replaceWithMultiple(this.sequenceExpressionToStatements(expression.node.expressions)); 76 | }, 77 | }; 78 | } 79 | 80 | private sequenceExpressionToStatements(expressions: t.Expression[]): t.Statement[] { 81 | const validExpressions = expressions.filter((exp) => { 82 | if (t.isMemberExpression(exp) && t.isIdentifier(exp.object) && t.isLiteral(exp.property)) return false; 83 | if (t.isMemberExpression(exp) && t.isIdentifier(exp.object) && t.isIdentifier(exp.property)) return false; 84 | return true; 85 | }); 86 | return validExpressions.map((exp) => t.expressionStatement(exp)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/editors/variables/noUndefinedExport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import * as t from '@babel/types'; 17 | import { Visitor } from '@babel/traverse'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | /** 21 | * Removes all instances of exports.? = undefined; 22 | */ 23 | export default class NoUndefinedExport extends Plugin { 24 | readonly pass = 2; 25 | name = 'NoUndefinedExport'; 26 | 27 | getVisitor(): Visitor { 28 | return { 29 | AssignmentExpression: (path) => { 30 | if (!t.isMemberExpression(path.node.left) || !t.isUnaryExpression(path.node.right) || !t.isIdentifier(path.node.left.object)) return; 31 | if (!t.isNumericLiteral(path.node.right.argument)) return; 32 | if (path.node.left.object.name !== 'exports' || path.node.right.operator !== 'void' || path.node.right.argument.value !== 0) return; 33 | 34 | const parentStatement = path.find((p) => p.isExpressionStatement()); 35 | if (!parentStatement) return; 36 | parentStatement.remove(); 37 | }, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/eslintConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Linter } from 'eslint'; 17 | 18 | // subset of rules from eslint-config-airbnb 19 | const eslintConfig: Linter.Config = { 20 | env: { 21 | node: true, 22 | }, 23 | parserOptions: { 24 | ecmaVersion: 2020, 25 | ecmaFeatures: { 26 | jsx: true, 27 | }, 28 | sourceType: 'module', 29 | }, 30 | plugins: [ 31 | 'react', 32 | 'import', 33 | ], 34 | rules: { 35 | 'one-var': ['error', 'never'], 36 | 'no-var': 'error', 37 | 'prefer-const': ['error', { 38 | destructuring: 'any', 39 | ignoreReadBeforeAssign: true, 40 | }], 41 | 'prefer-arrow-callback': ['error', { 42 | allowNamedFunctions: false, 43 | allowUnboundThis: true, 44 | }], 45 | eqeqeq: ['error', 'always', { null: 'ignore' }], 46 | semi: ['error', 'always'], 47 | 'arrow-body-style': ['error', 'as-needed', { 48 | requireReturnForObjectLiteral: false, 49 | }], 50 | 'no-confusing-arrow': ['error', { 51 | allowParens: true, 52 | }], 53 | 'eol-last': ['error', 'always'], 54 | indent: ['error', 2, { 55 | SwitchCase: 1, 56 | VariableDeclarator: 1, 57 | outerIIFEBody: 1, 58 | // MemberExpression: null, 59 | FunctionDeclaration: { 60 | parameters: 1, 61 | body: 1, 62 | }, 63 | FunctionExpression: { 64 | parameters: 1, 65 | body: 1, 66 | }, 67 | CallExpression: { 68 | arguments: 1, 69 | }, 70 | ArrayExpression: 1, 71 | ObjectExpression: 1, 72 | ImportDeclaration: 1, 73 | flatTernaryExpressions: false, 74 | // list derived from https://github.com/benjamn/ast-types/blob/HEAD/def/jsx.js 75 | // eslint-disable-next-line max-len 76 | ignoredNodes: ['JSXElement', 'JSXElement > *', 'JSXAttribute', 'JSXIdentifier', 'JSXNamespacedName', 'JSXMemberExpression', 'JSXSpreadAttribute', 'JSXExpressionContainer', 'JSXOpeningElement', 'JSXClosingElement', 'JSXFragment', 'JSXOpeningFragment', 'JSXClosingFragment', 'JSXText', 'JSXEmptyExpression', 'JSXSpreadChild'], 77 | ignoreComments: false, 78 | }], 79 | 'object-shorthand': ['error', 'always', { 80 | ignoreConstructors: false, 81 | avoidQuotes: true, 82 | }], 83 | 'comma-dangle': ['error', { 84 | arrays: 'always-multiline', 85 | objects: 'always-multiline', 86 | imports: 'always-multiline', 87 | exports: 'always-multiline', 88 | functions: 'always-multiline', 89 | }], 90 | 'space-before-function-paren': ['error', { 91 | anonymous: 'always', 92 | named: 'never', 93 | asyncArrow: 'always', 94 | }], 95 | curly: ['error', 'all'], 96 | 'block-spacing': ['error', 'always'], 97 | 'brace-style': ['error', '1tbs', { allowSingleLine: true }], 98 | yoda: 'error', 99 | 'no-trailing-spaces': ['error', { 100 | skipBlankLines: false, 101 | ignoreComments: false, 102 | }], 103 | 'prefer-template': 'error', 104 | 'template-curly-spacing': 'error', 105 | 'no-else-return': 'error', 106 | 'react/jsx-one-expression-per-line': 'error', 107 | 'no-undef-init': 'error', 108 | 'prefer-object-spread': 'error', 109 | 'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }], 110 | 'import/newline-after-import': 'error', 111 | 'import/first': 'error', 112 | }, 113 | }; 114 | 115 | export default eslintConfig; 116 | -------------------------------------------------------------------------------- /src/fileParsers/cacheParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import fs from 'fs-extra'; 17 | import crypto from 'crypto'; 18 | import path from 'path'; 19 | import * as babylon from '@babel/parser'; 20 | import traverse from '@babel/traverse'; 21 | import { CachedFile } from '../interfaces/cachedFile'; 22 | import CmdArgs from '../interfaces/cmdArgs'; 23 | import Module from '../module'; 24 | import FileParser from './fileParser'; 25 | import ProgressBar from '../util/progressBar'; 26 | 27 | export default class CacheParser implements FileParser { 28 | private readonly progressBar = ProgressBar.getInstance(); 29 | 30 | async canParse(args: CmdArgs): Promise { 31 | try { 32 | const cacheFilename = `${args.out}/${args.entry ?? 'null'}.cache`; 33 | const file: CachedFile = await fs.readJSON(cacheFilename); 34 | 35 | console.log('Cache detected, validating it...'); 36 | const currentChecksums = await this.generateInputChecksums(args.in); 37 | if (file.inputChecksum.some((checksum, i) => checksum !== currentChecksums[i])) { 38 | console.log('Cache invalidated due to checksum mismatch'); 39 | await fs.remove(cacheFilename); 40 | throw new Error('Cache was invalidated'); 41 | } 42 | console.log('Cache validated'); 43 | 44 | return true; 45 | } catch (e) { 46 | if (args.agressiveCache) throw new Error('A cache file must be generated before using --agressiveCache'); 47 | return false; 48 | } 49 | } 50 | 51 | private async generateInputChecksums(input: string): Promise { 52 | if ((await fs.lstat(input)).isDirectory()) { 53 | return fs.readdir(input) 54 | .then((fileNames) => Promise.all(fileNames.map((file) => fs.readFile(path.join(input, file))))) 55 | .then((files) => files.map((file) => crypto.createHash('md5').update(file).digest('hex'))); 56 | } 57 | 58 | return [crypto.createHash('md5').update(await fs.readFile(input)).digest('hex')]; 59 | } 60 | 61 | async parse(args: CmdArgs): Promise { 62 | console.log('Loading cache...'); 63 | 64 | const cacheFilename = `${args.out}/${args.entry ?? 'null'}.cache`; 65 | const cacheFile: CachedFile = await fs.readJSON(cacheFilename); 66 | 67 | const validCachedModules = cacheFile.modules.map((cachedModule) => { 68 | if (!args.agressiveCache || !cachedModule.ignored || cachedModule.isNpmModule) { 69 | return cachedModule; 70 | } 71 | return null; 72 | }); 73 | 74 | const modules: Module[] = []; 75 | this.progressBar.start(0, validCachedModules.length); 76 | 77 | validCachedModules.forEach((cached) => { 78 | if (cached == null) return; 79 | const canIgnoreModuleBody = args.agressiveCache && cached.isNpmModule && cached.code.length > 128; 80 | const originalFile = babylon.parse(canIgnoreModuleBody ? '(function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p){})' : cached.code); 81 | traverse(originalFile, { 82 | FunctionExpression(nodePath) { 83 | const module = new Module(originalFile, nodePath, cached.moduleId, args.agressiveCache ? cached.dependencies : cached.originalDependencies, cached.paramMappings); 84 | if (args.agressiveCache) { 85 | module.ignored = cached.ignored; 86 | module.isNpmModule = cached.isNpmModule; 87 | module.isPolyfill = cached.isPolyfill; 88 | module.isStatic = cached.isStatic; 89 | module.staticContent = cached.staticContent; 90 | module.moduleName = cached.moduleName; 91 | module.npmModuleVarName = cached.npmModuleVarName; 92 | } 93 | module.moduleStrings = cached.moduleStrings; 94 | module.moduleComments = cached.moduleComments; 95 | module.variableNames = new Set(cached.variableNames); 96 | module.originalCode = cached.code; 97 | module.previousRunChecksum = cached.previousRunChecksum; 98 | 99 | modules[cached.moduleId] = module; 100 | 101 | nodePath.skip(); 102 | }, 103 | }); 104 | this.progressBar.increment(); 105 | }); 106 | 107 | this.progressBar.stop(); 108 | 109 | return modules; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/fileParsers/fileParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import CmdArgs from '../interfaces/cmdArgs'; 17 | import Module from '../module'; 18 | 19 | export default interface FileParser { 20 | /** 21 | * Determines if this parser can parse the app with the given args 22 | * @param args The command line args 23 | */ 24 | canParse(args: CmdArgs): Promise; 25 | 26 | /** 27 | * Parses the source files from the given args into modules 28 | * @param args The command line args 29 | */ 30 | parse(args: CmdArgs): Promise; 31 | } 32 | -------------------------------------------------------------------------------- /src/fileParsers/fileParserRouter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import CmdArgs from '../interfaces/cmdArgs'; 17 | import Module from '../module'; 18 | import CacheParser from './cacheParser'; 19 | import FileParser from './fileParser'; 20 | import ReactNativeFolderParser from './reactNativeFolderParser'; 21 | import ReactNativeSingleParser from './reactNativeSingleParser'; 22 | import WebpackFolderParser from './webpackFolderParser'; 23 | import WebpackSingleParser from './webpackSingleParser'; 24 | 25 | /** 26 | * Attempts to route the cmd args to a valid file parser 27 | */ 28 | export default class FileParserRouter { 29 | private readonly list: FileParser[] = [ 30 | new CacheParser(), 31 | new ReactNativeSingleParser(), 32 | new ReactNativeFolderParser(), 33 | new WebpackSingleParser(), 34 | new WebpackFolderParser(), 35 | ]; 36 | 37 | async route(args: CmdArgs): Promise { 38 | const fileParser = await Promise.all(this.list.map((router) => router.canParse(args))) 39 | .then((results) => this.list[results.findIndex((e) => e)]); 40 | 41 | if (!fileParser) return null; 42 | 43 | return fileParser.parse(args); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/fileParsers/reactNativeFolderParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import fs from 'fs-extra'; 17 | import path from 'path'; 18 | import * as babylon from '@babel/parser'; 19 | import traverse from '@babel/traverse'; 20 | import { isIdentifier, isNumericLiteral } from '@babel/types'; 21 | import CmdArgs from '../interfaces/cmdArgs'; 22 | import Module from '../module'; 23 | import FileParser from './fileParser'; 24 | import PerformanceTracker from '../util/performanceTracker'; 25 | import ParamMappings from '../interfaces/paramMappings'; 26 | import ProgressBar from '../util/progressBar'; 27 | 28 | export default class ReactNativeFolderParser extends PerformanceTracker implements FileParser { 29 | private readonly SEVEN_PARAM_MAPPING: ParamMappings = { 30 | globals: 0, 31 | require: 1, 32 | module: 4, 33 | exports: 5, 34 | }; 35 | private readonly progressBar = ProgressBar.getInstance(); 36 | 37 | async canParse(args: CmdArgs): Promise { 38 | try { 39 | const fileNames = await fs.readdir(args.in); 40 | 41 | return fileNames.some((fileName) => { 42 | const file = fs.readFileSync(path.join(args.in, fileName), 'utf8'); 43 | return /__d\(function\([a-z],[a-z],[a-z],[a-z],[a-z],[a-z],[a-z]\)/.test(file) 44 | || /__d\(function\([a-z],[a-z],[a-z],[a-z],[a-z],[a-z]\)/.test(file) 45 | || /__d\(function\([a-z],[a-z],[a-z],[a-z],[a-z]\)/.test(file); 46 | }); 47 | } catch (e) { 48 | return false; 49 | } 50 | } 51 | 52 | async parse(args: CmdArgs): Promise { 53 | const fileNames = (await fs.readdir(args.in)).filter((fileName) => fileName.endsWith('.js')); 54 | 55 | console.log('Parsing folder...'); 56 | this.startTimer('parse'); 57 | this.progressBar.start(0, fileNames.length); 58 | 59 | const modules: Module[] = []; 60 | 61 | await Promise.all(fileNames.map(async (fileName) => { 62 | const file = await fs.readFile(path.join(args.in, fileName), 'utf8'); 63 | const ast = babylon.parse(file); 64 | 65 | traverse(ast, { 66 | CallExpression: (nodePath) => { 67 | if (isIdentifier(nodePath.node.callee) && nodePath.node.callee.name === '__d') { 68 | const functionArg = nodePath.get('arguments')[0]; 69 | const moduleId = nodePath.get('arguments')[1]; 70 | const dependencies = nodePath.get('arguments')[2]; 71 | if (functionArg.isFunctionExpression() && moduleId.isNumericLiteral() && dependencies.isArrayExpression() && functionArg.node.body.body.length) { 72 | const dependencyValues = dependencies.node.elements.map((e) => { 73 | if (!isNumericLiteral(e)) throw new Error('Not numeric literal'); 74 | return e.value; 75 | }); 76 | const newModule = new Module(ast, functionArg, moduleId.node.value, dependencyValues, this.SEVEN_PARAM_MAPPING); 77 | newModule.calculateFields(); 78 | modules[newModule.moduleId] = newModule; 79 | } 80 | } 81 | nodePath.skip(); 82 | }, 83 | }); 84 | 85 | this.progressBar.increment(); 86 | })); 87 | 88 | this.progressBar.stop(); 89 | this.stopAndPrintTime('parse'); 90 | 91 | return modules; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/fileParsers/reactNativeSingleParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import fs from 'fs-extra'; 17 | import * as babylon from '@babel/parser'; 18 | import traverse from '@babel/traverse'; 19 | import { isIdentifier, isNumericLiteral } from '@babel/types'; 20 | import CmdArgs from '../interfaces/cmdArgs'; 21 | import Module from '../module'; 22 | import FileParser from './fileParser'; 23 | import PerformanceTracker from '../util/performanceTracker'; 24 | import ParamMappings from '../interfaces/paramMappings'; 25 | 26 | export default class ReactNativeSingleParser extends PerformanceTracker implements FileParser { 27 | private readonly SEVEN_PARAM_MAPPING: ParamMappings = { 28 | globals: 0, 29 | require: 1, 30 | module: 4, 31 | exports: 5, 32 | }; 33 | 34 | async canParse(args: CmdArgs): Promise { 35 | try { 36 | const file = await fs.readFile(args.in, 'utf8'); 37 | 38 | return /__d\(function\([a-z],[a-z],[a-z],[a-z],[a-z],[a-z],[a-z]\)/.test(file) 39 | || /__d\(function\([a-z],[a-z],[a-z],[a-z],[a-z],[a-z]\)/.test(file) 40 | || /__d\(function\([a-z],[a-z],[a-z],[a-z],[a-z]\)/.test(file); 41 | } catch (e) { 42 | return false; 43 | } 44 | } 45 | 46 | async parse(args: CmdArgs): Promise { 47 | console.log('Parsing JS...'); 48 | this.startTimer('parse-js'); 49 | 50 | const file = await fs.readFile(args.in, 'utf8'); 51 | const ast = babylon.parse(file); 52 | 53 | this.stopAndPrintTime('parse-js'); 54 | 55 | const modules: Module[] = []; 56 | 57 | console.log('Finding modules...'); 58 | this.startTimer('find-modules'); 59 | 60 | traverse(ast, { 61 | CallExpression: (nodePath) => { 62 | if (isIdentifier(nodePath.node.callee) && nodePath.node.callee.name === '__d') { 63 | const functionArg = nodePath.get('arguments')[0]; 64 | const moduleId = nodePath.get('arguments')[1]; 65 | const dependencies = nodePath.get('arguments')[2]; 66 | if (functionArg.isFunctionExpression() && moduleId.isNumericLiteral() && dependencies.isArrayExpression() && functionArg.node.body.body.length) { 67 | const dependencyValues = dependencies.node.elements.map((e) => { 68 | if (!isNumericLiteral(e)) throw new Error('Not numeric literal'); 69 | return e.value; 70 | }); 71 | const newModule = new Module(ast, functionArg, moduleId.node.value, dependencyValues, this.SEVEN_PARAM_MAPPING); 72 | newModule.calculateFields(); 73 | modules[newModule.moduleId] = newModule; 74 | } 75 | } 76 | nodePath.skip(); 77 | }, 78 | }); 79 | 80 | this.stopAndPrintTime('find-modules'); 81 | 82 | return modules; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/fileParsers/webpackFolderParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import fs from 'fs-extra'; 17 | import path from 'path'; 18 | import * as babylon from '@babel/parser'; 19 | import CmdArgs from '../interfaces/cmdArgs'; 20 | import Module from '../module'; 21 | import FileParser from './fileParser'; 22 | import WebpackParser from './webpackParser'; 23 | 24 | export default class WebpackFolderParser extends WebpackParser implements FileParser { 25 | async canParse(args: CmdArgs): Promise { 26 | try { 27 | const fileNames = await fs.readdir(args.in); 28 | 29 | return fileNames.some((fileName) => this.fileIsWebpackEntry(fs.readFileSync(path.join(args.in, fileName), 'utf8'))); 30 | } catch (e) { 31 | return false; 32 | } 33 | } 34 | 35 | async parse(args: CmdArgs): Promise { 36 | console.log('Parsing JS...'); 37 | this.startTimer('parse-js'); 38 | 39 | const fileNames = await fs.readdir(args.in); 40 | const files = await Promise.all(fileNames.map((fileName) => fs.readFile(path.join(args.in, fileName), 'utf8'))); 41 | const asts = files.map((file) => babylon.parse(file)); 42 | 43 | this.stopAndPrintTime('parse-js'); 44 | 45 | const modules: Module[] = []; 46 | 47 | console.log('Finding modules...'); 48 | this.startTimer('find-modules'); 49 | 50 | asts.forEach((ast) => this.parseAst(ast, modules)); 51 | 52 | this.stopAndPrintTime('find-modules'); 53 | 54 | return modules; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/fileParsers/webpackParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import traverse, { NodePath } from '@babel/traverse'; 17 | import { 18 | isNumericLiteral, isFunctionExpression, isIdentifier, File, isMemberExpression, isAssignmentExpression, ArrayExpression, ObjectExpression, isStringLiteral, 19 | } from '@babel/types'; 20 | import ParamMappings from '../interfaces/paramMappings'; 21 | import Module from '../module'; 22 | import PerformanceTracker from '../util/performanceTracker'; 23 | 24 | export default class WebpackParser extends PerformanceTracker { 25 | private readonly PARAM_MAPPING: ParamMappings = { 26 | module: 0, 27 | exports: 1, 28 | require: 2, 29 | }; 30 | 31 | protected fileIsWebpackEntry = (file: string): boolean => file.includes('window.webpackHotUpdate') || (file.includes('"Loading chunk "') && file.includes('"ChunkLoadError"')); 32 | 33 | protected parseAst(ast: File, modules: Module[]): void { 34 | traverse(ast, { 35 | CallExpression: (nodePath) => { 36 | const firstArg = nodePath.get('arguments')[0]; 37 | if (isFunctionExpression(nodePath.node.callee) && firstArg?.isArrayExpression()) { // entrypoint 38 | this.parseArray(ast, firstArg, modules); 39 | } else if (isMemberExpression(nodePath.node.callee) && isAssignmentExpression(nodePath.node.callee.object) && firstArg?.isArrayExpression()) { // chunked 40 | const assignment = nodePath.node.callee.object; 41 | if (isMemberExpression(assignment.left)) { 42 | let leftPropName = ''; 43 | if (isIdentifier(assignment.left.property)) { 44 | leftPropName = assignment.left.property.name; 45 | } else if (isStringLiteral(assignment.left.property)) { 46 | leftPropName = assignment.left.property.value; 47 | } 48 | if (leftPropName.startsWith('webpackJsonp')) { 49 | const modulesObject = firstArg.get('elements')[1]; 50 | if (modulesObject.isArrayExpression()) { 51 | this.parseArray(ast, modulesObject, modules); 52 | } else { 53 | if (!modulesObject || !modulesObject.isObjectExpression()) throw new Error('Failed assertion'); 54 | this.parseObject(ast, modulesObject, modules); 55 | } 56 | } 57 | } 58 | } 59 | nodePath.skip(); 60 | }, 61 | }); 62 | } 63 | 64 | private parseArray(file: File, ast: NodePath, modules: Module[]): void { 65 | ast.get('elements').forEach((element, i) => { 66 | if (!element.isFunctionExpression()) return; 67 | if (element.node.body.body.length === 0) return; 68 | 69 | const dependencyValues: number[] = []; 70 | const requireIdentifer = element.node.params[2]; 71 | if (isIdentifier(requireIdentifer)) { 72 | element.traverse({ 73 | CallExpression: (dependencyPath) => { 74 | if (!isIdentifier(dependencyPath.node.callee) || !isNumericLiteral(dependencyPath.node.arguments[0])) return; 75 | if (dependencyPath.scope.bindingIdentifierEquals(dependencyPath.node.callee.name, requireIdentifer)) { 76 | dependencyValues[dependencyPath.node.arguments[0].value] = dependencyPath.node.arguments[0].value; 77 | } 78 | }, 79 | }); 80 | } 81 | 82 | const newModule = new Module(file, element, i, dependencyValues, this.PARAM_MAPPING); 83 | newModule.calculateFields(); 84 | modules[i] = newModule; 85 | }); 86 | } 87 | 88 | private parseObject(file: File, ast: NodePath, modules: Module[]): void { 89 | ast.get('properties').forEach((property) => { 90 | if (!property.isObjectProperty() || !isNumericLiteral(property.node.key)) return; 91 | 92 | const element = property.get('value'); 93 | const i = property.node.key.value; 94 | if (!element.isFunctionExpression()) return; 95 | if (element.node.body.body.length === 0) return; 96 | 97 | const dependencyValues: number[] = []; 98 | const requireIdentifer = element.node.params[2]; 99 | if (isIdentifier(requireIdentifer)) { 100 | element.traverse({ 101 | CallExpression: (dependencyPath) => { 102 | if (!isIdentifier(dependencyPath.node.callee) || !isNumericLiteral(dependencyPath.node.arguments[0])) return; 103 | if (dependencyPath.scope.bindingIdentifierEquals(dependencyPath.node.callee.name, requireIdentifer)) { 104 | dependencyValues[dependencyPath.node.arguments[0].value] = dependencyPath.node.arguments[0].value; 105 | } 106 | }, 107 | }); 108 | } 109 | 110 | const newModule = new Module(file, element, i, dependencyValues, this.PARAM_MAPPING); 111 | newModule.calculateFields(); 112 | modules[i] = newModule; 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/fileParsers/webpackSingleParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import fs from 'fs-extra'; 17 | import * as babylon from '@babel/parser'; 18 | import CmdArgs from '../interfaces/cmdArgs'; 19 | import Module from '../module'; 20 | import FileParser from './fileParser'; 21 | import WebpackParser from './webpackParser'; 22 | 23 | export default class WebpackSingleParser extends WebpackParser implements FileParser { 24 | async canParse(args: CmdArgs): Promise { 25 | try { 26 | const file = await fs.readFile(args.in, 'utf8'); 27 | 28 | return this.fileIsWebpackEntry(file); 29 | } catch (e) { 30 | return false; 31 | } 32 | } 33 | 34 | async parse(args: CmdArgs): Promise { 35 | console.log('Parsing JS...'); 36 | this.startTimer('parse-js'); 37 | 38 | const file = await fs.readFile(args.in, 'utf8'); 39 | const ast = babylon.parse(file); 40 | 41 | this.stopAndPrintTime('parse-js'); 42 | 43 | const modules: Module[] = []; 44 | 45 | console.log('Finding modules...'); 46 | this.startTimer('find-modules'); 47 | 48 | this.parseAst(ast, modules); 49 | 50 | this.stopAndPrintTime('find-modules'); 51 | 52 | return modules; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/interfaces/cachedFile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import ParamMappings from './paramMappings'; 17 | 18 | export interface CachedFile { 19 | inputChecksum: string[]; 20 | modules: CachedModule[]; 21 | } 22 | 23 | export interface CachedModule { 24 | code: string; 25 | moduleId: number; 26 | dependencies: number[]; 27 | originalDependencies: number[]; 28 | moduleStrings: string[]; 29 | moduleComments: string[]; 30 | variableNames: string[]; 31 | moduleName: string; 32 | npmModuleVarName?: string; 33 | isNpmModule: boolean; 34 | isPolyfill: boolean; 35 | isStatic: boolean; 36 | staticContent: string; 37 | ignored: boolean; 38 | previousRunChecksum: string; 39 | paramMappings: ParamMappings; 40 | } 41 | -------------------------------------------------------------------------------- /src/interfaces/cmdArgs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | export default interface CmdArgs { 17 | in: string; 18 | out: string; 19 | bundlesFolder: string; 20 | entry: number; 21 | performance: boolean; 22 | es6: boolean; 23 | verbose: boolean; 24 | decompileIgnored: boolean; 25 | /** skips some cache checks at the expense of possible cache desync */ 26 | agressiveCache: boolean; 27 | noEslint: boolean; 28 | noPrettier: boolean; 29 | unpackOnly: boolean; 30 | noProgress: boolean; 31 | debug: number; 32 | } 33 | -------------------------------------------------------------------------------- /src/interfaces/paramMappings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | /** 17 | * Maps param indexes to what they really mean 18 | */ 19 | export default interface ParamMappings { 20 | globals?: number; 21 | require?: number; 22 | module?: number; 23 | exports?: number; 24 | } 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | React Native Decompiler 5 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | import fsExtra from 'fs-extra'; 19 | import { performance } from 'perf_hooks'; 20 | import prettier from 'prettier'; 21 | import generator from '@babel/generator'; 22 | import commandLineArgs from 'command-line-args'; 23 | import chalk from 'chalk'; 24 | import crypto from 'crypto'; 25 | import { ESLint } from 'eslint'; 26 | import Module from './module'; 27 | import taggerList from './taggers/taggerList'; 28 | import editorList from './editors/editorList'; 29 | import Router from './router'; 30 | import decompilerList from './decompilers/decompilerList'; 31 | import CacheParse from './cacheParse'; 32 | import eslintConfig from './eslintConfig'; 33 | import CmdArgs from './interfaces/cmdArgs'; 34 | import FileParserRouter from './fileParsers/fileParserRouter'; 35 | import PerformanceTracker from './util/performanceTracker'; 36 | import ProgressBar from './util/progressBar'; 37 | 38 | function calculateModulesToIgnore(argValues: CmdArgs, modules: Module[]): Module[] { 39 | if (argValues.agressiveCache) return []; 40 | return modules.filter((mod) => { 41 | const dependentModules = modules.filter((otherMod) => otherMod.dependencies.includes(mod.moduleId)); 42 | return !mod.ignored && dependentModules.length > 0 && dependentModules.every((otherMod) => otherMod.ignored || mod.dependencies.includes(otherMod.moduleId)); 43 | }); 44 | } 45 | 46 | const argValues = commandLineArgs([ 47 | { name: 'in', alias: 'i' }, 48 | { name: 'out', alias: 'o' }, 49 | { name: 'entry', alias: 'e', type: Number }, 50 | { name: 'performance', alias: 'p', type: Boolean }, 51 | { name: 'verbose', alias: 'v', type: Boolean }, 52 | { name: 'es6', type: Boolean }, 53 | { name: 'noEslint', type: Boolean }, 54 | { name: 'noPrettier', type: Boolean }, 55 | { name: 'decompileIgnored', type: Boolean }, 56 | { name: 'agressiveCache', type: Boolean }, 57 | { name: 'unpackOnly', type: Boolean }, 58 | { name: 'noProgress', type: Boolean }, 59 | { name: 'debug', type: Number }, 60 | ]); 61 | if (!argValues.in || !argValues.out) { 62 | console.log(`react-native-decompiler 63 | Example command: react-native-decompiler -i index.android.bundle -o ./output 64 | 65 | Command params: 66 | 67 | -i (required) - the path to the input file/folder 68 | -o (required) - the path to the output folder 69 | -e - a module ID, if specified will only decompile that module & it's dependencies. also creates cache file to speed up future load times (useful for developing new plugins) 70 | -p - performance monitoring flag, will print out runtime for each decompiler plugin 71 | -v - verbose flag, does not include debug logging (use DEBUG=react-native-decompiler:* env flag for that) 72 | --es6 - attempts to decompile to ES6 module syntax. 73 | --noEslint - does not run ESLint after doing decompilation 74 | --noPrettier - does not run Prettier after doing decompilation 75 | --unpackOnly - only unpacks the app with no other adjustments 76 | --decompileIgnored - decompile ignored modules(modules are generally ignored if they are flagged as an NPM module) 77 | --agressiveCache - skips some cache checks at the expense of possible cache desync`); 78 | process.exit(0); 79 | } 80 | if (argValues.performance) { 81 | PerformanceTracker.enable(); 82 | } 83 | if (argValues.noProgress) { 84 | ProgressBar.disable(); 85 | } 86 | 87 | async function start() { 88 | try { 89 | const progressBar = ProgressBar.getInstance(); 90 | const cacheFileName = `${argValues.out}/${argValues.entry ?? 'null'}.cache`; 91 | let startTime = performance.now(); 92 | 93 | fsExtra.ensureDirSync(argValues.out); 94 | 95 | if (!fsExtra.existsSync(argValues.in)) { 96 | console.error(`${chalk.red('[!]')} "${argValues.in}" does not exist!"`); 97 | process.exit(1); 98 | } 99 | 100 | console.log('Reading file...'); 101 | 102 | const fileParserRouter = new FileParserRouter(); 103 | const modules = await fileParserRouter.route(argValues); 104 | 105 | if (modules == null || modules.length === 0) { 106 | console.error(`${chalk.red('[!]')} No modules were found!`); 107 | console.error(`${chalk.red('[!]')} Possible reasons:`); 108 | console.error(`${chalk.red('[!]')} - The React Native app is unbundled. If it is, export the "js-modules" folder from the app and provide it as the --js-modules argument`); 109 | console.error(`${chalk.red('[!]')} - The bundle is a Hermes/binary file (ex. Facebook, Instagram). These files are not supported`); 110 | console.error(`${chalk.red('[!]')} - The provided Webpack bundle input is not or does not contain the entrypoint bundle`); 111 | console.error(`${chalk.red('[!]')} - The provided Webpack bundle was built from V5, which is not supported`); 112 | console.error(`${chalk.red('[!]')} - The file provided is not a React Native or Webpack bundle.`); 113 | process.exit(1); 114 | } 115 | 116 | if (argValues.entry != null && (!argValues.agressiveCache)) { 117 | console.log('Entry module provided, filtering out unused modules'); 118 | const entryModuleDependencies = new Set(); 119 | let lastDependenciesSize = 0; 120 | 121 | entryModuleDependencies.add(argValues.entry); 122 | 123 | while (lastDependenciesSize !== entryModuleDependencies.size) { 124 | lastDependenciesSize = entryModuleDependencies.size; 125 | entryModuleDependencies.forEach((moduleId) => { 126 | const module = modules.find((mod) => mod?.moduleId === moduleId); 127 | if (module) { 128 | module.dependencies.forEach((dep) => entryModuleDependencies.add(dep)); 129 | } 130 | }); 131 | } 132 | 133 | modules.forEach((mod, i) => { 134 | if (!entryModuleDependencies.has(mod.moduleId)) { 135 | delete modules[i]; 136 | } 137 | }); 138 | } 139 | 140 | let nonIgnoredModules = modules.filter((mod) => argValues.decompileIgnored || !mod.ignored); 141 | 142 | console.log(`Took ${performance.now() - startTime}ms`); 143 | startTime = performance.now(); 144 | console.log('Pre-parsing modules...'); 145 | 146 | progressBar.start(0, nonIgnoredModules.length); 147 | nonIgnoredModules.forEach((module) => { 148 | module.validate(); 149 | module.unpack(); 150 | 151 | progressBar.increment(); 152 | }); 153 | 154 | progressBar.stop(); 155 | console.log(`Took ${performance.now() - startTime}ms`); 156 | 157 | if (!argValues.unpackOnly) { 158 | startTime = performance.now(); 159 | console.log('Tagging...'); 160 | progressBar.start(0, nonIgnoredModules.length); 161 | 162 | const taggerRouters = nonIgnoredModules.map((m) => new Router(taggerList, m, modules, argValues)); 163 | for (let pass = 1; pass <= taggerRouters[0].maxPass; pass += 1) { 164 | taggerRouters.forEach((r) => r.runPass(pass)); 165 | if (pass === taggerRouters[0].maxPass) { 166 | progressBar.increment(); 167 | } 168 | } 169 | 170 | progressBar.stop(); 171 | if (argValues.performance) { 172 | console.log(`Traversal took ${Router.traverseTimeTaken}ms`); 173 | console.log(Router.timeTaken); 174 | Router.timeTaken = {}; 175 | Router.traverseTimeTaken = 0; 176 | } 177 | console.log(`Took ${performance.now() - startTime}ms`); 178 | startTime = performance.now(); 179 | 180 | console.log('Filtering out modules only depended on ignored modules...'); 181 | 182 | let modulesToIgnore: Module[] = []; 183 | 184 | modulesToIgnore = calculateModulesToIgnore(argValues, modules); 185 | while (modulesToIgnore.length) { 186 | modulesToIgnore.forEach((mod) => { 187 | mod.ignored = true; 188 | }); 189 | modulesToIgnore = calculateModulesToIgnore(argValues, modules); 190 | } 191 | 192 | if (argValues.verbose) { 193 | console.table(modules.map((mod) => { 194 | const dependentModules = modules.filter((otherMod) => otherMod.dependencies.includes(mod.moduleId)); 195 | return { 196 | // moduleId: mod.moduleId, 197 | moduleName: mod.moduleName, 198 | ignored: mod.ignored, 199 | dependencies: mod.dependencies.filter((e) => e != null), 200 | dependents: dependentModules.map((m) => m.moduleId), 201 | }; 202 | })); 203 | console.table(modules.filter((m) => !m.ignored || m.isNpmModule).map((mod) => { 204 | const dependentModules = modules.filter((otherMod) => otherMod.dependencies.includes(mod.moduleId)); 205 | if (mod.isNpmModule && !dependentModules.filter((m) => !m.ignored).length) return null; 206 | return { 207 | // moduleId: mod.moduleId, 208 | moduleName: mod.moduleName, 209 | ignored: mod.ignored, 210 | dependencies: mod.dependencies.filter((e) => e != null), 211 | dependents: dependentModules.filter((m) => !m.ignored).map((m) => m.moduleId), 212 | }; 213 | }).filter((e) => e != null)); 214 | } 215 | 216 | nonIgnoredModules = modules.filter((mod) => argValues.decompileIgnored || !mod.ignored); 217 | 218 | console.log(`${nonIgnoredModules.length} remain to be decompiled`); 219 | 220 | console.log(`Took ${performance.now() - startTime}ms`); 221 | startTime = performance.now(); 222 | console.log('Decompiling...'); 223 | progressBar.start(0, nonIgnoredModules.length); 224 | 225 | const editorRouters = nonIgnoredModules.map((m) => new Router(editorList, m, modules, argValues)); 226 | for (let pass = 1; pass <= editorRouters[0].maxPass; pass += 1) { 227 | editorRouters.forEach((r) => r.runPass(pass)); 228 | } 229 | 230 | const decompilerRouter = nonIgnoredModules.map((m) => new Router(decompilerList, m, modules, argValues)); 231 | for (let pass = 1; pass <= decompilerRouter[0].maxPass; pass += 1) { 232 | decompilerRouter.forEach((r) => r.runPass(pass)); 233 | if (pass === decompilerRouter[0].maxPass) { 234 | progressBar.increment(); 235 | } 236 | } 237 | 238 | progressBar.stop(); 239 | if (argValues.performance) { 240 | console.log(`Traversal took ${Router.traverseTimeTaken}ms`); 241 | console.log(`Recrawl took ${Router.recrawlTimeTaken}ms`); 242 | console.log(Router.timeTaken); 243 | } 244 | console.log(`Took ${performance.now() - startTime}ms`); 245 | } 246 | 247 | startTime = performance.now(); 248 | console.log('Generating code...'); 249 | progressBar.start(0, nonIgnoredModules.length); 250 | 251 | const eslint = new ESLint({ 252 | fix: true, 253 | ignore: false, 254 | useEslintrc: false, 255 | extensions: ['.js', '.jsx'], 256 | overrideConfig: eslintConfig, 257 | }); 258 | 259 | const generatedFiles = await Promise.all(nonIgnoredModules.map(async (module) => { 260 | if (module.previousRunChecksum === crypto.createHash('md5').update(JSON.stringify(module.moduleCode.body)).digest('hex')) return null; 261 | const returnValue = { 262 | name: module.moduleId, 263 | extension: module.tags.includes('jsx') ? 'jsx' : 'js', 264 | code: generator({ 265 | ...module.originalFile.program, 266 | type: 'Program', 267 | body: module.moduleCode.body, 268 | }).code, 269 | }; 270 | if (!argValues.noEslint && !argValues.unpackOnly) { 271 | try { 272 | const lintedCode = await eslint.lintText(returnValue.code); 273 | returnValue.code = lintedCode[0].output ?? returnValue.code; 274 | } catch (e) {} 275 | } 276 | if (!argValues.noPrettier) { 277 | try { 278 | returnValue.code = prettier.format(returnValue.code, { parser: 'babel', singleQuote: true, printWidth: 180 }); 279 | } catch (e) {} 280 | } 281 | progressBar.increment(); 282 | return returnValue; 283 | })); 284 | 285 | progressBar.stop(); 286 | console.log(`Took ${performance.now() - startTime}ms`); 287 | startTime = performance.now(); 288 | console.log('Saving...'); 289 | progressBar.start(0, nonIgnoredModules.length); 290 | 291 | generatedFiles.forEach((file) => { 292 | if (file == null) return; 293 | const filePath = `${argValues.out}/${file.name}.${file.extension}`; 294 | if (!fsExtra.existsSync(filePath) || fsExtra.readFileSync(filePath, 'utf-8') !== file.code) { 295 | fsExtra.writeFileSync(filePath, file.code); 296 | } 297 | progressBar.increment(); 298 | }); 299 | modules.forEach((m) => { 300 | if (!m.isStatic) return; 301 | const filePath = `${argValues.out}/${m.moduleId}.${m.tags.includes('css') ? 'css' : '?'}`; 302 | if (!fsExtra.existsSync(filePath) || fsExtra.readFileSync(filePath, 'utf-8') !== m.staticContent) { 303 | fsExtra.writeFileSync(filePath, m.staticContent); 304 | } 305 | }); 306 | 307 | progressBar.stop(); 308 | 309 | if (!fsExtra.existsSync(cacheFileName) || !argValues.agressiveCache) { 310 | console.log('Writing to cache...'); 311 | await new CacheParse(argValues).writeCache(cacheFileName, modules); 312 | } 313 | 314 | console.log(`Took ${performance.now() - startTime}ms`); 315 | console.log('Done!'); 316 | } catch (e) { 317 | console.error(`${chalk.red('[!]')} Error occurred! You should probably report this.`); 318 | console.error(e); 319 | process.exit(1); 320 | } 321 | } 322 | 323 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 324 | start(); 325 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { NodePath } from '@babel/traverse'; 17 | import generator from '@babel/generator'; 18 | import crypto from 'crypto'; 19 | import { 20 | Identifier, BlockStatement, File, FunctionExpression, expressionStatement, 21 | } from '@babel/types'; 22 | import ParamMappings from './interfaces/paramMappings'; 23 | import { CachedModule } from './interfaces/cachedFile'; 24 | 25 | export default class Module { 26 | /** The original file that held this module */ 27 | originalFile: File; 28 | /** The root path describing the function enclosing the module in the original file */ 29 | rootPath: NodePath; 30 | /** The module code */ 31 | moduleCode: BlockStatement; 32 | /** The ID of the module */ 33 | moduleId: number; 34 | /** The dependencies of this module */ 35 | dependencies: number[]; 36 | /** The param mapping used */ 37 | private paramMappings: ParamMappings; 38 | /** Original deps used for cache */ 39 | private originalDependencies: number[]; 40 | 41 | /** The module's global variable */ 42 | globalsParam?: Identifier; 43 | /** The module's require variable */ 44 | requireParam?: Identifier; 45 | /** The module's module variable */ 46 | moduleParam?: Identifier; 47 | /** The module's exports variable */ 48 | exportsParam?: Identifier; 49 | 50 | originalCode = ''; 51 | previousRunChecksum = ''; 52 | moduleStrings: string[] = []; 53 | moduleComments: string[] = []; 54 | variableNames: Set = new Set(); 55 | 56 | // modifiable fields 57 | /** The name of the module */ 58 | moduleName: string; 59 | /** The variable to use if this is an NPM module */ 60 | npmModuleVarName?: string; 61 | /** If this is a NPM module */ 62 | isNpmModule = false; 63 | /** If this is a polyfill */ 64 | isPolyfill = false; 65 | /** If this is a static content. You should also set the ignored flag */ 66 | isStatic = false; 67 | /** If this is static content, what the content is */ 68 | staticContent = ''; 69 | /** If the module should not be decompiled nor outputted */ 70 | ignored = false; 71 | /** If the module failed to decompile */ 72 | failedToDecompile = false; 73 | /** The module tags */ 74 | tags: string[] = []; 75 | 76 | constructor(originalFile: File, rootPath: NodePath, moduleId: number, dependencies: number[], paramMappings: ParamMappings) { 77 | this.originalFile = originalFile; 78 | this.rootPath = rootPath; 79 | this.moduleId = moduleId; 80 | this.dependencies = dependencies; 81 | this.originalDependencies = dependencies; 82 | this.paramMappings = paramMappings; 83 | 84 | this.moduleCode = rootPath.node.body; 85 | this.moduleName = this.moduleId.toString(); 86 | 87 | this.globalsParam = this.getFunctionParam(paramMappings.globals); 88 | this.requireParam = this.getFunctionParam(paramMappings.require); 89 | this.moduleParam = this.getFunctionParam(paramMappings.module); 90 | this.exportsParam = this.getFunctionParam(paramMappings.exports); 91 | } 92 | 93 | private getFunctionParam(index?: number): Identifier | undefined { 94 | if (index == null) return undefined; 95 | const param = this.rootPath.get('params')[index]; 96 | if (!param || !param.isIdentifier()) return undefined; 97 | return param.node; 98 | } 99 | 100 | calculateFields(): void { 101 | this.originalCode = generator({ 102 | ...this.originalFile.program, 103 | type: 'Program', 104 | body: [expressionStatement(this.rootPath.node)], 105 | }, { compact: true }).code; 106 | 107 | this.rootPath.traverse({ 108 | StringLiteral: (path) => { 109 | this.moduleStrings.push(path.node.value); 110 | }, 111 | Identifier: (path) => { 112 | if (path.node.name.length > 1) { 113 | this.variableNames.add(path.node.name); 114 | } 115 | }, 116 | }); 117 | 118 | this.moduleComments = this.originalFile.comments 119 | ?.filter((comment) => this.rootPath.node.start && this.rootPath.node.end && comment.start > this.rootPath.node.start && comment.end < this.rootPath.node.end) 120 | ?.map((comment) => comment.value) || []; 121 | } 122 | 123 | validate(): void { 124 | if (!this.originalCode) throw new Error('Original code is required'); 125 | if (!this.moduleStrings) throw new Error('Module strings is required'); 126 | if (!this.moduleComments) throw new Error('Module comments is required'); 127 | } 128 | 129 | unpack(): void { 130 | if (this.globalsParam?.name) { 131 | this.rootPath.scope.rename(this.globalsParam?.name, 'globals'); 132 | } 133 | if (this.requireParam?.name) { 134 | this.rootPath.scope.rename(this.requireParam?.name, 'require'); 135 | } 136 | if (this.moduleParam?.name) { 137 | this.rootPath.scope.rename(this.moduleParam?.name, 'module'); 138 | } 139 | if (this.exportsParam?.name) { 140 | this.rootPath.scope.rename(this.exportsParam?.name, 'exports'); 141 | } 142 | } 143 | 144 | toCache(): CachedModule { 145 | return { 146 | code: this.originalCode, 147 | dependencies: this.dependencies, 148 | originalDependencies: this.originalDependencies, 149 | ignored: this.ignored, 150 | isNpmModule: this.isNpmModule, 151 | isPolyfill: this.isPolyfill, 152 | isStatic: this.isStatic, 153 | staticContent: this.staticContent, 154 | moduleId: this.moduleId, 155 | moduleName: this.moduleName, 156 | moduleStrings: this.moduleStrings, 157 | moduleComments: this.moduleComments, 158 | variableNames: [...this.variableNames], 159 | paramMappings: this.paramMappings, 160 | npmModuleVarName: this.npmModuleVarName, 161 | previousRunChecksum: crypto.createHash('md5').update(JSON.stringify(this.moduleCode.body)).digest('hex'), 162 | }; 163 | } 164 | 165 | debugToCode(): string { 166 | return generator({ 167 | ...this.originalFile.program, 168 | type: 'Program', 169 | body: this.moduleCode.body, 170 | }).code; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import * as t from '@babel/types'; 17 | import generator from '@babel/generator'; 18 | import { Binding, NodePath, Visitor } from '@babel/traverse'; 19 | import debug from 'debug'; 20 | import Module from './module'; 21 | import CmdArgs from './interfaces/cmdArgs'; 22 | 23 | export interface PluginConstructor { 24 | new(cmdArgs: CmdArgs, module: Module, moduleList: Module[]): T; 25 | } 26 | 27 | export abstract class Plugin { 28 | /** Which pass this plugin should run. Starts at pass #1. Set to 0 or less on construction to skip. */ 29 | abstract readonly pass: number; 30 | /** The name of the plugin */ 31 | readonly name?: string; 32 | protected readonly cmdArgs: CmdArgs; 33 | protected readonly module: Module; 34 | protected readonly moduleList: Module[]; 35 | 36 | constructor(cmdArgs: CmdArgs, module: Module, moduleList: Module[]) { 37 | this.cmdArgs = cmdArgs; 38 | this.module = module; 39 | this.moduleList = moduleList; 40 | } 41 | 42 | /** 43 | * Get a visitor that contains the plugin parsing. Use this for simplier plugins. 44 | * Do not use path.skip() or path.stop() if your plugin uses this method. 45 | */ 46 | getVisitor?(rerunPlugin: (pluginConstructor: PluginConstructor) => void): Visitor; 47 | 48 | /** Do a full evaluation. Use this for advanced plugins, or for plugins that don't do traversals. */ 49 | evaluate?(block: NodePath, rerunPlugin: (pluginConstructor: PluginConstructor) => void): void; 50 | 51 | /** Runs after the pass completes. Note that the AST of the module may have changed if you stored stuff in getVisitor or evaluate. */ 52 | afterPass?(rerunPlugin: (pluginConstructor: PluginConstructor) => void): void; 53 | 54 | private getDebugName() { 55 | return `react-native-decompiler:${this.name ?? 'plugin'}-${this.module.moduleId}`; 56 | } 57 | 58 | protected debugLog(formatter: unknown, ...args: unknown[]): void { 59 | debug(this.getDebugName())(formatter, ...args); 60 | } 61 | 62 | /** 63 | * [DEBUG] Returns the code of the path 64 | * @param path The path to generate code from 65 | */ 66 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 67 | protected debugPathToCode(path: NodePath): string { 68 | if (!debug(this.getDebugName()).enabled) return ''; 69 | return generator({ 70 | ...this.module.originalFile.program, 71 | type: 'Program', 72 | body: [t.isStatement(path.node) ? path.node : t.expressionStatement(path.node)], 73 | }).code; 74 | } 75 | 76 | protected navigateToModuleBody(path: NodePath): NodePath { 77 | return path.get('body'); 78 | } 79 | 80 | protected hasTag(tag: string): boolean { 81 | return this.module.tags.includes(tag); 82 | } 83 | 84 | protected addTag(tag: string): void { 85 | this.module.tags.push(tag); 86 | } 87 | 88 | protected variableIsForDependency(path: NodePath | NodePath, dep: string | string[]): boolean { 89 | const depArray = dep instanceof Array ? dep : [dep]; 90 | 91 | if (path.isVariableDeclarator()) { 92 | const callExpression = path.get('init'); 93 | if (!callExpression.isCallExpression()) return false; 94 | 95 | const requireValue = t.isStringLiteral(callExpression.node.arguments[0]) ? callExpression.node.arguments[0].value : null; 96 | const dependencyName = this.getModuleDependency(callExpression)?.moduleName ?? requireValue ?? ''; 97 | 98 | return depArray.includes(dependencyName); 99 | } 100 | if (path.isImportDeclaration()) { 101 | if (!t.isStringLiteral(path.node.source)) return false; 102 | 103 | return depArray.includes(path.node.source.value); 104 | } 105 | return false; 106 | } 107 | 108 | protected getModuleDependency(path: NodePath): Module | null { 109 | if (!t.isIdentifier(path.node.callee)) return null; 110 | if (!t.isNumericLiteral(path.node.arguments[0]) && !t.isMemberExpression(path.node.arguments[0]) && !t.isStringLiteral(path.node.arguments[0])) return null; 111 | if (path.scope.getBindingIdentifier(path.node.callee.name)?.start !== this.module.requireParam?.start) return null; 112 | 113 | if (t.isMemberExpression(path.node.arguments[0]) && t.isNumericLiteral(path.node.arguments[0].property)) { 114 | return this.moduleList[this.module.dependencies[path.node.arguments[0].property.value]] ?? null; 115 | } 116 | 117 | if (t.isStringLiteral(path.node.arguments[0])) { 118 | const nonNpmRegexTest = /\.\/([0-9]+)/.exec(path.node.arguments[0].value); 119 | if (nonNpmRegexTest != null) { 120 | return this.moduleList[this.module.dependencies[+nonNpmRegexTest[1]]]; 121 | } 122 | return this.moduleList.find((mod) => t.isStringLiteral(path.node.arguments[0]) && mod?.moduleName === path.node.arguments[0].value) ?? null; 123 | } 124 | 125 | if (t.isNumericLiteral(path.node.arguments[0])) { 126 | return this.moduleList[this.module.dependencies[path.node.arguments[0].value]] ?? null; 127 | } 128 | 129 | return null; 130 | } 131 | 132 | /** 133 | * Does a visit of all nodes within the scope of the giving binding 134 | * @param binding The binding to set traversing bounds 135 | * @param varName The variable name for the binding 136 | * @param visitor A visitor object 137 | */ 138 | protected bindingTraverse(binding: Binding, varName: string, visitor: Visitor): void { 139 | binding.scope.traverse(binding.scope.block, { 140 | ...visitor, 141 | Scope: (path) => { 142 | if (!path.scope.bindingIdentifierEquals(varName, binding.identifier)) { 143 | path.skip(); 144 | } 145 | }, 146 | }); 147 | } 148 | 149 | /** 150 | * Merges the given variable into another 151 | * @param path The path that declares the variable. This path will be deleted afterwards! 152 | * @param from The name of the variable to be removed 153 | * @param to The name of the variable to merge into 154 | */ 155 | protected mergeBindings(path: NodePath, from: string, to: string): void { 156 | const oldBinding = path.scope.bindings[to]; 157 | path.scope.rename(from, to); 158 | path.remove(); 159 | path.scope.bindings[to] = oldBinding; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { performance } from 'perf_hooks'; 17 | import { NodePath } from '@babel/traverse'; 18 | import { PluginConstructor, Plugin } from './plugin'; 19 | import Module from './module'; 20 | import CmdArgs from './interfaces/cmdArgs'; 21 | 22 | export default class Router> { 23 | static traverseTimeTaken = 0; 24 | static recrawlTimeTaken = 0; 25 | static timeTaken: { [index: string]: number } = {}; 26 | 27 | readonly maxPass: number; 28 | private readonly module: Module; 29 | private readonly moduleList: Module[]; 30 | private readonly list: T[]; 31 | private readonly listConstructors: TConstructor[]; 32 | private readonly args: CmdArgs; 33 | 34 | constructor(list: TConstructor[], module: Module, moduleList: Module[], args: CmdArgs) { 35 | this.listConstructors = list; 36 | this.args = args; 37 | this.list = list.map((PluginToLoad) => { 38 | if (this.args.performance && Router.timeTaken[PluginToLoad.name] == null) { 39 | Router.timeTaken[PluginToLoad.name] = 0; 40 | } 41 | return new PluginToLoad(args, module, moduleList); 42 | }); 43 | this.maxPass = Math.max(...this.list.map((plugin) => plugin.pass)); 44 | 45 | this.module = module; 46 | this.moduleList = moduleList; 47 | } 48 | 49 | runPass = (pass: number): void => { 50 | if (this.module.failedToDecompile) return; 51 | try { 52 | const passPlugins = this.list.map((plugin, index) => ({ plugin, index })).filter(({ plugin }) => plugin.pass === pass); 53 | 54 | if (this.args.debug === this.module.moduleId) { 55 | this.runDebugPass(passPlugins.map(({ plugin }) => plugin)); 56 | } 57 | 58 | let startTime = performance.now(); 59 | const visitorFunctions: { [index: string]: ((path: NodePath) => void)[] } = {}; 60 | 61 | passPlugins.forEach(({ plugin, index }) => { 62 | if (plugin.evaluate) { 63 | this.performanceTrack(this.listConstructors[index].name, () => plugin.evaluate && plugin.evaluate(this.module.rootPath, this.runPlugin)); 64 | } else if (plugin.getVisitor) { 65 | // disable some eslint rules from object mapping 66 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ 67 | const visitor: any = plugin.getVisitor(this.runPlugin); 68 | Object.keys(visitor).forEach((key) => { 69 | if (!visitorFunctions[key]) { 70 | visitorFunctions[key] = []; 71 | } 72 | if (this.args.performance) { 73 | visitorFunctions[key].push((path: NodePath) => { 74 | Router.traverseTimeTaken += performance.now() - startTime; 75 | this.performanceTrack(this.listConstructors[index].name, () => visitor[key](path)); 76 | startTime = performance.now(); 77 | }); 78 | } else { 79 | visitorFunctions[key].push(visitor[key]); 80 | } 81 | }); 82 | } else { 83 | throw new Error('Plugin does not have getVisitor nor evaluate'); 84 | } 85 | }); 86 | 87 | const visitor: any = {}; 88 | Object.keys(visitorFunctions).forEach((key) => { 89 | visitor[key] = this.processVisit(visitorFunctions[key]); 90 | }); 91 | /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ 92 | if (Object.keys(visitor).length > 0) { 93 | startTime = performance.now(); 94 | this.module.rootPath.traverse(visitor); 95 | Router.traverseTimeTaken += performance.now() - startTime; 96 | } 97 | 98 | passPlugins.forEach(({ plugin, index }) => { 99 | this.performanceTrack(this.listConstructors[index].name, () => plugin.afterPass && plugin.afterPass(this.runPlugin)); 100 | }); 101 | } catch (e) { 102 | console.error(`An error occured parsing module ${this.module.moduleId}, it will be outputted as is!`); 103 | console.error(e); 104 | this.module.failedToDecompile = true; 105 | } 106 | }; 107 | 108 | private runDebugPass = (passPlugins: Plugin[]): void => { 109 | let lastCode = ''; 110 | passPlugins.forEach((plugin) => { 111 | if (plugin.evaluate) { 112 | plugin.evaluate(this.module.rootPath, this.runPlugin); 113 | } else if (plugin.getVisitor) { 114 | this.module.rootPath.traverse(plugin.getVisitor(this.runPlugin)); 115 | } else { 116 | throw new Error('Plugin does not have getVisitor nor evaluate'); 117 | } 118 | }); 119 | passPlugins.forEach((plugin) => { 120 | if (plugin.afterPass) { 121 | plugin.afterPass(this.runPlugin); 122 | } 123 | console.log('after', plugin.name ?? 'unknown_name:'); 124 | const newCode = this.module.debugToCode(); 125 | if (lastCode !== newCode) { 126 | console.log(newCode); 127 | lastCode = newCode; 128 | } else { 129 | console.log('No change'); 130 | } 131 | }); 132 | }; 133 | 134 | private performanceTrack = (key: string, cb: () => void): void => { 135 | if (!this.args.performance) { 136 | cb(); 137 | } else { 138 | const startTime = performance.now(); 139 | cb(); 140 | Router.timeTaken[key] += performance.now() - startTime; 141 | } 142 | }; 143 | 144 | private processVisit = (plugins: ((path: NodePath) => void)[]) => (path: NodePath): void => { 145 | plugins.forEach((fn) => fn(path)); 146 | }; 147 | 148 | private runPlugin = (PluginToRun: PluginConstructor): void => { 149 | const plugin = new PluginToRun(this.args, this.module, this.moduleList); 150 | if (plugin.evaluate) { 151 | plugin.evaluate(this.module.rootPath, this.runPlugin); 152 | } else if (plugin.getVisitor) { 153 | this.module.rootPath.traverse(plugin.getVisitor(this.runPlugin)); 154 | } else { 155 | throw new Error('Plugin does not have getVisitor nor evaluate'); 156 | } 157 | if (plugin.afterPass) { 158 | plugin.afterPass(this.runPlugin); 159 | } 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /src/taggers/npmModuleFinders/babelModuleFinder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | import ModuleFinder from './moduleFinder'; 16 | 17 | /** 18 | * Finds babel modules 19 | */ 20 | export default class BabelModuleFinder extends ModuleFinder { 21 | private readonly moduleMap: Record = { 22 | '@babel/runtime/helpers/classCallCheck': [ 23 | // 'Cannot call a class as a function', 24 | ], 25 | '@babel/runtime/helpers/toConsumableArray': [ 26 | /{var .=.\(.\[0]\),.=.\(.\[1]\),.=.\(.\[2]\),.=.\(.\[3]\);.\.exports=function\(.\){return .\(.\)\|\|.\(.\)\|\|.\(.\)\|\|.\(\);};}/, 27 | ], 28 | '@babel/runtime/helpers/slicedToArray': [ 29 | /{var .=.\(.\[0]\),.=.\(.\[1]\),.=.\(.\[2]\),.=.\(.\[3]\);.\.exports=function\(.,.\){return .\(.\)\|\|.\(.,.\)\|\|.\(.,.\)\|\|.\(\);};}/, 30 | ], 31 | '@babel/runtime/helpers/interopRequireDefault': [ 32 | /.\.exports=function\(.\){return .&&.\.__esModule\?.:{default:.}/, 33 | /.\.exports=function\(obj\){return obj&&obj\.__esModule\?obj:{default:obj}/, 34 | ], 35 | '@babel/runtime/helpers/interopRequireWildcard': [ 36 | /function .\(\){if\("function"!=typeof WeakMap\)return null;var .=new WeakMap\(\);return .=function\(\){return .;},.;}/, 37 | ], 38 | '@babel/runtime/helpers/createClass': [ 39 | /.\.exports=function\(.,.,.\){return .&&.\(.\.prototype,.\),.&&.\(.,.\),.;};/, 40 | ], 41 | '@babel/runtime/helpers/defineEnumerableProperties': [ 42 | /.\.exports=function\(.,.\){if\(null==.\)return{};var .,.,.=.\(.,.\);if\(Object\.getOwnPropertySymbols\){var .=Object\.getOwnPropertySymbols\(.\);/, 43 | ], 44 | }; 45 | 46 | evaluate(): void { 47 | Object.keys(this.moduleMap).forEach((moduleName) => { 48 | const matchers = this.moduleMap[moduleName]; 49 | if (matchers.some((matcher) => (matcher instanceof RegExp ? matcher.test(this.module.originalCode) : this.module.moduleStrings.includes(matcher)))) { 50 | this.tagAsNpmModule(moduleName); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/taggers/npmModuleFinders/moduleFinder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { NodePath } from '@babel/traverse'; 17 | import { FunctionExpression } from '@babel/types'; 18 | import { Plugin } from '../../plugin'; 19 | 20 | export default abstract class ModuleFinder extends Plugin { 21 | readonly pass = 1; 22 | 23 | protected tagAsNpmModule(moduleName: string, varName?: string): void { 24 | if (this.module.isNpmModule && this.module.moduleName !== moduleName) { 25 | throw new Error(`Module #${this.module.moduleId} is already the ${this.module.moduleName} module but tried to re-tag as ${moduleName}`); 26 | } 27 | 28 | this.module.isNpmModule = true; 29 | this.module.ignored = true; 30 | this.module.moduleName = moduleName; 31 | this.module.npmModuleVarName = varName; 32 | } 33 | 34 | abstract evaluate(path: NodePath): void; 35 | } 36 | -------------------------------------------------------------------------------- /src/taggers/npmModuleFinders/polyfillModuleFinder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | /* eslint-disable max-len */ 17 | import ModuleFinder from './moduleFinder'; 18 | 19 | /** 20 | * Simple searcher of polyfills 21 | */ 22 | export default class PolyfillModuleFinder extends ModuleFinder { 23 | name = 'PolyfillModuleFinder'; 24 | 25 | private readonly commentMappings: Record = { 26 | }; 27 | 28 | private readonly stringMappings: Record = { 29 | }; 30 | 31 | private readonly codeRegexMappings: Record = { 32 | ResizeObserver: [/var .=void 0!==.\.ResizeObserver\?.\.ResizeObserver:.;.\.default=./], 33 | }; 34 | 35 | evaluate(): void { 36 | const commentMappingMatch = Object.keys(this.commentMappings).find((key) => this.test(this.module.moduleComments, this.commentMappings[key])); 37 | if (commentMappingMatch) { 38 | this.debugLog(`${this.module.moduleId} matched module ${commentMappingMatch} via comment`); 39 | this.tagAsNpmModule(commentMappingMatch, commentMappingMatch); 40 | this.module.isPolyfill = true; 41 | return; 42 | } 43 | 44 | const stringMappingMatch = Object.keys(this.stringMappings).find((key) => this.test(this.module.moduleStrings, this.stringMappings[key])); 45 | if (stringMappingMatch) { 46 | this.debugLog(`${this.module.moduleId} matched module ${stringMappingMatch} via string`); 47 | this.tagAsNpmModule(stringMappingMatch, stringMappingMatch); 48 | this.module.isPolyfill = true; 49 | return; 50 | } 51 | 52 | const codeRegexMatch = Object.keys(this.codeRegexMappings).find((key) => this.regexTest(this.module.originalCode, this.codeRegexMappings[key])); 53 | if (codeRegexMatch) { 54 | this.debugLog(`${this.module.moduleId} matched module ${codeRegexMatch} via code regex`); 55 | this.tagAsNpmModule(codeRegexMatch, codeRegexMatch); 56 | this.module.isPolyfill = true; 57 | } 58 | } 59 | 60 | private test(moduleStrings: string[], stringsToFind: string[]): boolean { 61 | return stringsToFind.every((stringToFind) => moduleStrings.some((moduleString) => moduleString.includes(stringToFind))); 62 | } 63 | 64 | private regexTest(originalCode: string, regexes: RegExp[]): boolean { 65 | return regexes.every((regex) => regex.test(originalCode)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/taggers/npmModuleFinders/simpleModuleFinder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | /* eslint-disable max-len */ 17 | import ModuleFinder from './moduleFinder'; 18 | 19 | /** 20 | * Simple searcher of NPM modules through string matching 21 | */ 22 | export default class SimpleModuleFinder extends ModuleFinder { 23 | name = 'SimpleModuleFinder'; 24 | 25 | private readonly commentMappings: Record = { 26 | react: ['react.production.min.js'], 27 | 'react-dom': ['react-dom.production.min.js'], 28 | classnames: ['http://jedwatson.github.io/classnames'], 29 | 'safe-buffer': ['safe-buffer. MIT License. Feross Aboukhadijeh'], 30 | buffer: ['The buffer module from node.js, for the browser.'], 31 | }; 32 | 33 | private readonly stringMappings: Record = { 34 | 'react-dom': ['suspended while rendering, but no fallback UI was specified'], 35 | react: ['https://reactjs.org/docs/error-decoder.html?invariant='], 36 | 'react-native-web': ['Text strings must be rendered within a component.'], 37 | 'base64-js': ['Invalid string. Length must be a multiple of 4'], 38 | 'redux-react-hook': ['redux-react-hook requires your Redux store to be passed through context via the '], 39 | 'pusher-js': ['You must pass your app key when you instantiate Pusher.'], 40 | 'regenerator-runtime': ['try statement without catch or finally'], 41 | '@sentry/browser': ['addGlobalEventProcessor', 'getHubFromCarrier'], 42 | 'react-native': ['progress-bar-android-moved'], 43 | 'url-parse': ['^[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]+'], 44 | 'crypto-browserify': ['https://github.com/crypto-browserify/crypto-browserify'], 45 | 'style-loader': ['https://github.com/webpack-contrib/style-loader#insertat'], 46 | 'prop-types': ['Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types'], 47 | 'crypto-js': ['Native crypto module could not be used to get secure random number.'], 48 | 'react-i18next': ['pass in an i18next instance by using initReactI18next'], 49 | i18next: ['accessing an object - but returnObjects options is not enabled!'], 50 | }; 51 | 52 | private readonly codeRegexMappings: Record = { 53 | jsonwebtoken: [/verify:.\(/, /sign:.\(/, /JsonWebTokenError:.\(/, /NotBeforeError:.\(/, /TokenExpiredError:.\(/], 54 | 'asn1.js': [/.\.bignum=.\(/, /.\.define=.\(/, /.\.base=.\(/, /.\.constants=.\(/, /.\.decoders=.\(/, /.\.encoders=.\(/], 55 | elliptic: [/.\.base=.\(/, /.\.mont=.\(/, /.\.short=.\(/, /.\.edwards=.\(/], 56 | 'crypto-js/aes': [/encryptBlock:function\(.,.\){this\._doCryptBlock\(.,.,this\._keySchedule,.,.,.,.,.\);?}/], 57 | 'lz-string': [/compressToEncodedURIComponent:function\(.\){return null==.\?"":.\._compress\(.,6,function\(.\){return .\.charAt\(.\);?}\);?}/], 58 | }; 59 | 60 | private readonly moduleVarNames: Record = { 61 | react: 'React', 62 | 'react-dom': 'ReactDOM', 63 | 'base64-js': 'base64js', 64 | 'pusher-js': 'Pusher', 65 | 'regenerator-runtime': 'regeneratorRuntime', 66 | '@sentry/browser': 'Sentry', 67 | 'react-native': 'ReactNative', 68 | 'url-parse': 'Url', 69 | classnames: 'classnames', 70 | 'safe-buffer': 'Buffer', 71 | buffer: 'Buffer', 72 | 'crypto-browserify': 'crypto', 73 | 'prop-types': 'PropTypes', 74 | 'crypto-js': 'CryptoJS', 75 | 'crypto-js/aes': 'AES', 76 | jsonwebtoken: 'jwt', 77 | i18next: 'i18next', 78 | asn: 'asn', 79 | 'lz-string': 'LZString', 80 | }; 81 | 82 | evaluate(): void { 83 | const commentMappingMatch = Object.keys(this.commentMappings).find((key) => this.test(this.module.moduleComments, this.commentMappings[key])); 84 | if (commentMappingMatch) { 85 | this.debugLog(`${this.module.moduleId} matched module ${commentMappingMatch} via comment`); 86 | this.tagAsNpmModule(commentMappingMatch, this.moduleVarNames[commentMappingMatch]); 87 | return; 88 | } 89 | 90 | const stringMappingMatch = Object.keys(this.stringMappings).find((key) => this.test(this.module.moduleStrings, this.stringMappings[key])); 91 | if (stringMappingMatch) { 92 | this.debugLog(`${this.module.moduleId} matched module ${stringMappingMatch} via string`); 93 | this.tagAsNpmModule(stringMappingMatch, this.moduleVarNames[stringMappingMatch]); 94 | } 95 | 96 | const codeRegexMatch = Object.keys(this.codeRegexMappings).find((key) => this.regexTest(this.module.originalCode, this.codeRegexMappings[key])); 97 | if (codeRegexMatch) { 98 | this.debugLog(`${this.module.moduleId} matched module ${codeRegexMatch} via code regex`); 99 | this.tagAsNpmModule(codeRegexMatch, this.moduleVarNames[codeRegexMatch]); 100 | } 101 | } 102 | 103 | private test(moduleStrings: string[], stringsToFind: string[]): boolean { 104 | return stringsToFind.every((stringToFind) => moduleStrings.some((moduleString) => moduleString.includes(stringToFind))); 105 | } 106 | 107 | private regexTest(originalCode: string, regexes: RegExp[]): boolean { 108 | return regexes.every((regex) => regex.test(originalCode)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/taggers/remappers/passthroughModuleRemapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Visitor } from '@babel/traverse'; 17 | import { 18 | isMemberExpression, 19 | isIdentifier, 20 | } from '@babel/types'; 21 | import { Plugin } from '../../plugin'; 22 | 23 | /** 24 | * Resolves and bypasses modules that just export other modules. 25 | */ 26 | export default class PassthroughModuleRemapper extends Plugin { 27 | readonly pass = 2; 28 | name = 'PassthroughModuleRemapper'; 29 | 30 | getVisitor(): Visitor { 31 | if (this.module.moduleCode.body.length !== 1) return {}; 32 | 33 | return { 34 | AssignmentExpression: (path) => { 35 | if (!isMemberExpression(path.node.left) || !isIdentifier(path.node.left?.object) || !isIdentifier(path.node.left?.property)) return; 36 | if (path.scope.getBindingIdentifier(path.node.left.object.name)?.start !== this.module.moduleParam?.start) return; 37 | if (path.node.left.property.name !== 'exports') return; 38 | 39 | const right = path.get('right'); 40 | if (!right.isCallExpression()) return; 41 | const rightCallee = right.get('callee'); 42 | if (!rightCallee.isIdentifier() && !rightCallee.isCallExpression()) return; 43 | 44 | const dependency = this.getModuleDependency(rightCallee.isCallExpression() ? rightCallee : right); 45 | if (!dependency) return; 46 | if (rightCallee.isCallExpression() && !dependency.moduleStrings.find((str) => str.includes('Calling PropTypes validators directly is not supported'))) return; 47 | if (!this.moduleList.some((m) => m.dependencies.includes(this.module.moduleId))) return; 48 | 49 | this.debugLog(`bypassing ${this.module.moduleId} for ${dependency.moduleId} ${dependency.moduleName}`); 50 | 51 | const passthroughDependency = this.moduleList[dependency.moduleId]; 52 | this.module.ignored = true; 53 | this.module.isNpmModule = true; // flag as NPM module in case this module pass through NPM module 54 | this.module.moduleName = `${this.module.moduleId} PASSTHROUGH TO ${passthroughDependency.moduleName}`; 55 | this.moduleList.forEach((module) => { 56 | module.dependencies = module.dependencies.map((dep) => (dep === this.module.moduleId ? passthroughDependency.moduleId : dep)); 57 | }); 58 | }, 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/taggers/static/cssFinder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Plugin } from '../../plugin'; 17 | 18 | /** 19 | * Finds CSS files loaded by css-loader & style-loader 20 | */ 21 | export default class CssFinder extends Plugin { 22 | readonly pass = 2; 23 | name = 'CssFinder'; 24 | 25 | evaluate(): void { 26 | if (this.module.dependencies.filter((e) => e != null).length !== 2) return; 27 | 28 | const styleLoaderDep = this.module.dependencies.find((dep) => this.moduleList[dep] && this.moduleList[dep].moduleName === 'style-loader'); 29 | if (styleLoaderDep == null) return; 30 | 31 | const cssDep = this.module.dependencies.find((dep) => dep != null && dep !== styleLoaderDep); 32 | if (cssDep == null) return; 33 | 34 | this.module.isStatic = true; 35 | this.module.ignored = true; 36 | this.module.staticContent = this.moduleList[cssDep].moduleStrings[0]; 37 | this.module.tags.push('css'); 38 | 39 | this.moduleList[cssDep].ignored = true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/taggers/taggerList.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import PassthroughModuleRemapper from './remappers/passthroughModuleRemapper'; 17 | import SimpleModuleFinder from './npmModuleFinders/simpleModuleFinder'; 18 | import BabelModuleFinder from './npmModuleFinders/babelModuleFinder'; 19 | import { PluginConstructor } from '../plugin'; 20 | import EmptyIgnorer from './vanilla/emptyIgnorer'; 21 | import PolyfillModuleFinder from './npmModuleFinders/polyfillModuleFinder'; 22 | import CssFinder from './static/cssFinder'; 23 | 24 | const taggerList: PluginConstructor[] = [ 25 | // pass 1 26 | EmptyIgnorer, 27 | SimpleModuleFinder, 28 | PolyfillModuleFinder, 29 | BabelModuleFinder, 30 | // pass 2 31 | PassthroughModuleRemapper, 32 | CssFinder, 33 | ]; 34 | 35 | export default taggerList; 36 | -------------------------------------------------------------------------------- /src/taggers/vanilla/emptyIgnorer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Plugin } from '../../plugin'; 17 | 18 | /** 19 | * Ignores empty files 20 | */ 21 | export default class EmptyIgnorer extends Plugin { 22 | readonly pass = 1; 23 | name = 'EmptyIgnorer'; 24 | 25 | evaluate(): void { 26 | if (this.module.rootPath.node.body.body.length === 0) { 27 | this.debugLog(`Ignored ${this.module.moduleId} because it is empty`); 28 | this.module.ignored = true; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/util/arrayMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | /** 17 | * Map that supports multiple values in one key 18 | */ 19 | export default class ArrayMap implements Map { 20 | private map: Map = new Map(); 21 | 22 | /** 23 | * Gets the given key array 24 | * @param key The key to get 25 | */ 26 | get(key: K): V[] { 27 | if (!this.map.get(key)) { 28 | this.map.set(key, []); 29 | } 30 | return this.map.get(key) ?? []; 31 | } 32 | 33 | /** 34 | * Returns if the given key has elements in it 35 | * @param key The key to find 36 | */ 37 | has(key: K): boolean { 38 | return this.get(key).length > 0; 39 | } 40 | 41 | /** 42 | * Pushes the given value into the given key 43 | * @param key The key to push a value into 44 | * @param value The value to push 45 | */ 46 | push(key: K, value: V): void { 47 | this.get(key).push(value); 48 | } 49 | 50 | /** 51 | * Iterates through each element of the given key 52 | * @param key The key to iterate through 53 | * @param fn The callback function 54 | */ 55 | forEachElement(key: K, fn: (value: V, index: number, array: V[]) => void): void { 56 | this.get(key).forEach(fn); 57 | } 58 | 59 | // passthrough functions 60 | get size(): number { 61 | return this.map.size; 62 | } 63 | clear(): void { 64 | this.map.clear(); 65 | } 66 | delete(key: K): boolean { 67 | return this.map.delete(key); 68 | } 69 | forEach(callbackfn: (value: V[], key: K, map: Map) => void, thisArg?: unknown): void { 70 | this.map.forEach(callbackfn, thisArg); 71 | } 72 | set(key: K, value: V[]): this { 73 | this.map.set(key, value); 74 | return this; 75 | } 76 | [Symbol.iterator](): IterableIterator<[K, V[]]> { 77 | return this.map.entries(); 78 | } 79 | entries(): IterableIterator<[K, V[]]> { 80 | return this.map.entries(); 81 | } 82 | keys(): IterableIterator { 83 | return this.map.keys(); 84 | } 85 | values(): IterableIterator { 86 | return this.map.values(); 87 | } 88 | [Symbol.toStringTag]: string; 89 | } 90 | -------------------------------------------------------------------------------- /src/util/bindingArrayMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import { Binding } from '@babel/traverse'; 17 | import ArrayMap from './arrayMap'; 18 | 19 | /** 20 | * Map that supports multiple values in one key 21 | */ 22 | export default class BindingArrayMap implements Map { 23 | private map: ArrayMap = new ArrayMap(); 24 | private innerBindingMap: ArrayMap = new ArrayMap(); 25 | 26 | get size(): number { 27 | let size = 0; 28 | this.innerBindingMap.forEach((e) => { 29 | size += e.length; 30 | }); 31 | return size; 32 | } 33 | 34 | get(key: Binding): V[] { 35 | const bindingIndex = this.findBindingIndex(key); 36 | return bindingIndex === -1 ? [] : this.map.get(key.identifier.name)[bindingIndex]; 37 | } 38 | 39 | push(key: Binding, value: V): void { 40 | const bindingIndex = this.findOrPushBindingIndex(key); 41 | this.map.get(key.identifier.name)[bindingIndex].push(value); 42 | } 43 | 44 | forEachElement(key: Binding, callbackfn: (value: V) => void): void { 45 | this.get(key).forEach(callbackfn); 46 | } 47 | 48 | private findBindingIndex(binding: Binding): number { 49 | return this.innerBindingMap.get(binding.identifier.name).findIndex((e) => e === binding); 50 | } 51 | 52 | private findOrPushBindingIndex(binding: Binding): number { 53 | const bindingIndex = this.findBindingIndex(binding); 54 | if (bindingIndex === -1) { 55 | this.innerBindingMap.push(binding.identifier.name, binding); 56 | this.map.get(binding.identifier.name).push([]); 57 | return this.findBindingIndex(binding); 58 | } 59 | return bindingIndex; 60 | } 61 | 62 | clear(): void { 63 | throw new Error('Method not implemented.'); 64 | } 65 | delete(_key: Binding): boolean { 66 | throw new Error('Method not implemented.'); 67 | } 68 | forEach(_callbackfn: (value: V[], key: Binding, map: Map) => void, _thisArg?: unknown): void { 69 | throw new Error('Method not implemented.'); 70 | } 71 | has(_key: Binding): boolean { 72 | throw new Error('Method not implemented.'); 73 | } 74 | set(_key: Binding, _value: V[]): this { 75 | throw new Error('Method not implemented.'); 76 | } 77 | [Symbol.iterator](): IterableIterator<[Binding, V[]]> { 78 | throw new Error('Method not implemented.'); 79 | } 80 | entries(): IterableIterator<[Binding, V[]]> { 81 | throw new Error('Method not implemented.'); 82 | } 83 | keys(): IterableIterator { 84 | throw new Error('Method not implemented.'); 85 | } 86 | values(): IterableIterator { 87 | throw new Error('Method not implemented.'); 88 | } 89 | [Symbol.toStringTag]: string; 90 | } 91 | -------------------------------------------------------------------------------- /src/util/performanceTracker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import crypto from 'crypto'; 17 | import { performance } from 'perf_hooks'; 18 | 19 | export default class PerformanceTracker { 20 | private static performanceTimes: Record = {}; 21 | private static enabled = false; 22 | 23 | static isEnabled(): boolean { 24 | return this.enabled; 25 | } 26 | 27 | static enable(): void { 28 | this.enabled = true; 29 | } 30 | 31 | protected tag: string = crypto.randomBytes(20).toString('hex'); 32 | 33 | protected startTimer(tag = this.tag): void { 34 | if (!PerformanceTracker.enabled) return; 35 | PerformanceTracker.performanceTimes[tag] = performance.now(); 36 | } 37 | 38 | protected pauseTimer(tag = this.tag): void { 39 | if (!PerformanceTracker.enabled) return; 40 | 41 | PerformanceTracker.performanceTimes[tag] = this.stopTimer(); 42 | } 43 | 44 | protected unpauseTimer(tag = this.tag): void { 45 | if (!PerformanceTracker.enabled) return; 46 | const elapsedTime = PerformanceTracker.performanceTimes[tag]; 47 | if (!elapsedTime) throw new Error('Timer not paused'); 48 | 49 | PerformanceTracker.performanceTimes[tag] = performance.now() - elapsedTime; 50 | } 51 | 52 | protected stopTimer(tag = this.tag): number { 53 | if (!PerformanceTracker.enabled) return 0; 54 | const startTime = PerformanceTracker.performanceTimes[tag]; 55 | if (!startTime) throw new Error('Timer not started'); 56 | 57 | const totalTime = performance.now() - startTime; 58 | PerformanceTracker.performanceTimes[tag] = undefined; 59 | 60 | return totalTime; 61 | } 62 | 63 | protected stopAndPrintTime(tag = this.tag): void { 64 | if (!PerformanceTracker.enabled) return; 65 | console.log(`[${tag}] Took ${this.stopTimer(tag)}ms`); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/util/progressBar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React Native Decompiler 3 | Copyright (C) 2020-2022 Richard Fu, Numan and contributors 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | You should have received a copy of the GNU Affero General Public License 13 | along with this program. If not, see . 14 | */ 15 | 16 | import CliProgress from 'cli-progress'; 17 | 18 | export default class ProgressBar { 19 | private static instance?: ProgressBar; 20 | private progressBar = new CliProgress.SingleBar({ etaBuffer: 200 }, CliProgress.Presets.shades_classic); 21 | private static disabled = false; 22 | 23 | static getInstance(): ProgressBar { 24 | if (!this.instance) { 25 | this.instance = new ProgressBar(); 26 | } 27 | return this.instance; 28 | } 29 | 30 | static disable(): void { 31 | ProgressBar.disabled = true; 32 | } 33 | 34 | private constructor() {} 35 | 36 | /** 37 | * Starts the progress bar and set the total and initial value 38 | * @param startValue The initial value 39 | * @param total The max value 40 | */ 41 | start(startValue: number, total: number): void { 42 | if (ProgressBar.disabled) return; 43 | this.progressBar.start(total, startValue); 44 | } 45 | 46 | /** 47 | * Increments the progress bar 48 | * @param amount The amount to increment, default = 1 49 | */ 50 | increment(amount?: number): void { 51 | if (ProgressBar.disabled) return; 52 | this.progressBar.increment(amount); 53 | } 54 | 55 | /** 56 | * Stops the progress bar 57 | */ 58 | stop(): void { 59 | if (ProgressBar.disabled) return; 60 | this.progressBar.stop(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "ES2018", 6 | "noImplicitAny": true, 7 | "strict": true, 8 | "allowJs": false, 9 | "strictNullChecks": true, 10 | "removeComments": true, 11 | "preserveConstEnums": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "inlineSourceMap": true, 15 | "esModuleInterop": true, 16 | "incremental": true, 17 | "outDir": "out" 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "output", 25 | "**/*.spec.ts", 26 | "**/*.js", 27 | "**/*.android.bundle" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------