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.
7 Checks a policy_templates.json file for conformity to its syntax specification.
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]+$')
22 # Convert a 'type' to its corresponding schema type.
29 'int-enum': 'integer',
30 'string-enum': 'string',
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',
40 'DisableAuthNegotiateCnameLookup',
41 'DisablePluginFinder',
42 'DisablePrintPreview',
43 'DisableSafeBrowsingProceedAnyway',
46 'DisableSSLRecordSplitting',
48 'DriveDisabledOverCellular',
49 'ExternalStorageDisabled',
50 'SavingBrowserHistoryDisabled',
54 class PolicyTemplateChecker(object):
58 self.warning_count = 0
61 self.num_policies_in_groups = 0
65 def _Error(self, message, parent_element=None, identifier=None,
66 offending_snippet=None):
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)
75 def _CheckContains(self, container, key, value_type,
77 parent_element='policy',
80 offending='__CONTAINER__',
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.
87 The other parameters are needed to generate, if applicable, an appropriate
88 human-readable error message of the following form:
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]|
98 Returns: |container[key]| if the key is present, None otherwise.
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:
110 self._Error('%s must have a %s "%s".' %
111 (container_name.title(), value_type.__name__, key),
112 container_name, identifier, offending)
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)
125 def _AddPolicyID(self, id, policy_ids, policy):
127 Adds |id| to |policy_ids|. Generates an error message if the
128 |id| exists already; |policy| is needed for this message.
131 self._Error('Duplicate id', 'policy', policy.get('name'),
136 def _CheckPolicyIDs(self, policy_ids):
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.
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))
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' %
157 # Checks that boolean policies are not negated (which makes them harder to
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'))
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)
172 # There should not be any unknown keys 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))
182 # Each policy must have a name.
183 self._CheckContains(policy, 'name', str, regexp_check=NO_WHITESPACE)
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.
194 # Each policy must have a caption message.
195 self._CheckContains(policy, 'caption', str)
197 # Each policy must have a description message.
198 self._CheckContains(policy, 'desc', str)
200 # If 'label' is present, it must be a string.
201 self._CheckContains(policy, 'label', str, True)
203 # If 'deprecated' is present, it must be a bool.
204 self._CheckContains(policy, 'deprecated', bool, True)
206 # If 'future' is present, it must be a bool.
207 self._CheckContains(policy, 'future', bool, True)
209 if policy_type == 'group':
210 # Groups must not be nested.
212 self._Error('Policy groups must not be nested.', 'policy', policy)
214 # Each policy group must have a list of policies.
215 policies = self._CheckContains(policy, 'policies', list)
217 # Check sub-policies.
218 if policies is not None:
219 for nested_policy in policies:
220 self._CheckPolicy(nested_policy, True, policy_ids)
222 # Groups must not have an |id|.
224 self._Error('Policies of type "group" must not have an "id" field.',
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)
235 # 'schema' is the new 'type'.
236 # TODO(joaodasilva): remove the 'type' checks once 'schema' is used
238 self._CheckPolicySchema(policy, policy_type)
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)
248 # Each policy must have a 'features' dict.
249 features = self._CheckContains(policy, 'features', dict)
251 # All the features must have a documenting message.
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))
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'))
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'))
272 # Each policy must have an 'example_value' of appropriate type.
273 if policy_type == 'main':
275 elif policy_type in ('string', 'string-enum'):
277 elif policy_type in ('int', 'int-enum'):
279 elif policy_type == 'list':
281 elif policy_type in ('dict', 'external'):
284 raise NotImplementedError('Unimplemented policy type: %s' % policy_type)
285 self._CheckContains(policy, 'example_value', value_type)
288 self.num_policies += 1
290 self.num_policies_in_groups += 1
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:
297 self._Error('"items" must not be empty.', 'policy', policy, 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
303 self._CheckContains(item, 'name', str, container_name='item',
304 identifier=policy.get('name'),
305 regexp_check=NO_WHITESPACE)
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'))
311 # Each item must have a caption.
312 self._CheckContains(item, 'caption', str, container_name='item',
313 identifier=policy.get('name'))
315 if policy_type == 'external':
316 # Each policy referencing external data must specify a maximum data size.
317 self._CheckContains(policy, 'max_size', int)
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)
325 if not isinstance(value, dict):
326 self._Error('Each message must be a dictionary.', 'message', key, value)
329 # Each message must have a desc.
330 self._CheckContains(value, 'desc', str, parent_element='message',
333 # Each message must have a text.
334 self._CheckContains(value, 'text', str, parent_element='message',
337 # There should not be any unknown keys 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)
343 def _LeadingWhitespace(self, line):
344 match = LEADING_WHITESPACE.match(line)
346 return match.group(1)
349 def _TrailingWhitespace(self, line):
350 match = TRAILING_WHITESPACE.match(line)
352 return match.group(1)
355 def _LineError(self, message, line_number):
356 self.error_count += 1
357 print 'In line %d: Error: %s' % (line_number, message)
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))
364 def _CheckFormat(self, filename):
367 with open(filename) as f:
372 line = line.rstrip('\n')
373 # Check for trailing whitespace.
374 trailing_whitespace = self._TrailingWhitespace(line)
375 if len(trailing_whitespace) > 0:
378 self._LineWarning('Trailing whitespace.', line_number)
380 self._LineError('Trailing whitespace.', line_number)
383 fixed_lines += ['\n']
386 if line == trailing_whitespace:
387 # This also catches the case of an empty line.
389 # Check for correct amount of leading whitespace.
390 leading_whitespace = self._LeadingWhitespace(line)
391 if leading_whitespace.count('\t') > 0:
393 leading_whitespace = leading_whitespace.replace('\t', ' ')
394 line = leading_whitespace + line.lstrip()
395 self._LineWarning('Tab character found.', line_number)
397 self._LineError('Tab character found.', line_number)
398 if line[len(leading_whitespace)] in (']', '}'):
400 if line[0] != '#': # Ignore 0-indented comments.
401 if len(leading_whitespace) != indent:
403 line = ' ' * indent + line.lstrip()
404 self._LineWarning('Indentation should be ' + str(indent) +
405 ' spaces.', line_number)
407 self._LineError('Bad indentation. Should be ' + str(indent) +
408 ' spaces.', line_number)
409 if line[-1] in ('[', '{'):
412 fixed_lines.append(line + '\n')
414 # If --fix is specified: backup the file (deleting any existing backup),
415 # then write the fixed version with the old filename.
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)
425 def Main(self, filename, options):
427 with open(filename) as f:
428 data = eval(f.read())
431 traceback.print_exc(file=sys.stdout)
432 self._Error('Invalid JSON syntax.')
435 self._Error('Invalid JSON syntax.')
437 self.options = options
439 # First part: check JSON structure.
441 # Check (non-policy-specific) message definitions.
442 messages = self._CheckContains(data, 'messages', dict,
444 container_name='The root element',
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:])
452 # Check policy definitions.
453 policy_definitions = self._CheckContains(data, 'policy_definitions', list,
455 container_name='The root element',
457 if policy_definitions is not None:
459 for policy in policy_definitions:
460 self._CheckPolicy(policy, False, policy_ids)
461 self._CheckPolicyIDs(policy_ids)
463 # Second part: check formatting.
464 self._CheckFormat(filename)
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)))
476 print self.num_policies, 'policies, 0 policy groups.'
477 if self.error_count > 0:
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)
497 return self.Main(filename, options)
500 if __name__ == '__main__':
501 sys.exit(PolicyTemplateChecker().Run(sys.argv))