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