diff options
author | Alexander Hendrich <hendrich@chromium.org> | 2018-08-29 18:39:22 +0900 |
---|---|---|
committer | Qijiang Fan <fqj@google.com> | 2020-06-05 10:24:33 +0900 |
commit | e2dbc3b3f14b243febc1150f4c20d02b30fc8c09 (patch) | |
tree | 46f00a3a82c7849968b4e40186f3124ef82b728a /components | |
parent | d59f0fbbc3a4e5b0175845e68549c3ded706e7c6 (diff) | |
download | libchrome-e2dbc3b3f14b243febc1150f4c20d02b30fc8c09.tar.gz |
Move schema validation from json_schema_validator.h/cc to schema.h/cc
This CL is part of a larger clean-up operation and moves the schema
validation from json_schema_validator.h/cc into schema.h/cc.
json_schema_constants.h/cc is moved to components/policy/core/common.
Also removes src/components/json_schema.
Final clean-up of schema/value validation will follow in the next CL.
Bug: 873641
Change-Id: I2275e5e91c8e75204528d9a6a7ee5eba447e4bfc
Reviewed-on: https://chromium-review.googlesource.com/1181426
Reviewed-by: Cait Phillips <caitkp@chromium.org>
Reviewed-by: Lutz Justen <ljusten@chromium.org>
Commit-Queue: Alexander Hendrich <hendrich@chromium.org>
Cr-Commit-Position: refs/heads/master@{#587051}
CrOS-Libchrome-Original-Commit: 6f09586aa55208c90b80b6671433435199e1c581
Diffstat (limited to 'components')
-rw-r--r-- | components/json_schema/README | 6 | ||||
-rw-r--r-- | components/json_schema/json_schema_validator.cc | 341 | ||||
-rw-r--r-- | components/json_schema/json_schema_validator.h | 83 | ||||
-rw-r--r-- | components/json_schema/json_schema_validator_unittest.cc | 165 | ||||
-rw-r--r-- | components/policy/core/common/json_schema_constants.cc (renamed from components/json_schema/json_schema_constants.cc) | 2 | ||||
-rw-r--r-- | components/policy/core/common/json_schema_constants.h (renamed from components/json_schema/json_schema_constants.h) | 6 | ||||
-rw-r--r-- | components/policy/core/common/schema.cc | 314 | ||||
-rw-r--r-- | components/policy/core/common/schema.h | 24 | ||||
-rw-r--r-- | components/policy/core/common/schema_unittest.cc | 160 |
9 files changed, 497 insertions, 604 deletions
diff --git a/components/json_schema/README b/components/json_schema/README deleted file mode 100644 index c7453db06d..0000000000 --- a/components/json_schema/README +++ /dev/null @@ -1,6 +0,0 @@ -The //components/json_schema component provides: - -a) JSON schema constants, which can be used to inspect schema objects. - -b) The JSONSchemaValidator class, which can be used to parse and validate JSON -schemas, and to validate JSON objects against the parsed schema. diff --git a/components/json_schema/json_schema_validator.cc b/components/json_schema/json_schema_validator.cc deleted file mode 100644 index 18ed7625a8..0000000000 --- a/components/json_schema/json_schema_validator.cc +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright 2013 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "components/json_schema/json_schema_validator.h" - -#include <stddef.h> - -#include <algorithm> -#include <cfloat> -#include <cmath> -#include <memory> -#include <vector> - -#include "base/json/json_reader.h" -#include "base/logging.h" -#include "base/macros.h" -#include "base/memory/ptr_util.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/string_util.h" -#include "base/strings/stringprintf.h" -#include "base/values.h" -#include "components/json_schema/json_schema_constants.h" -#include "third_party/re2/src/re2/re2.h" - -namespace schema = json_schema_constants; - -namespace { - -bool IsValidType(const std::string& type) { - static const char* kValidTypes[] = { - schema::kAny, - schema::kArray, - schema::kBoolean, - schema::kInteger, - schema::kNull, - schema::kNumber, - schema::kObject, - schema::kString, - }; - const char** end = kValidTypes + arraysize(kValidTypes); - return std::find(kValidTypes, end, type) != end; -} - -// Maps a schema attribute name to its expected type. -struct ExpectedType { - const char* key; - base::Value::Type type; -}; - -// Helper for std::lower_bound. -bool CompareToString(const ExpectedType& entry, const std::string& key) { - return entry.key < key; -} - -// If |value| is a dictionary, returns the "name" attribute of |value| or NULL -// if |value| does not contain a "name" attribute. Otherwise, returns |value|. -const base::Value* ExtractNameFromDictionary(const base::Value* value) { - const base::DictionaryValue* value_dict = nullptr; - const base::Value* name_value = nullptr; - if (value->GetAsDictionary(&value_dict)) { - value_dict->Get("name", &name_value); - return name_value; - } - return value; -} - -bool IsValidSchema(const base::DictionaryValue* dict, - int options, - std::string* error) { - // This array must be sorted, so that std::lower_bound can perform a - // binary search. - static const ExpectedType kExpectedTypes[] = { - // Note: kRef == "$ref", kSchema == "$schema" - { schema::kRef, base::Value::Type::STRING }, - { schema::kSchema, base::Value::Type::STRING }, - - { schema::kAdditionalProperties, base::Value::Type::DICTIONARY }, - { schema::kChoices, base::Value::Type::LIST }, - { schema::kDescription, base::Value::Type::STRING }, - { schema::kEnum, base::Value::Type::LIST }, - { schema::kId, base::Value::Type::STRING }, - { schema::kMaxItems, base::Value::Type::INTEGER }, - { schema::kMaxLength, base::Value::Type::INTEGER }, - { schema::kMaximum, base::Value::Type::DOUBLE }, - { schema::kMinItems, base::Value::Type::INTEGER }, - { schema::kMinLength, base::Value::Type::INTEGER }, - { schema::kMinimum, base::Value::Type::DOUBLE }, - { schema::kOptional, base::Value::Type::BOOLEAN }, - { schema::kPattern, base::Value::Type::STRING }, - { schema::kPatternProperties, base::Value::Type::DICTIONARY }, - { schema::kProperties, base::Value::Type::DICTIONARY }, - { schema::kRequired, base::Value::Type::LIST }, - { schema::kTitle, base::Value::Type::STRING }, - }; - - bool has_type_or_ref = false; - const base::ListValue* list_value = nullptr; - const base::DictionaryValue* dictionary_value = nullptr; - std::string string_value; - - const base::ListValue* required_properties_value = nullptr; - const base::DictionaryValue* properties_value = nullptr; - - for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) { - // Validate the "type" attribute, which may be a string or a list. - if (it.key() == schema::kType) { - switch (it.value().type()) { - case base::Value::Type::STRING: - it.value().GetAsString(&string_value); - if (!IsValidType(string_value)) { - *error = "Invalid value for type attribute"; - return false; - } - break; - case base::Value::Type::LIST: - it.value().GetAsList(&list_value); - for (size_t i = 0; i < list_value->GetSize(); ++i) { - if (!list_value->GetString(i, &string_value) || - !IsValidType(string_value)) { - *error = "Invalid value for type attribute"; - return false; - } - } - break; - default: - *error = "Invalid value for type attribute"; - return false; - } - has_type_or_ref = true; - continue; - } - - // Validate the "items" attribute, which is a schema or a list of schemas. - if (it.key() == schema::kItems) { - if (it.value().GetAsDictionary(&dictionary_value)) { - if (!IsValidSchema(dictionary_value, options, error)) { - DCHECK(!error->empty()); - return false; - } - } else if (it.value().GetAsList(&list_value)) { - for (size_t i = 0; i < list_value->GetSize(); ++i) { - if (!list_value->GetDictionary(i, &dictionary_value)) { - *error = base::StringPrintf( - "Invalid entry in items attribute at index %d", - static_cast<int>(i)); - return false; - } - if (!IsValidSchema(dictionary_value, options, error)) { - DCHECK(!error->empty()); - return false; - } - } - } else { - *error = "Invalid value for items attribute"; - return false; - } - continue; - } - - // All the other attributes have a single valid type. - const ExpectedType* end = kExpectedTypes + arraysize(kExpectedTypes); - const ExpectedType* entry = std::lower_bound( - kExpectedTypes, end, it.key(), CompareToString); - if (entry == end || entry->key != it.key()) { - if (options & JSONSchemaValidator::OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES) - continue; - *error = base::StringPrintf("Invalid attribute %s", it.key().c_str()); - return false; - } - - // Integer can be converted to double. - if (!(it.value().type() == entry->type || - (it.value().is_int() && entry->type == base::Value::Type::DOUBLE))) { - *error = base::StringPrintf("Invalid value for %s attribute", - it.key().c_str()); - return false; - } - - // base::Value::Type::INTEGER attributes must be >= 0. - // This applies to "minItems", "maxItems", "minLength" and "maxLength". - if (it.value().is_int()) { - int integer_value; - it.value().GetAsInteger(&integer_value); - if (integer_value < 0) { - *error = base::StringPrintf("Value of %s must be >= 0, got %d", - it.key().c_str(), integer_value); - return false; - } - } - - // Validate the "properties" attribute. Each entry maps a key to a schema. - if (it.key() == schema::kProperties) { - it.value().GetAsDictionary(&properties_value); - for (base::DictionaryValue::Iterator iter(*properties_value); - !iter.IsAtEnd(); iter.Advance()) { - if (!iter.value().GetAsDictionary(&dictionary_value)) { - *error = "properties must be a dictionary"; - return false; - } - if (!IsValidSchema(dictionary_value, options, error)) { - DCHECK(!error->empty()); - return false; - } - } - } - - // Validate the "patternProperties" attribute. Each entry maps a regular - // expression to a schema. The validity of the regular expression expression - // won't be checked here for performance reasons. Instead, invalid regular - // expressions will be caught as validation errors in Validate(). - if (it.key() == schema::kPatternProperties) { - it.value().GetAsDictionary(&dictionary_value); - for (base::DictionaryValue::Iterator iter(*dictionary_value); - !iter.IsAtEnd(); iter.Advance()) { - if (!iter.value().GetAsDictionary(&dictionary_value)) { - *error = "patternProperties must be a dictionary"; - return false; - } - if (!IsValidSchema(dictionary_value, options, error)) { - DCHECK(!error->empty()); - return false; - } - } - } - - // Validate "additionalProperties" attribute, which is a schema. - if (it.key() == schema::kAdditionalProperties) { - it.value().GetAsDictionary(&dictionary_value); - if (!IsValidSchema(dictionary_value, options, error)) { - DCHECK(!error->empty()); - return false; - } - } - - // Validate "required" attribute. - if (it.key() == schema::kRequired) { - it.value().GetAsList(&required_properties_value); - for (const base::Value& value : *required_properties_value) { - if (value.type() != base::Value::Type::STRING) { - *error = "Invalid value in 'required' attribute"; - return false; - } - } - } - - // Validate the values contained in an "enum" attribute. - if (it.key() == schema::kEnum) { - it.value().GetAsList(&list_value); - for (size_t i = 0; i < list_value->GetSize(); ++i) { - const base::Value* value = nullptr; - list_value->Get(i, &value); - // Sometimes the enum declaration is a dictionary with the enum value - // under "name". - value = ExtractNameFromDictionary(value); - if (!value) { - *error = "Invalid value in enum attribute"; - return false; - } - switch (value->type()) { - case base::Value::Type::NONE: - case base::Value::Type::BOOLEAN: - case base::Value::Type::INTEGER: - case base::Value::Type::DOUBLE: - case base::Value::Type::STRING: - break; - default: - *error = "Invalid value in enum attribute"; - return false; - } - } - } - - // Validate the schemas contained in a "choices" attribute. - if (it.key() == schema::kChoices) { - it.value().GetAsList(&list_value); - for (size_t i = 0; i < list_value->GetSize(); ++i) { - if (!list_value->GetDictionary(i, &dictionary_value)) { - *error = "Invalid choices attribute"; - return false; - } - if (!IsValidSchema(dictionary_value, options, error)) { - DCHECK(!error->empty()); - return false; - } - } - } - - if (it.key() == schema::kRef) - has_type_or_ref = true; - } - - // Check that properties in'required' are in the 'properties' object. - if (required_properties_value) { - for (const base::Value& value : required_properties_value->GetList()) { - const std::string& name = value.GetString(); - if (!properties_value || !properties_value->HasKey(name)) { - *error = "Property '" + name + - "' was listed in 'required', but not defined in 'properties'."; - return false; - } - } - } - - if (!has_type_or_ref) { - *error = "Schema must have a type or a $ref attribute"; - return false; - } - - return true; -} - -} // namespace - -// static -std::unique_ptr<base::DictionaryValue> JSONSchemaValidator::IsValidSchema( - const std::string& schema, - std::string* error) { - return JSONSchemaValidator::IsValidSchema(schema, 0, error); -} - -// static -std::unique_ptr<base::DictionaryValue> JSONSchemaValidator::IsValidSchema( - const std::string& schema, - int validator_options, - std::string* error) { - base::JSONParserOptions json_options = base::JSON_PARSE_RFC; - std::unique_ptr<base::Value> json = base::JSONReader::ReadAndReturnError( - schema, json_options, nullptr, error); - if (!json) - return std::unique_ptr<base::DictionaryValue>(); - base::DictionaryValue* dict = nullptr; - if (!json->GetAsDictionary(&dict)) { - *error = "Schema must be a JSON object"; - return std::unique_ptr<base::DictionaryValue>(); - } - if (!::IsValidSchema(dict, validator_options, error)) - return std::unique_ptr<base::DictionaryValue>(); - ignore_result(json.release()); - return base::WrapUnique(dict); -} diff --git a/components/json_schema/json_schema_validator.h b/components/json_schema/json_schema_validator.h deleted file mode 100644 index 0b2f4bd463..0000000000 --- a/components/json_schema/json_schema_validator.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2013 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef COMPONENTS_JSON_SCHEMA_JSON_SCHEMA_VALIDATOR_H_ -#define COMPONENTS_JSON_SCHEMA_JSON_SCHEMA_VALIDATOR_H_ - -#include <map> -#include <memory> -#include <string> -#include <vector> - -#include "base/macros.h" - -namespace base { -class DictionaryValue; -} - -//============================================================================== -// This class implements a subset of JSON Schema. -// See: http://www.json.com/json-schema-proposal/ for more details. -// -// There is also an older JavaScript implementation of the same functionality in -// chrome/renderer/resources/json_schema.js. -// -// The following features of JSON Schema are not implemented: -// - requires -// - unique -// - disallow -// - union types (but replaced with 'choices') -// - number.maxDecimal -// -// The following properties are not applicable to the interface exposed by -// this class: -// - options -// - readonly -// - title -// - description -// - format -// - default -// - transient -// - hidden -// -// There are also these departures from the JSON Schema proposal: -// - null counts as 'unspecified' for optional values -// - added the 'choices' property, to allow specifying a list of possible types -// for a value -// - by default an "object" typed schema does not allow additional properties. -// if present, "additionalProperties" is to be a schema against which all -// additional properties will be validated. -// - regular expression supports all syntaxes that re2 accepts. -// See https://github.com/google/re2/blob/master/doc/syntax.txt for details. -//============================================================================== -class JSONSchemaValidator { - public: - enum Options { - // Ignore unknown attributes. If this option is not set then unknown - // attributes will make the schema validation fail. - OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES = 1 << 0, - }; - - // Verifies if |schema| is a valid JSON v3 schema. When this validation passes - // then |schema| is valid JSON that can be parsed into a DictionaryValue, - // and that DictionaryValue can be used to build a JSONSchemaValidator. - // Returns the parsed DictionaryValue when |schema| validated, otherwise - // returns NULL. In that case, |error| contains an error description. - // For performance reasons, currently IsValidSchema() won't check the - // correctness of regular expressions used in "pattern" and - // "patternProperties" and in Validate() invalid regular expression don't - // accept any strings. - static std::unique_ptr<base::DictionaryValue> IsValidSchema( - const std::string& schema, - std::string* error); - - // Same as above but with |options|, which is a bitwise-OR combination of the - // Options above. - static std::unique_ptr<base::DictionaryValue> - IsValidSchema(const std::string& schema, int options, std::string* error); - - DISALLOW_COPY_AND_ASSIGN(JSONSchemaValidator); -}; - -#endif // COMPONENTS_JSON_SCHEMA_JSON_SCHEMA_VALIDATOR_H_ diff --git a/components/json_schema/json_schema_validator_unittest.cc b/components/json_schema/json_schema_validator_unittest.cc deleted file mode 100644 index cd96325fbc..0000000000 --- a/components/json_schema/json_schema_validator_unittest.cc +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2013 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include <stddef.h> - -#include "base/values.h" -#include "components/json_schema/json_schema_validator.h" -#include "testing/gtest/include/gtest/gtest.h" - -class JSONSchemaValidatorCPPTest : public testing::Test { - public: - JSONSchemaValidatorCPPTest() {} - -}; - -TEST(JSONSchemaValidator, IsValidSchema) { - std::string error; - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema("", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema("\0", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema("string", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema("\"string\"", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema("[]", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema("{}", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{ \"type\": 123 }", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{ \"type\": \"invalid\" }", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"object\"," - " \"properties\": []" // Invalid properties type. - "}", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"string\"," - " \"maxLength\": -1" // Must be >= 0. - "}", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"string\"," - " \"enum\": [ {} ]" // "enum" dict values must contain "name". - "}", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"string\"," - " \"enum\": [ { \"name\": {} } ]" // "enum" name must be a simple value. - "}", &error)); - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"array\"," - " \"items\": [ 123 ]," // "items" must contain a schema or schemas. - "}", &error)); - EXPECT_TRUE(JSONSchemaValidator::IsValidSchema( - "{ \"type\": \"object\" }", &error)) << error; - EXPECT_TRUE(JSONSchemaValidator::IsValidSchema( - "{ \"type\": [\"object\", \"array\"] }", &error)) << error; - EXPECT_TRUE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": [\"object\", \"array\"]," - " \"properties\": {" - " \"string-property\": {" - " \"type\": \"string\"," - " \"minLength\": 1," - " \"maxLength\": 100," - " \"title\": \"The String Policy\"," - " \"description\": \"This policy controls the String widget.\"" - " }," - " \"integer-property\": {" - " \"type\": \"number\"," - " \"minimum\": 1000.0," - " \"maximum\": 9999.0" - " }," - " \"enum-property\": {" - " \"type\": \"integer\"," - " \"enum\": [0, 1, {\"name\": 10}, 100]" - " }," - " \"items-property\": {" - " \"type\": \"array\"," - " \"items\": {" - " \"type\": \"string\"" - " }" - " }," - " \"items-list-property\": {" - " \"type\": \"array\"," - " \"items\": [" - " { \"type\": \"string\" }," - " { \"type\": \"integer\" }" - " ]" - " }" - " }," - " \"additionalProperties\": {" - " \"type\": \"any\"" - " }" - "}", &error)) << error; - EXPECT_TRUE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"object\"," - " \"patternProperties\": {" - " \".\": { \"type\": \"any\" }," - " \"foo\": { \"type\": \"any\" }," - " \"^foo$\": { \"type\": \"any\" }," - " \"foo+\": { \"type\": \"any\" }," - " \"foo?\": { \"type\": \"any\" }," - " \"fo{2,4}\": { \"type\": \"any\" }," - " \"(left)|(right)\": { \"type\": \"any\" }" - " }" - "}", &error)) << error; - EXPECT_TRUE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"object\"," - " \"unknown attribute\": \"that should just be ignored\"" - "}", - JSONSchemaValidator::OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES, - &error)) << error; - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema( - "{" - " \"type\": \"object\"," - " \"unknown attribute\": \"that will cause a failure\"" - "}", 0, &error)) << error; - - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema(R"( - { - "type": "object", - "properties": {"foo": {"type": "number"}}, - "required": 123 - })", - 0, &error)) - << error; - - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema(R"( - { - "type": "object", - "properties": {"foo": {"type": "number"}}, - "required": [ 123 ] - })", - 0, &error)) - << error; - - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema(R"( - { - "type": "object", - "properties": {"foo": {"type": "number"}}, - "required": ["bar"] - })", - 0, &error)) - << error; - - EXPECT_FALSE(JSONSchemaValidator::IsValidSchema(R"( - { - "type": "object", - "required": ["bar"] - })", - 0, &error)) - << error; - - EXPECT_TRUE(JSONSchemaValidator::IsValidSchema(R"( - { - "type": "object", - "properties": {"foo": {"type": "number"}}, - "required": ["foo"] - })", - 0, &error)) - << error; -} diff --git a/components/json_schema/json_schema_constants.cc b/components/policy/core/common/json_schema_constants.cc index 79c5eaf415..1a6d1ac0fd 100644 --- a/components/json_schema/json_schema_constants.cc +++ b/components/policy/core/common/json_schema_constants.cc @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "components/json_schema/json_schema_constants.h" +#include "components/policy/core/common/json_schema_constants.h" namespace json_schema_constants { diff --git a/components/json_schema/json_schema_constants.h b/components/policy/core/common/json_schema_constants.h index 19394a915f..66b7941d46 100644 --- a/components/json_schema/json_schema_constants.h +++ b/components/policy/core/common/json_schema_constants.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef COMPONENTS_JSON_SCHEMA_JSON_SCHEMA_CONSTANTS_H_ -#define COMPONENTS_JSON_SCHEMA_JSON_SCHEMA_CONSTANTS_H_ +#ifndef COMPONENTS_POLICY_CORE_COMMON_JSON_SCHEMA_CONSTANTS_H_ +#define COMPONENTS_POLICY_CORE_COMMON_JSON_SCHEMA_CONSTANTS_H_ // These constants are shared by code that uses JSON schemas. namespace json_schema_constants { @@ -40,4 +40,4 @@ extern const char kType[]; } // namespace json_schema_constants -#endif // COMPONENTS_JSON_SCHEMA_JSON_SCHEMA_CONSTANTS_H_ +#endif // COMPONENTS_POLICY_CORE_COMMON_JSON_SCHEMA_CONSTANTS_H_ diff --git a/components/policy/core/common/schema.cc b/components/policy/core/common/schema.cc index a864ea5592..be475c4ff2 100644 --- a/components/policy/core/common/schema.cc +++ b/components/policy/core/common/schema.cc @@ -15,12 +15,13 @@ #include "base/compiler_specific.h" #include "base/containers/flat_set.h" +#include "base/json/json_reader.h" #include "base/logging.h" #include "base/macros.h" +#include "base/memory/ptr_util.h" #include "base/stl_util.h" #include "base/strings/stringprintf.h" -#include "components/json_schema/json_schema_constants.h" -#include "components/json_schema/json_schema_validator.h" +#include "components/policy/core/common/json_schema_constants.h" #include "components/policy/core/common/schema_internal.h" #include "third_party/re2/src/re2/re2.h" @@ -154,6 +155,283 @@ void AddDictKeyPrefixToPath(const std::string& key, std::string* path) { } } +bool IsValidType(const std::string& type) { + static const char* kValidTypes[] = { + schema::kAny, schema::kArray, schema::kBoolean, schema::kInteger, + schema::kNull, schema::kNumber, schema::kObject, schema::kString, + }; + const char** end = kValidTypes + base::size(kValidTypes); + return std::find(kValidTypes, end, type) != end; +} + +// Maps a schema attribute name to its expected type. +struct ExpectedType { + const char* key; + base::Value::Type type; +}; + +// Helper for std::lower_bound. +bool CompareToString(const ExpectedType& entry, const std::string& key) { + return entry.key < key; +} + +// If |value| is a dictionary, returns the "name" attribute of |value| or NULL +// if |value| does not contain a "name" attribute. Otherwise, returns |value|. +const base::Value* ExtractNameFromDictionary(const base::Value* value) { + const base::DictionaryValue* value_dict = nullptr; + const base::Value* name_value = nullptr; + if (value->GetAsDictionary(&value_dict)) { + value_dict->Get("name", &name_value); + return name_value; + } + return value; +} + +bool IsValidSchema(const base::DictionaryValue* dict, + int options, + std::string* error) { + // This array must be sorted, so that std::lower_bound can perform a + // binary search. + static const ExpectedType kExpectedTypes[] = { + // Note: kRef == "$ref", kSchema == "$schema" + {schema::kRef, base::Value::Type::STRING}, + {schema::kSchema, base::Value::Type::STRING}, + + {schema::kAdditionalProperties, base::Value::Type::DICTIONARY}, + {schema::kChoices, base::Value::Type::LIST}, + {schema::kDescription, base::Value::Type::STRING}, + {schema::kEnum, base::Value::Type::LIST}, + {schema::kId, base::Value::Type::STRING}, + {schema::kMaxItems, base::Value::Type::INTEGER}, + {schema::kMaxLength, base::Value::Type::INTEGER}, + {schema::kMaximum, base::Value::Type::DOUBLE}, + {schema::kMinItems, base::Value::Type::INTEGER}, + {schema::kMinLength, base::Value::Type::INTEGER}, + {schema::kMinimum, base::Value::Type::DOUBLE}, + {schema::kOptional, base::Value::Type::BOOLEAN}, + {schema::kPattern, base::Value::Type::STRING}, + {schema::kPatternProperties, base::Value::Type::DICTIONARY}, + {schema::kProperties, base::Value::Type::DICTIONARY}, + {schema::kRequired, base::Value::Type::LIST}, + {schema::kTitle, base::Value::Type::STRING}, + }; + + bool has_type_or_ref = false; + const base::ListValue* list_value = nullptr; + const base::DictionaryValue* dictionary_value = nullptr; + std::string string_value; + + const base::ListValue* required_properties_value = nullptr; + const base::DictionaryValue* properties_value = nullptr; + + for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) { + // Validate the "type" attribute, which may be a string or a list. + if (it.key() == schema::kType) { + switch (it.value().type()) { + case base::Value::Type::STRING: + it.value().GetAsString(&string_value); + if (!IsValidType(string_value)) { + *error = "Invalid value for type attribute"; + return false; + } + break; + case base::Value::Type::LIST: + it.value().GetAsList(&list_value); + for (size_t i = 0; i < list_value->GetSize(); ++i) { + if (!list_value->GetString(i, &string_value) || + !IsValidType(string_value)) { + *error = "Invalid value for type attribute"; + return false; + } + } + break; + default: + *error = "Invalid value for type attribute"; + return false; + } + has_type_or_ref = true; + continue; + } + + // Validate the "items" attribute, which is a schema or a list of schemas. + if (it.key() == schema::kItems) { + if (it.value().GetAsDictionary(&dictionary_value)) { + if (!IsValidSchema(dictionary_value, options, error)) { + DCHECK(!error->empty()); + return false; + } + } else if (it.value().GetAsList(&list_value)) { + for (size_t i = 0; i < list_value->GetSize(); ++i) { + if (!list_value->GetDictionary(i, &dictionary_value)) { + *error = base::StringPrintf( + "Invalid entry in items attribute at index %d", + static_cast<int>(i)); + return false; + } + if (!IsValidSchema(dictionary_value, options, error)) { + DCHECK(!error->empty()); + return false; + } + } + } else { + *error = "Invalid value for items attribute"; + return false; + } + continue; + } + + // All the other attributes have a single valid type. + const ExpectedType* end = kExpectedTypes + base::size(kExpectedTypes); + const ExpectedType* entry = + std::lower_bound(kExpectedTypes, end, it.key(), CompareToString); + if (entry == end || entry->key != it.key()) { + if (options & Schema::OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES) + continue; + *error = base::StringPrintf("Invalid attribute %s", it.key().c_str()); + return false; + } + + // Integer can be converted to double. + if (!(it.value().type() == entry->type || + (it.value().is_int() && entry->type == base::Value::Type::DOUBLE))) { + *error = base::StringPrintf("Invalid value for %s attribute", + it.key().c_str()); + return false; + } + + // base::Value::Type::INTEGER attributes must be >= 0. + // This applies to "minItems", "maxItems", "minLength" and "maxLength". + if (it.value().is_int()) { + int integer_value; + it.value().GetAsInteger(&integer_value); + if (integer_value < 0) { + *error = base::StringPrintf("Value of %s must be >= 0, got %d", + it.key().c_str(), integer_value); + return false; + } + } + + // Validate the "properties" attribute. Each entry maps a key to a schema. + if (it.key() == schema::kProperties) { + it.value().GetAsDictionary(&properties_value); + for (base::DictionaryValue::Iterator iter(*properties_value); + !iter.IsAtEnd(); iter.Advance()) { + if (!iter.value().GetAsDictionary(&dictionary_value)) { + *error = "properties must be a dictionary"; + return false; + } + if (!IsValidSchema(dictionary_value, options, error)) { + DCHECK(!error->empty()); + return false; + } + } + } + + // Validate the "patternProperties" attribute. Each entry maps a regular + // expression to a schema. The validity of the regular expression expression + // won't be checked here for performance reasons. Instead, invalid regular + // expressions will be caught as validation errors in Validate(). + if (it.key() == schema::kPatternProperties) { + it.value().GetAsDictionary(&dictionary_value); + for (base::DictionaryValue::Iterator iter(*dictionary_value); + !iter.IsAtEnd(); iter.Advance()) { + if (!iter.value().GetAsDictionary(&dictionary_value)) { + *error = "patternProperties must be a dictionary"; + return false; + } + if (!IsValidSchema(dictionary_value, options, error)) { + DCHECK(!error->empty()); + return false; + } + } + } + + // Validate "additionalProperties" attribute, which is a schema. + if (it.key() == schema::kAdditionalProperties) { + it.value().GetAsDictionary(&dictionary_value); + if (!IsValidSchema(dictionary_value, options, error)) { + DCHECK(!error->empty()); + return false; + } + } + + // Validate "required" attribute. + if (it.key() == schema::kRequired) { + it.value().GetAsList(&required_properties_value); + for (const base::Value& value : *required_properties_value) { + if (value.type() != base::Value::Type::STRING) { + *error = "Invalid value in 'required' attribute"; + return false; + } + } + } + + // Validate the values contained in an "enum" attribute. + if (it.key() == schema::kEnum) { + it.value().GetAsList(&list_value); + for (size_t i = 0; i < list_value->GetSize(); ++i) { + const base::Value* value = nullptr; + list_value->Get(i, &value); + // Sometimes the enum declaration is a dictionary with the enum value + // under "name". + value = ExtractNameFromDictionary(value); + if (!value) { + *error = "Invalid value in enum attribute"; + return false; + } + switch (value->type()) { + case base::Value::Type::NONE: + case base::Value::Type::BOOLEAN: + case base::Value::Type::INTEGER: + case base::Value::Type::DOUBLE: + case base::Value::Type::STRING: + break; + default: + *error = "Invalid value in enum attribute"; + return false; + } + } + } + + // Validate the schemas contained in a "choices" attribute. + if (it.key() == schema::kChoices) { + it.value().GetAsList(&list_value); + for (size_t i = 0; i < list_value->GetSize(); ++i) { + if (!list_value->GetDictionary(i, &dictionary_value)) { + *error = "Invalid choices attribute"; + return false; + } + if (!IsValidSchema(dictionary_value, options, error)) { + DCHECK(!error->empty()); + return false; + } + } + } + + if (it.key() == schema::kRef) + has_type_or_ref = true; + } + + // Check that properties in'required' are in the 'properties' object. + if (required_properties_value) { + for (const base::Value& value : required_properties_value->GetList()) { + const std::string& name = value.GetString(); + if (!properties_value || !properties_value->HasKey(name)) { + *error = "Property '" + name + + "' was listed in 'required', but not defined in 'properties'."; + return false; + } + } + } + + if (!has_type_or_ref) { + *error = "Schema must have a type or a $ref attribute"; + return false; + } + + return true; +} + } // namespace // Contains the internal data representation of a Schema. This can either wrap @@ -1089,9 +1367,7 @@ Schema Schema::Parse(const std::string& content, std::string* error) { // Validate as a generic JSON schema, and ignore unknown attributes; they // may become used in a future version of the schema format. std::unique_ptr<base::DictionaryValue> dict = - JSONSchemaValidator::IsValidSchema( - content, JSONSchemaValidator::OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES, - error); + IsValidSchema(content, Schema::OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES, error); if (!dict) return Schema(); @@ -1119,6 +1395,34 @@ Schema Schema::Parse(const std::string& content, std::string* error) { return Schema(storage, storage->root_node()); } +// static +std::unique_ptr<base::DictionaryValue> Schema::IsValidSchema( + const std::string& schema, + std::string* error) { + return Schema::IsValidSchema(schema, 0, error); +} + +// static +std::unique_ptr<base::DictionaryValue> Schema::IsValidSchema( + const std::string& schema, + int validator_options, + std::string* error) { + base::JSONParserOptions json_options = base::JSON_ALLOW_TRAILING_COMMAS; + std::unique_ptr<base::Value> json = base::JSONReader::ReadAndReturnError( + schema, json_options, nullptr, error); + if (!json) + return nullptr; + std::unique_ptr<base::DictionaryValue> dict = + base::DictionaryValue::From(std::move(json)); + if (!dict) { + *error = "Schema must be a JSON object"; + return nullptr; + } + if (!policy::IsValidSchema(dict.get(), validator_options, error)) + return nullptr; + return dict; +} + base::Value::Type Schema::type() const { CHECK(valid()); return node_->type; diff --git a/components/policy/core/common/schema.h b/components/policy/core/common/schema.h index 1eb1c9819a..44ca58a15c 100644 --- a/components/policy/core/common/schema.h +++ b/components/policy/core/common/schema.h @@ -62,6 +62,12 @@ typedef std::vector<Schema> SchemaList; // standard. class POLICY_EXPORT Schema { public: + enum Options { + // Ignore unknown attributes. If this option is not set then unknown + // attributes will make the schema validation fail. + OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES = 1 << 0, + }; + // Used internally to store shared data. class InternalStorage; @@ -85,6 +91,24 @@ class POLICY_EXPORT Schema { // is returned and |error| contains a reason for the failure. static Schema Parse(const std::string& schema, std::string* error); + // Verifies if |schema| is a valid JSON v3 schema. When this validation passes + // then |schema| is valid JSON that can be parsed into a DictionaryValue, + // and that DictionaryValue can be used to build a |Schema|. + // Returns the parsed DictionaryValue when |schema| validated, otherwise + // returns NULL. In that case, |error| contains an error description. + // For performance reasons, currently IsValidSchema() won't check the + // correctness of regular expressions used in "pattern" and + // "patternProperties" and in Validate() invalid regular expression don't + // accept any strings. + static std::unique_ptr<base::DictionaryValue> IsValidSchema( + const std::string& schema, + std::string* error); + + // Same as above but with |options|, which is a bitwise-OR combination of the + // Options above. + static std::unique_ptr<base::DictionaryValue> + IsValidSchema(const std::string& schema, int options, std::string* error); + // Returns true if this Schema is valid. Schemas returned by the methods below // may be invalid, and in those cases the other methods must not be used. bool valid() const { return node_ != NULL; } diff --git a/components/policy/core/common/schema_unittest.cc b/components/policy/core/common/schema_unittest.cc index b7a443495e..1fe75c7647 100644 --- a/components/policy/core/common/schema_unittest.cc +++ b/components/policy/core/common/schema_unittest.cc @@ -1357,4 +1357,164 @@ TEST(SchemaTest, RangedRestriction) { })"))); } +TEST(SchemaTest, IsValidSchema) { + std::string error; + EXPECT_FALSE(Schema::IsValidSchema("", &error)); + EXPECT_FALSE(Schema::IsValidSchema("\0", &error)); + EXPECT_FALSE(Schema::IsValidSchema("string", &error)); + EXPECT_FALSE(Schema::IsValidSchema("\"string\"", &error)); + EXPECT_FALSE(Schema::IsValidSchema("[]", &error)); + EXPECT_FALSE(Schema::IsValidSchema("{}", &error)); + EXPECT_FALSE(Schema::IsValidSchema("{ \"type\": 123 }", &error)); + EXPECT_FALSE(Schema::IsValidSchema("{ \"type\": \"invalid\" }", &error)); + EXPECT_FALSE( + Schema::IsValidSchema("{" + " \"type\": \"object\"," + " \"properties\": []" // Invalid properties type. + "}", + &error)); + EXPECT_FALSE( + Schema::IsValidSchema("{" + " \"type\": \"string\"," + " \"maxLength\": -1" // Must be >= 0. + "}", + &error)); + EXPECT_FALSE(Schema::IsValidSchema( + "{" + " \"type\": \"string\"," + " \"enum\": [ {} ]" // "enum" dict values must contain "name". + "}", + &error)); + EXPECT_FALSE(Schema::IsValidSchema( + "{" + " \"type\": \"string\"," + " \"enum\": [ { \"name\": {} } ]" // "enum" name must be a simple value. + "}", + &error)); + EXPECT_FALSE(Schema::IsValidSchema( + "{" + " \"type\": \"array\"," + " \"items\": [ 123 ]," // "items" must contain a schema or schemas. + "}", + &error)); + EXPECT_TRUE(Schema::IsValidSchema("{ \"type\": \"object\" }", &error)) + << error; + EXPECT_TRUE( + Schema::IsValidSchema("{ \"type\": [\"object\", \"array\"] }", &error)) + << error; + EXPECT_TRUE(Schema::IsValidSchema( + "{" + " \"type\": [\"object\", \"array\"]," + " \"properties\": {" + " \"string-property\": {" + " \"type\": \"string\"," + " \"minLength\": 1," + " \"maxLength\": 100," + " \"title\": \"The String Policy\"," + " \"description\": \"This policy controls the String widget.\"" + " }," + " \"integer-property\": {" + " \"type\": \"number\"," + " \"minimum\": 1000.0," + " \"maximum\": 9999.0" + " }," + " \"enum-property\": {" + " \"type\": \"integer\"," + " \"enum\": [0, 1, {\"name\": 10}, 100]" + " }," + " \"items-property\": {" + " \"type\": \"array\"," + " \"items\": {" + " \"type\": \"string\"" + " }" + " }," + " \"items-list-property\": {" + " \"type\": \"array\"," + " \"items\": [" + " { \"type\": \"string\" }," + " { \"type\": \"integer\" }" + " ]" + " }" + " }," + " \"additionalProperties\": {" + " \"type\": \"any\"" + " }" + "}", + &error)) + << error; + EXPECT_TRUE( + Schema::IsValidSchema("{" + " \"type\": \"object\"," + " \"patternProperties\": {" + " \".\": { \"type\": \"any\" }," + " \"foo\": { \"type\": \"any\" }," + " \"^foo$\": { \"type\": \"any\" }," + " \"foo+\": { \"type\": \"any\" }," + " \"foo?\": { \"type\": \"any\" }," + " \"fo{2,4}\": { \"type\": \"any\" }," + " \"(left)|(right)\": { \"type\": \"any\" }" + " }" + "}", + &error)) + << error; + EXPECT_TRUE(Schema::IsValidSchema( + "{" + " \"type\": \"object\"," + " \"unknown attribute\": \"that should just be ignored\"" + "}", + Schema::OPTIONS_IGNORE_UNKNOWN_ATTRIBUTES, &error)) + << error; + EXPECT_FALSE(Schema::IsValidSchema( + "{" + " \"type\": \"object\"," + " \"unknown attribute\": \"that will cause a failure\"" + "}", + 0, &error)) + << error; + + EXPECT_FALSE(Schema::IsValidSchema(R"( + { + "type": "object", + "properties": {"foo": {"type": "number"}}, + "required": 123 + })", + 0, &error)) + << error; + + EXPECT_FALSE(Schema::IsValidSchema(R"( + { + "type": "object", + "properties": {"foo": {"type": "number"}}, + "required": [ 123 ] + })", + 0, &error)) + << error; + + EXPECT_FALSE(Schema::IsValidSchema(R"( + { + "type": "object", + "properties": {"foo": {"type": "number"}}, + "required": ["bar"] + })", + 0, &error)) + << error; + + EXPECT_FALSE(Schema::IsValidSchema(R"( + { + "type": "object", + "required": ["bar"] + })", + 0, &error)) + << error; + + EXPECT_TRUE(Schema::IsValidSchema(R"( + { + "type": "object", + "properties": {"foo": {"type": "number"}}, + "required": ["foo"] + })", + 0, &error)) + << error; +} + } // namespace policy |