OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "chrome/common/json_schema_validator.h" | |
6 | |
7 #include <cfloat> | |
8 #include <cmath> | |
9 | |
10 #include "base/string_util.h" | |
11 #include "base/strings/string_number_conversions.h" | |
12 #include "base/values.h" | |
13 #include "chrome/common/json_schema_constants.h" | |
14 #include "ui/base/l10n/l10n_util.h" | |
15 | |
16 namespace schema = json_schema_constants; | |
17 | |
18 namespace { | |
19 | |
20 double GetNumberValue(const Value* value) { | |
21 double result = 0; | |
22 CHECK(value->GetAsDouble(&result)) | |
23 << "Unexpected value type: " << value->GetType(); | |
24 return result; | |
25 } | |
26 | |
27 } // namespace | |
28 | |
29 | |
30 JSONSchemaValidator::Error::Error() { | |
31 } | |
32 | |
33 JSONSchemaValidator::Error::Error(const std::string& message) | |
34 : path(message) { | |
35 } | |
36 | |
37 JSONSchemaValidator::Error::Error(const std::string& path, | |
38 const std::string& message) | |
39 : path(path), message(message) { | |
40 } | |
41 | |
42 | |
43 const char JSONSchemaValidator::kUnknownTypeReference[] = | |
44 "Unknown schema reference: *."; | |
45 const char JSONSchemaValidator::kInvalidChoice[] = | |
46 "Value does not match any valid type choices."; | |
47 const char JSONSchemaValidator::kInvalidEnum[] = | |
48 "Value does not match any valid enum choices."; | |
49 const char JSONSchemaValidator::kObjectPropertyIsRequired[] = | |
50 "Property is required."; | |
51 const char JSONSchemaValidator::kUnexpectedProperty[] = | |
52 "Unexpected property."; | |
53 const char JSONSchemaValidator::kArrayMinItems[] = | |
54 "Array must have at least * items."; | |
55 const char JSONSchemaValidator::kArrayMaxItems[] = | |
56 "Array must not have more than * items."; | |
57 const char JSONSchemaValidator::kArrayItemRequired[] = | |
58 "Item is required."; | |
59 const char JSONSchemaValidator::kStringMinLength[] = | |
60 "String must be at least * characters long."; | |
61 const char JSONSchemaValidator::kStringMaxLength[] = | |
62 "String must not be more than * characters long."; | |
63 const char JSONSchemaValidator::kStringPattern[] = | |
64 "String must match the pattern: *."; | |
65 const char JSONSchemaValidator::kNumberMinimum[] = | |
66 "Value must not be less than *."; | |
67 const char JSONSchemaValidator::kNumberMaximum[] = | |
68 "Value must not be greater than *."; | |
69 const char JSONSchemaValidator::kInvalidType[] = | |
70 "Expected '*' but got '*'."; | |
71 const char JSONSchemaValidator::kInvalidTypeIntegerNumber[] = | |
72 "Expected 'integer' but got 'number', consider using Math.round()."; | |
73 | |
74 | |
75 // static | |
76 std::string JSONSchemaValidator::GetJSONSchemaType(const Value* value) { | |
77 switch (value->GetType()) { | |
78 case Value::TYPE_NULL: | |
79 return schema::kNull; | |
80 case Value::TYPE_BOOLEAN: | |
81 return schema::kBoolean; | |
82 case Value::TYPE_INTEGER: | |
83 return schema::kInteger; | |
84 case Value::TYPE_DOUBLE: { | |
85 double double_value = 0; | |
86 value->GetAsDouble(&double_value); | |
87 if (std::abs(double_value) <= std::pow(2.0, DBL_MANT_DIG) && | |
88 double_value == floor(double_value)) { | |
89 return schema::kInteger; | |
90 } else { | |
91 return schema::kNumber; | |
92 } | |
93 } | |
94 case Value::TYPE_STRING: | |
95 return schema::kString; | |
96 case Value::TYPE_DICTIONARY: | |
97 return schema::kObject; | |
98 case Value::TYPE_LIST: | |
99 return schema::kArray; | |
100 default: | |
101 NOTREACHED() << "Unexpected value type: " << value->GetType(); | |
102 return ""; | |
103 } | |
104 } | |
105 | |
106 // static | |
107 std::string JSONSchemaValidator::FormatErrorMessage(const std::string& format, | |
108 const std::string& s1) { | |
109 std::string ret_val = format; | |
110 ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1); | |
111 return ret_val; | |
112 } | |
113 | |
114 // static | |
115 std::string JSONSchemaValidator::FormatErrorMessage(const std::string& format, | |
116 const std::string& s1, | |
117 const std::string& s2) { | |
118 std::string ret_val = format; | |
119 ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1); | |
120 ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s2); | |
121 return ret_val; | |
122 } | |
123 | |
124 JSONSchemaValidator::JSONSchemaValidator(DictionaryValue* schema) | |
125 : schema_root_(schema), default_allow_additional_properties_(false) { | |
126 } | |
127 | |
128 JSONSchemaValidator::JSONSchemaValidator(DictionaryValue* schema, | |
129 ListValue* types) | |
130 : schema_root_(schema), default_allow_additional_properties_(false) { | |
131 if (!types) | |
132 return; | |
133 | |
134 for (size_t i = 0; i < types->GetSize(); ++i) { | |
135 DictionaryValue* type = NULL; | |
136 CHECK(types->GetDictionary(i, &type)); | |
137 | |
138 std::string id; | |
139 CHECK(type->GetString(schema::kId, &id)); | |
140 | |
141 CHECK(types_.find(id) == types_.end()); | |
142 types_[id] = type; | |
143 } | |
144 } | |
145 | |
146 JSONSchemaValidator::~JSONSchemaValidator() {} | |
147 | |
148 bool JSONSchemaValidator::Validate(const Value* instance) { | |
149 errors_.clear(); | |
150 Validate(instance, schema_root_, ""); | |
151 return errors_.empty(); | |
152 } | |
153 | |
154 void JSONSchemaValidator::Validate(const Value* instance, | |
155 const DictionaryValue* schema, | |
156 const std::string& path) { | |
157 // If this schema defines itself as reference type, save it in this.types. | |
158 std::string id; | |
159 if (schema->GetString(schema::kId, &id)) { | |
160 TypeMap::iterator iter = types_.find(id); | |
161 if (iter == types_.end()) | |
162 types_[id] = schema; | |
163 else | |
164 DCHECK(iter->second == schema); | |
165 } | |
166 | |
167 // If the schema has a $ref property, the instance must validate against | |
168 // that schema. It must be present in types_ to be referenced. | |
169 std::string ref; | |
170 if (schema->GetString(schema::kRef, &ref)) { | |
171 TypeMap::iterator type = types_.find(ref); | |
172 if (type == types_.end()) { | |
173 errors_.push_back( | |
174 Error(path, FormatErrorMessage(kUnknownTypeReference, ref))); | |
175 } else { | |
176 Validate(instance, type->second, path); | |
177 } | |
178 return; | |
179 } | |
180 | |
181 // If the schema has a choices property, the instance must validate against at | |
182 // least one of the items in that array. | |
183 const ListValue* choices = NULL; | |
184 if (schema->GetList(schema::kChoices, &choices)) { | |
185 ValidateChoices(instance, choices, path); | |
186 return; | |
187 } | |
188 | |
189 // If the schema has an enum property, the instance must be one of those | |
190 // values. | |
191 const ListValue* enumeration = NULL; | |
192 if (schema->GetList(schema::kEnum, &enumeration)) { | |
193 ValidateEnum(instance, enumeration, path); | |
194 return; | |
195 } | |
196 | |
197 std::string type; | |
198 schema->GetString(schema::kType, &type); | |
199 CHECK(!type.empty()); | |
200 if (type != schema::kAny) { | |
201 if (!ValidateType(instance, type, path)) | |
202 return; | |
203 | |
204 // These casts are safe because of checks in ValidateType(). | |
205 if (type == schema::kObject) { | |
206 ValidateObject(static_cast<const DictionaryValue*>(instance), | |
207 schema, | |
208 path); | |
209 } else if (type == schema::kArray) { | |
210 ValidateArray(static_cast<const ListValue*>(instance), schema, path); | |
211 } else if (type == schema::kString) { | |
212 // Intentionally NOT downcasting to StringValue*. TYPE_STRING only implies | |
213 // GetAsString() can safely be carried out, not that it's a StringValue. | |
214 ValidateString(instance, schema, path); | |
215 } else if (type == schema::kNumber || type == schema::kInteger) { | |
216 ValidateNumber(instance, schema, path); | |
217 } else if (type != schema::kBoolean && type != schema::kNull) { | |
218 NOTREACHED() << "Unexpected type: " << type; | |
219 } | |
220 } | |
221 } | |
222 | |
223 void JSONSchemaValidator::ValidateChoices(const Value* instance, | |
224 const ListValue* choices, | |
225 const std::string& path) { | |
226 size_t original_num_errors = errors_.size(); | |
227 | |
228 for (size_t i = 0; i < choices->GetSize(); ++i) { | |
229 const DictionaryValue* choice = NULL; | |
230 CHECK(choices->GetDictionary(i, &choice)); | |
231 | |
232 Validate(instance, choice, path); | |
233 if (errors_.size() == original_num_errors) | |
234 return; | |
235 | |
236 // We discard the error from each choice. We only want to know if any of the | |
237 // validations succeeded. | |
238 errors_.resize(original_num_errors); | |
239 } | |
240 | |
241 // Now add a generic error that no choices matched. | |
242 errors_.push_back(Error(path, kInvalidChoice)); | |
243 return; | |
244 } | |
245 | |
246 void JSONSchemaValidator::ValidateEnum(const Value* instance, | |
247 const ListValue* choices, | |
248 const std::string& path) { | |
249 for (size_t i = 0; i < choices->GetSize(); ++i) { | |
250 const Value* choice = NULL; | |
251 CHECK(choices->Get(i, &choice)); | |
252 switch (choice->GetType()) { | |
253 case Value::TYPE_NULL: | |
254 case Value::TYPE_BOOLEAN: | |
255 case Value::TYPE_STRING: | |
256 if (instance->Equals(choice)) | |
257 return; | |
258 break; | |
259 | |
260 case Value::TYPE_INTEGER: | |
261 case Value::TYPE_DOUBLE: | |
262 if (instance->IsType(Value::TYPE_INTEGER) || | |
263 instance->IsType(Value::TYPE_DOUBLE)) { | |
264 if (GetNumberValue(choice) == GetNumberValue(instance)) | |
265 return; | |
266 } | |
267 break; | |
268 | |
269 default: | |
270 NOTREACHED() << "Unexpected type in enum: " << choice->GetType(); | |
271 } | |
272 } | |
273 | |
274 errors_.push_back(Error(path, kInvalidEnum)); | |
275 } | |
276 | |
277 void JSONSchemaValidator::ValidateObject(const DictionaryValue* instance, | |
278 const DictionaryValue* schema, | |
279 const std::string& path) { | |
280 const DictionaryValue* properties = NULL; | |
281 schema->GetDictionary(schema::kProperties, &properties); | |
282 if (properties) { | |
283 for (DictionaryValue::Iterator it(*properties); !it.IsAtEnd(); | |
284 it.Advance()) { | |
285 std::string prop_path = path.empty() ? it.key() : (path + "." + it.key()); | |
286 const DictionaryValue* prop_schema = NULL; | |
287 CHECK(it.value().GetAsDictionary(&prop_schema)); | |
288 | |
289 const Value* prop_value = NULL; | |
290 if (instance->Get(it.key(), &prop_value)) { | |
291 Validate(prop_value, prop_schema, prop_path); | |
292 } else { | |
293 // Properties are required unless there is an optional field set to | |
294 // 'true'. | |
295 bool is_optional = false; | |
296 prop_schema->GetBoolean(schema::kOptional, &is_optional); | |
297 if (!is_optional) { | |
298 errors_.push_back(Error(prop_path, kObjectPropertyIsRequired)); | |
299 } | |
300 } | |
301 } | |
302 } | |
303 | |
304 const DictionaryValue* additional_properties_schema = NULL; | |
305 if (SchemaAllowsAnyAdditionalItems(schema, &additional_properties_schema)) | |
306 return; | |
307 | |
308 // Validate additional properties. | |
309 for (DictionaryValue::Iterator it(*instance); !it.IsAtEnd(); it.Advance()) { | |
310 if (properties && properties->HasKey(it.key())) | |
311 continue; | |
312 | |
313 std::string prop_path = path.empty() ? it.key() : path + "." + it.key(); | |
314 if (!additional_properties_schema) { | |
315 errors_.push_back(Error(prop_path, kUnexpectedProperty)); | |
316 } else { | |
317 Validate(&it.value(), additional_properties_schema, prop_path); | |
318 } | |
319 } | |
320 } | |
321 | |
322 void JSONSchemaValidator::ValidateArray(const ListValue* instance, | |
323 const DictionaryValue* schema, | |
324 const std::string& path) { | |
325 const DictionaryValue* single_type = NULL; | |
326 size_t instance_size = instance->GetSize(); | |
327 if (schema->GetDictionary(schema::kItems, &single_type)) { | |
328 int min_items = 0; | |
329 if (schema->GetInteger(schema::kMinItems, &min_items)) { | |
330 CHECK(min_items >= 0); | |
331 if (instance_size < static_cast<size_t>(min_items)) { | |
332 errors_.push_back(Error(path, FormatErrorMessage( | |
333 kArrayMinItems, base::IntToString(min_items)))); | |
334 } | |
335 } | |
336 | |
337 int max_items = 0; | |
338 if (schema->GetInteger(schema::kMaxItems, &max_items)) { | |
339 CHECK(max_items >= 0); | |
340 if (instance_size > static_cast<size_t>(max_items)) { | |
341 errors_.push_back(Error(path, FormatErrorMessage( | |
342 kArrayMaxItems, base::IntToString(max_items)))); | |
343 } | |
344 } | |
345 | |
346 // If the items property is a single schema, each item in the array must | |
347 // validate against that schema. | |
348 for (size_t i = 0; i < instance_size; ++i) { | |
349 const Value* item = NULL; | |
350 CHECK(instance->Get(i, &item)); | |
351 std::string i_str = base::UintToString(i); | |
352 std::string item_path = path.empty() ? i_str : (path + "." + i_str); | |
353 Validate(item, single_type, item_path); | |
354 } | |
355 | |
356 return; | |
357 } | |
358 | |
359 // Otherwise, the list must be a tuple type, where each item in the list has a | |
360 // particular schema. | |
361 ValidateTuple(instance, schema, path); | |
362 } | |
363 | |
364 void JSONSchemaValidator::ValidateTuple(const ListValue* instance, | |
365 const DictionaryValue* schema, | |
366 const std::string& path) { | |
367 const ListValue* tuple_type = NULL; | |
368 schema->GetList(schema::kItems, &tuple_type); | |
369 size_t tuple_size = tuple_type ? tuple_type->GetSize() : 0; | |
370 if (tuple_type) { | |
371 for (size_t i = 0; i < tuple_size; ++i) { | |
372 std::string i_str = base::UintToString(i); | |
373 std::string item_path = path.empty() ? i_str : (path + "." + i_str); | |
374 const DictionaryValue* item_schema = NULL; | |
375 CHECK(tuple_type->GetDictionary(i, &item_schema)); | |
376 const Value* item_value = NULL; | |
377 instance->Get(i, &item_value); | |
378 if (item_value && item_value->GetType() != Value::TYPE_NULL) { | |
379 Validate(item_value, item_schema, item_path); | |
380 } else { | |
381 bool is_optional = false; | |
382 item_schema->GetBoolean(schema::kOptional, &is_optional); | |
383 if (!is_optional) { | |
384 errors_.push_back(Error(item_path, kArrayItemRequired)); | |
385 return; | |
386 } | |
387 } | |
388 } | |
389 } | |
390 | |
391 const DictionaryValue* additional_properties_schema = NULL; | |
392 if (SchemaAllowsAnyAdditionalItems(schema, &additional_properties_schema)) | |
393 return; | |
394 | |
395 size_t instance_size = instance->GetSize(); | |
396 if (additional_properties_schema) { | |
397 // Any additional properties must validate against the additionalProperties | |
398 // schema. | |
399 for (size_t i = tuple_size; i < instance_size; ++i) { | |
400 std::string i_str = base::UintToString(i); | |
401 std::string item_path = path.empty() ? i_str : (path + "." + i_str); | |
402 const Value* item_value = NULL; | |
403 CHECK(instance->Get(i, &item_value)); | |
404 Validate(item_value, additional_properties_schema, item_path); | |
405 } | |
406 } else if (instance_size > tuple_size) { | |
407 errors_.push_back(Error(path, FormatErrorMessage( | |
408 kArrayMaxItems, base::UintToString(tuple_size)))); | |
409 } | |
410 } | |
411 | |
412 void JSONSchemaValidator::ValidateString(const Value* instance, | |
413 const DictionaryValue* schema, | |
414 const std::string& path) { | |
415 std::string value; | |
416 CHECK(instance->GetAsString(&value)); | |
417 | |
418 int min_length = 0; | |
419 if (schema->GetInteger(schema::kMinLength, &min_length)) { | |
420 CHECK(min_length >= 0); | |
421 if (value.size() < static_cast<size_t>(min_length)) { | |
422 errors_.push_back(Error(path, FormatErrorMessage( | |
423 kStringMinLength, base::IntToString(min_length)))); | |
424 } | |
425 } | |
426 | |
427 int max_length = 0; | |
428 if (schema->GetInteger(schema::kMaxLength, &max_length)) { | |
429 CHECK(max_length >= 0); | |
430 if (value.size() > static_cast<size_t>(max_length)) { | |
431 errors_.push_back(Error(path, FormatErrorMessage( | |
432 kStringMaxLength, base::IntToString(max_length)))); | |
433 } | |
434 } | |
435 | |
436 CHECK(!schema->HasKey(schema::kPattern)) << "Pattern is not supported."; | |
437 } | |
438 | |
439 void JSONSchemaValidator::ValidateNumber(const Value* instance, | |
440 const DictionaryValue* schema, | |
441 const std::string& path) { | |
442 double value = GetNumberValue(instance); | |
443 | |
444 // TODO(aa): It would be good to test that the double is not infinity or nan, | |
445 // but isnan and isinf aren't defined on Windows. | |
446 | |
447 double minimum = 0; | |
448 if (schema->GetDouble(schema::kMinimum, &minimum)) { | |
449 if (value < minimum) | |
450 errors_.push_back(Error(path, FormatErrorMessage( | |
451 kNumberMinimum, base::DoubleToString(minimum)))); | |
452 } | |
453 | |
454 double maximum = 0; | |
455 if (schema->GetDouble(schema::kMaximum, &maximum)) { | |
456 if (value > maximum) | |
457 errors_.push_back(Error(path, FormatErrorMessage( | |
458 kNumberMaximum, base::DoubleToString(maximum)))); | |
459 } | |
460 } | |
461 | |
462 bool JSONSchemaValidator::ValidateType(const Value* instance, | |
463 const std::string& expected_type, | |
464 const std::string& path) { | |
465 std::string actual_type = GetJSONSchemaType(instance); | |
466 if (expected_type == actual_type || | |
467 (expected_type == schema::kNumber && actual_type == schema::kInteger)) { | |
468 return true; | |
469 } else if (expected_type == schema::kInteger && | |
470 actual_type == schema::kNumber) { | |
471 errors_.push_back(Error(path, kInvalidTypeIntegerNumber)); | |
472 return false; | |
473 } else { | |
474 errors_.push_back(Error(path, FormatErrorMessage( | |
475 kInvalidType, expected_type, actual_type))); | |
476 return false; | |
477 } | |
478 } | |
479 | |
480 bool JSONSchemaValidator::SchemaAllowsAnyAdditionalItems( | |
481 const DictionaryValue* schema, | |
482 const DictionaryValue** additional_properties_schema) { | |
483 // If the validator allows additional properties globally, and this schema | |
484 // doesn't override, then we can exit early. | |
485 schema->GetDictionary(schema::kAdditionalProperties, | |
486 additional_properties_schema); | |
487 | |
488 if (*additional_properties_schema) { | |
489 std::string additional_properties_type(schema::kAny); | |
490 CHECK((*additional_properties_schema)->GetString( | |
491 schema::kType, &additional_properties_type)); | |
492 return additional_properties_type == schema::kAny; | |
493 } else { | |
494 return default_allow_additional_properties_; | |
495 } | |
496 } | |
OLD | NEW |