1/*
2 * Copyright (c) 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  ArkObfuscator,
18  ObfuscationResultType,
19  PropCollections,
20  performancePrinter,
21  renameIdentifierModule
22} from './ArkObfuscator';
23import { readProjectProperties } from './common/ApiReaderForTest';
24import { FileUtils } from './utils/FileUtils';
25import { EventList } from './utils/PrinterUtils';
26import { handleReservedConfig } from './utils/TransformUtil';
27import {
28  IDENTIFIER_CACHE,
29  NAME_CACHE_SUFFIX,
30  PROPERTY_CACHE_FILE,
31  deleteLineInfoForNameString,
32  getMapFromJson,
33  readCache,
34  writeCache
35} from './utils/NameCacheUtil';
36
37import * as fs from 'fs';
38import path from 'path';
39import filterFileArray from './configs/test262filename/filterFilenameList.json';
40import { UnobfuscationCollections } from './utils/CommonCollections';
41import { unobfuscationNamesObj } from './initialization/CommonObject';
42import { printUnobfuscationReasons } from './initialization/ConfigResolver';
43import { mergeSet, convertSetToArray } from './initialization/utils';
44
45import type { IOptions } from './configs/IOptions';
46
47const JSON_TEXT_INDENT_LENGTH: number = 2;
48
49interface OutPathObj {
50  outputPath: string;
51  relativePath: string;
52}
53
54export class ArkObfuscatorForTest extends ArkObfuscator {
55  // A list of source file path
56  private readonly mSourceFiles: string[];
57
58  // Path of obfuscation configuration file.
59  private readonly mConfigPath: string;
60
61  private mTestType: string | undefined = undefined;
62
63  constructor(sourceFiles?: string[], configPath?: string) {
64    super();
65    this.mSourceFiles = sourceFiles;
66    this.mConfigPath = configPath;
67  }
68
69  public get configPath(): string {
70    return this.mConfigPath;
71  }
72
73  public setTestType(testType: string | undefined): void {
74    this.mTestType = testType;
75  }
76
77  /**
78   * init ArkObfuscator according to user config
79   * should be called after constructor
80   */
81  public init(config: IOptions | undefined): boolean {
82    if (!config) {
83        console.error('obfuscation config file is not found and no given config.');
84        return false;
85    }
86    UnobfuscationCollections.printKeptName = config.mUnobfuscationOption?.mPrintKeptNames;
87
88    handleReservedConfig(config, 'mNameObfuscation', 'mReservedProperties', 'mUniversalReservedProperties');
89    handleReservedConfig(config, 'mNameObfuscation', 'mReservedToplevelNames', 'mUniversalReservedToplevelNames');
90    return super.init(config);
91  }
92
93  /**
94   * Obfuscate all the source files.
95   */
96  public async obfuscateFiles(): Promise<void> {
97    if (!path.isAbsolute(this.mCustomProfiles.mOutputDir)) {
98      this.mCustomProfiles.mOutputDir = path.join(path.dirname(this.mConfigPath), this.mCustomProfiles.mOutputDir);
99    }
100
101    performancePrinter?.filesPrinter?.startEvent(EventList.ALL_FILES_OBFUSCATION);
102    readProjectProperties(this.mSourceFiles, structuredClone(this.mCustomProfiles), this);
103    const propertyCachePath = path.join(this.mCustomProfiles.mOutputDir, 
104                                        path.basename(this.mSourceFiles[0])); // Get dir name
105    this.readPropertyCache(propertyCachePath);
106
107    // support directory and file obfuscate
108    for (const sourcePath of this.mSourceFiles) {
109      if (!fs.existsSync(sourcePath)) {
110        console.error(`File ${FileUtils.getFileName(sourcePath)} is not found.`);
111        return;
112      }
113
114      if (fs.lstatSync(sourcePath).isFile()) {
115        await this.obfuscateFile(sourcePath, this.mCustomProfiles.mOutputDir);
116        continue;
117      }
118
119      const dirPrefix: string = FileUtils.getPrefix(sourcePath);
120      await this.obfuscateDir(sourcePath, dirPrefix);
121    }
122
123    if (this.mCustomProfiles.mUnobfuscationOption?.mPrintKeptNames) {
124      const dir = path.dirname(this.mSourceFiles[0]).replace('grammar', 'local');
125      const basename = path.basename(this.mSourceFiles[0]);
126      let printKeptNamesPath = path.join(dir, basename, '/keptNames.unobf.json');
127      let printWhitelistPath = path.join(dir, basename, '/whitelist.unobf.json');
128      this.writeUnobfuscationContentForTest(printKeptNamesPath, printWhitelistPath);
129    }
130
131    this.producePropertyCache(propertyCachePath);
132    performancePrinter?.filesPrinter?.endEvent(EventList.ALL_FILES_OBFUSCATION);
133    performancePrinter?.timeSumPrinter?.print('Sum up time of processes');
134    performancePrinter?.timeSumPrinter?.summarizeEventDuration();
135  }
136
137  private writeUnobfuscationContentForTest(printKeptNamesPath: string, printWhitelistPath: string): void {
138    printUnobfuscationReasons('', printKeptNamesPath);
139    this.printWhitelist(this.mCustomProfiles, printWhitelistPath);
140  }
141
142  private printWhitelist(obfuscationOptions: IOptions, printPath: string): void {
143    const nameOption = obfuscationOptions.mNameObfuscation;
144    const enableToplevel = nameOption.mTopLevel;
145    const enableProperty = nameOption.mRenameProperties;
146    const enableStringProp = !nameOption.mKeepStringProperty;
147    const enableExport = obfuscationOptions.mExportObfuscation;
148    const reservedConfToplevelArrary = nameOption.mReservedToplevelNames ?? [];
149    const reservedConfPropertyArray = nameOption.mReservedProperties ?? [];
150
151    let whitelistObj = {
152      lang: [],
153      conf: [],
154      struct: [],
155      exported: [],
156      strProp: []
157    };
158  
159    if (enableExport || enableProperty) {
160      const languageSet = mergeSet(UnobfuscationCollections.reservedLangForProperty, UnobfuscationCollections.reservedLangForTopLevel);
161      whitelistObj.lang = convertSetToArray(languageSet);
162      const strutSet = UnobfuscationCollections.reservedStruct;
163      whitelistObj.struct = convertSetToArray(strutSet);
164      const exportSet = mergeSet(UnobfuscationCollections.reservedExportName, UnobfuscationCollections.reservedExportNameAndProp);
165      whitelistObj.exported = convertSetToArray(exportSet);
166      if (!enableStringProp) {
167        const stringSet = UnobfuscationCollections.reservedStrProp;
168        whitelistObj.strProp = convertSetToArray(stringSet);
169      }
170    }
171  
172    const hasPropertyConfig = enableProperty && reservedConfPropertyArray?.length > 0;
173    const hasTopLevelConfig = enableToplevel && reservedConfToplevelArrary?.length > 0;
174    if (hasPropertyConfig) {
175      // if -enable-property-obfuscation and -enable-toplevel-obfuscation,
176      // the mReservedToplevelNames has already been merged into the mReservedToplevelNames.
177      whitelistObj.conf.push(...reservedConfPropertyArray);
178      this.handleUniversalReservedList(nameOption.mUniversalReservedProperties, whitelistObj.conf);
179    } else if (hasTopLevelConfig) {
180      whitelistObj.conf.push(...reservedConfToplevelArrary);
181      this.handleUniversalReservedList(nameOption.mUniversalReservedToplevelNames, whitelistObj.conf);
182    }
183  
184    let whitelistContent = JSON.stringify(whitelistObj, null, 2);
185    if (!fs.existsSync(path.dirname(printPath))) {
186      fs.mkdirSync(path.dirname(printPath), { recursive: true });
187    }
188    fs.writeFileSync(printPath, whitelistContent);
189  }
190
191  private handleUniversalReservedList(universalList: RegExp[] | undefined, configArray: string[]): void {
192    if (universalList?.length > 0) {
193      universalList.forEach((value) => {
194        const originalString = UnobfuscationCollections.reservedWildcardMap.get(value);
195        if (originalString) {
196          configArray.push(originalString);
197        }
198      });
199    }
200  }
201
202  /**
203   * obfuscate directory
204   * @private
205   */
206  private async obfuscateDir(dirName: string, dirPrefix: string): Promise<void> {
207    const currentDir: string = FileUtils.getPathWithoutPrefix(dirName, dirPrefix);
208    let newDir: string = this.mCustomProfiles.mOutputDir;
209    // there is no need to create directory because the directory names will be obfuscated.
210    if (!this.mCustomProfiles.mRenameFileName?.mEnable) {
211      newDir = path.join(this.mCustomProfiles.mOutputDir, currentDir);
212    }
213
214    const fileNames: string[] = fs.readdirSync(dirName);
215    for (let fileName of fileNames) {
216      const filePath: string = path.join(dirName, fileName);
217      if (fs.lstatSync(filePath).isFile()) {
218        await this.obfuscateFile(filePath, newDir);
219        continue;
220      }
221
222      await this.obfuscateDir(filePath, dirPrefix);
223    }
224  }
225
226  /**
227   * Obfuscate single source file with path provided
228   *
229   * @param sourceFilePath single source file path
230   * @param outputDir
231   */
232  public async obfuscateFile(sourceFilePath: string, outputDir: string): Promise<void> {
233    const fileName: string = FileUtils.getFileName(sourceFilePath);
234    const config = this.mCustomProfiles;
235    if (this.isObfsIgnoreFile(fileName)) {
236      fs.mkdirSync(outputDir, { recursive: true });
237      fs.copyFileSync(sourceFilePath, path.join(outputDir, fileName));
238      return;
239    }
240
241    const test262Filename = this.getPathAfterTest262SecondLevel(sourceFilePath);
242    const isFileInArray = filterFileArray.includes(test262Filename);
243    // To skip the path where 262 test will fail.
244    if (isFileInArray) {
245      return;
246    }
247
248    // Add the whitelist of file name obfuscation for ut.
249    if (config.mRenameFileName?.mEnable) {
250      const reservedArray = config.mRenameFileName.mReservedFileNames;
251      FileUtils.collectPathReservedString(this.mConfigPath, reservedArray);
252    }
253    let content: string = FileUtils.readFile(sourceFilePath);
254    this.readNameCache(sourceFilePath, outputDir);
255    performancePrinter?.filesPrinter?.startEvent(sourceFilePath);
256    let filePath = { buildFilePath: sourceFilePath, relativeFilePath: sourceFilePath };
257    const mixedInfo: ObfuscationResultType = await this.obfuscate(content, filePath);
258    performancePrinter?.filesPrinter?.endEvent(sourceFilePath, undefined, true);
259
260    if (this.mWriteOriginalFile && mixedInfo) {
261      // Write the obfuscated content directly to orignal file.
262      fs.writeFileSync(sourceFilePath, mixedInfo.content);
263      return;
264    }
265    if (outputDir && mixedInfo) {
266      const outputPathObj: OutPathObj = this.getOutputPath(sourceFilePath, mixedInfo);
267      this.writeContent(outputPathObj.outputPath, outputPathObj.relativePath, mixedInfo);
268    }
269  }
270
271  private getOutputPath(sourceFilePath: string, mixedInfo: ObfuscationResultType): OutPathObj {
272    const config = this.mCustomProfiles;
273    if (this.mTestType === 'grammar') {
274      const testCasesRootPath = path.join(__dirname, '../', 'test/grammar');
275      let relativePath = '';
276      if (config.mRenameFileName?.mEnable && mixedInfo.filePath) {
277        relativePath = mixedInfo.filePath.replace(testCasesRootPath, '');
278      } else {
279        relativePath = sourceFilePath.replace(testCasesRootPath, '');
280      }
281      const resultPath = path.join(config.mOutputDir, relativePath);
282      return {outputPath: resultPath, relativePath: relativePath};
283    } else if (this.mTestType === 'combinations') {
284      const outputDir = this.mCustomProfiles.mOutputDir;
285      const directory = outputDir.substring(0, outputDir.lastIndexOf('/') + 1);
286      const sourceBaseDir = directory.replace('local/combinations', 'combinations');
287      const relativePath = sourceFilePath.replace(sourceBaseDir, '');
288      const resultPath = path.join(this.mCustomProfiles.mOutputDir, relativePath);
289      return {outputPath: resultPath, relativePath: relativePath};
290    } else {
291      throw new Error('Please select a test type');
292    }
293  }
294
295  private writeContent(outputPath: string, relativePath: string, mixedInfo: ObfuscationResultType): void {
296    if (!fs.existsSync(path.dirname(outputPath))) {
297      fs.mkdirSync(path.dirname(outputPath), { recursive: true });
298    }
299
300    fs.writeFileSync(outputPath, mixedInfo.content);
301
302    if (this.mCustomProfiles.mEnableSourceMap && mixedInfo.sourceMap) {
303      fs.writeFileSync(path.join(outputPath + '.map'),
304        JSON.stringify(mixedInfo.sourceMap, null, JSON_TEXT_INDENT_LENGTH));
305    }
306
307    if (this.mCustomProfiles.mEnableNameCache && this.mCustomProfiles.mEnableNameCache) {
308      this.produceNameCache(mixedInfo.nameCache, outputPath);
309    }
310
311    if (mixedInfo.unobfuscationNameMap) {
312      this.loadunobfuscationNameMap(mixedInfo, relativePath);
313    }
314  }
315
316  private loadunobfuscationNameMap(mixedInfo: ObfuscationResultType, relativePath: string): void {
317    let arrayObject: Record<string, string[]> = {};
318    // The type of unobfuscationNameMap's value is Set, convert Set to Array.
319    mixedInfo.unobfuscationNameMap.forEach((value: Set<string>, key: string) => {
320      let array: string[] = Array.from(value);
321      arrayObject[key] = array;
322    });
323    unobfuscationNamesObj[relativePath] = arrayObject;
324  }
325
326  private getPathAfterTest262SecondLevel(fullPath: string): string {
327    const pathParts = fullPath.split('/');
328    const dataIndex = pathParts.indexOf('test262');
329    // 2: Calculate the index of the second-level directory after 'test262'
330    const secondLevelIndex = dataIndex + 2;
331
332    if (dataIndex !== -1 && secondLevelIndex < pathParts.length) {
333      return pathParts.slice(secondLevelIndex).join('/');
334    }
335  
336    return fullPath;
337  }
338
339  private produceNameCache(namecache: { [k: string]: string | {} }, resultPath: string): void {
340    const nameCachePath: string = resultPath + NAME_CACHE_SUFFIX;
341    fs.writeFileSync(nameCachePath, JSON.stringify(namecache, null, JSON_TEXT_INDENT_LENGTH));
342  }
343
344  private readNameCache(sourceFile: string, outputDir: string): void {
345    if (!this.mCustomProfiles.mNameObfuscation?.mEnable || !this.mCustomProfiles.mEnableNameCache) {
346      return;
347    }
348
349    const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX);
350    const nameCache: Object = readCache(nameCachePath);
351    let historyNameCache = new Map<string, string>();
352    let identifierCache = nameCache ? Reflect.get(nameCache, IDENTIFIER_CACHE) : undefined;
353    deleteLineInfoForNameString(historyNameCache, identifierCache);
354
355    renameIdentifierModule.historyNameCache = historyNameCache;
356  }
357
358  private producePropertyCache(outputDir: string): void {
359    if (this.mCustomProfiles.mNameObfuscation &&
360      this.mCustomProfiles.mNameObfuscation.mRenameProperties &&
361      this.mCustomProfiles.mEnableNameCache) {
362      const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE);
363      writeCache(PropCollections.globalMangledTable, propertyCachePath);
364    }
365  }
366
367  private readPropertyCache(outputDir: string): void {
368    if (!this.mCustomProfiles.mNameObfuscation?.mRenameProperties || !this.mCustomProfiles.mEnableNameCache) {
369      return;
370    }
371
372    const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE);
373    const propertyCache: Object = readCache(propertyCachePath);
374    if (!propertyCache) {
375      return;
376    }
377
378    PropCollections.historyMangledTable = getMapFromJson(propertyCache);
379  }
380}