1#!/usr/bin/env python3
2# SPDX-License-Identifier: BSD-3-Clause
3# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
4#
5
6from argparse import ArgumentParser
7import locale
8import traceback
9import sys
10import re
11import os
12from os import access, R_OK
13from os.path import isfile
14
15SPDX_LICENSE_TAG = 'SPDX-License-Identifier:'
16SPDX_COPYRIGHT_TAG = 'SPDX-FileCopyrightText:'
17SPDX_DEFAULT_LICENSE = 'BSD-3-Clause'
18OTHER_PROJECTS_FILES = []
19PREFERRED_SPDX_LICENSE_LINE_NUMBER = 2
20RST_PREFERRED_SPDX_LICENSE_LINE_NUMBER = 1
21
22COPYRIGHT_KEYWORD = 'Copyright'
23LICENSE_TAG_PATTERN = r'^(#|\*|//|..|/\*| \*)[ \t*#/]* ' + SPDX_LICENSE_TAG + r'[\w\W]*'
24COPYRIGHT_TAG_PATTERN=r'^(#|\*|//|..|/\*| \*)[ \t*#/]* ' + SPDX_COPYRIGHT_TAG + r'[\w\W]*'
25
26THIRD_PARTY_FILE_TABLE = '.. list-table:: \*\*List of files with different license\*\*'
27RST_TABLE_COL1_PATTERN = r'^( |\t)*\* - [\w\W]*'
28RST_TABLE_COL2_PATTERN = r'^( |\t)*- [\w\W]*'
29
30# check if 'file' is a regular file and it is readable
31def file_readable(file):
32    if not isfile(file):
33        print(file + ": WARNING: File not found")
34        return 0
35
36    if not access(file, R_OK):
37        print(file + ": WARNING: File not readable")
38        return 0
39
40    return 1
41
42# exit program with rc
43def print_error_and_exit(total_errors):
44    if total_errors:
45        print("total: " + str(total_errors) + " errors")
46        sys.exit(1)
47    else:
48        sys.exit(0)
49
50# Get other project file and its license name
51def get_other_projects_files(rst_file):
52    if not file_readable(rst_file):
53        return ""
54
55    empty_line = 0
56    col_num = 1
57    add_row = 0
58
59    with open(rst_file, encoding='utf-8') as fh:
60        for line in fh:
61            if re.search(r'^' + THIRD_PARTY_FILE_TABLE + r'$', line):
62                # Parse the rst table
63                for line in fh:
64                    line = line.rstrip()
65                    # second empty line denotes end of table
66                    if empty_line > 1:
67                        break
68
69                    # ignore first empty line
70                    if not line:
71                        empty_line += 1
72                        continue
73
74                    if col_num == 1 and re.search(RST_TABLE_COL1_PATTERN, line):
75                        col1 = line.split('-',1)[1].strip()
76                        col_num = 2
77                    elif col_num == 2 and re.search(RST_TABLE_COL2_PATTERN,
78                                                    line):
79                        col2 = line.split('-',1)[1].strip()
80                        col_num = 1
81                        add_row = 1
82                    else:
83                        print(rst_file + ": WARNING: Invalid list-table " +
84                              "format in line \"" + line + "\"")
85                        break
86
87                    if add_row:
88                        OTHER_PROJECTS_FILES.append(col1 + "%" + col2)
89                        add_row = 0
90
91                # after parsing the table break
92                break
93
94    return
95
96def license_in_other_project(file, license):
97    search_str = file + "%" + license
98    if search_str in OTHER_PROJECTS_FILES:
99        return 1
100    else:
101        return 0
102
103# Check "SPDX-License-Identifier" tag is at required line number
104# Check if "SPDX-License-Identifier" has a valid license
105def verify_spdx_license_tag(file, line, line_number):
106    errors = 0
107
108    if re.search(r'\.rst$', file):
109        preferred_line_no = RST_PREFERRED_SPDX_LICENSE_LINE_NUMBER
110    else:
111        preferred_line_no = PREFERRED_SPDX_LICENSE_LINE_NUMBER
112
113    if line_number != preferred_line_no:
114        print(file + ": ERROR: \"" + SPDX_LICENSE_TAG + "\" is at line:" +
115              str(line_number) + " preferred line number is " +
116              str(preferred_line_no))
117        errors += 1
118
119    license_string = line.split(SPDX_LICENSE_TAG)[1].strip()
120    if license_string:
121        license = license_string.split()[0]
122        if (license != SPDX_DEFAULT_LICENSE and
123            not license_in_other_project(file, license)):
124            print(file + ": ERROR: Invalid license \"" + license +
125                  "\" at line: " + str(line_number))
126            errors += 1
127    else:
128        print(file + ": ERROR: License name not found at line: " +
129              str(line_number))
130        errors += 1
131
132    return errors
133
134# Check if "SPDX-FileCopyrightText:" starts with COPYRIGHT_KEYWORD
135def verify_spdx_copyright_tag(file, line, line_number):
136    errors = 0
137
138    cpr_string = line.split(SPDX_COPYRIGHT_TAG)[1].strip()
139    if not cpr_string or COPYRIGHT_KEYWORD != cpr_string.split()[0]:
140        print(file + ": ERROR: Copyright text doesn't starts with \"" +
141              COPYRIGHT_KEYWORD + " \" keyword at line: " + str(line_number))
142        errors += 1
143
144    return errors
145
146#
147# Check for tags: "SPDX-License-Identifier", "SPDX-FileCopyrightText"
148#
149# Check if "SPDX-FileCopyrightText" is present.
150# This tag must appear be after "SPDX-License-Identifier" tag
151#
152def verify_spdx_headers(file):
153    print("Checking file: " + file)
154    if not file_readable(file):
155        return 0
156
157    lic_tag_found = 0
158    cpr_tag_found = 0
159    errors = 0
160
161    # read first 25 lines
162    with open(file, encoding='utf-8') as fh:
163        for l in range(1, 25):
164            line = fh.readline()
165            if not line: # EOF
166                break
167            line = line.rstrip()
168            if not line: # empty line
169                continue
170
171            if re.search(LICENSE_TAG_PATTERN, line):
172                if lic_tag_found >= 1:
173                    print(file + ": ERROR: Duplicate \"" + SPDX_LICENSE_TAG +
174                          "\" tag at line: " + str(l))
175                    errors += 1
176                else:
177                    errors += verify_spdx_license_tag(file, line, l)
178                lic_tag_found += 1
179                continue
180
181            if re.search(COPYRIGHT_TAG_PATTERN, line):
182                if not lic_tag_found:
183                    print(file + ": ERROR: \"" + SPDX_COPYRIGHT_TAG +
184                          "\" at line: " + str(l) + " must come after \""
185                          + SPDX_LICENSE_TAG + "\"")
186                    errors += 1
187                errors += verify_spdx_copyright_tag(file, line, l)
188                cpr_tag_found += 1
189                continue
190
191    if not lic_tag_found:
192        print(file + ": ERROR: Missing \"" + SPDX_LICENSE_TAG + "\" tag")
193        errors += 1
194
195    if not cpr_tag_found:
196        print(file + ": ERROR: Missing \"" + SPDX_COPYRIGHT_TAG + "\" tag")
197        errors += 1
198
199    if errors:
200        print(file + ": " + str(errors) + " errors found")
201
202    return errors
203
204# main
205if __name__ == '__main__':
206    ap = ArgumentParser(description='Check SPDX headers')
207    ap.add_argument('files', nargs='*', help='Check files.')
208    ap.add_argument('-r', '--readme-rst', type=str,
209                    help='path to readme.rst file', required=True)
210    args = ap.parse_args()
211
212    total_errors = 0
213    readme_file = args.readme_rst
214
215    # Parse readme file and get the list of files that have non-default license
216    get_other_projects_files(readme_file)
217
218    for file in args.files:
219        total_errors += verify_spdx_headers(file)
220
221    print_error_and_exit(total_errors)
222