/** React Native Decompiler Copyright (C) 2020-2022 Richard Fu, Numan and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { NodePath } from '@babel/traverse'; import generator from '@babel/generator'; import crypto from 'crypto'; import { Identifier, BlockStatement, File, FunctionExpression, expressionStatement, } from '@babel/types'; import ParamMappings from './interfaces/paramMappings'; import { CachedModule } from './interfaces/cachedFile'; export default class Module { /** The original file that held this module */ originalFile: File; /** The root path describing the function enclosing the module in the original file */ rootPath: NodePath<FunctionExpression>; /** The module code */ moduleCode: BlockStatement; /** The ID of the module */ moduleId: number; /** The dependencies of this module */ dependencies: number[]; /** The param mapping used */ private paramMappings: ParamMappings; /** Original deps used for cache */ private originalDependencies: number[]; /** The module's global variable */ globalsParam?: Identifier; /** The module's require variable */ requireParam?: Identifier; /** The module's module variable */ moduleParam?: Identifier; /** The module's exports variable */ exportsParam?: Identifier; originalCode = ''; previousRunChecksum = ''; moduleStrings: string[] = []; moduleComments: string[] = []; variableNames: Set<string> = new Set(); // modifiable fields /** The name of the module */ moduleName: string; /** The variable to use if this is an NPM module */ npmModuleVarName?: string; /** If this is a NPM module */ isNpmModule = false; /** If this is a polyfill */ isPolyfill = false; /** If this is a static content. You should also set the ignored flag */ isStatic = false; /** If this is static content, what the content is */ staticContent = ''; /** If the module should not be decompiled nor outputted */ ignored = false; /** If the module failed to decompile */ failedToDecompile = false; /** The module tags */ tags: string[] = []; constructor(originalFile: File, rootPath: NodePath<FunctionExpression>, moduleId: number, dependencies: number[], paramMappings: ParamMappings) { this.originalFile = originalFile; this.rootPath = rootPath; this.moduleId = moduleId; this.dependencies = dependencies; this.originalDependencies = dependencies; this.paramMappings = paramMappings; this.moduleCode = rootPath.node.body; this.moduleName = this.moduleId.toString(); this.globalsParam = this.getFunctionParam(paramMappings.globals); this.requireParam = this.getFunctionParam(paramMappings.require); this.moduleParam = this.getFunctionParam(paramMappings.module); this.exportsParam = this.getFunctionParam(paramMappings.exports); } private getFunctionParam(index?: number): Identifier | undefined { if (index == null) return undefined; const param = this.rootPath.get('params')[index]; if (!param || !param.isIdentifier()) return undefined; return param.node; } calculateFields(): void { this.originalCode = generator({ ...this.originalFile.program, type: 'Program', body: [expressionStatement(this.rootPath.node)], }, { compact: true }).code; this.rootPath.traverse({ StringLiteral: (path) => { this.moduleStrings.push(path.node.value); }, Identifier: (path) => { if (path.node.name.length > 1) { this.variableNames.add(path.node.name); } }, }); this.moduleComments = this.originalFile.comments ?.filter((comment) => this.rootPath.node.start && this.rootPath.node.end && comment.start > this.rootPath.node.start && comment.end < this.rootPath.node.end) ?.map((comment) => comment.value) || []; } validate(): void { if (!this.originalCode) throw new Error('Original code is required'); if (!this.moduleStrings) throw new Error('Module strings is required'); if (!this.moduleComments) throw new Error('Module comments is required'); } unpack(): void { if (this.globalsParam?.name) { this.rootPath.scope.rename(this.globalsParam?.name, 'globals'); } if (this.requireParam?.name) { this.rootPath.scope.rename(this.requireParam?.name, 'require'); } if (this.moduleParam?.name) { this.rootPath.scope.rename(this.moduleParam?.name, 'module'); } if (this.exportsParam?.name) { this.rootPath.scope.rename(this.exportsParam?.name, 'exports'); } } toCache(): CachedModule { return { code: this.originalCode, dependencies: this.dependencies, originalDependencies: this.originalDependencies, ignored: this.ignored, isNpmModule: this.isNpmModule, isPolyfill: this.isPolyfill, isStatic: this.isStatic, staticContent: this.staticContent, moduleId: this.moduleId, moduleName: this.moduleName, moduleStrings: this.moduleStrings, moduleComments: this.moduleComments, variableNames: [...this.variableNames], paramMappings: this.paramMappings, npmModuleVarName: this.npmModuleVarName, previousRunChecksum: crypto.createHash('md5').update(JSON.stringify(this.moduleCode.body)).digest('hex'), }; } debugToCode(): string { return generator({ ...this.originalFile.program, type: 'Program', body: this.moduleCode.body, }).code; } }