#!/usr/bin/env python3 import json, sys def assert_non_empty_string(obj, field): assert field in obj, 'Missing field "%s"' % field assert isinstance(obj[field], basestring), \ 'Field "%s" must be a string' % field assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field def assert_non_empty_list(obj, field): assert isinstance(obj[field], list), \ '%s must be a list' % field assert len(obj[field]) > 0, \ '%s list must not be empty' % field def assert_non_empty_dict(obj, field): assert isinstance(obj[field], dict), \ '%s must be a dict' % field assert len(obj[field]) > 0, \ '%s dict must not be empty' % field def assert_contains(obj, field): assert field in obj, 'Must contain field "%s"' % field def assert_value_from(obj, field, items): assert obj[field] in items, \ 'Field "%s" must be from: %s' % (field, str(items)) def assert_atom_or_list_items_from(obj, field, items): if isinstance(obj[field], basestring) or isinstance( obj[field], int) or obj[field] is None: assert_value_from(obj, field, items) return assert isinstance(obj[field], list), '%s must be a list' % field for allowed_value in obj[field]: assert allowed_value != '*', "Wildcard is not supported for lists!" assert allowed_value in items, \ 'Field "%s" must be from: %s' % (field, str(items)) def assert_contains_only_fields(obj, expected_fields): for expected_field in expected_fields: assert_contains(obj, expected_field) for actual_field in obj: assert actual_field in expected_fields, \ 'Unexpected field "%s".' % actual_field def leaf_values(schema): if isinstance(schema, list): return schema ret = [] for _, sub_schema in schema.iteritems(): ret += leaf_values(sub_schema) return ret def assert_value_unique_in(value, used_values): assert value not in used_values, 'Duplicate value "%s"!' % str(value) used_values[value] = True def assert_valid_artifact(exp_pattern, artifact_key, schema): if isinstance(schema, list): assert_atom_or_list_items_from(exp_pattern, artifact_key, ["*"] + schema) return for sub_artifact_key, sub_schema in schema.iteritems(): assert_valid_artifact(exp_pattern[artifact_key], sub_artifact_key, sub_schema) def validate(spec_json, details): """ Validates the json specification for generating tests. """ details['object'] = spec_json assert_contains_only_fields(spec_json, [ "selection_pattern", "test_file_path_pattern", "test_description_template", "test_page_title_template", "specification", "delivery_key", "subresource_schema", "source_context_schema", "source_context_list_schema", "test_expansion_schema", "excluded_tests" ]) assert_non_empty_list(spec_json, "specification") assert_non_empty_dict(spec_json, "test_expansion_schema") assert_non_empty_list(spec_json, "excluded_tests") specification = spec_json['specification'] test_expansion_schema = spec_json['test_expansion_schema'] excluded_tests = spec_json['excluded_tests'] valid_test_expansion_fields = test_expansion_schema.keys() # Should be consistent with `sourceContextMap` in # `/common/security-features/resources/common.sub.js`. valid_source_context_names = [ "top", "iframe", "iframe-blank", "srcdoc", "worker-classic", "worker-module", "worker-classic-data", "worker-module-data", "sharedworker-classic", "sharedworker-module", "sharedworker-classic-data", "sharedworker-module-data" ] valid_subresource_names = [ "a-tag", "area-tag", "audio-tag", "form-tag", "iframe-tag", "img-tag", "link-css-tag", "link-prefetch-tag", "object-tag", "picture-tag", "script-tag", "script-tag-dynamic-import", "video-tag" ] + ["beacon", "fetch", "xhr", "websocket"] + [ "worker-classic", "worker-module", "worker-import", "worker-import-data", "sharedworker-classic", "sharedworker-module", "sharedworker-import", "sharedworker-import-data", "serviceworker-classic", "serviceworker-module", "serviceworker-import", "serviceworker-import-data" ] + [ "worklet-animation", "worklet-audio", "worklet-layout", "worklet-paint", "worklet-animation-import", "worklet-audio-import", "worklet-layout-import", "worklet-paint-import", "worklet-animation-import-data", "worklet-audio-import-data", "worklet-layout-import-data", "worklet-paint-import-data" ] # Validate each single spec. for spec in specification: details['object'] = spec # Validate required fields for a single spec. assert_contains_only_fields(spec, [ 'title', 'description', 'specification_url', 'test_expansion' ]) assert_non_empty_string(spec, 'title') assert_non_empty_string(spec, 'description') assert_non_empty_string(spec, 'specification_url') assert_non_empty_list(spec, 'test_expansion') for spec_exp in spec['test_expansion']: details['object'] = spec_exp assert_contains_only_fields(spec_exp, valid_test_expansion_fields) for artifact in test_expansion_schema: details['test_expansion_field'] = artifact assert_valid_artifact(spec_exp, artifact, test_expansion_schema[artifact]) del details['test_expansion_field'] # Validate source_context_schema. details['object'] = spec_json['source_context_schema'] assert_contains_only_fields( spec_json['source_context_schema'], ['supported_delivery_type', 'supported_subresource']) assert_contains_only_fields( spec_json['source_context_schema']['supported_delivery_type'], valid_source_context_names) for source_context in spec_json['source_context_schema'][ 'supported_delivery_type']: assert_valid_artifact( spec_json['source_context_schema']['supported_delivery_type'], source_context, test_expansion_schema['delivery_type']) assert_contains_only_fields( spec_json['source_context_schema']['supported_subresource'], valid_source_context_names) for source_context in spec_json['source_context_schema'][ 'supported_subresource']: assert_valid_artifact( spec_json['source_context_schema']['supported_subresource'], source_context, leaf_values(test_expansion_schema['subresource'])) # Validate subresource_schema. details['object'] = spec_json['subresource_schema'] assert_contains_only_fields(spec_json['subresource_schema'], ['supported_delivery_type']) assert_contains_only_fields( spec_json['subresource_schema']['supported_delivery_type'], leaf_values(test_expansion_schema['subresource'])) for subresource in spec_json['subresource_schema'][ 'supported_delivery_type']: assert_valid_artifact( spec_json['subresource_schema']['supported_delivery_type'], subresource, test_expansion_schema['delivery_type']) # Validate the test_expansion schema members. details['object'] = test_expansion_schema assert_contains_only_fields(test_expansion_schema, [ 'expansion', 'source_scheme', 'source_context_list', 'delivery_type', 'delivery_value', 'redirection', 'subresource', 'origin', 'expectation' ]) assert_atom_or_list_items_from(test_expansion_schema, 'expansion', ['default', 'override']) assert_atom_or_list_items_from(test_expansion_schema, 'source_scheme', ['http', 'https']) assert_atom_or_list_items_from( test_expansion_schema, 'source_context_list', spec_json['source_context_list_schema'].keys()) # Should be consistent with `preprocess_redirection` in # `/common/security-features/subresource/subresource.py`. assert_atom_or_list_items_from(test_expansion_schema, 'redirection', [ 'no-redirect', 'keep-origin', 'swap-origin', 'keep-scheme', 'swap-scheme', 'downgrade' ]) for subresource in leaf_values(test_expansion_schema['subresource']): assert subresource in valid_subresource_names, "Invalid subresource %s" % subresource # Should be consistent with getSubresourceOrigin() in # `/common/security-features/resources/common.sub.js`. assert_atom_or_list_items_from(test_expansion_schema, 'origin', [ 'same-http', 'same-https', 'same-ws', 'same-wss', 'cross-http', 'cross-https', 'cross-ws', 'cross-wss', 'same-http-downgrade', 'cross-http-downgrade', 'same-ws-downgrade', 'cross-ws-downgrade' ]) # Validate excluded tests. details['object'] = excluded_tests for excluded_test_expansion in excluded_tests: assert_contains_only_fields(excluded_test_expansion, valid_test_expansion_fields) details['object'] = excluded_test_expansion for artifact in test_expansion_schema: details['test_expansion_field'] = artifact assert_valid_artifact(excluded_test_expansion, artifact, test_expansion_schema[artifact]) del details['test_expansion_field'] del details['object'] def assert_valid_spec_json(spec_json): error_details = {} try: validate(spec_json, error_details) except AssertionError as err: print('ERROR:', err.message) print(json.dumps(error_details, indent=4)) sys.exit(1) def main(): spec_json = load_spec_json() assert_valid_spec_json(spec_json) print("Spec JSON is valid.") if __name__ == '__main__': main()