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}