• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import {
17  factory,
18  forEachChild,
19  isBreakOrContinueStatement,
20  isIdentifier,
21  isLabeledStatement,
22  isSourceFile,
23  setParentRecursive,
24  visitEachChild,
25} from 'typescript';
26
27import type {
28  Identifier,
29  Node,
30  SourceFile,
31  Symbol,
32  TransformationContext,
33  Transformer,
34  TransformerFactory,
35  TypeChecker
36} from 'typescript';
37
38import {
39  createScopeManager,
40  isClassScope,
41  isGlobalScope,
42  isEnumScope,
43  isInterfaceScope,
44  isObjectLiteralScope
45} from '../../utils/ScopeAnalyzer';
46
47import type {
48  Label,
49  Scope,
50  ScopeManager
51} from '../../utils/ScopeAnalyzer';
52
53import type {INameGenerator, NameGeneratorOptions} from '../../generator/INameGenerator';
54import type {IOptions} from '../../configs/IOptions';
55import type {INameObfuscationOption} from '../../configs/INameObfuscationOption';
56import type {TransformPlugin} from '../TransformPlugin';
57import {getNameGenerator, NameGeneratorType} from '../../generator/NameFactory';
58import {TypeUtils} from '../../utils/TypeUtils';
59import {collectIdentifiers} from '../../utils/TransformUtil';
60import {NodeUtils} from '../../utils/NodeUtils';
61
62namespace secharmony {
63  /**
64   * Rename Identifiers, including:
65   * 1. variable name
66   * 2. function name
67   * 3. label name
68   * 4. class name/interface name/ label name
69   * we need implement some features:
70   * 1. rename identifiers
71   * 2. store/restore name to/from nameCache file.
72   * 3. do scope analysis for identifier obfuscations
73   *
74   * @param option
75   */
76  const createRenameIdentifierFactory = function (option: IOptions): TransformerFactory<Node> {
77    const profile: INameObfuscationOption | undefined = option?.mNameObfuscation;
78    if (!profile || !profile.mEnable) {
79      return null;
80    }
81
82    const openTopLevel: boolean = option?.mTopLevel;
83    return renameIdentifierFactory;
84
85    function renameIdentifierFactory(context: TransformationContext): Transformer<Node> {
86      let reservedNames: string[] = [...(profile?.mReservedNames ?? []), 'this', '__global'];
87      let mangledSymbolNames: Map<Symbol, string> = new Map<Symbol, string>();
88      let mangledLabelNames: Map<Label, string> = new Map<Label, string>();
89
90      let options: NameGeneratorOptions = {};
91      if (profile.mNameGeneratorType === NameGeneratorType.HEX) {
92        options.hexWithPrefixSuffix = true;
93      }
94
95      let generator: INameGenerator = getNameGenerator(profile.mNameGeneratorType, options);
96
97      let historyMangledNames: Set<string> = undefined;
98      if (historyNameCache && historyNameCache.size > 0) {
99        historyMangledNames = new Set<string>(Array.from(historyNameCache.values()));
100      }
101
102      let checker: TypeChecker = undefined;
103      let manager: ScopeManager = createScopeManager();
104      let shadowIdentifiers: Identifier[] = undefined;
105
106      let identifierIndex: number = 0;
107      return renameTransformer;
108
109      /**
110       * Transformer to rename identifiers
111       *
112       * @param node ast node of a file.
113       */
114      function renameTransformer(node: Node): Node {
115        if (!isSourceFile(node)) {
116          return node;
117        }
118
119        const shadowSourceAst: SourceFile = TypeUtils.createNewSourceFile(node);
120        checker = TypeUtils.createChecker(shadowSourceAst);
121        manager.analyze(shadowSourceAst, checker);
122
123        manager.getReservedNames().forEach((name) => {
124          reservedNames.push(name);
125        });
126        // collect all identifiers of shadow sourceFile
127        shadowIdentifiers = collectIdentifiers(shadowSourceAst, context);
128
129        if (nameCache === undefined) {
130          nameCache = new Map<string, string>();
131        }
132
133        let root: Scope = manager.getRootScope();
134        renameInScope(root);
135        return setParentRecursive(visit(node), true);
136      }
137
138      /**
139       * rename symbol table store in scopes...
140       *
141       * @param scope scope, such as global, module, function, block
142       */
143      function renameInScope(scope: Scope): void {
144        // process labels in scope, the label can't rename as the name of top labels.
145        renameLabelsInScope(scope);
146        // process symbols in scope, exclude property name.
147        renameNamesInScope(scope);
148
149        for (const subScope of scope.children) {
150          renameInScope(subScope);
151        }
152      }
153
154      function renameNamesInScope(scope: Scope): void {
155        if (scope.parent) {
156          scope.parent.mangledNames.forEach((value) => {
157            scope.mangledNames.add(value);
158          });
159
160          scope.parent.importNames.forEach((value) => {
161            scope.importNames.add(value);
162          });
163        }
164
165        if (isExcludeScope(scope)) {
166          return;
167        }
168
169        scope.defs.forEach((def) => {
170          if (scope.importNames.has(def.name)) {
171            scope.defs.delete(def);
172          }
173        });
174
175        generator.reset();
176        renames(scope, scope.defs, generator);
177      }
178
179      function renames(scope: Scope, defs: Set<Symbol>, generator: INameGenerator): void {
180        const localCache: Map<string, string> = new Map<string, string>();
181        findNoSymbolIdentifiers(scope);
182
183        defs.forEach((def) => {
184          const original: string = def.name;
185          let mangled: string = original;
186          // No allow to rename reserved names.
187          if (reservedNames.includes(original) || scope.exportNames.has(def.name) || isSkippedGlobal(openTopLevel, scope)) {
188            scope.mangledNames.add(mangled);
189            mangledSymbolNames.set(def, mangled);
190            return;
191          }
192
193          if (mangledSymbolNames.has(def)) {
194            return;
195          }
196
197          const path: string = scope.loc + '#' + original;
198          const historyName: string = historyNameCache?.get(path);
199          const specifyName: string = historyName ? historyName : nameCache.get(path);
200          if (specifyName) {
201            mangled = specifyName;
202          } else {
203            const sameMangled: string = localCache.get(original);
204            mangled = sameMangled ? sameMangled : getMangled(scope, generator);
205          }
206
207          // add new names to name cache
208          nameCache.set(path, mangled);
209          scope.mangledNames.add(mangled);
210          mangledSymbolNames.set(def, mangled);
211          localCache.set(original, mangled);
212        });
213      }
214
215      function isExcludeScope(scope: Scope): boolean {
216        if (isClassScope(scope)) {
217          return true;
218        }
219
220        if (isInterfaceScope(scope)) {
221          return true;
222        }
223
224        if (isEnumScope(scope)) {
225          return true;
226        }
227
228        return isObjectLiteralScope(scope);
229      }
230
231      function getMangled(scope: Scope, localGenerator: INameGenerator): string {
232        let mangled: string = '';
233        do {
234          mangled = localGenerator.getName()!;
235          // if it is a globally reserved name, it needs to be regenerated
236          if (reservedNames.includes(mangled)) {
237            mangled = '';
238            continue;
239          }
240
241          if (scope.importNames && scope.importNames.has(mangled)) {
242            mangled = '';
243            continue;
244          }
245
246          if (scope.exportNames && scope.exportNames.has(mangled)) {
247            mangled = '';
248            continue;
249          }
250
251          if (historyMangledNames && historyMangledNames.has(mangled)) {
252            mangled = '';
253            continue;
254          }
255
256          // the anme has already been generated in the current scope
257          if (scope.mangledNames.has(mangled)) {
258            mangled = '';
259          }
260        } while (mangled === '');
261
262        return mangled;
263      }
264
265      function renameLabelsInScope(scope: Scope): void {
266        const labels: Label[] = scope.labels;
267        if (labels.length > 0) {
268          let upperMangledLabels = getUpperMangledLabelNames(labels[0]);
269          for (const label of labels) {
270            let mangledLabel = getMangledLabel(label, upperMangledLabels);
271            mangledLabelNames.set(label, mangledLabel);
272          }
273        }
274      }
275
276      function getMangledLabel(label: Label, mangledLabels: string[]): string {
277        let mangledLabel: string = '';
278        do {
279          mangledLabel = generator.getName();
280          if (mangledLabel === label.name) {
281            mangledLabel = '';
282          }
283
284          if (mangledLabels.includes(mangledLabel)) {
285            mangledLabel = '';
286          }
287        } while (mangledLabel === '');
288
289        return mangledLabel;
290      }
291
292      function getUpperMangledLabelNames(label: Label): string[] {
293        const results: string[] = [];
294        let parent: Label = label.parent;
295        while (parent) {
296          let mangledLabelName: string = mangledLabelNames.get(parent);
297          if (mangledLabelName) {
298            results.push(mangledLabelName);
299          }
300          parent = parent.parent;
301        }
302
303        return results;
304      }
305
306      /**
307       * visit each node to change identifier name to mangled name
308       *  - calculate shadow name index to find shadow node
309       * @param node
310       */
311      function visit(node: Node): Node {
312        if (!isIdentifier(node) || !node.parent) {
313          return visitEachChild(node, visit, context);
314        }
315
316        if (isLabeledStatement(node.parent) || isBreakOrContinueStatement(node.parent)) {
317          identifierIndex += 1;
318          return updateLabelNode(node);
319        }
320
321        const shadowNode: Identifier = shadowIdentifiers[identifierIndex];
322        identifierIndex += 1;
323        return updateNameNode(node, shadowNode);
324      }
325
326      function findNoSymbolIdentifiers(scope: Scope): void {
327        const noSymbolVisit = (targetNode: Node): void => {
328          if (!isIdentifier(targetNode)) {
329            forEachChild(targetNode, noSymbolVisit);
330            return;
331          }
332
333          // skip property in property access expression
334          if (NodeUtils.isPropertyAccessNode(targetNode)) {
335            return;
336          }
337
338          const sym: Symbol | undefined = checker.getSymbolAtLocation(targetNode);
339          if (!sym) {
340            scope.mangledNames.add((targetNode as Identifier).escapedText.toString());
341          }
342        };
343
344        noSymbolVisit(scope.block);
345      }
346
347      function updateNameNode(node: Identifier, shadowNode: Identifier): Node {
348        // skip property in property access expression
349        if (NodeUtils.isPropertyAccessNode(node)) {
350          return node;
351        }
352
353        const sym: Symbol | undefined = checker.getSymbolAtLocation(shadowNode);
354        if (!sym || sym.name === 'default') {
355          return node;
356        }
357
358        const mangledName: string = mangledSymbolNames.get(sym);
359        if (!mangledName || mangledName === sym.name) {
360          return node;
361        }
362
363        return factory.createIdentifier(mangledName);
364      }
365
366      function updateLabelNode(node: Identifier): Node {
367        let label: Label | undefined;
368        let labelName: string = '';
369
370        mangledLabelNames.forEach((value, key) => {
371          if (key.refs.includes(node)) {
372            label = key;
373            labelName = value;
374          }
375        });
376
377        return label ? factory.createIdentifier(labelName) : node;
378      }
379    }
380  };
381
382  function isSkippedGlobal(enableTopLevel: boolean, scope: Scope): boolean {
383    return !enableTopLevel && isGlobalScope(scope);
384  }
385
386  const TRANSFORMER_ORDER: number = 9;
387  export let transformerPlugin: TransformPlugin = {
388    'name': 'renameIdentifierPlugin',
389    'order': (1 << TRANSFORMER_ORDER),
390    'createTransformerFactory': createRenameIdentifierFactory
391  };
392
393  export let nameCache: Map<string, string> = undefined;
394  export let historyNameCache: Map<string, string> = undefined;
395}
396
397export = secharmony;
398