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/ApiReader'; 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'; 40 41import type { IOptions } from './configs/IOptions'; 42 43const JSON_TEXT_INDENT_LENGTH: number = 2; 44 45export class ArkObfuscatorForTest extends ArkObfuscator { 46 // A list of source file path 47 private readonly mSourceFiles: string[]; 48 49 // Path of obfuscation configuration file. 50 private readonly mConfigPath: string; 51 52 constructor(sourceFiles?: string[], configPath?: string) { 53 super(); 54 this.mSourceFiles = sourceFiles; 55 this.mConfigPath = configPath; 56 } 57 58 public get configPath(): string { 59 return this.mConfigPath; 60 } 61 62 /** 63 * init ArkObfuscator according to user config 64 * should be called after constructor 65 */ 66 public init(config: IOptions | undefined): boolean { 67 if (!config) { 68 console.error('obfuscation config file is not found and no given config.'); 69 return false; 70 } 71 72 handleReservedConfig(config, 'mNameObfuscation', 'mReservedProperties', 'mUniversalReservedProperties'); 73 handleReservedConfig(config, 'mNameObfuscation', 'mReservedToplevelNames', 'mUniversalReservedToplevelNames'); 74 return super.init(config); 75 } 76 77 /** 78 * Obfuscate all the source files. 79 */ 80 public async obfuscateFiles(): Promise<void> { 81 if (!path.isAbsolute(this.mCustomProfiles.mOutputDir)) { 82 this.mCustomProfiles.mOutputDir = path.join(path.dirname(this.mConfigPath), this.mCustomProfiles.mOutputDir); 83 } 84 if (this.mCustomProfiles.mOutputDir && !fs.existsSync(this.mCustomProfiles.mOutputDir)) { 85 fs.mkdirSync(this.mCustomProfiles.mOutputDir); 86 } 87 88 performancePrinter?.filesPrinter?.startEvent(EventList.ALL_FILES_OBFUSCATION); 89 readProjectProperties(this.mSourceFiles, this.mCustomProfiles); 90 const propertyCachePath = path.join(this.mCustomProfiles.mOutputDir, 91 path.basename(this.mSourceFiles[0])); // Get dir name 92 this.readPropertyCache(propertyCachePath); 93 94 // support directory and file obfuscate 95 for (const sourcePath of this.mSourceFiles) { 96 if (!fs.existsSync(sourcePath)) { 97 console.error(`File ${FileUtils.getFileName(sourcePath)} is not found.`); 98 return; 99 } 100 101 if (fs.lstatSync(sourcePath).isFile()) { 102 await this.obfuscateFile(sourcePath, this.mCustomProfiles.mOutputDir); 103 continue; 104 } 105 106 const dirPrefix: string = FileUtils.getPrefix(sourcePath); 107 await this.obfuscateDir(sourcePath, dirPrefix); 108 } 109 110 this.producePropertyCache(propertyCachePath); 111 performancePrinter?.filesPrinter?.endEvent(EventList.ALL_FILES_OBFUSCATION); 112 performancePrinter?.timeSumPrinter?.print('Sum up time of processes'); 113 performancePrinter?.timeSumPrinter?.summarizeEventDuration(); 114 } 115 116 /** 117 * obfuscate directory 118 * @private 119 */ 120 private async obfuscateDir(dirName: string, dirPrefix: string): Promise<void> { 121 const currentDir: string = FileUtils.getPathWithoutPrefix(dirName, dirPrefix); 122 let newDir: string = this.mCustomProfiles.mOutputDir; 123 // there is no need to create directory because the directory names will be obfuscated. 124 if (!this.mCustomProfiles.mRenameFileName?.mEnable) { 125 newDir = path.join(this.mCustomProfiles.mOutputDir, currentDir); 126 if (!fs.existsSync(newDir)) { 127 fs.mkdirSync(newDir); 128 } 129 } 130 131 const fileNames: string[] = fs.readdirSync(dirName); 132 for (let fileName of fileNames) { 133 const filePath: string = path.join(dirName, fileName); 134 if (fs.lstatSync(filePath).isFile()) { 135 await this.obfuscateFile(filePath, newDir); 136 continue; 137 } 138 139 await this.obfuscateDir(filePath, dirPrefix); 140 } 141 } 142 143 /** 144 * Obfuscate single source file with path provided 145 * 146 * @param sourceFilePath single source file path 147 * @param outputDir 148 */ 149 public async obfuscateFile(sourceFilePath: string, outputDir: string): Promise<void> { 150 const fileName: string = FileUtils.getFileName(sourceFilePath); 151 if (this.isObfsIgnoreFile(fileName)) { 152 fs.copyFileSync(sourceFilePath, path.join(outputDir, fileName)); 153 return; 154 } 155 156 const test262Filename = this.getPathAfterTest262SecondLevel(sourceFilePath); 157 const isFileInArray = filterFileArray.includes(test262Filename); 158 // To skip the path where 262 test will fail. 159 if (isFileInArray) { 160 return; 161 } 162 163 // Add the whitelist of file name obfuscation for ut. 164 if (this.mCustomProfiles.mRenameFileName?.mEnable) { 165 const reservedArray = this.mCustomProfiles.mRenameFileName.mReservedFileNames; 166 FileUtils.collectPathReservedString(this.mConfigPath, reservedArray); 167 } 168 let content: string = FileUtils.readFile(sourceFilePath); 169 this.readNameCache(sourceFilePath, outputDir); 170 performancePrinter?.filesPrinter?.startEvent(sourceFilePath); 171 const mixedInfo: ObfuscationResultType = await this.obfuscate(content, sourceFilePath); 172 performancePrinter?.filesPrinter?.endEvent(sourceFilePath, undefined, true); 173 174 if (this.mWriteOriginalFile && mixedInfo) { 175 // Write the obfuscated content directly to orignal file. 176 fs.writeFileSync(sourceFilePath, mixedInfo.content); 177 return; 178 } 179 if (outputDir && mixedInfo) { 180 // the writing file is for the ut. 181 const testCasesRootPath = path.join(__dirname, '../', 'test/grammar'); 182 let relativePath = ''; 183 let resultPath = ''; 184 if (this.mCustomProfiles.mRenameFileName?.mEnable && mixedInfo.filePath) { 185 relativePath = mixedInfo.filePath.replace(testCasesRootPath, ''); 186 } else { 187 relativePath = sourceFilePath.replace(testCasesRootPath, ''); 188 } 189 resultPath = path.join(this.mCustomProfiles.mOutputDir, relativePath); 190 fs.mkdirSync(path.dirname(resultPath), { recursive: true }); 191 fs.writeFileSync(resultPath, mixedInfo.content); 192 193 if (this.mCustomProfiles.mEnableSourceMap && mixedInfo.sourceMap) { 194 fs.writeFileSync(path.join(resultPath + '.map'), 195 JSON.stringify(mixedInfo.sourceMap, null, JSON_TEXT_INDENT_LENGTH)); 196 } 197 198 if (this.mCustomProfiles.mEnableNameCache && this.mCustomProfiles.mEnableNameCache) { 199 this.produceNameCache(mixedInfo.nameCache, resultPath); 200 } 201 } 202 } 203 204 private getPathAfterTest262SecondLevel(fullPath: string): string { 205 const pathParts = fullPath.split('/'); 206 const dataIndex = pathParts.indexOf('test262'); 207 // 2: Calculate the index of the second-level directory after 'test262' 208 const secondLevelIndex = dataIndex + 2; 209 210 if (dataIndex !== -1 && secondLevelIndex < pathParts.length) { 211 return pathParts.slice(secondLevelIndex).join('/'); 212 } 213 214 return fullPath; 215 } 216 217 private produceNameCache(namecache: { [k: string]: string | {} }, resultPath: string): void { 218 const nameCachePath: string = resultPath + NAME_CACHE_SUFFIX; 219 fs.writeFileSync(nameCachePath, JSON.stringify(namecache, null, JSON_TEXT_INDENT_LENGTH)); 220 } 221 222 private readNameCache(sourceFile: string, outputDir: string): void { 223 if (!this.mCustomProfiles.mNameObfuscation?.mEnable || !this.mCustomProfiles.mEnableNameCache) { 224 return; 225 } 226 227 const nameCachePath: string = path.join(outputDir, FileUtils.getFileName(sourceFile) + NAME_CACHE_SUFFIX); 228 const nameCache: Object = readCache(nameCachePath); 229 let historyNameCache = new Map<string, string>(); 230 let identifierCache = nameCache ? Reflect.get(nameCache, IDENTIFIER_CACHE) : undefined; 231 deleteLineInfoForNameString(historyNameCache, identifierCache); 232 233 renameIdentifierModule.historyNameCache = historyNameCache; 234 } 235 236 private producePropertyCache(outputDir: string): void { 237 if (this.mCustomProfiles.mNameObfuscation && 238 this.mCustomProfiles.mNameObfuscation.mRenameProperties && 239 this.mCustomProfiles.mEnableNameCache) { 240 const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE); 241 writeCache(PropCollections.globalMangledTable, propertyCachePath); 242 } 243 } 244 245 private readPropertyCache(outputDir: string): void { 246 if (!this.mCustomProfiles.mNameObfuscation?.mRenameProperties || !this.mCustomProfiles.mEnableNameCache) { 247 return; 248 } 249 250 const propertyCachePath: string = path.join(outputDir, PROPERTY_CACHE_FILE); 251 const propertyCache: Object = readCache(propertyCachePath); 252 if (!propertyCache) { 253 return; 254 } 255 256 PropCollections.historyMangledTable = getMapFromJson(propertyCache); 257 } 258}