├── .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 |
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 |
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 | [](#contributors-)
148 |
149 |
150 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
151 |
152 |
153 |
154 |
155 |
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 |
--------------------------------------------------------------------------------