OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 ''' | |
7 Checks a policy_templates.json file for conformity to its syntax specification. | |
8 ''' | |
9 | |
10 import json | |
11 import optparse | |
12 import os | |
13 import re | |
14 import sys | |
15 | |
16 | |
17 LEADING_WHITESPACE = re.compile('^([ \t]*)') | |
18 TRAILING_WHITESPACE = re.compile('.*?([ \t]+)$') | |
19 # Matches all non-empty strings that contain no whitespaces. | |
20 NO_WHITESPACE = re.compile('[^\s]+$') | |
21 | |
22 # Convert a 'type' to its corresponding schema type. | |
23 TYPE_TO_SCHEMA = { | |
24 'int': 'integer', | |
25 'list': 'array', | |
26 'dict': 'object', | |
27 'main': 'boolean', | |
28 'string': 'string', | |
29 'int-enum': 'integer', | |
30 'string-enum': 'string', | |
31 'external': 'object', | |
32 } | |
33 | |
34 # List of boolean policies that have been introduced with negative polarity in | |
35 # the past and should not trigger the negative polarity check. | |
36 LEGACY_INVERTED_POLARITY_WHITELIST = [ | |
37 'DeveloperToolsDisabled', | |
38 'DeviceAutoUpdateDisabled', | |
39 'Disable3DAPIs', | |
40 'DisableAuthNegotiateCnameLookup', | |
41 'DisablePluginFinder', | |
42 'DisablePrintPreview', | |
43 'DisableSafeBrowsingProceedAnyway', | |
44 'DisableScreenshots', | |
45 'DisableSpdy', | |
46 'DisableSSLRecordSplitting', | |
47 'DriveDisabled', | |
48 'DriveDisabledOverCellular', | |
49 'ExternalStorageDisabled', | |
50 'SavingBrowserHistoryDisabled', | |
51 'SyncDisabled', | |
52 ] | |
53 | |
54 class PolicyTemplateChecker(object): | |
55 | |
56 def __init__(self): | |
57 self.error_count = 0 | |
58 self.warning_count = 0 | |
59 self.num_policies = 0 | |
60 self.num_groups = 0 | |
61 self.num_policies_in_groups = 0 | |
62 self.options = None | |
63 self.features = [] | |
64 | |
65 def _Error(self, message, parent_element=None, identifier=None, | |
66 offending_snippet=None): | |
67 self.error_count += 1 | |
68 error = '' | |
69 if identifier is not None and parent_element is not None: | |
70 error += 'In %s %s: ' % (parent_element, identifier) | |
71 print error + 'Error: ' + message | |
72 if offending_snippet is not None: | |
73 print ' Offending:', json.dumps(offending_snippet, indent=2) | |
74 | |
75 def _CheckContains(self, container, key, value_type, | |
76 optional=False, | |
77 parent_element='policy', | |
78 container_name=None, | |
79 identifier=None, | |
80 offending='__CONTAINER__', | |
81 regexp_check=None): | |
82 ''' | |
83 Checks |container| for presence of |key| with value of type |value_type|. | |
84 If |value_type| is string and |regexp_check| is specified, then an error is | |
85 reported when the value does not match the regular expression object. | |
86 | |
87 The other parameters are needed to generate, if applicable, an appropriate | |
88 human-readable error message of the following form: | |
89 | |
90 In |parent_element| |identifier|: | |
91 (if the key is not present): | |
92 Error: |container_name| must have a |value_type| named |key|. | |
93 Offending snippet: |offending| (if specified; defaults to |container|) | |
94 (if the value does not have the required type): | |
95 Error: Value of |key| must be a |value_type|. | |
96 Offending snippet: |container[key]| | |
97 | |
98 Returns: |container[key]| if the key is present, None otherwise. | |
99 ''' | |
100 if identifier is None: | |
101 identifier = container.get('name') | |
102 if container_name is None: | |
103 container_name = parent_element | |
104 if offending == '__CONTAINER__': | |
105 offending = container | |
106 if key not in container: | |
107 if optional: | |
108 return | |
109 else: | |
110 self._Error('%s must have a %s "%s".' % | |
111 (container_name.title(), value_type.__name__, key), | |
112 container_name, identifier, offending) | |
113 return None | |
114 value = container[key] | |
115 if not isinstance(value, value_type): | |
116 self._Error('Value of "%s" must be a %s.' % | |
117 (key, value_type.__name__), | |
118 container_name, identifier, value) | |
119 if value_type == str and regexp_check and not regexp_check.match(value): | |
120 self._Error('Value of "%s" must match "%s".' % | |
121 (key, regexp_check.pattern), | |
122 container_name, identifier, value) | |
123 return value | |
124 | |
125 def _AddPolicyID(self, id, policy_ids, policy): | |
126 ''' | |
127 Adds |id| to |policy_ids|. Generates an error message if the | |
128 |id| exists already; |policy| is needed for this message. | |
129 ''' | |
130 if id in policy_ids: | |
131 self._Error('Duplicate id', 'policy', policy.get('name'), | |
132 id) | |
133 else: | |
134 policy_ids.add(id) | |
135 | |
136 def _CheckPolicyIDs(self, policy_ids): | |
137 ''' | |
138 Checks a set of policy_ids to make sure it contains a continuous range | |
139 of entries (i.e. no holes). | |
140 Holes would not be a technical problem, but we want to ensure that nobody | |
141 accidentally omits IDs. | |
142 ''' | |
143 for i in range(len(policy_ids)): | |
144 if (i + 1) not in policy_ids: | |
145 self._Error('No policy with id: %s' % (i + 1)) | |
146 | |
147 def _CheckPolicySchema(self, policy, policy_type): | |
148 '''Checks that the 'schema' field matches the 'type' field.''' | |
149 self._CheckContains(policy, 'schema', dict) | |
150 if isinstance(policy.get('schema'), dict): | |
151 self._CheckContains(policy['schema'], 'type', str) | |
152 schema_type = policy['schema'].get('type') | |
153 if schema_type != TYPE_TO_SCHEMA[policy_type]: | |
154 self._Error('Schema type must match the existing type for policy %s' % | |
155 policy.get('name')) | |
156 | |
157 # Checks that boolean policies are not negated (which makes them harder to | |
158 # reason about). | |
159 if (schema_type == 'boolean' and | |
160 'disable' in policy.get('name').lower() and | |
161 policy.get('name') not in LEGACY_INVERTED_POLARITY_WHITELIST): | |
162 self._Error(('Boolean policy %s uses negative polarity, please make ' + | |
163 'new boolean policies follow the XYZEnabled pattern. ' + | |
164 'See also http://crbug.com/85687') % policy.get('name')) | |
165 | |
166 | |
167 def _CheckPolicy(self, policy, is_in_group, policy_ids): | |
168 if not isinstance(policy, dict): | |
169 self._Error('Each policy must be a dictionary.', 'policy', None, policy) | |
170 return | |
171 | |
172 # There should not be any unknown keys in |policy|. | |
173 for key in policy: | |
174 if key not in ('name', 'type', 'caption', 'desc', 'device_only', | |
175 'supported_on', 'label', 'policies', 'items', | |
176 'example_value', 'features', 'deprecated', 'future', | |
177 'id', 'schema', 'max_size'): | |
178 self.warning_count += 1 | |
179 print ('In policy %s: Warning: Unknown key: %s' % | |
180 (policy.get('name'), key)) | |
181 | |
182 # Each policy must have a name. | |
183 self._CheckContains(policy, 'name', str, regexp_check=NO_WHITESPACE) | |
184 | |
185 # Each policy must have a type. | |
186 policy_types = ('group', 'main', 'string', 'int', 'list', 'int-enum', | |
187 'string-enum', 'dict', 'external') | |
188 policy_type = self._CheckContains(policy, 'type', str) | |
189 if policy_type not in policy_types: | |
190 self._Error('Policy type must be one of: ' + ', '.join(policy_types), | |
191 'policy', policy.get('name'), policy_type) | |
192 return # Can't continue for unsupported type. | |
193 | |
194 # Each policy must have a caption message. | |
195 self._CheckContains(policy, 'caption', str) | |
196 | |
197 # Each policy must have a description message. | |
198 self._CheckContains(policy, 'desc', str) | |
199 | |
200 # If 'label' is present, it must be a string. | |
201 self._CheckContains(policy, 'label', str, True) | |
202 | |
203 # If 'deprecated' is present, it must be a bool. | |
204 self._CheckContains(policy, 'deprecated', bool, True) | |
205 | |
206 # If 'future' is present, it must be a bool. | |
207 self._CheckContains(policy, 'future', bool, True) | |
208 | |
209 if policy_type == 'group': | |
210 # Groups must not be nested. | |
211 if is_in_group: | |
212 self._Error('Policy groups must not be nested.', 'policy', policy) | |
213 | |
214 # Each policy group must have a list of policies. | |
215 policies = self._CheckContains(policy, 'policies', list) | |
216 | |
217 # Check sub-policies. | |
218 if policies is not None: | |
219 for nested_policy in policies: | |
220 self._CheckPolicy(nested_policy, True, policy_ids) | |
221 | |
222 # Groups must not have an |id|. | |
223 if 'id' in policy: | |
224 self._Error('Policies of type "group" must not have an "id" field.', | |
225 'policy', policy) | |
226 | |
227 # Statistics. | |
228 self.num_groups += 1 | |
229 | |
230 else: # policy_type != group | |
231 # Each policy must have a protobuf ID. | |
232 id = self._CheckContains(policy, 'id', int) | |
233 self._AddPolicyID(id, policy_ids, policy) | |
234 | |
235 # 'schema' is the new 'type'. | |
236 # TODO(joaodasilva): remove the 'type' checks once 'schema' is used | |
237 # everywhere. | |
238 self._CheckPolicySchema(policy, policy_type) | |
239 | |
240 # Each policy must have a supported_on list. | |
241 supported_on = self._CheckContains(policy, 'supported_on', list) | |
242 if supported_on is not None: | |
243 for s in supported_on: | |
244 if not isinstance(s, str): | |
245 self._Error('Entries in "supported_on" must be strings.', 'policy', | |
246 policy, supported_on) | |
247 | |
248 # Each policy must have a 'features' dict. | |
249 features = self._CheckContains(policy, 'features', dict) | |
250 | |
251 # All the features must have a documenting message. | |
252 if features: | |
253 for feature in features: | |
254 if not feature in self.features: | |
255 self._Error('Unknown feature "%s". Known features must have a ' | |
256 'documentation string in the messages dictionary.' % | |
257 feature, 'policy', policy.get('name', policy)) | |
258 | |
259 # All user policies must have a per_profile feature flag. | |
260 if (not policy.get('device_only', False) and | |
261 not policy.get('deprecated', False) and | |
262 not filter(re.compile('^chrome_frame:.*').match, supported_on)): | |
263 self._CheckContains(features, 'per_profile', bool, | |
264 container_name='features', | |
265 identifier=policy.get('name')) | |
266 | |
267 # All policies must declare whether they allow changes at runtime. | |
268 self._CheckContains(features, 'dynamic_refresh', bool, | |
269 container_name='features', | |
270 identifier=policy.get('name')) | |
271 | |
272 # Each policy must have an 'example_value' of appropriate type. | |
273 if policy_type == 'main': | |
274 value_type = bool | |
275 elif policy_type in ('string', 'string-enum'): | |
276 value_type = str | |
277 elif policy_type in ('int', 'int-enum'): | |
278 value_type = int | |
279 elif policy_type == 'list': | |
280 value_type = list | |
281 elif policy_type in ('dict', 'external'): | |
282 value_type = dict | |
283 else: | |
284 raise NotImplementedError('Unimplemented policy type: %s' % policy_type) | |
285 self._CheckContains(policy, 'example_value', value_type) | |
286 | |
287 # Statistics. | |
288 self.num_policies += 1 | |
289 if is_in_group: | |
290 self.num_policies_in_groups += 1 | |
291 | |
292 if policy_type in ('int-enum', 'string-enum'): | |
293 # Enums must contain a list of items. | |
294 items = self._CheckContains(policy, 'items', list) | |
295 if items is not None: | |
296 if len(items) < 1: | |
297 self._Error('"items" must not be empty.', 'policy', policy, items) | |
298 for item in items: | |
299 # Each item must have a name. | |
300 # Note: |policy.get('name')| is used instead of |policy['name']| | |
301 # because it returns None rather than failing when no key called | |
302 # 'name' exists. | |
303 self._CheckContains(item, 'name', str, container_name='item', | |
304 identifier=policy.get('name'), | |
305 regexp_check=NO_WHITESPACE) | |
306 | |
307 # Each item must have a value of the correct type. | |
308 self._CheckContains(item, 'value', value_type, container_name='item', | |
309 identifier=policy.get('name')) | |
310 | |
311 # Each item must have a caption. | |
312 self._CheckContains(item, 'caption', str, container_name='item', | |
313 identifier=policy.get('name')) | |
314 | |
315 if policy_type == 'external': | |
316 # Each policy referencing external data must specify a maximum data size. | |
317 self._CheckContains(policy, 'max_size', int) | |
318 | |
319 def _CheckMessage(self, key, value): | |
320 # |key| must be a string, |value| a dict. | |
321 if not isinstance(key, str): | |
322 self._Error('Each message key must be a string.', 'message', key, key) | |
323 return | |
324 | |
325 if not isinstance(value, dict): | |
326 self._Error('Each message must be a dictionary.', 'message', key, value) | |
327 return | |
328 | |
329 # Each message must have a desc. | |
330 self._CheckContains(value, 'desc', str, parent_element='message', | |
331 identifier=key) | |
332 | |
333 # Each message must have a text. | |
334 self._CheckContains(value, 'text', str, parent_element='message', | |
335 identifier=key) | |
336 | |
337 # There should not be any unknown keys in |value|. | |
338 for vkey in value: | |
339 if vkey not in ('desc', 'text'): | |
340 self.warning_count += 1 | |
341 print 'In message %s: Warning: Unknown key: %s' % (key, vkey) | |
342 | |
343 def _LeadingWhitespace(self, line): | |
344 match = LEADING_WHITESPACE.match(line) | |
345 if match: | |
346 return match.group(1) | |
347 return '' | |
348 | |
349 def _TrailingWhitespace(self, line): | |
350 match = TRAILING_WHITESPACE.match(line) | |
351 if match: | |
352 return match.group(1) | |
353 return '' | |
354 | |
355 def _LineError(self, message, line_number): | |
356 self.error_count += 1 | |
357 print 'In line %d: Error: %s' % (line_number, message) | |
358 | |
359 def _LineWarning(self, message, line_number): | |
360 self.warning_count += 1 | |
361 print ('In line %d: Warning: Automatically fixing formatting: %s' | |
362 % (line_number, message)) | |
363 | |
364 def _CheckFormat(self, filename): | |
365 if self.options.fix: | |
366 fixed_lines = [] | |
367 with open(filename) as f: | |
368 indent = 0 | |
369 line_number = 0 | |
370 for line in f: | |
371 line_number += 1 | |
372 line = line.rstrip('\n') | |
373 # Check for trailing whitespace. | |
374 trailing_whitespace = self._TrailingWhitespace(line) | |
375 if len(trailing_whitespace) > 0: | |
376 if self.options.fix: | |
377 line = line.rstrip() | |
378 self._LineWarning('Trailing whitespace.', line_number) | |
379 else: | |
380 self._LineError('Trailing whitespace.', line_number) | |
381 if self.options.fix: | |
382 if len(line) == 0: | |
383 fixed_lines += ['\n'] | |
384 continue | |
385 else: | |
386 if line == trailing_whitespace: | |
387 # This also catches the case of an empty line. | |
388 continue | |
389 # Check for correct amount of leading whitespace. | |
390 leading_whitespace = self._LeadingWhitespace(line) | |
391 if leading_whitespace.count('\t') > 0: | |
392 if self.options.fix: | |
393 leading_whitespace = leading_whitespace.replace('\t', ' ') | |
394 line = leading_whitespace + line.lstrip() | |
395 self._LineWarning('Tab character found.', line_number) | |
396 else: | |
397 self._LineError('Tab character found.', line_number) | |
398 if line[len(leading_whitespace)] in (']', '}'): | |
399 indent -= 2 | |
400 if line[0] != '#': # Ignore 0-indented comments. | |
401 if len(leading_whitespace) != indent: | |
402 if self.options.fix: | |
403 line = ' ' * indent + line.lstrip() | |
404 self._LineWarning('Indentation should be ' + str(indent) + | |
405 ' spaces.', line_number) | |
406 else: | |
407 self._LineError('Bad indentation. Should be ' + str(indent) + | |
408 ' spaces.', line_number) | |
409 if line[-1] in ('[', '{'): | |
410 indent += 2 | |
411 if self.options.fix: | |
412 fixed_lines.append(line + '\n') | |
413 | |
414 # If --fix is specified: backup the file (deleting any existing backup), | |
415 # then write the fixed version with the old filename. | |
416 if self.options.fix: | |
417 if self.options.backup: | |
418 backupfilename = filename + '.bak' | |
419 if os.path.exists(backupfilename): | |
420 os.remove(backupfilename) | |
421 os.rename(filename, backupfilename) | |
422 with open(filename, 'w') as f: | |
423 f.writelines(fixed_lines) | |
424 | |
425 def Main(self, filename, options): | |
426 try: | |
427 with open(filename) as f: | |
428 data = eval(f.read()) | |
429 except: | |
430 import traceback | |
431 traceback.print_exc(file=sys.stdout) | |
432 self._Error('Invalid JSON syntax.') | |
433 return | |
434 if data == None: | |
435 self._Error('Invalid JSON syntax.') | |
436 return | |
437 self.options = options | |
438 | |
439 # First part: check JSON structure. | |
440 | |
441 # Check (non-policy-specific) message definitions. | |
442 messages = self._CheckContains(data, 'messages', dict, | |
443 parent_element=None, | |
444 container_name='The root element', | |
445 offending=None) | |
446 if messages is not None: | |
447 for message in messages: | |
448 self._CheckMessage(message, messages[message]) | |
449 if message.startswith('doc_feature_'): | |
450 self.features.append(message[12:]) | |
451 | |
452 # Check policy definitions. | |
453 policy_definitions = self._CheckContains(data, 'policy_definitions', list, | |
454 parent_element=None, | |
455 container_name='The root element', | |
456 offending=None) | |
457 if policy_definitions is not None: | |
458 policy_ids = set() | |
459 for policy in policy_definitions: | |
460 self._CheckPolicy(policy, False, policy_ids) | |
461 self._CheckPolicyIDs(policy_ids) | |
462 | |
463 # Second part: check formatting. | |
464 self._CheckFormat(filename) | |
465 | |
466 # Third part: summary and exit. | |
467 print ('Finished checking %s. %d errors, %d warnings.' % | |
468 (filename, self.error_count, self.warning_count)) | |
469 if self.options.stats: | |
470 if self.num_groups > 0: | |
471 print ('%d policies, %d of those in %d groups (containing on ' | |
472 'average %.1f policies).' % | |
473 (self.num_policies, self.num_policies_in_groups, self.num_groups, | |
474 (1.0 * self.num_policies_in_groups / self.num_groups))) | |
475 else: | |
476 print self.num_policies, 'policies, 0 policy groups.' | |
477 if self.error_count > 0: | |
478 return 1 | |
479 return 0 | |
480 | |
481 def Run(self, argv, filename=None): | |
482 parser = optparse.OptionParser( | |
483 usage='usage: %prog [options] filename', | |
484 description='Syntax check a policy_templates.json file.') | |
485 parser.add_option('--fix', action='store_true', | |
486 help='Automatically fix formatting.') | |
487 parser.add_option('--backup', action='store_true', | |
488 help='Create backup of original file (before fixing).') | |
489 parser.add_option('--stats', action='store_true', | |
490 help='Generate statistics.') | |
491 (options, args) = parser.parse_args(argv) | |
492 if filename is None: | |
493 if len(args) != 2: | |
494 parser.print_help() | |
495 sys.exit(1) | |
496 filename = args[1] | |
497 return self.Main(filename, options) | |
498 | |
499 | |
500 if __name__ == '__main__': | |
501 sys.exit(PolicyTemplateChecker().Run(sys.argv)) | |
OLD | NEW |