TODO epan/dissectors/asn1/kerberos/packet-kerberos-template.c new GSS flags
[wireshark-sm.git] / tools / check_val_to_str.py
blob4ce2ca8c65022f485e4f218eacb742ea42a944d1
1 #!/usr/bin/env python3
2 # Wireshark - Network traffic analyzer
3 # By Gerald Combs <gerald@wireshark.org>
4 # Copyright 1998 Gerald Combs
6 # SPDX-License-Identifier: GPL-2.0-or-later
8 # Scan dissectors for calls to val_to_str() and friends,
9 # checking for appropriate format specifier strings in
10 # 'unknown' arg.
11 # TODO:
12 # - more detailed format specifier checking (check letter, that there is only 1)
13 # - scan conformance (.cnf) files for ASN1 dissectors?
15 import os
16 import re
17 import subprocess
18 import argparse
19 import signal
22 # Try to exit soon after Ctrl-C is pressed.
23 should_exit = False
25 def signal_handler(sig, frame):
26 global should_exit
27 should_exit = True
28 print('You pressed Ctrl+C - exiting')
30 signal.signal(signal.SIGINT, signal_handler)
33 # Test for whether the given file was automatically generated.
34 def isGeneratedFile(filename):
35 # Check file exists - e.g. may have been deleted in a recent commit.
36 if not os.path.exists(filename):
37 return False
39 # Open file
40 f_read = open(os.path.join(filename), 'r', encoding="utf8")
41 lines_tested = 0
42 for line in f_read:
43 # The comment to say that its generated is near the top, so give up once
44 # get a few lines down.
45 if lines_tested > 10:
46 f_read.close()
47 return False
48 if (line.find('Generated automatically') != -1 or
49 line.find('Generated Automatically') != -1 or
50 line.find('Autogenerated from') != -1 or
51 line.find('is autogenerated') != -1 or
52 line.find('automatically generated by Pidl') != -1 or
53 line.find('Created by: The Qt Meta Object Compiler') != -1 or
54 line.find('This file was generated') != -1 or
55 line.find('This filter was automatically generated') != -1 or
56 line.find('This file is auto generated, do not edit!') != -1 or
57 line.find('This file is auto generated') != -1):
59 f_read.close()
60 return True
61 lines_tested = lines_tested + 1
63 # OK, looks like a hand-written file!
64 f_read.close()
65 return False
69 def removeComments(code_string):
70 code_string = re.sub(re.compile(r"/\*.*?\*/",re.DOTALL ) ,"" ,code_string) # C-style comment
71 code_string = re.sub(re.compile(r"//.*?\n" ) ,"" ,code_string) # C++-style comment
72 return code_string
75 def is_dissector_file(filename):
76 p = re.compile(r'.*(packet|file)-.*\.c')
77 return p.match(filename)
79 def findDissectorFilesInFolder(folder, recursive=False):
80 dissector_files = []
82 if recursive:
83 for root, subfolders, files in os.walk(folder):
84 for f in files:
85 if should_exit:
86 return
87 f = os.path.join(root, f)
88 dissector_files.append(f)
89 else:
90 for f in sorted(os.listdir(folder)):
91 if should_exit:
92 return
93 filename = os.path.join(folder, f)
94 dissector_files.append(filename)
96 return [x for x in filter(is_dissector_file, dissector_files)]
100 warnings_found = 0
101 errors_found = 0
103 # Check the given dissector file.
104 def checkFile(filename, generated):
105 global warnings_found
106 global errors_found
108 # Check file exists - e.g. may have been deleted in a recent commit.
109 if not os.path.exists(filename):
110 print(filename, 'does not exist!')
111 return
113 with open(filename, 'r', encoding="utf8") as f:
114 contents = f.read()
116 # Remove comments so as not to trip up RE.
117 contents = removeComments(contents)
119 matches = re.finditer(r'(?<!try_)(?<!char_)(?<!bytes)(r?val_to_str(?:_ext|)(?:_const|))\(.*?,.*?,\s*(".*?\")\s*\)', contents)
120 for m in matches:
121 function = m.group(1)
122 format_string = m.group(2)
124 # Ignore what appears to be a macro.
125 if format_string.find('#') != -1:
126 continue
128 if function.endswith('_const'):
129 # These ones shouldn't have a specifier - its an error if they do.
130 # TODO: I suppose it could be escaped, but haven't seen this...
131 if format_string.find('%') != -1:
132 # This is an error as format specifier would show in app
133 print('Error:', filename, " ", m.group(0),
134 ' - should not have specifiers in unknown string',
135 '(GENERATED)' if generated else '')
136 errors_found += 1
137 else:
138 # These ones need to have a specifier, and it should be suitable for an int
139 count = format_string.count('%')
140 if count == 0:
141 print('Warning:', filename, " ", m.group(0),
142 ' - should have suitable format specifier in unknown string (or use _const()?)',
143 '(GENERATED)' if generated else '')
144 warnings_found += 1
145 elif count > 1:
146 print('Warning:', filename, " ", m.group(0),
147 ' - has more than one specifier?',
148 '(GENERATED)' if generated else '')
149 # TODO: check allowed specifiers (d, u, x, ?) and modifiers (0-9*) in re ?
150 if format_string.find('%s') != -1:
151 # This is an error as this likely causes a crash
152 print('Error:', filename, " ", m.group(0),
153 ' - inappropriate format specifier in unknown string',
154 '(GENERATED)' if generated else '')
155 errors_found += 1
159 #################################################################
160 # Main logic.
162 # command-line args. Controls which dissector files should be checked.
163 # If no args given, will scan all dissectors.
164 parser = argparse.ArgumentParser(description='Check calls in dissectors')
165 parser.add_argument('--file', action='append',
166 help='specify individual dissector file to test')
167 parser.add_argument('--commits', action='store',
168 help='last N commits to check')
169 parser.add_argument('--open', action='store_true',
170 help='check open files')
171 parser.add_argument('--generated', action='store_true',
172 help='check generated files')
174 args = parser.parse_args()
177 # Get files from wherever command-line args indicate.
178 files = []
179 if args.file:
180 # Add specified file(s)
181 for f in args.file:
182 if not os.path.isfile(f) and not f.startswith('epan'):
183 f = os.path.join('epan', 'dissectors', f)
184 if not os.path.isfile(f):
185 print('Chosen file', f, 'does not exist.')
186 exit(1)
187 else:
188 files.append(f)
189 elif args.commits:
190 # Get files affected by specified number of commits.
191 command = ['git', 'diff', '--name-only', 'HEAD~' + args.commits]
192 files = [f.decode('utf-8')
193 for f in subprocess.check_output(command).splitlines()]
194 # Will examine dissector files only
195 files = list(filter(lambda f : is_dissector_file(f), files))
196 elif args.open:
197 # Unstaged changes.
198 command = ['git', 'diff', '--name-only']
199 files = [f.decode('utf-8')
200 for f in subprocess.check_output(command).splitlines()]
201 # Only interested in dissector files.
202 files = list(filter(lambda f : is_dissector_file(f), files))
203 # Staged changes.
204 command = ['git', 'diff', '--staged', '--name-only']
205 files_staged = [f.decode('utf-8')
206 for f in subprocess.check_output(command).splitlines()]
207 # Only interested in dissector files.
208 files_staged = list(filter(lambda f : is_dissector_file(f), files_staged))
209 for f in files_staged:
210 if f not in files:
211 files.append(f)
212 else:
213 # Find all dissector files from folder.
214 files = findDissectorFilesInFolder(os.path.join('epan', 'dissectors'))
215 files += findDissectorFilesInFolder(os.path.join('plugins', 'epan'), recursive=True)
216 files += findDissectorFilesInFolder(os.path.join('epan', 'dissectors', 'asn1'), recursive=True)
219 # If scanning a subset of files, list them here.
220 print('Examining:')
221 if args.file or args.commits or args.open:
222 if files:
223 print(' '.join(files), '\n')
224 else:
225 print('No files to check.\n')
226 else:
227 print('All dissectors\n')
230 # Now check the chosen files
231 for f in files:
232 if should_exit:
233 exit(1)
234 generated = isGeneratedFile(f)
235 if args.generated or not generated:
236 checkFile(f, generated)
239 # Show summary.
240 print(warnings_found, 'warnings found')
241 if errors_found:
242 print(errors_found, 'errors found')
243 exit(1)