• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}