1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright (c) 2022 Huawei Device Co., Ltd.
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17import stat
18import json
19import argparse
20import re
21import pandas as pd
22from bundle_check.bundle_check_common import BundleCheckTools
23from bundle_check.warning_info import BCWarnInfo
24
25
26class OhosInfo:
27    g_root_path = BundleCheckTools.get_root_path()
28    g_ohos_version = BundleCheckTools.get_ohos_version(g_root_path)
29
30
31def check_all_bundle_json(path: str) -> list:
32    '''
33    @func: 检查指定目录下所有 bundle.json 的文件规范。
34    '''
35
36    if os.path.isabs(path):
37        target_path = path
38    else:
39        target_path = os.path.join(OhosInfo.g_root_path, os.path.normpath(path))
40
41    cur_path = os.getcwd()
42    os.chdir(target_path)
43
44    all_bundle = get_all_bundle_json()
45    all_error = []
46
47    for bundle_json_path in all_bundle:
48        bundle_path = bundle_json_path.strip()
49        bundle = BundleJson(bundle_path)
50        bundle_error = bundle.check()
51
52        subsystem_name = bundle.subsystem_name
53        component_name = bundle.component_name
54        if len(subsystem_name) == 0:
55            subsystem_name = "Unknow"
56        if len(component_name) == 0:
57            component_name = "Unknow"
58
59        if not bundle_error:
60            continue
61        for item in bundle_error:
62            item['rule'] = BCWarnInfo.CHECK_RULE_2_1
63            item['path'] = bundle_path
64            item['component'] = component_name
65            item['subsystem'] = subsystem_name
66        all_error.extend(bundle_error)
67    count = len(all_error)
68
69    print('-------------------------------')
70    print('Bundle.json check successfully!')
71    print('There are {} issues in total'.format(count))
72    print('-------------------------------')
73    os.chdir(cur_path)
74    return all_error
75
76
77def get_all_bundle_json(path: str = '.') -> list:
78    '''
79    @func: 获取所有源码工程中所有 bundle.json 文件。
80    '''
81    exclude_list = [
82        r'"./out/*"',
83        r'"./.repo/*"'
84    ]
85    cmd = "find {} -name {}".format(path, "bundle.json")
86    for i in exclude_list:
87        cmd += " ! -path {}".format(i)
88    with os.popen(cmd) as input_cmd:
89        bundle_josn_list = input_cmd.readlines()
90    return bundle_josn_list
91
92
93class BundlesCheck:
94    '''导出全量检查的结果。'''
95
96    @staticmethod
97    def to_json(all_errors: dict,
98                output_path: str = '.',
99                output_name: str = 'all_bundle_error.json'):
100        '''@func: 导出所有错误到 json 格式文件中。'''
101        all_errors = check_all_bundle_json(OhosInfo.g_root_path)
102        all_error_json = json.dumps(all_errors,
103                                    indent=4,
104                                    ensure_ascii=False,
105                                    separators=(', ', ': '))
106        out_path = os.path.normpath(output_path) + '/' + output_name
107
108        flags = os.O_WRONLY | os.O_CREAT
109        modes = stat.S_IWUSR | stat.S_IRUSR
110        with os.fdopen(os.open(out_path, flags, modes), 'w') as file:
111            file.write(all_error_json)
112        print("Please check " + out_path)
113
114    @staticmethod
115    def to_df(path: str = None) -> pd.DataFrame:
116        '''将所有错误的 dict 数据类型转为 pd.DataFrame 类型。'''
117        if path is None:
118            path = OhosInfo.g_root_path
119        else:
120            path = os.path.join(OhosInfo.g_root_path, path)
121        all_errors = check_all_bundle_json(path)
122        columns = ['子系统', '部件', '文件', '违反规则', '详细', '说明']
123        errors_list = []
124        for item in all_errors:
125            error_temp = [
126                item['subsystem'],
127                item['component'],
128                item['path'],
129                item['rule'],
130                "line" + str(item['line']) + ": " + item['contents'],
131                item['description']
132            ]
133            errors_list.append(error_temp)
134        ret = pd.DataFrame(errors_list, columns=columns)
135        return ret
136
137    @staticmethod
138    def to_excel(output_path: str = '.',
139                 output_name: str = 'all_bundle_error.xlsx'):
140        '''
141        @func: 导出所有错误到 excel 格式文件中。
142        '''
143        err_df = BundlesCheck.to_df()
144        outpath = os.path.normpath(output_path) + '/' + output_name
145        err_df.to_excel(outpath, index=None)
146        print('Please check ' + outpath)
147
148
149class BundleJson(object):
150    '''以 bundle.josn 路径来初始化的对象,包含关于该 bundle.josn 的一些属性和操作。
151    @var:
152      - ``__all_errors`` : 表示该文件的所有错误列表。
153      - ``__json`` : 表示将该 josn 文件转为 dict 类型后的内容。
154      - ``__lines`` : 表示将该 josn 文件转为 list 类型后的内容。
155
156    @method:
157      - ``component_name()`` : 返回该 bundle.json 所在部件名。
158      - ``subsystem_name()`` : 返回该 bundle.json 所在子系统名。
159      - ``readlines()`` : 返回该 bundle.json 以每一行内容为元素的 list。
160      - ``get_line_number(s)`` : 返回 s 字符串在该 bundle.josn 中的行号,未知则返回 0。
161      - ``check()`` : 静态检查该 bundle.json,返回错误告警 list。
162    '''
163
164    def __init__(self, path: str) -> None:
165        self.__all_errors = []  # 该文件的所有错误列表
166        self.__json = {}  # 将该 josn 文件转为字典类型内容
167        self.__lines = []  # 将该 josn 文件转为列表类型内容
168        with open(path, 'r') as file:
169            try:
170                self.__json = json.load(file)
171            except json.decoder.JSONDecodeError as error:
172                raise ValueError("'" + path + "'" + " is not a json file.")
173        with open(path, 'r') as file:
174            self.__lines = file.readlines()
175
176    @property
177    def component_name(self) -> str:
178        return self.__json.get('component').get('name')
179
180    @property
181    def subsystem_name(self) -> str:  # 目前存在为空的情况
182        return self.__json.get('component').get('subsystem')
183
184    def readlines(self) -> list:
185        return self.__lines
186
187    def get_line_number(self, string) -> int:
188        '''
189        @func: 获取指定字符串所在行号。
190        '''
191        line_num = 0
192        for line in self.__lines:
193            line_num += 1
194            if string in line:
195                return line_num
196        return 0
197
198    def check(self) -> list:
199        '''
200        @func: 检查该 bundle.json 规范。
201        @note: 去除检查 version 字段。
202        '''
203        err_name = self.check_name()
204        err_segment = self.check_segment()
205        err_component = self.check_component()
206        if err_name:
207            self.__all_errors.append(err_name)
208        if err_segment:
209            self.__all_errors.extend(err_segment)
210        if err_component:
211            self.__all_errors.extend(err_component)
212
213        return self.__all_errors
214
215    # name
216    def check_name(self) -> dict:
217        bundle_error = dict(line=0, contents='"name"')
218
219        if 'name' not in self.__json:
220            bundle_error["description"] = BCWarnInfo.NAME_NO_FIELD
221            return bundle_error
222
223        name = self.__json['name']
224        bundle_error["line"] = self.get_line_number('"name"')
225        if not name:  # 为空
226            bundle_error["description"] = BCWarnInfo.NAME_EMPTY
227            return bundle_error
228
229        bundle_error["description"] = BCWarnInfo.NAME_FORMAT_ERROR + \
230                                      BCWarnInfo.COMPONENT_NAME_FROMAT + \
231                                      BCWarnInfo.COMPONENT_NAME_FROMAT_LEN
232        match = BundleCheckTools.match_bundle_full_name(name)
233        if not match:
234            return bundle_error
235        match = BundleCheckTools.match_unix_like_name(name.split('/')[1])
236        if not match:
237            return bundle_error
238
239        return dict()
240
241    # version
242    def check_version(self) -> dict:
243        bundle_error = dict(line=0, contents='version')
244
245        if 'version' not in self.__json:
246            bundle_error["description"] = BCWarnInfo.VERSION_NO_FIELD
247            return bundle_error
248
249        bundle_error["line"] = self.get_line_number('"version": ')
250        if len(self.__json['version']) < 3:  # example 3.1
251            bundle_error["description"] = BCWarnInfo.VERSION_ERROR
252            return bundle_error
253
254        if self.__json['version'] != OhosInfo.g_ohos_version:
255            bundle_error['description'] = BCWarnInfo.VERSION_ERROR + \
256                                          ' current ohos version is: ' + OhosInfo.g_ohos_version
257            return bundle_error
258        return dict()
259
260    # segment
261    def check_segment(self) -> list:
262        bundle_error_segment = []
263        bundle_error = dict(line=0, contents='"segment"')
264
265        if 'segment' not in self.__json:
266            bundle_error["description"] = BCWarnInfo.SEGMENT_NO_FIELD
267            bundle_error_segment.append(bundle_error)
268            return bundle_error_segment
269
270        bundle_error["line"] = self.get_line_number('"segment":')
271        if 'destPath' not in self.__json['segment']:
272            bundle_error["description"] = BCWarnInfo.SEGMENT_DESTPATH_NO_FIELD
273            bundle_error_segment.append(bundle_error)
274            return bundle_error_segment
275
276        path = self.__json['segment']['destPath']
277        bundle_error["line"] = self.get_line_number('"destPath":')
278        bundle_error["contents"] = '"segment:destPath"'
279        if not path:
280            bundle_error["description"] = BCWarnInfo.SEGMENT_DESTPATH_EMPTY
281            bundle_error_segment.append(bundle_error)
282            return bundle_error_segment
283
284        if isinstance(path, str):
285            bundle_error["description"] = BCWarnInfo.SEGMENT_DESTPATH_UNIQUE
286            bundle_error_segment.append(bundle_error)
287            return bundle_error_segment
288
289        if os.path.isabs(path):
290            bundle_error["description"] = BCWarnInfo.SEGMENT_DESTPATH_ABS
291            bundle_error_segment.append(bundle_error)
292            return bundle_error_segment
293
294        return bundle_error_segment
295
296    # component
297    def check_component(self) -> list:
298        bundle_error_component = []
299
300        if 'component' not in self.__json:
301            bundle_error = dict(line=0, contents='"component"',
302                                description=BCWarnInfo.COMPONENT_NO_FIELD)
303            bundle_error_component.append(bundle_error)
304            return bundle_error_component
305
306        component = self.__json.get('component')
307        component_line = self.get_line_number('"component":')
308        self._check_component_name(component, component_line, bundle_error_component)
309        self._check_component_subsystem(component, component_line, bundle_error_component)
310        self._check_component_syscap(component, bundle_error_component)
311        self._check_component_ast(component, component_line, bundle_error_component)
312        self._check_component_rom(component, component_line, bundle_error_component)
313        self._check_component_ram(component, component_line, bundle_error_component)
314        self._check_component_deps(component, component_line, bundle_error_component)
315
316        return bundle_error_component
317
318        # component name
319
320    def _check_component_name(self, component: dict, component_line: int, bundle_error_component: list):
321        if 'name' not in component:
322            bundle_error = dict(line=component_line,
323                                contents='"component"',
324                                description=BCWarnInfo.COMPONENT_NAME_NO_FIELD)
325            bundle_error_component.append(bundle_error)
326        else:
327            bundle_error = dict(line=component_line + 1,
328                                contents='"component:name"')  # 同名 "name" 暂用 "component" 行号+1
329            if not component['name']:
330                bundle_error["description"] = BCWarnInfo.COMPONENT_NAME_EMPTY
331                bundle_error_component.append(bundle_error)
332            elif 'name' in self.__json and '/' in self.__json['name']:
333                if component['name'] != self.__json['name'].split('/')[1]:
334                    bundle_error["description"] = BCWarnInfo.COMPONENT_NAME_VERACITY
335                    bundle_error_component.append(bundle_error)
336
337        # component subsystem
338
339    def _check_component_subsystem(self, component: dict, component_line: int,
340                                   bundle_error_component: list):
341        if 'subsystem' not in component:
342            bundle_error = dict(line=component_line,
343                                contents="component",
344                                description=BCWarnInfo.COMPONENT_SUBSYSTEM_NO_FIELD)
345            bundle_error_component.append(bundle_error)
346        else:
347            bundle_error = dict(line=self.get_line_number('"subsystem":'),
348                                contents='"component:subsystem"')
349            if not component['subsystem']:
350                bundle_error["description"] = BCWarnInfo.COMPONENT_SUBSYSTEM_EMPTY
351                bundle_error_component.append(bundle_error)
352            elif not BundleCheckTools.is_all_lower(component['subsystem']):
353                bundle_error["description"] = BCWarnInfo.COMPONENT_SUBSYSTEM_LOWCASE
354                bundle_error_component.append(bundle_error)
355
356        # component syscap 可选且可以为空
357
358    def _check_component_syscap(self, component: dict, bundle_error_component: list):
359        if 'syscap' not in component:
360            pass
361        elif component['syscap']:
362            bundle_error = dict(line=self.get_line_number('"syscap":'),
363                                contents='"component:syscap"')
364            err = []  # 收集所有告警
365            for i in component['syscap']:
366                # syscap string empty
367                if not i:
368                    err.append(BCWarnInfo.COMPONENT_SYSCAP_STRING_EMPTY)
369                    continue
370                match = re.match(r'^SystemCapability(\.[A-Z][a-zA-Z]{1,63}){2,6}$', i)
371                if not match:
372                    err.append(BCWarnInfo.COMPONENT_SYSCAP_STRING_FORMAT_ERROR)
373            errs = list(set(err))  # 去重告警
374            if errs:
375                bundle_error["description"] = str(errs)
376                bundle_error_component.append(bundle_error)
377
378        # component adapted_system_type
379
380    def _check_component_ast(self, component: dict, component_line: int, bundle_error_component: list):
381        if 'adapted_system_type' not in component:
382            bundle_error = dict(line=component_line, contents='"component"',
383                                description=BCWarnInfo.COMPONENT_AST_NO_FIELD)
384            bundle_error_component.append(bundle_error)
385            return
386
387        bundle_error = dict(line=self.get_line_number('"adapted_system_type":'),
388                            contents='"component:adapted_system_type"')
389        ast = component["adapted_system_type"]
390        if not ast:
391            bundle_error["description"] = BCWarnInfo.COMPONENT_AST_EMPTY
392            bundle_error_component.append(bundle_error)
393            return
394
395        type_set = tuple(set(ast))
396        if len(ast) > 3 or len(type_set) != len(ast):
397            bundle_error["description"] = BCWarnInfo.COMPONENT_AST_NO_REP
398            bundle_error_component.append(bundle_error)
399            return
400
401        all_type_list = ["mini", "small", "standard"]
402        # 不符合要求的 type
403        error_type = [i for i in ast if i not in all_type_list]
404        if error_type:
405            bundle_error["description"] = BCWarnInfo.COMPONENT_AST_NO_REP
406            bundle_error_component.append(bundle_error)
407        return
408
409        # component rom
410
411    def _check_component_rom(self, component: dict, component_line: int, bundle_error_component: list):
412        if 'rom' not in component:
413            bundle_error = dict(line=component_line, contents='"component:rom"',
414                                description=BCWarnInfo.COMPONENT_ROM_NO_FIELD)
415            bundle_error_component.append(bundle_error)
416        elif not component["rom"]:
417            bundle_error = dict(line=self.get_line_number('"rom":'),
418                                contents='"component:rom"',
419                                description=BCWarnInfo.COMPONENT_ROM_EMPTY)
420            bundle_error_component.append(bundle_error)
421        else:
422            bundle_error = dict(line=self.get_line_number('"rom":'),
423                                contents='"component:rom"')
424            num, unit = BundleCheckTools.split_by_unit(component["rom"])
425            if num < 0:
426                bundle_error["description"] = BCWarnInfo.COMPONENT_ROM_SIZE_ERROR  # 非数值或小于0
427                bundle_error_component.append(bundle_error)
428            elif unit:
429                unit_list = ["KB", "KByte", "MByte", "MB"]
430                if unit not in unit_list:
431                    bundle_error["description"] = BCWarnInfo.COMPONENT_ROM_UNIT_ERROR  # 单位有误
432                    bundle_error_component.append(bundle_error)
433
434        # component ram
435
436    def _check_component_ram(self, component: dict, component_line: int, bundle_error_component: list):
437        if 'ram' not in component:
438            bundle_error = dict(line=component_line, contents='"component:ram"',
439                                description=BCWarnInfo.COMPONENT_RAM_NO_FIELD)
440            bundle_error_component.append(bundle_error)
441        elif not component["ram"]:
442            bundle_error = dict(line=self.get_line_number('"ram":'),
443                                contents='"component:ram"',
444                                description=BCWarnInfo.COMPONENT_RAM_EMPTY)
445            bundle_error_component.append(bundle_error)
446        else:
447            bundle_error = dict(line=self.get_line_number('"ram":'),
448                                contents='"component:ram"')
449            num, unit = BundleCheckTools.split_by_unit(component["ram"])
450            if num <= 0:
451                bundle_error["description"] = BCWarnInfo.COMPONENT_RAM_SIZE_ERROR  # 非数值或小于0
452                bundle_error_component.append(bundle_error)
453            elif unit:
454                unit_list = ["KB", "KByte", "MByte", "MB"]
455                if unit not in unit_list:
456                    bundle_error["description"] = BCWarnInfo.COMPONENT_RAM_UNIT_ERROR  # 单位有误
457                    bundle_error_component.append(bundle_error)
458
459        # component deps
460
461    def _check_component_deps(self, component: dict, component_line: int, bundle_error_component: list):
462        if 'deps' not in component:
463            bundle_error = dict(line=component_line, contents='"component:deps"',
464                                description=BCWarnInfo.COMPONENT_DEPS_NO_FIELD)
465            bundle_error_component.append(bundle_error)
466        else:
467            pass
468
469
470def parse_args():
471    parser = argparse.ArgumentParser()
472    # exclusive output format
473    ex_format = parser.add_mutually_exclusive_group()
474    ex_format.add_argument("--xlsx", help="output format: xls(default).",
475                           action="store_true")
476    ex_format.add_argument("--json", help="output format: json.",
477                           action="store_true")
478    # exclusive input
479    ex_input = parser.add_mutually_exclusive_group()
480    ex_input.add_argument("-P", "--project", help="project root path.", type=str)
481    ex_input.add_argument("-p", "--path", help="bundle.json path list.", nargs='+')
482    # output path
483    parser.add_argument("-o", "--output", help="ouput path.")
484    args = parser.parse_args()
485
486    export_path = '.'
487    if args.output:
488        export_path = args.output
489
490    if args.project:
491        if not BundleCheckTools.is_project(args.project):
492            print("'" + args.project + "' is not a oopeharmony project.")
493            exit(1)
494
495        if args.json:
496            BundlesCheck.to_json(export_path)
497        else:
498            BundlesCheck.to_excel(export_path)
499    elif args.path:
500        bundle_list_error = {}
501        for bundle_json_path in args.path:
502            bundle = BundleJson(bundle_json_path)
503            error_field = bundle.check()
504            if error_field:
505                bundle_list_error[bundle_json_path] = \
506                    dict([(BCWarnInfo.CHECK_RULE_2_1, error_field)])
507        # temp
508        test_json = json.dumps(bundle_list_error,
509                               indent=4, separators=(', ', ': '),
510                               ensure_ascii=False)
511        print(test_json)
512    else:
513        print("use '-h' get help.")
514
515
516if __name__ == '__main__':
517    parse_args()
518