• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-2024 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  createPrinter,
18  createTextWriter,
19  transform,
20  createObfTextSingleLineWriter,
21} from 'typescript';
22
23import type {
24  CompilerOptions,
25  EmitTextWriter,
26  Node,
27  Printer,
28  PrinterOptions,
29  RawSourceMap,
30  SourceFile,
31  SourceMapGenerator,
32  TransformationResult,
33  TransformerFactory,
34} from 'typescript';
35
36import path from 'path';
37
38import { PropCollections } from './utils/CommonCollections';
39import type { IOptions } from './configs/IOptions';
40import { FileUtils } from './utils/FileUtils';
41import { TransformerManager } from './transformers/TransformerManager';
42import { getSourceMapGenerator } from './utils/SourceMapUtil';
43import {
44  decodeSourcemap,
45  ExistingDecodedSourceMap,
46  Source,
47  SourceMapLink,
48  SourceMapSegmentObj,
49  mergeSourceMap
50} from './utils/SourceMapMergingUtil';
51import {
52  deleteLineInfoForNameString,
53  getMapFromJson,
54  IDENTIFIER_CACHE,
55  MEM_METHOD_CACHE
56} from './utils/NameCacheUtil';
57import { ListUtil } from './utils/ListUtil';
58import { needReadApiInfo, readProjectPropertiesByCollectedPaths } from './common/ApiReader';
59import { ApiExtractor } from './common/ApiExtractor';
60import esInfo from './configs/preset/es_reserved_properties.json';
61import { EventList, TimeSumPrinter, TimeTracker } from './utils/PrinterUtils';
62import { Extension, type ProjectInfo } from './common/type';
63export { FileUtils } from './utils/FileUtils';
64export { MemoryUtils } from './utils/MemoryUtils';
65import { TypeUtils } from './utils/TypeUtils';
66import { handleReservedConfig } from './utils/TransformUtil';
67export { separateUniversalReservedItem, containWildcards, wildcardTransformer } from './utils/TransformUtil';
68export type { ReservedNameInfo } from './utils/TransformUtil';
69
70export { initObfuscationConfig } from './initialization/Initializer';
71export { nameCacheMap } from './initialization/CommonObject';
72export {
73  collectResevedFileNameInIDEConfig, // For running unit test.
74  enableObfuscatedFilePathConfig,
75  enableObfuscateFileName,
76  generateConsumerObConfigFile,
77  getRelativeSourcePath,
78  handleObfuscatedFilePath,
79  handleUniversalPathInObf,
80  mangleFilePath,
81  MergedConfig,
82  ObConfigResolver,
83  readNameCache,
84  writeObfuscationNameCache
85} from './initialization/ConfigResolver';
86
87export const renameIdentifierModule = require('./transformers/rename/RenameIdentifierTransformer');
88export const renameFileNameModule = require('./transformers/rename/RenameFileNameTransformer');
89
90export { getMapFromJson, readProjectPropertiesByCollectedPaths, deleteLineInfoForNameString, ApiExtractor, PropCollections };
91export let orignalFilePathForSearching: string | undefined;
92export let cleanFileMangledNames: boolean = false;
93export interface PerformancePrinter {
94  filesPrinter?: TimeTracker;
95  singleFilePrinter?: TimeTracker;
96  timeSumPrinter?: TimeSumPrinter;
97  iniPrinter: TimeTracker;
98}
99export let performancePrinter: PerformancePrinter = {
100  iniPrinter: new TimeTracker(),
101};
102
103// When the module is compiled, call this function to clear global collections.
104export function clearGlobalCaches(): void {
105  PropCollections.clearPropsCollections();
106  renameFileNameModule.clearCaches();
107}
108
109export type ObfuscationResultType = {
110  content: string;
111  sourceMap?: RawSourceMap;
112  nameCache?: { [k: string]: string | {} };
113  filePath?: string;
114};
115
116const JSON_TEXT_INDENT_LENGTH: number = 2;
117export class ArkObfuscator {
118  // Used only for testing
119  protected mWriteOriginalFile: boolean = false;
120
121  // A text writer of Printer
122  private mTextWriter: EmitTextWriter;
123
124  // Compiler Options for typescript,use to parse ast
125  private readonly mCompilerOptions: CompilerOptions;
126
127  // User custom obfuscation profiles.
128  protected mCustomProfiles: IOptions;
129
130  private mTransformers: TransformerFactory<Node>[];
131
132  static mProjectInfo: ProjectInfo | undefined;
133
134  // If isKeptCurrentFile is true, both identifier and property obfuscation are skipped.
135  static mIsKeptCurrentFile: boolean = false;
136
137  public constructor() {
138    this.mCompilerOptions = {};
139    this.mTransformers = [];
140  }
141
142  public setWriteOriginalFile(flag: boolean): void {
143    this.mWriteOriginalFile = flag;
144  }
145
146  public addReservedProperties(newReservedProperties: string[]): void {
147    if (newReservedProperties.length === 0) {
148      return;
149    }
150    const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation;
151    nameObfuscationConfig.mReservedProperties = ListUtil.uniqueMergeList(newReservedProperties,
152      nameObfuscationConfig?.mReservedProperties);
153  }
154
155  public addReservedNames(newReservedNames: string[]): void {
156    if (newReservedNames.length === 0) {
157      return;
158    }
159    const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation;
160    nameObfuscationConfig.mReservedNames = ListUtil.uniqueMergeList(newReservedNames,
161      nameObfuscationConfig?.mReservedNames);
162  }
163
164  public addReservedToplevelNames(newReservedGlobalNames: string[]): void {
165    if (newReservedGlobalNames.length === 0) {
166      return;
167    }
168    const nameObfuscationConfig = this.mCustomProfiles.mNameObfuscation;
169    nameObfuscationConfig.mReservedToplevelNames = ListUtil.uniqueMergeList(newReservedGlobalNames,
170      nameObfuscationConfig.mReservedToplevelNames);
171  }
172
173  public setKeepSourceOfPaths(mKeepSourceOfPaths: Set<string>): void {
174    this.mCustomProfiles.mKeepFileSourceCode.mKeepSourceOfPaths = mKeepSourceOfPaths;
175  }
176
177  public handleTsHarComments(sourceFile: SourceFile, originalPath: string | undefined): void {
178    if (ArkObfuscator.projectInfo?.useTsHar && (originalPath?.endsWith(Extension.ETS) && !originalPath?.endsWith(Extension.DETS))) {
179      // @ts-ignore
180      sourceFile.writeTsHarComments = true;
181    }
182  }
183
184  public get customProfiles(): IOptions {
185    return this.mCustomProfiles;
186  }
187
188  public static get isKeptCurrentFile(): boolean {
189    return ArkObfuscator.mIsKeptCurrentFile;
190  }
191
192  public static set isKeptCurrentFile(isKeptFile: boolean) {
193    ArkObfuscator.mIsKeptCurrentFile = isKeptFile;
194  }
195
196  public static get projectInfo(): ProjectInfo {
197    return ArkObfuscator.mProjectInfo;
198  }
199
200  public static set projectInfo(projectInfo: ProjectInfo) {
201    ArkObfuscator.mProjectInfo = projectInfo;
202  }
203
204  private isCurrentFileInKeepPaths(customProfiles: IOptions, originalFilePath: string): boolean {
205    const keepFileSourceCode = customProfiles.mKeepFileSourceCode;
206    if (keepFileSourceCode === undefined || keepFileSourceCode.mKeepSourceOfPaths.size === 0) {
207      return false;
208    }
209    const keepPaths: Set<string> = keepFileSourceCode.mKeepSourceOfPaths;
210    const originalPath = FileUtils.toUnixPath(originalFilePath);
211    return keepPaths.has(originalPath);
212  }
213
214  /**
215   * init ArkObfuscator according to user config
216   * should be called after constructor
217   */
218  public init(config: IOptions | undefined): boolean {
219    if (!config) {
220      console.error('obfuscation config file is not found and no given config.');
221      return false;
222    }
223
224    handleReservedConfig(config, 'mRenameFileName', 'mReservedFileNames', 'mUniversalReservedFileNames');
225    handleReservedConfig(config, 'mRemoveDeclarationComments', 'mReservedComments', 'mUniversalReservedComments', 'mEnable');
226    this.mCustomProfiles = config;
227
228    if (this.mCustomProfiles.mCompact) {
229      this.mTextWriter = createObfTextSingleLineWriter();
230    } else {
231      this.mTextWriter = createTextWriter('\n');
232    }
233
234    if (this.mCustomProfiles.mEnableSourceMap) {
235      this.mCompilerOptions.sourceMap = true;
236    }
237
238    const enableTopLevel: boolean = this.mCustomProfiles.mNameObfuscation?.mTopLevel;
239    const exportObfuscation: boolean = this.mCustomProfiles.mExportObfuscation;
240    const propertyObfuscation: boolean = this.mCustomProfiles.mNameObfuscation?.mRenameProperties;
241    /**
242     * clean mangledNames in case skip name check when generating names
243     */
244    cleanFileMangledNames = enableTopLevel && !exportObfuscation && !propertyObfuscation;
245
246    this.initPerformancePrinter();
247    // load transformers
248    this.mTransformers = new TransformerManager(this.mCustomProfiles).getTransformers();
249
250    if (needReadApiInfo(this.mCustomProfiles)) {
251      this.mCustomProfiles.mNameObfuscation.mReservedProperties = ListUtil.uniqueMergeList(
252        this.mCustomProfiles.mNameObfuscation.mReservedProperties,
253        this.mCustomProfiles.mNameObfuscation.mReservedNames,
254        [...esInfo.es2015, ...esInfo.es2016, ...esInfo.es2017, ...esInfo.es2018, ...esInfo.es2019, ...esInfo.es2020,
255          ...esInfo.es2021]);
256    }
257
258    return true;
259  }
260
261  private initPerformancePrinter(): void {
262    if (this.mCustomProfiles.mPerformancePrinter) {
263      const printConfig = this.mCustomProfiles.mPerformancePrinter;
264      const printPath = printConfig.mOutputPath;
265
266      if (printConfig.mFilesPrinter) {
267        performancePrinter.filesPrinter = performancePrinter.iniPrinter;
268        performancePrinter.filesPrinter.setOutputPath(printPath);
269      } else {
270        performancePrinter.iniPrinter = undefined;
271      }
272
273      if (printConfig.mSingleFilePrinter) {
274        performancePrinter.singleFilePrinter = new TimeTracker(printPath);
275      }
276
277      if (printConfig.mSumPrinter) {
278        performancePrinter.timeSumPrinter = new TimeSumPrinter(printPath);
279      }
280    } else {
281      performancePrinter = undefined;
282    }
283  }
284
285  /**
286   * A Printer to output obfuscated codes.
287   */
288  public createObfsPrinter(isDeclarationFile: boolean): Printer {
289    // set print options
290    let printerOptions: PrinterOptions = {};
291    let removeOption = this.mCustomProfiles.mRemoveDeclarationComments;
292    let hasReservedList = removeOption?.mReservedComments?.length || removeOption?.mUniversalReservedComments?.length;
293    let keepDeclarationComments = hasReservedList || !removeOption?.mEnable;
294
295    if (isDeclarationFile && keepDeclarationComments) {
296      printerOptions.removeComments = false;
297    }
298    if ((!isDeclarationFile && this.mCustomProfiles.mRemoveComments) || (isDeclarationFile && !keepDeclarationComments)) {
299      printerOptions.removeComments = true;
300    }
301
302    return createPrinter(printerOptions);
303  }
304
305  protected isObfsIgnoreFile(fileName: string): boolean {
306    let suffix: string = FileUtils.getFileExtension(fileName);
307
308    return suffix !== 'js' && suffix !== 'ts' && suffix !== 'ets';
309  }
310
311  private convertLineBasedOnSourceMap(targetCache: string, sourceMapLink?: SourceMapLink): Map<string, string> {
312    let originalCache: Map<string, string> = renameIdentifierModule.nameCache.get(targetCache);
313    let updatedCache: Map<string, string> = new Map<string, string>();
314    for (const [key, value] of originalCache) {
315      if (!key.includes(':')) {
316        // No need to save line info for identifier which is not function-like, i.e. key without ':' here.
317        updatedCache[key] = value;
318        continue;
319      }
320      const [scopeName, oldStartLine, oldStartColumn, oldEndLine, oldEndColumn] = key.split(':');
321      let newKey: string = key;
322      if (!sourceMapLink) {
323        // In Arkguard, we save line info of source code, so do not need to use sourcemap mapping.
324        newKey = `${scopeName}:${oldStartLine}:${oldEndLine}`;
325        updatedCache[newKey] = value;
326        continue;
327      }
328      const startPosition: SourceMapSegmentObj | null = sourceMapLink.traceSegment(
329        // 1: The line number in originalCache starts from 1 while in source map starts from 0.
330        Number(oldStartLine) - 1, Number(oldStartColumn) - 1, ''); // Minus 1 to get the correct original position.
331      if (!startPosition) {
332        // Do not save methods that do not exist in the source code, e.g. 'build' in ArkUI.
333        continue;
334      }
335      const endPosition: SourceMapSegmentObj | null = sourceMapLink.traceSegment(
336        Number(oldEndLine) - 1, Number(oldEndColumn) - 1, ''); // 1: Same as above.
337      if (!endPosition) {
338        // Do not save methods that do not exist in the source code, e.g. 'build' in ArkUI.
339        continue;
340      }
341      const startLine = startPosition.line + 1; // 1: The final line number in updatedCache should starts from 1.
342      const endLine = endPosition.line + 1; // 1: Same as above.
343      newKey = `${scopeName}:${startLine}:${endLine}`;
344      updatedCache[newKey] = value;
345    }
346    return updatedCache;
347  }
348
349  /**
350   * Obfuscate ast of a file.
351   * @param content ast or source code of a source file
352   * @param sourceFilePath
353   * @param previousStageSourceMap
354   * @param historyNameCache
355   * @param originalFilePath When filename obfuscation is enabled, it is used as the source code path.
356   */
357  public async obfuscate(
358    content: SourceFile | string,
359    sourceFilePath: string,
360    previousStageSourceMap?: RawSourceMap,
361    historyNameCache?: Map<string, string>,
362    originalFilePath?: string,
363    projectInfo?: ProjectInfo,
364  ): Promise<ObfuscationResultType> {
365    ArkObfuscator.projectInfo = projectInfo;
366    let result: ObfuscationResultType = { content: undefined };
367    if (this.isObfsIgnoreFile(sourceFilePath)) {
368      // need add return value
369      return result;
370    }
371
372    let ast: SourceFile = this.createAst(content, sourceFilePath);
373    if (ast.statements.length === 0) {
374      return result;
375    }
376
377    if (historyNameCache && historyNameCache.size > 0 && this.mCustomProfiles.mNameObfuscation) {
378      renameIdentifierModule.historyNameCache = historyNameCache;
379    }
380    originalFilePath = originalFilePath ?? ast.fileName;
381    if (this.mCustomProfiles.mRenameFileName?.mEnable) {
382      orignalFilePathForSearching = originalFilePath;
383    }
384    ArkObfuscator.isKeptCurrentFile = this.isCurrentFileInKeepPaths(this.mCustomProfiles, originalFilePath);
385
386    this.handleDeclarationFile(ast);
387
388    ast = this.obfuscateAst(ast);
389
390    this.writeObfuscationResult(ast, sourceFilePath, result, previousStageSourceMap, originalFilePath);
391
392    this.clearCaches();
393    return result;
394  }
395
396  private createAst(content: SourceFile | string, sourceFilePath: string): SourceFile {
397    performancePrinter?.singleFilePrinter?.startEvent(EventList.CREATE_AST, performancePrinter.timeSumPrinter, sourceFilePath);
398    let ast: SourceFile;
399    if (typeof content === 'string') {
400      ast = TypeUtils.createObfSourceFile(sourceFilePath, content);
401    } else {
402      ast = content;
403    }
404    performancePrinter?.singleFilePrinter?.endEvent(EventList.CREATE_AST, performancePrinter.timeSumPrinter);
405
406    return ast;
407  }
408
409  private obfuscateAst(ast: SourceFile): SourceFile {
410    performancePrinter?.singleFilePrinter?.startEvent(EventList.OBFUSCATE_AST, performancePrinter.timeSumPrinter);
411    let transformedResult: TransformationResult<Node> = transform(ast, this.mTransformers, this.mCompilerOptions);
412    performancePrinter?.singleFilePrinter?.endEvent(EventList.OBFUSCATE_AST, performancePrinter.timeSumPrinter);
413    ast = transformedResult.transformed[0] as SourceFile;
414    return ast;
415  }
416
417  private handleDeclarationFile(ast: SourceFile): void {
418    if (ast.isDeclarationFile) {
419      if (!this.mCustomProfiles.mRemoveDeclarationComments || !this.mCustomProfiles.mRemoveDeclarationComments.mEnable) {
420        //@ts-ignore
421        ast.reservedComments = undefined;
422        //@ts-ignore
423        ast.universalReservedComments = undefined;
424      } else {
425        //@ts-ignore
426        ast.reservedComments ??= this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments ?
427          this.mCustomProfiles.mRemoveDeclarationComments.mReservedComments : [];
428        //@ts-ignore
429        ast.universalReservedComments = this.mCustomProfiles.mRemoveDeclarationComments.mUniversalReservedComments ?? [];
430      }
431    } else {
432      //@ts-ignore
433      ast.reservedComments = this.mCustomProfiles.mRemoveComments ? [] : undefined;
434      //@ts-ignore
435      ast.universalReservedComments = this.mCustomProfiles.mRemoveComments ? [] : undefined;
436    }
437  }
438
439  /**
440   * write obfuscated code, sourcemap and namecache
441   */
442  private writeObfuscationResult(ast: SourceFile, sourceFilePath: string, result: ObfuscationResultType,
443    previousStageSourceMap?: RawSourceMap, originalFilePath?: string): void {
444    // convert ast to output source file and generate sourcemap if needed.
445    let sourceMapGenerator: SourceMapGenerator = undefined;
446    if (this.mCustomProfiles.mEnableSourceMap) {
447      sourceMapGenerator = getSourceMapGenerator(sourceFilePath);
448    }
449
450    if (sourceFilePath.endsWith('.js')) {
451      TypeUtils.tsToJs(ast);
452    }
453    this.handleTsHarComments(ast, originalFilePath);
454    performancePrinter?.singleFilePrinter?.startEvent(EventList.CREATE_PRINTER, performancePrinter.timeSumPrinter);
455    this.createObfsPrinter(ast.isDeclarationFile).writeFile(ast, this.mTextWriter, sourceMapGenerator);
456    performancePrinter?.singleFilePrinter?.endEvent(EventList.CREATE_PRINTER, performancePrinter.timeSumPrinter);
457
458    result.filePath = ast.fileName;
459    result.content = this.mTextWriter.getText();
460
461    if (this.mCustomProfiles.mEnableSourceMap && sourceMapGenerator) {
462      this.handleSourceMapAndNameCache(sourceMapGenerator, sourceFilePath, result, previousStageSourceMap);
463    }
464  }
465
466  private handleSourceMapAndNameCache(sourceMapGenerator: SourceMapGenerator, sourceFilePath: string,
467    result: ObfuscationResultType, previousStageSourceMap?: RawSourceMap): void {
468    let sourceMapJson: RawSourceMap = sourceMapGenerator.toJSON();
469    sourceMapJson.sourceRoot = '';
470    sourceMapJson.file = path.basename(sourceFilePath);
471    if (previousStageSourceMap) {
472      sourceMapJson = mergeSourceMap(previousStageSourceMap as RawSourceMap, sourceMapJson);
473    }
474    result.sourceMap = sourceMapJson;
475    let nameCache = renameIdentifierModule.nameCache;
476    if (this.mCustomProfiles.mEnableNameCache) {
477      let newIdentifierCache!: Object;
478      let newMemberMethodCache!: Object;
479      if (previousStageSourceMap) {
480        // The process in sdk, need to use sourcemap mapping.
481        // 1: Only one file in the source map; 0: The first and the only one.
482        const sourceFileName = previousStageSourceMap.sources?.length === 1 ? previousStageSourceMap.sources[0] : '';
483        const source: Source = new Source(sourceFileName, null);
484        const decodedSourceMap: ExistingDecodedSourceMap = decodeSourcemap(previousStageSourceMap);
485        let sourceMapLink: SourceMapLink = new SourceMapLink(decodedSourceMap, [source]);
486        newIdentifierCache = this.convertLineBasedOnSourceMap(IDENTIFIER_CACHE, sourceMapLink);
487        newMemberMethodCache = this.convertLineBasedOnSourceMap(MEM_METHOD_CACHE, sourceMapLink);
488      } else {
489        // The process in Arkguard.
490        newIdentifierCache = this.convertLineBasedOnSourceMap(IDENTIFIER_CACHE);
491        newMemberMethodCache = this.convertLineBasedOnSourceMap(MEM_METHOD_CACHE);
492      }
493      nameCache.set(IDENTIFIER_CACHE, newIdentifierCache);
494      nameCache.set(MEM_METHOD_CACHE, newMemberMethodCache);
495      result.nameCache = { [IDENTIFIER_CACHE]: newIdentifierCache, [MEM_METHOD_CACHE]: newMemberMethodCache };
496    }
497  }
498
499  private clearCaches(): void {
500    // clear cache of text writer
501    this.mTextWriter.clear();
502    renameIdentifierModule.clearCaches();
503    if (cleanFileMangledNames) {
504      PropCollections.globalMangledTable.clear();
505      PropCollections.newlyOccupiedMangledProps.clear();
506    }
507  }
508}
509