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