xref: /openbmc/libcper/json-schema.c (revision e42fb487839b242371b0150ab5b0b89c2d232976)
1  /**
2   * A very basic, non-complete implementation of a validator for the JSON Schema specification,
3   * for validating CPER-JSON.
4   *
5   * Author: Lawrence.Tang@arm.com
6   **/
7  
8  #include <stdio.h>
9  #include <string.h>
10  #include <unistd.h>
11  #include <libgen.h>
12  #include <limits.h>
13  #include <stdarg.h>
14  #include <json.h>
15  #include <libcper/json-schema.h>
16  #include <libcper/BaseTypes.h>
17  
18  //Field definitions.
19  int json_validator_debug = 0;
20  
21  //Private pre-definitions.
22  int validate_field(const char *name, json_object *schema, json_object *object,
23  		   char *error_message);
24  int validate_integer(const char *field_name, json_object *schema,
25  		     json_object *object, char *error_message);
26  int validate_string(const char *field_name, json_object *schema,
27  		    json_object *object, const char *error_message);
28  int validate_object(const char *field_name, json_object *schema,
29  		    json_object *object, char *error_message);
30  int validate_array(const char *field_name, json_object *schema,
31  		   json_object *object, char *error_message);
32  void log_validator_error(char *error_message, const char *format, ...);
33  void log_validator_debug(const char *format, ...);
34  void log_validator_msg(const char *format, va_list args);
35  
36  //Validates a single JSON object against a provided schema file, returning 1 on success and 0 on failure to validate.
37  //Error message space must be allocated prior to call.
validate_schema_from_file(const char * schema_file,json_object * object,char * error_message)38  int validate_schema_from_file(const char *schema_file, json_object *object,
39  			      char *error_message)
40  {
41  	//Load schema IR from file.
42  	json_object *schema_ir = json_object_from_file(schema_file);
43  	if (schema_ir == NULL) {
44  		log_validator_error(error_message,
45  				    "Failed to load schema from file '%s'.",
46  				    schema_file);
47  		return 0;
48  	}
49  
50  	//Get the directory of the file.
51  	char *schema_file_copy = malloc(strlen(schema_file) + 1);
52  	strcpy(schema_file_copy, schema_file);
53  	char *schema_dir = dirname(schema_file_copy);
54  
55  	int result =
56  		validate_schema(schema_ir, schema_dir, object, error_message);
57  
58  	//Free memory from directory call.
59  	free(schema_file_copy);
60  	json_object_put(schema_ir);
61  
62  	return result;
63  }
64  
65  //Validates a single JSON object against a provided schema, returning 1 on success and 0 on failure to validate.
66  //Error message space must be allocated prior to call.
67  //If the schema does not include any other sub-schemas using "$ref", then leaving schema_directory as NULL is valid.
validate_schema(json_object * schema,char * schema_directory,json_object * object,char * error_message)68  int validate_schema(json_object *schema, char *schema_directory,
69  		    json_object *object, char *error_message)
70  {
71  	//Check that the schema version is the same as this validator.
72  	json_object *schema_ver = json_object_object_get(schema, "$schema");
73  	if (schema_ver == NULL || strcmp(json_object_get_string(schema_ver),
74  					 JSON_SCHEMA_VERSION) != 0) {
75  		log_validator_error(
76  			error_message,
77  			"Provided schema is not of the same version that is referenced by this validator, or is not a schema.");
78  		return 0;
79  	}
80  
81  	//Change current directory into the schema directory.
82  	char *original_cwd = malloc(PATH_MAX);
83  	if (getcwd(original_cwd, PATH_MAX) == NULL) {
84  		log_validator_error(error_message,
85  				    "Failed fetching the current directory.");
86  		if (original_cwd) {
87  			free(original_cwd);
88  		}
89  		return 0;
90  	}
91  	if (chdir(schema_directory)) {
92  		log_validator_error(error_message,
93  				    "Failed to chdir into schema directory.");
94  		if (original_cwd) {
95  			free(original_cwd);
96  		}
97  		return 0;
98  	}
99  
100  	//Parse the top level structure appropriately.
101  	int result = validate_field("parent", schema, object, error_message);
102  	if (result < 0) {
103  		log_validator_error(error_message,
104  				    "Failed validating schema for parent.");
105  		result = 0;
106  	}
107  
108  	//Change back to original CWD.
109  	if (chdir(original_cwd)) {
110  		log_validator_error(error_message,
111  				    "Failed to chdir into original directory.");
112  	}
113  
114  	free(original_cwd);
115  
116  	if (result) {
117  		log_validator_debug(
118  			"Successfully validated the provided object against schema.");
119  	}
120  	return result;
121  }
122  
123  //Validates a single JSON field given a schema/object.
124  //Returns -1 on fatal/error failure, 0 on validation failure, and 1 on validation.
validate_field(const char * field_name,json_object * schema,json_object * object,char * error_message)125  int validate_field(const char *field_name, json_object *schema,
126  		   json_object *object, char *error_message)
127  {
128  	int ret = -1;
129  
130  	log_validator_debug("Validating field '%s'...", field_name);
131  
132  	//If there is a "$ref" field, attempt to load the referenced schema.
133  	json_object *ref_field = json_object_object_get(schema, "$ref");
134  	if (ref_field != NULL &&
135  	    json_object_get_type(ref_field) == json_type_string) {
136  		log_validator_debug("$ref schema detected for field '%s'.",
137  				    field_name);
138  
139  		//Attempt to load. If loading fails, report error.
140  		const char *ref_path = json_object_get_string(ref_field);
141  		json_object *ref_schema = json_object_from_file(ref_path);
142  		if (ref_schema == NULL) {
143  			log_validator_error(
144  				error_message,
145  				"Failed to open referenced schema file '%s'.",
146  				ref_path);
147  			return -1;
148  		}
149  
150  		log_validator_debug("loaded schema path '%s' for field '%s'.",
151  				    ref_path, field_name);
152  
153  		//Validate field with schema.
154  		ret = validate_field(field_name, ref_schema, object,
155  				     error_message);
156  		json_object_put(ref_schema);
157  
158  		return ret;
159  	}
160  
161  	//Get the schema field type.
162  	json_object *desired_field_type =
163  		json_object_object_get(schema, "type");
164  	if (desired_field_type == NULL ||
165  	    !json_object_is_type(desired_field_type, json_type_string)) {
166  		log_validator_error(
167  			error_message,
168  			"Desired field type not provided within schema/is not a string for field '%s' (schema violation).",
169  			field_name);
170  		return 0;
171  	}
172  
173  	//Check the field types are actually equal.
174  	const char *desired_field_type_str =
175  		json_object_get_string(desired_field_type);
176  	if (!((!strcmp(desired_field_type_str, "object") &&
177  	       json_object_is_type(object, json_type_object)) ||
178  	      (!strcmp(desired_field_type_str, "array") &&
179  	       json_object_is_type(object, json_type_array)) ||
180  	      (!strcmp(desired_field_type_str, "integer") &&
181  	       json_object_is_type(object, json_type_int)) ||
182  	      (!strcmp(desired_field_type_str, "string") &&
183  	       json_object_is_type(object, json_type_string)) ||
184  	      (!strcmp(desired_field_type_str, "boolean") &&
185  	       json_object_is_type(object, json_type_boolean)) ||
186  	      (!strcmp(desired_field_type_str, "double") &&
187  	       json_object_is_type(object, json_type_double)))) {
188  		log_validator_error(error_message,
189  				    "Field type match failed for field '%s'.",
190  				    field_name);
191  		return 0;
192  	}
193  
194  	//If the schema contains a "oneOf" array, we need to validate the field against each of the
195  	//possible options in turn.
196  	json_object *one_of = json_object_object_get(schema, "oneOf");
197  	if (one_of != NULL && json_object_get_type(one_of) == json_type_array) {
198  		log_validator_debug("oneOf options detected for field '%s'.",
199  				    field_name);
200  
201  		int len = json_object_array_length(one_of);
202  		int validated = 0;
203  		for (int i = 0; i < len; i++) {
204  			//If the "oneOf" member isn't an object, warn on schema violation.
205  			json_object *one_of_option =
206  				json_object_array_get_idx(one_of, i);
207  			if (one_of_option == NULL ||
208  			    json_object_get_type(one_of_option) !=
209  				    json_type_object) {
210  				log_validator_debug(
211  					"Schema Warning: 'oneOf' member for field '%s' is not an object, schema violation.",
212  					field_name);
213  				continue;
214  			}
215  
216  			//Validate field with schema.
217  			validated = validate_field(field_name, one_of_option,
218  						   object, error_message);
219  			if (validated == -1) {
220  				return -1;
221  			}
222  			if (validated) {
223  				break;
224  			}
225  		}
226  
227  		//Return if failed all checks.
228  		if (!validated) {
229  			log_validator_error(
230  				error_message,
231  				"No schema object structures matched provided object for field '%s'.",
232  				field_name);
233  			return 0;
234  		}
235  	}
236  
237  	//Switch and validate each type in turn.
238  	switch (json_object_get_type(object)) {
239  	case json_type_int:
240  		return validate_integer(field_name, schema, object,
241  					error_message);
242  	case json_type_string:
243  		return validate_string(field_name, schema, object,
244  				       error_message);
245  	case json_type_object:
246  		return validate_object(field_name, schema, object,
247  				       error_message);
248  	case json_type_array:
249  		return validate_array(field_name, schema, object,
250  				      error_message);
251  
252  	//We don't perform extra validation on this type.
253  	default:
254  		log_validator_debug(
255  			"validation passed for '%s' (no extra validation).",
256  			field_name);
257  		return 1;
258  	}
259  }
260  
261  //Validates a single integer value according to the given specification.
validate_integer(const char * field_name,json_object * schema,json_object * object,char * error_message)262  int validate_integer(const char *field_name, json_object *schema,
263  		     json_object *object, char *error_message)
264  {
265  	//Is there a minimum/maximum specified? If so, check those.
266  	//Validate minimum.
267  	json_object *min_value = json_object_object_get(schema, "minimum");
268  	if (min_value != NULL &&
269  	    json_object_is_type(min_value, json_type_int)) {
270  		int min_value_int = json_object_get_int(min_value);
271  		if (json_object_get_uint64(object) < (uint64_t)min_value_int) {
272  			log_validator_error(
273  				error_message,
274  				"Failed to validate integer field '%s'. Value was below minimum of %d.",
275  				field_name, min_value_int);
276  			return 0;
277  		}
278  	}
279  
280  	//Validate maximum.
281  	json_object *max_value = json_object_object_get(schema, "maximum");
282  	if (max_value != NULL &&
283  	    json_object_is_type(max_value, json_type_int)) {
284  		int max_value_int = json_object_get_int(max_value);
285  		if (json_object_get_uint64(object) > (uint64_t)max_value_int) {
286  			log_validator_error(
287  				error_message,
288  				"Failed to validate integer field '%s'. Value was above maximum of %d.",
289  				field_name, max_value_int);
290  			return 0;
291  		}
292  	}
293  
294  	return 1;
295  }
296  
297  //Validates a single string value according to the given specification.
validate_string(const char * field_name,json_object * schema,json_object * object,const char * error_message)298  int validate_string(const char *field_name, json_object *schema,
299  		    json_object *object, const char *error_message)
300  {
301  	//todo: if there is a "pattern" field, verify the string with RegEx.
302  	(void)field_name;
303  	(void)schema;
304  	(void)object;
305  	(void)error_message;
306  
307  	return 1;
308  }
309  
310  //Validates a single object value according to the given specification.
validate_object(const char * field_name,json_object * schema,json_object * object,char * error_message)311  int validate_object(const char *field_name, json_object *schema,
312  		    json_object *object, char *error_message)
313  {
314  	//Are there a set of "required" fields? If so, check they all exist.
315  	json_object *required_fields =
316  		json_object_object_get(schema, "required");
317  	if (required_fields != NULL &&
318  	    json_object_get_type(required_fields) == json_type_array) {
319  		log_validator_debug(
320  			"Required fields found for '%s', matching...",
321  			field_name);
322  
323  		int len = json_object_array_length(required_fields);
324  		for (int i = 0; i < len; i++) {
325  			//Get the required field from schema.
326  			json_object *required_field =
327  				json_object_array_get_idx(required_fields, i);
328  			if (json_object_get_type(required_field) !=
329  			    json_type_string) {
330  				log_validator_error(
331  					error_message,
332  					"Required field for object '%s' is not a string (schema violation).",
333  					field_name);
334  				return 0;
335  			}
336  
337  			//Does it exist in the object?
338  			const char *required_field_str =
339  				json_object_get_string(required_field);
340  			if (json_object_object_get(
341  				    object, required_field_str) == NULL) {
342  				log_validator_error(
343  					error_message,
344  					"Required field '%s' was not present in object '%s'.",
345  					required_field_str, field_name);
346  				return 0;
347  			}
348  		}
349  	}
350  
351  	//Get additional properties value in advance.
352  	json_object *additional_properties =
353  		json_object_object_get(schema, "additionalProperties");
354  	int additional_properties_allowed = 0;
355  	if (additional_properties != NULL &&
356  	    json_object_get_type(additional_properties) == json_type_boolean) {
357  		additional_properties_allowed =
358  			json_object_get_boolean(additional_properties);
359  	}
360  
361  	//Run through the "properties" object and validate each of those in turn.
362  	json_object *properties = json_object_object_get(schema, "properties");
363  	if (properties != NULL &&
364  	    json_object_get_type(properties) == json_type_object) {
365  		json_object_object_foreach(properties, key, value)
366  		{
367  			//If the given property name does not exist on the target object, ignore and continue next.
368  			json_object *object_prop =
369  				json_object_object_get(object, key);
370  			if (object_prop == NULL) {
371  				continue;
372  			}
373  
374  			//Validate against the schema.
375  			if (!validate_field(key, value, object_prop,
376  					    error_message)) {
377  				return 0;
378  			}
379  		}
380  
381  		//If additional properties are banned, validate that no additional properties exist.
382  		if (!additional_properties_allowed) {
383  			json_object_object_foreach(object, key, value)
384  			{
385  				//Avoid compiler warning
386  				(void)value;
387  
388  				//If the given property name does not exist on the schema object, fail validation.
389  				const json_object *schema_prop =
390  					json_object_object_get(properties, key);
391  				if (schema_prop == NULL) {
392  					log_validator_error(
393  						error_message,
394  						"Invalid additional property '%s' detected on field '%s'.",
395  						key, field_name);
396  					return 0;
397  				}
398  			}
399  		}
400  	}
401  
402  	return 1;
403  }
404  
405  //Validates a single array value according to the given specification.
validate_array(const char * field_name,json_object * schema,json_object * object,char * error_message)406  int validate_array(const char *field_name, json_object *schema,
407  		   json_object *object, char *error_message)
408  {
409  	//Iterate all items in the array, and validate according to the "items" schema.
410  	json_object *items_schema = json_object_object_get(schema, "items");
411  	if (items_schema != NULL &&
412  	    json_object_get_type(items_schema) == json_type_object) {
413  		int array_len = json_object_array_length(object);
414  		for (int i = 0; i < array_len; i++) {
415  			if (!validate_field(field_name, items_schema,
416  					    json_object_array_get_idx(object,
417  								      i),
418  					    error_message)) {
419  				return 0;
420  			}
421  		}
422  	}
423  
424  	return 1;
425  }
426  
427  //Enables/disables debugging globally for the JSON validator.
validate_schema_debug_enable()428  void validate_schema_debug_enable()
429  {
430  	json_validator_debug = 1;
431  }
validate_schema_debug_disable()432  void validate_schema_debug_disable()
433  {
434  	json_validator_debug = 0;
435  }
436  
437  //Logs an error message to the given error message location and (optionally) provides debug output.
log_validator_error(char * error_message,const char * format,...)438  void log_validator_error(char *error_message, const char *format, ...)
439  {
440  	va_list args;
441  
442  	//Log error to error out.
443  	va_start(args, format);
444  	vsnprintf(error_message, JSON_ERROR_MSG_MAX_LEN, format, args);
445  	va_end(args);
446  
447  	//Debug message if necessary.
448  	va_start(args, format);
449  	log_validator_msg(format, args);
450  	va_end(args);
451  }
452  
453  //Logs a debug message to stdout, if validator debug is enabled.
log_validator_debug(const char * format,...)454  void log_validator_debug(const char *format, ...)
455  {
456  	va_list args;
457  	va_start(args, format);
458  	log_validator_msg(format, args);
459  	va_end(args);
460  }
461  
462  //Logs a single validator debug/error message.
log_validator_msg(const char * format,va_list args)463  void log_validator_msg(const char *format, va_list args)
464  {
465  	//Print debug output if debug is on.
466  	if (json_validator_debug) {
467  		//Make new format string for error.
468  		const char *header = "json_validator: ";
469  		char *new_format = malloc(strlen(header) + strlen(format) + 2);
470  		strcpy(new_format, header);
471  		strcat(new_format, format);
472  		strcat(new_format, "\n");
473  
474  		//Print & free format.
475  		vfprintf(stdout, new_format, args);
476  		free(new_format);
477  	}
478  }
479