1/* 2 * Copyright (c) 2024 - 2025 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 */ 15import * as fs from 'fs'; 16import * as path from 'path'; 17import { createInterface, Interface } from 'readline'; 18import { DisableText } from './Disable'; 19import { Sdk } from 'arkanalyzer/lib/Config'; 20import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger'; 21import { FileToCheck, ProjectConfig, SelectedFileInfo } from '../../model/ProjectConfig'; 22import { RuleConfig } from '../../model/RuleConfig'; 23import { GlobMatch } from './GlobMatch'; 24 25const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'FileUtils'); 26export class FileUtils { 27 /** 28 * 读取指定文件并返回其内容 29 * @param {string} fileName - 要读取的文件名 30 * @returns {string} - 文件内容 31 */ 32 public static readFile(fileName: string): string { 33 return fs.readFileSync(fileName, 'utf8').replace(/^\ufeff/u, ''); 34 } 35 36 /** 37 * 根据给定的文件列表和规则配置,过滤出符合规则的文件列表。 38 * @param fileList 文件列表。 39 * @param ruleConfig 规则配置,包含匹配和忽略文件的规则,以及可能的重写规则。 40 * @returns 返回符合规则的文件列表,异常情况下返回空数组。 41 */ 42 public static async getFiltedFiles(fileList: string[], ruleConfig: RuleConfig): Promise<string[]> { 43 try { 44 let result = await this.matchFiles(fileList, ruleConfig.files, ruleConfig.ignore); 45 const overrides = ruleConfig.overrides; 46 if (overrides.length > 0) { 47 for (const override of overrides) { 48 result = result.concat(await this.getFiltedFiles(fileList, override)); 49 result = [...new Set(result)]; 50 } 51 } 52 return result; 53 } catch (error) { 54 logger.error(`Error occurred while reading files: ${error}`); 55 return []; 56 } 57 } 58 59 /** 60 * 匹配文件列表中的文件,返回符合条件的文件路径列表 61 * @param fileList 文件路径列表 62 * @param fileTypes 文件类型列表,使用glob模式匹配 63 * @param ignoreDirs 要忽略的目录列表,使用glob模式匹配,默认为空数组 64 * @returns 符合条件的文件路径列表 65 */ 66 public static async matchFiles(fileList: string[], fileGlob: GlobMatch, ignoreGlob: GlobMatch): Promise<string[]> { 67 let result: string[] = []; 68 for (const filePath of fileList) { 69 if (!ignoreGlob?.matchGlob(filePath) && fileGlob.matchGlob(filePath)) { 70 try { 71 // 读取file文件内容首行,若为屏蔽行则跳过 72 const firstLineText = await this.readLinesFromFile(filePath, 1); 73 if (firstLineText.includes(DisableText.FILE_DISABLE_TEXT)) { 74 continue; 75 } 76 result.push(filePath); 77 } catch (e) { 78 logger.error(e); 79 } 80 } 81 } 82 return result; 83 } 84 85 86 /** 87 * 从文件中读取指定行或全部行 88 * @param filePath 文件路径 89 * @param lineNo 要读取的行号,不传或者0值则读取全部行 90 * @returns 读取到的行组成的字符串数组 91 * @throws 如果读取文件时发生错误,将抛出异常 92 */ 93 public static async readLinesFromFile(filePath: string, lineNo?: number): Promise<string[]> { 94 return new Promise<string[]>((resolve, reject) => { 95 let lines: string[] = []; 96 let readLineNo = 1; 97 98 const readStream = fs.createReadStream(filePath); 99 const rl = createInterface({ 100 input: readStream, 101 crlfDelay: Infinity 102 }); 103 104 const handleLine = (line: string): void => { 105 if (lineNo) { 106 if (readLineNo === lineNo) { 107 lines.push(line); 108 rl.close(); 109 } 110 } else { 111 lines.push(line); 112 } 113 readLineNo++; 114 }; 115 116 rl.on('line', handleLine); 117 118 rl.on('close', () => { 119 readStream.destroy(); 120 resolve(lines); 121 }); 122 123 rl.on('error', (err) => { 124 readStream.destroy(); 125 reject(err); 126 }); 127 128 readStream.on('error', (err) => { 129 rl.close(); 130 reject(err); 131 }); 132 }); 133 } 134 135 private processHandleLine(lineNo: number | undefined, readLineNo: number, lines: string[], rl: Interface): void { 136 const handleLine = (line: string): void => { 137 if (lineNo) { 138 if (readLineNo === lineNo) { 139 lines.push(line); 140 rl.close(); 141 } 142 } else { 143 lines.push(line); 144 } 145 readLineNo++; 146 }; 147 148 rl.on('line', handleLine); 149 } 150 151 /** 152 * 检查文件是否存在 153 * @param filePath 文件路径 154 * @returns 如果文件存在则返回true,否则返回false 155 */ 156 public static isExistsSync(filePath: string): boolean { 157 return fs.existsSync(filePath); 158 } 159 160 /** 161 * 从指定路径的JSON文件中获取符合条件的文件信息列表 162 * @param jsonPath JSON文件路径 163 * @param exts 文件扩展名数组 164 * @returns 符合条件的文件信息数组 165 */ 166 public static getSeletctedFileInfos(jsonPath: string, exts: string[]): SelectedFileInfo[] { 167 const fileInfoList: SelectedFileInfo[] = []; 168 try { 169 const jsonData = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); 170 jsonData.checkPath?.forEach((fileInfo: SelectedFileInfo) => { 171 if (exts.includes(path.extname(fileInfo.filePath))) { 172 fileInfoList.push(fileInfo); 173 } 174 }); 175 } catch (error) { 176 logger.error(`Error occurred while reading file list from ${jsonPath}: ${error}`); 177 } 178 return fileInfoList; 179 } 180 181 public static getFileInfoFromFileList(fileOrFolderList: string[]): SelectedFileInfo[] { 182 const fileInfoList: SelectedFileInfo[] = []; 183 fileOrFolderList.forEach((fileOrFolderPath) => { 184 if (fs.statSync(fileOrFolderPath).isFile()) { 185 fileInfoList.push(new FileToCheck(fileOrFolderPath)); 186 } else { 187 const filesInFolder = FileUtils.getAllFiles(fileOrFolderPath, []); 188 filesInFolder.forEach((filePath) => { 189 fileInfoList.push(new FileToCheck(filePath)); 190 }); 191 } 192 }); 193 return fileInfoList; 194 } 195 196 /** 197 * 获取指定目录下所有符合条件的文件 198 * @param dirPath - 目录路径 199 * @param exts - 文件扩展名数组,如果为空则获取所有文件,['.ts', '.ets', '.json5'] 200 * @param filenameArr - 存储符合条件的文件路径的数组,默认为空数组 201 * @param visited - 已访问的目录集合,默认为空集合 202 * @returns 符合条件的文件路径数组 203 */ 204 public static getAllFiles(dirPath: string, exts: string[], filenameArr: string[] = [], visited: Set<string> = new Set<string>()): string[] { 205 // 检查目录是否存在 206 if (!fs.existsSync(dirPath)) { 207 logger.error(`'${dirPath}' is not exist, please check!`); 208 return filenameArr; 209 } 210 // 获取目录的绝对路径 211 const realSrc = fs.realpathSync(dirPath); 212 // 避免重复访问 213 if (visited.has(realSrc)) { 214 return filenameArr; 215 } 216 visited.add(realSrc); 217 // 读取目录下的文件和文件夹 218 fs.readdirSync(realSrc).forEach(fileName => { 219 if (this.shouldSkipFile(fileName)) { 220 return; 221 } 222 const realFile = path.resolve(realSrc, fileName); 223 // 如果是文件夹,则递归调用 224 if (fs.statSync(realFile).isDirectory()) { 225 this.getAllFiles(realFile, exts, filenameArr, visited); 226 } else { 227 // 如果扩展名为空,则添加所有文件 228 if (exts.length === 0) { 229 filenameArr.push(realFile); 230 } else if (this.shouldAddFile(realFile, exts)) { 231 filenameArr.push(realFile); 232 } 233 } 234 }); 235 return filenameArr; 236 } 237 238 private static shouldSkipFile(fileName: string): boolean { 239 return ['oh_modules', 'node_modules', 'hvigorfile.ts', 'hvigorfile.js', 'hvigor-wrapper.js', 'ohosTest'].includes(fileName); 240 } 241 242 private static shouldAddFile(filePath: string, exts: string[]): boolean { 243 if (exts.length === 0) { 244 return true; 245 } 246 const ext = path.extname(filePath).toLowerCase(); 247 return exts.includes(ext); 248 } 249 250 /** 251 * 生成SDK数组 252 * @param projectConfig - 项目配置 253 * @returns Sdk[] - SDK数组 254 */ 255 public static genSdks(projectConfig: ProjectConfig): Sdk[] { 256 let sdks: Sdk[] = []; 257 const sdkConfigPath = path.join(projectConfig.arkCheckPath, 'resources', 'sdkConfig.json'); 258 if (fs.existsSync(sdkConfigPath)) { 259 const configurations = JSON.parse(fs.readFileSync(sdkConfigPath, 'utf-8')); 260 sdks = configurations.sdks ?? []; 261 } 262 if (!projectConfig.ohosSdkPath && !projectConfig.hmsSdkPath) { 263 return sdks; 264 } 265 sdks.forEach(sdk => { 266 if (sdk.name === 'ohosSdk') { 267 sdk.path = projectConfig.ohosSdkPath; 268 } else if (sdk.name === 'hmsSdk') { 269 sdk.path = projectConfig.hmsSdkPath; 270 } else { 271 sdk.path = path.join(projectConfig.arkCheckPath, sdk.path); 272 } 273 }); 274 projectConfig.sdksThirdParty?.forEach(sdkThirdParty => { 275 let sdkJson = JSON.parse(JSON.stringify(sdkThirdParty)); 276 let sdkFd = sdks.find(sdk => sdk.name === sdkJson.name); 277 if (!sdkFd) { 278 let sdk3rd: Sdk = { 279 name: sdkJson.name, 280 path: path.resolve(sdkJson.path), 281 moduleName: sdkJson.moduleName, 282 }; 283 sdks.push(sdk3rd); 284 } else { 285 sdkFd.path = path.resolve(sdkJson.path); 286 sdkFd.moduleName = sdkJson.moduleName; 287 } 288 }); 289 return sdks; 290 } 291 292 293 /** 294 * 写入文件,同步接口 295 * @param filePath 文件路径 296 * @param content 写入的内容 297 * @param mode 写入模式,不传默认为追加模式 298 **/ 299 public static writeToFile(filePath: string, content: string, mode: WriteFileMode = WriteFileMode.APPEND): void { 300 const dirName = path.dirname(filePath); 301 if (!fs.existsSync(dirName)) { 302 fs.mkdirSync(dirName, { recursive: true }); 303 } 304 if (mode === WriteFileMode.OVERWRITE) { 305 fs.writeFileSync(filePath, content, { encoding: 'utf8' }); 306 } else if (mode === WriteFileMode.APPEND) { 307 fs.appendFileSync(filePath, content, { encoding: 'utf8' }); 308 } else { 309 logger.error(`Invalid write mode: ${mode}`); 310 } 311 } 312} 313 314export enum WriteFileMode { 315 OVERWRITE, 316 APPEND 317}