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}