mail archive of the barebox mailing list
 help / color / mirror / Atom feed
* [PATCH 0/4] lib: add jsmn JSON parser
@ 2023-01-10  8:49 Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 1/4] lib: add jsmn JSON parser support Ahmad Fatoum
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Ahmad Fatoum @ 2023-01-10  8:49 UTC (permalink / raw)
  To: barebox

Board code I have needs to read out factory data encoded into JSON blob
on EEPROM. Import JSMN, a minimal JSON parser written in C.

I don't know if the board code will be upstreamed in future, but for
now, add a selftest that exercises the API. As JSON could be useful
elsewhere as well, I think it's good to have it in-tree.

Ahmad Fatoum (4):
  lib: add jsmn JSON parser support
  lib: extend jsmn with simple JSONPath lookup helpers
  vsprintf: implement %pJP for printing JSONPaths
  test: self: add json parser test

 include/bselftest.h |   3 +-
 include/jsmn.h      | 148 ++++++++++++++
 lib/Kconfig         |   5 +
 lib/Makefile        |   1 +
 lib/jsmn.c          | 457 ++++++++++++++++++++++++++++++++++++++++++++
 lib/vsprintf.c      |  44 +++++
 test/self/Kconfig   |   5 +
 test/self/Makefile  |   1 +
 test/self/json.c    | 146 ++++++++++++++
 test/self/printf.c  |  17 ++
 10 files changed, 826 insertions(+), 1 deletion(-)
 create mode 100644 include/jsmn.h
 create mode 100644 lib/jsmn.c
 create mode 100644 test/self/json.c

-- 
2.30.2




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/4] lib: add jsmn JSON parser support
  2023-01-10  8:49 [PATCH 0/4] lib: add jsmn JSON parser Ahmad Fatoum
@ 2023-01-10  8:49 ` Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 2/4] lib: extend jsmn with simple JSONPath lookup helpers Ahmad Fatoum
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Ahmad Fatoum @ 2023-01-10  8:49 UTC (permalink / raw)
  To: barebox; +Cc: Ahmad Fatoum

Board code may require JSON support to parse factory data or to verify
JSON web tokens in locked-down systems. Import the current master
state[1] of JSMN, a minimalistic JSON parser, with slight changes
to make it compile in barebox

[1]: https://github.com/zserge/jsmn/commit/25647e6

Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 include/jsmn.h |  91 ++++++++++++
 lib/Kconfig    |   5 +
 lib/Makefile   |   1 +
 lib/jsmn.c     | 371 +++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 468 insertions(+)
 create mode 100644 include/jsmn.h
 create mode 100644 lib/jsmn.c

diff --git a/include/jsmn.h b/include/jsmn.h
new file mode 100644
index 000000000000..394ffc467487
--- /dev/null
+++ b/include/jsmn.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2010 Serge Zaitsev
+ */
+#ifndef __JSMN_H_
+#define __JSMN_H_
+
+#define JSMN_STRICT
+#define JSMN_PARENT_LINKS
+
+#include <stddef.h>
+#include <errno.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef JSMN_STATIC
+#define JSMN_API static
+#else
+#define JSMN_API extern
+#endif
+
+/**
+ * JSON type identifier. Basic types are:
+ * 	o Object
+ * 	o Array
+ * 	o String
+ * 	o Other primitive: number, boolean (true/false) or null
+ */
+typedef enum {
+	JSMN_UNDEFINED = 0,
+	JSMN_OBJECT = 1 << 0,
+	JSMN_ARRAY = 1 << 1,
+	JSMN_STRING = 1 << 2,
+	JSMN_PRIMITIVE = 1 << 3
+} jsmntype_t;
+
+enum jsmnerr {
+	/* Not enough tokens were provided */
+	JSMN_ERROR_NOMEM = -ENOMEM,
+	/* Invalid character inside JSON string */
+	JSMN_ERROR_INVAL = -EINVAL,
+	/* The string is not a full JSON packet, more bytes expected */
+	JSMN_ERROR_PART = -EMSGSIZE
+};
+
+/**
+ * JSON token description.
+ * type		type (object, array, string etc.)
+ * start	start position in JSON data string
+ * end		end position in JSON data string
+ */
+typedef struct jsmntok {
+	jsmntype_t type;
+	int start;
+	int end;
+	int size;
+#ifdef JSMN_PARENT_LINKS
+	int parent;
+#endif
+} jsmntok_t;
+
+/**
+ * JSON parser. Contains an array of token blocks available. Also stores
+ * the string being parsed now and current position in that string.
+ */
+typedef struct jsmn_parser {
+	unsigned int pos;     /* offset in the JSON string */
+	unsigned int toknext; /* next token to allocate */
+	int toksuper;         /* superior token node, e.g. parent object or array */
+} jsmn_parser;
+
+/**
+ * Create JSON parser over an array of tokens
+ */
+JSMN_API void jsmn_init(jsmn_parser *parser);
+
+/**
+ * Run JSON parser. It parses a JSON data string into and array of tokens, each
+ * describing
+ * a single JSON object.
+ */
+JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
+			jsmntok_t *tokens, const unsigned int num_tokens);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* JSMN_H */
diff --git a/lib/Kconfig b/lib/Kconfig
index a0f28b935cf7..5af7ea33f27b 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -8,6 +8,11 @@ config UNCOMPRESS
 	bool
 	select FILETYPE
 
+config JSMN
+	bool "JSMN JSON Parser" if COMPILE_TEST
+	help
+	  A minimalistic JSON parser.
+
 config XXHASH
 	bool
 
diff --git a/lib/Makefile b/lib/Makefile
index 21afb233facd..4717b8aec364 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -19,6 +19,7 @@ obj-y			+= readkey.o
 obj-y			+= kfifo.o
 obj-y			+= libbb.o
 obj-y			+= libgen.o
+obj-$(CONFIG_JSMN)	+= jsmn.o
 obj-$(CONFIG_BLOBGEN)	+= blobgen.o
 obj-y			+= stringlist.o
 obj-y			+= cmdlinepart.o
diff --git a/lib/jsmn.c b/lib/jsmn.c
new file mode 100644
index 000000000000..3a68f89337fc
--- /dev/null
+++ b/lib/jsmn.c
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright (c) 2010 Serge Zaitsev
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <jsmn.h>
+
+/**
+ * Allocates a fresh unused token from the token pool.
+ */
+static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
+				   const size_t num_tokens) {
+	jsmntok_t *tok;
+	if (parser->toknext >= num_tokens) {
+		return NULL;
+	}
+	tok = &tokens[parser->toknext++];
+	tok->start = tok->end = -1;
+	tok->size = 0;
+#ifdef JSMN_PARENT_LINKS
+	tok->parent = -1;
+#endif
+	return tok;
+}
+
+/**
+ * Fills token type and boundaries.
+ */
+static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
+			    const int start, const int end) {
+	token->type = type;
+	token->start = start;
+	token->end = end;
+	token->size = 0;
+}
+
+/**
+ * Fills next available token with JSON primitive.
+ */
+static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
+				const size_t len, jsmntok_t *tokens,
+				const size_t num_tokens) {
+	jsmntok_t *token;
+	int start;
+
+	start = parser->pos;
+
+	for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+		switch (js[parser->pos]) {
+#ifndef JSMN_STRICT
+			/* In strict mode primitive must be followed by "," or "}" or "]" */
+		case ':':
+#endif
+		case '\t':
+		case '\r':
+		case '\n':
+		case ' ':
+		case ',':
+		case ']':
+		case '}':
+			goto found;
+		default:
+			/* to quiet a warning from gcc*/
+			break;
+		}
+		if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
+			parser->pos = start;
+			return JSMN_ERROR_INVAL;
+		}
+	}
+#ifdef JSMN_STRICT
+	/* In strict mode primitive must be followed by a comma/object/array */
+	parser->pos = start;
+	return JSMN_ERROR_PART;
+#endif
+
+found:
+	if (tokens == NULL) {
+		parser->pos--;
+		return 0;
+	}
+	token = jsmn_alloc_token(parser, tokens, num_tokens);
+	if (token == NULL) {
+		parser->pos = start;
+		return JSMN_ERROR_NOMEM;
+	}
+	jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+	token->parent = parser->toksuper;
+#endif
+	parser->pos--;
+	return 0;
+}
+
+/**
+ * Fills next token with JSON string.
+ */
+static int jsmn_parse_string(jsmn_parser *parser, const char *js,
+			     const size_t len, jsmntok_t *tokens,
+			     const size_t num_tokens) {
+	jsmntok_t *token;
+
+	int start = parser->pos;
+
+	/* Skip starting quote */
+	parser->pos++;
+
+	for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+		char c = js[parser->pos];
+
+		/* Quote: end of string */
+		if (c == '\"') {
+			if (tokens == NULL) {
+				return 0;
+			}
+			token = jsmn_alloc_token(parser, tokens, num_tokens);
+			if (token == NULL) {
+				parser->pos = start;
+				return JSMN_ERROR_NOMEM;
+			}
+			jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+			token->parent = parser->toksuper;
+#endif
+			return 0;
+		}
+
+		/* Backslash: Quoted symbol expected */
+		if (c == '\\' && parser->pos + 1 < len) {
+			int i;
+			parser->pos++;
+			switch (js[parser->pos]) {
+				/* Allowed escaped symbols */
+			case '\"':
+			case '/':
+			case '\\':
+			case 'b':
+			case 'f':
+			case 'r':
+			case 'n':
+			case 't':
+				break;
+				/* Allows escaped symbol \uXXXX */
+			case 'u':
+				parser->pos++;
+				for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0';
+				     i++) {
+					/* If it isn't a hex character we have an error */
+					if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) ||   /* 0-9 */
+					      (js[parser->pos] >= 65 && js[parser->pos] <= 70) ||   /* A-F */
+					      (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
+						parser->pos = start;
+						return JSMN_ERROR_INVAL;
+					}
+					parser->pos++;
+				}
+				parser->pos--;
+				break;
+				/* Unexpected symbol */
+			default:
+				parser->pos = start;
+				return JSMN_ERROR_INVAL;
+			}
+		}
+	}
+	parser->pos = start;
+	return JSMN_ERROR_PART;
+}
+
+/**
+ * Parse JSON string and fill tokens.
+ */
+JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
+			jsmntok_t *tokens, const unsigned int num_tokens) {
+	int r;
+	int i;
+	jsmntok_t *token;
+	int count = parser->toknext;
+
+	for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+		char c;
+		jsmntype_t type;
+
+		c = js[parser->pos];
+		switch (c) {
+		case '{':
+		case '[':
+			count++;
+			if (tokens == NULL) {
+				break;
+			}
+			token = jsmn_alloc_token(parser, tokens, num_tokens);
+			if (token == NULL) {
+				return JSMN_ERROR_NOMEM;
+			}
+			if (parser->toksuper != -1) {
+				jsmntok_t *t = &tokens[parser->toksuper];
+#ifdef JSMN_STRICT
+				/* In strict mode an object or array can't become a key */
+				if (t->type == JSMN_OBJECT) {
+					return JSMN_ERROR_INVAL;
+				}
+#endif
+				t->size++;
+#ifdef JSMN_PARENT_LINKS
+				token->parent = parser->toksuper;
+#endif
+			}
+			token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
+			token->start = parser->pos;
+			parser->toksuper = parser->toknext - 1;
+			break;
+		case '}':
+		case ']':
+			if (tokens == NULL) {
+				break;
+			}
+			type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
+#ifdef JSMN_PARENT_LINKS
+			if (parser->toknext < 1) {
+				return JSMN_ERROR_INVAL;
+			}
+			token = &tokens[parser->toknext - 1];
+			for (;;) {
+				if (token->start != -1 && token->end == -1) {
+					if (token->type != type) {
+						return JSMN_ERROR_INVAL;
+					}
+					token->end = parser->pos + 1;
+					parser->toksuper = token->parent;
+					break;
+				}
+				if (token->parent == -1) {
+					if (token->type != type || parser->toksuper == -1) {
+						return JSMN_ERROR_INVAL;
+					}
+					break;
+				}
+				token = &tokens[token->parent];
+			}
+#else
+			for (i = parser->toknext - 1; i >= 0; i--) {
+				token = &tokens[i];
+				if (token->start != -1 && token->end == -1) {
+					if (token->type != type) {
+						return JSMN_ERROR_INVAL;
+					}
+					parser->toksuper = -1;
+					token->end = parser->pos + 1;
+					break;
+				}
+			}
+			/* Error if unmatched closing bracket */
+			if (i == -1) {
+				return JSMN_ERROR_INVAL;
+			}
+			for (; i >= 0; i--) {
+				token = &tokens[i];
+				if (token->start != -1 && token->end == -1) {
+					parser->toksuper = i;
+					break;
+				}
+			}
+#endif
+			break;
+		case '\"':
+			r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
+			if (r < 0) {
+				return r;
+			}
+			count++;
+			if (parser->toksuper != -1 && tokens != NULL) {
+				tokens[parser->toksuper].size++;
+			}
+			break;
+		case '\t':
+		case '\r':
+		case '\n':
+		case ' ':
+			break;
+		case ':':
+			parser->toksuper = parser->toknext - 1;
+			break;
+		case ',':
+			if (tokens != NULL && parser->toksuper != -1 &&
+			    tokens[parser->toksuper].type != JSMN_ARRAY &&
+			    tokens[parser->toksuper].type != JSMN_OBJECT) {
+#ifdef JSMN_PARENT_LINKS
+				parser->toksuper = tokens[parser->toksuper].parent;
+#else
+				for (i = parser->toknext - 1; i >= 0; i--) {
+					if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
+						if (tokens[i].start != -1 && tokens[i].end == -1) {
+							parser->toksuper = i;
+							break;
+						}
+					}
+				}
+#endif
+			}
+			break;
+#ifdef JSMN_STRICT
+			/* In strict mode primitives are: numbers and booleans */
+		case '-':
+		case '0':
+		case '1':
+		case '2':
+		case '3':
+		case '4':
+		case '5':
+		case '6':
+		case '7':
+		case '8':
+		case '9':
+		case 't':
+		case 'f':
+		case 'n':
+			/* And they must not be keys of the object */
+			if (tokens != NULL && parser->toksuper != -1) {
+				const jsmntok_t *t = &tokens[parser->toksuper];
+				if (t->type == JSMN_OBJECT ||
+				    (t->type == JSMN_STRING && t->size != 0)) {
+					return JSMN_ERROR_INVAL;
+				}
+			}
+#else
+			/* In non-strict mode every unquoted value is a primitive */
+		default:
+#endif
+			r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
+			if (r < 0) {
+				return r;
+			}
+			count++;
+			if (parser->toksuper != -1 && tokens != NULL) {
+				tokens[parser->toksuper].size++;
+			}
+			break;
+
+#ifdef JSMN_STRICT
+			/* Unexpected char in strict mode */
+		default:
+			return JSMN_ERROR_INVAL;
+#endif
+		}
+	}
+
+	if (tokens != NULL) {
+		for (i = parser->toknext - 1; i >= 0; i--) {
+			/* Unmatched opened object or array */
+			if (tokens[i].start != -1 && tokens[i].end == -1) {
+				return JSMN_ERROR_PART;
+			}
+		}
+	}
+
+	return count;
+}
+
+/**
+ * Creates a new parser based over a given buffer with an array of tokens
+ * available.
+ */
+JSMN_API void jsmn_init(jsmn_parser *parser) {
+	parser->pos = 0;
+	parser->toknext = 0;
+	parser->toksuper = -1;
+}
-- 
2.30.2




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 2/4] lib: extend jsmn with simple JSONPath lookup helpers
  2023-01-10  8:49 [PATCH 0/4] lib: add jsmn JSON parser Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 1/4] lib: add jsmn JSON parser support Ahmad Fatoum
@ 2023-01-10  8:49 ` Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 3/4] vsprintf: implement %pJP for printing JSONPaths Ahmad Fatoum
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Ahmad Fatoum @ 2023-01-10  8:49 UTC (permalink / raw)
  To: barebox; +Cc: Ahmad Fatoum

Board code may not be interested in iterating fully over the JSON blob
and instead just wants to lookup a value at a specific JSONPath.

Add helpers to facilitate this.

Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 include/jsmn.h | 57 +++++++++++++++++++++++++++++++++
 lib/jsmn.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 143 insertions(+)

diff --git a/include/jsmn.h b/include/jsmn.h
index 394ffc467487..8f6db8d534f4 100644
--- a/include/jsmn.h
+++ b/include/jsmn.h
@@ -84,6 +84,63 @@ JSMN_API void jsmn_init(jsmn_parser *parser);
 JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
 			jsmntok_t *tokens, const unsigned int num_tokens);
 
+/** Returns `true` if `token` is a string and equal to `str`. */
+JSMN_API bool jsmn_str_eq(const char *str, const char *json, const jsmntok_t *token);
+
+/** Returns `true` if `token` is to `str`. */
+JSMN_API bool jsmn_eq(const char *val, const char *json, const jsmntok_t *token);
+
+/** Returns the token after the value at `tokens[0]`. */
+JSMN_API const jsmntok_t *jsmn_skip_value(const jsmntok_t *tokens);
+
+/**
+ * Returns a pointer to the value associated with `key` inside `json` starting
+ * at `tokens[0]`, which is expected to be an object, or returns `NULL` if `key`
+ * cannot be found.
+ */
+JSMN_API const jsmntok_t *jsmn_find_value(const char *key, const char *json,
+					  const jsmntok_t *tokens);
+
+/**
+ * Locate the token at `path` inside `json` in a manner similar to JSONPath.
+ *
+ * Example:
+ * \code
+ * {
+ *   "date": "...",
+ *   "product": {
+ *	 "serial": "1234",
+ *   }
+ * }
+ * \endcode
+ *
+ * To locate the serial number in the JSON object above, you would use the
+ * JSONPath expression "$.product.serial". The same thing can be accomplished
+ * with this call:
+ *
+ * \code
+ * const char *path[] = {"product", "serial", NULL};
+ * const jsmntok_t *token = jsmn_locate(path, json, tokens);
+ * \endcode
+ *
+ * \remark This function cannot search inside arrays.
+ *
+ * @param path		Null-terminated list of path elements.
+ * @param json		JSON string to search in.
+ * @param tokens	Tokens for `json`.
+ *
+ * @return Pointer to the value token or `NULL` if the token could not be found.
+ */
+JSMN_API const jsmntok_t *jsmn_locate(const char *path[], const char *json,
+				      const jsmntok_t *tokens);
+
+/**
+ * Similar to `jsmn_locate` but returns a copy of the value or `NULL` if the
+ * value does not exist or is not a string. The caller takes ownership of the
+ * pointer returned.
+ */
+JSMN_API char *jsmn_strcpy(const char *path[], const char *json, const jsmntok_t *tokens);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/jsmn.c b/lib/jsmn.c
index 3a68f89337fc..7bdcc90f2f4b 100644
--- a/lib/jsmn.c
+++ b/lib/jsmn.c
@@ -369,3 +369,89 @@ JSMN_API void jsmn_init(jsmn_parser *parser) {
 	parser->toknext = 0;
 	parser->toksuper = -1;
 }
+
+JSMN_API bool jsmn_eq(const char *val, const char *json, const jsmntok_t *token)
+{
+	size_t token_size = token->end - token->start;
+	return strlen(val) == token_size
+		&& strncmp(json + token->start, val, token_size) == 0;
+}
+
+JSMN_API bool jsmn_str_eq(const char *str, const char *json, const jsmntok_t *token)
+{
+	return token->type == JSMN_STRING && jsmn_eq(str, json, token);
+}
+
+JSMN_API const jsmntok_t *jsmn_skip_value(const jsmntok_t *tokens)
+{
+	int max_index = tokens[0].end;
+	do {
+		++tokens;
+	} while (tokens[0].start < max_index);
+	return &tokens[0];
+}
+
+JSMN_API const jsmntok_t *jsmn_find_value(const char *key, const char *json,
+					  const jsmntok_t *tokens)
+{
+	int items;
+	if (tokens[0].type != JSMN_OBJECT || tokens[0].size == 0)
+		return NULL;
+
+	items = tokens[0].size;
+	++tokens;
+
+	do {
+		if (jsmn_str_eq(key, json, tokens))
+			return &tokens[1];
+		tokens = --items ? jsmn_skip_value(&tokens[1]) : NULL;
+	} while (tokens);
+
+	return NULL;
+}
+
+JSMN_API const jsmntok_t *jsmn_locate(const char *path[], const char *json,
+				      const jsmntok_t *tokens)
+{
+	int i = 0;
+	while (path[i] != NULL) {
+		const jsmntok_t *value = jsmn_find_value(path[i], json, tokens);
+		if (!value)
+			return NULL;
+
+		switch (value->type) {
+		case JSMN_OBJECT:
+		case JSMN_ARRAY:
+			tokens = value;
+			++i;
+			break;
+		case JSMN_UNDEFINED:
+		case JSMN_STRING:
+		case JSMN_PRIMITIVE:
+			return value;
+		}
+	}
+
+	return tokens;
+}
+
+JSMN_API char *jsmn_strcpy(const char *path[], const char *json,
+			   const jsmntok_t *tokens)
+{
+	const jsmntok_t *node;
+	int value_size;
+	char *value;
+
+	node = jsmn_locate(path, json, tokens);
+	if (!node || node->type != JSMN_STRING)
+		return NULL;
+
+	value_size = node->end - node->start;
+	value = malloc(value_size + 1);
+	if (value) {
+		strncpy(value, json + node->start, value_size);
+		value[value_size] = '\0';
+	}
+
+	return value;
+}
-- 
2.30.2




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 3/4] vsprintf: implement %pJP for printing JSONPaths
  2023-01-10  8:49 [PATCH 0/4] lib: add jsmn JSON parser Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 1/4] lib: add jsmn JSON parser support Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 2/4] lib: extend jsmn with simple JSONPath lookup helpers Ahmad Fatoum
@ 2023-01-10  8:49 ` Ahmad Fatoum
  2023-01-10  8:49 ` [PATCH 4/4] test: self: add json parser test Ahmad Fatoum
  2023-01-10 15:10 ` [PATCH 0/4] lib: add jsmn JSON parser Sascha Hauer
  4 siblings, 0 replies; 6+ messages in thread
From: Ahmad Fatoum @ 2023-01-10  8:49 UTC (permalink / raw)
  To: barebox; +Cc: Ahmad Fatoum

We use an array of strings to represnt a JSONPath. Add a new %pJP format
specifier that can be used to print them. This is useful for error
messages when the lookup fails.

Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 lib/vsprintf.c     | 44 ++++++++++++++++++++++++++++++++++++++++++++
 test/self/printf.c | 17 +++++++++++++++++
 2 files changed, 61 insertions(+)

diff --git a/lib/vsprintf.c b/lib/vsprintf.c
index 693f3c0cc0c8..cdb3906ea2ff 100644
--- a/lib/vsprintf.c
+++ b/lib/vsprintf.c
@@ -197,6 +197,27 @@ static char *string(char *buf, const char *end, const char *s, int field_width,
 	return trailing_spaces(buf, end, len, &field_width, flags);
 }
 
+static __maybe_unused char *string_array(char *buf, const char *end, char *const *s,
+					 int field_width, int precision, int flags,
+					 const char *separator)
+{
+	size_t i, len = strlen(separator);
+
+	while (*s) {
+		buf = string(buf, end, *s, field_width, precision, flags);
+		if (!*++s)
+			break;
+
+		for (i = 0; i < len; ++i) {
+			if (buf < end)
+				*buf = separator[i];
+			++buf;
+		}
+	}
+
+	return buf;
+}
+
 static char *wstring(char *buf, const char *end, const wchar_t *s, int field_width,
 		     int precision, int flags)
 {
@@ -368,6 +389,26 @@ char *hex_string(char *buf, const char *end, const u8 *addr, int field_width,
 	return buf;
 }
 
+static noinline_for_stack
+char *jsonpath_string(char *buf, const char *end, char *const *path, int field_width,
+		 int precision, int flags, const char *fmt)
+{
+	if ((unsigned long)path < PAGE_SIZE)
+		return string(buf, end, "<NULL>", field_width, precision, flags);
+
+	if (buf < end)
+		*buf = '$';
+	++buf;
+
+	if (*path) {
+		if (buf < end)
+			*buf = '.';
+		++buf;
+	}
+
+	return string_array(buf, end, path, field_width, precision, flags, ".");
+}
+
 static noinline_for_stack
 char *address_val(char *buf, const char *end, const void *addr,
 		  int field_width, int precision, int flags, const char *fmt)
@@ -458,6 +499,9 @@ static char *pointer(const char *fmt, char *buf, const char *end, const void *pt
 		if (IS_ENABLED(CONFIG_PRINTF_HEXSTR))
 			return hex_string(buf, end, ptr, field_width, precision, flags, fmt);
 		break;
+	case 'J':
+		if (fmt[1] == 'P' && IS_ENABLED(CONFIG_JSMN))
+			return jsonpath_string(buf, end, ptr, field_width, precision, flags, fmt);
 	}
 
 	return raw_pointer(buf, end, ptr, field_width, precision, flags);
diff --git a/test/self/printf.c b/test/self/printf.c
index 7a74660868c2..eae40ed242c1 100644
--- a/test/self/printf.c
+++ b/test/self/printf.c
@@ -305,6 +305,22 @@ test_pointer(void)
 	errptr();
 }
 
+static void __init
+test_jsonpath(void)
+{
+	if (!IS_ENABLED(CONFIG_JSMN)) {
+		pr_info("skipping jsonpath tests: CONFIG_JSMN disabled in config\n");
+		skipped_tests += 5;
+		return;
+	}
+
+	test("<NULL>", "%pJP",  NULL);
+	test("$", "%pJP",  &(char *[]){ NULL });
+	test("$.1", "%pJP",  &(char *[]){ "1", NULL });
+	test("$.1.23", "%pJP",  &(char *[]){ "1", "23", NULL });
+	test("$.1.23.456", "%pJP",  &(char *[]){ "1", "23", "456", NULL });
+}
+
 static void __init test_printf(void)
 {
 	alloced_buffer = malloc(BUF_SIZE + 2*PAD_SIZE);
@@ -317,6 +333,7 @@ static void __init test_printf(void)
 	test_string();
 	test_pointer();
 	test_hexstr();
+	test_jsonpath();
 
 	free(alloced_buffer);
 }
-- 
2.30.2




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 4/4] test: self: add json parser test
  2023-01-10  8:49 [PATCH 0/4] lib: add jsmn JSON parser Ahmad Fatoum
                   ` (2 preceding siblings ...)
  2023-01-10  8:49 ` [PATCH 3/4] vsprintf: implement %pJP for printing JSONPaths Ahmad Fatoum
@ 2023-01-10  8:49 ` Ahmad Fatoum
  2023-01-10 15:10 ` [PATCH 0/4] lib: add jsmn JSON parser Sascha Hauer
  4 siblings, 0 replies; 6+ messages in thread
From: Ahmad Fatoum @ 2023-01-10  8:49 UTC (permalink / raw)
  To: barebox; +Cc: Ahmad Fatoum

Newly added JSON parsing code doesn't yet have a consumer in-tree.
Add a self-test to exercise the API for now.

Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 include/bselftest.h |   3 +-
 test/self/Kconfig   |   5 ++
 test/self/Makefile  |   1 +
 test/self/json.c    | 146 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 154 insertions(+), 1 deletion(-)
 create mode 100644 test/self/json.c

diff --git a/include/bselftest.h b/include/bselftest.h
index f03c803b6553..2bae6cd1393d 100644
--- a/include/bselftest.h
+++ b/include/bselftest.h
@@ -7,7 +7,8 @@
 #include <init.h>
 
 enum bselftest_group {
-	BSELFTEST_core
+	BSELFTEST_core,
+	BSELFTEST_parser,
 };
 
 struct selftest {
diff --git a/test/self/Kconfig b/test/self/Kconfig
index 5c6981959921..ce5048c70ec9 100644
--- a/test/self/Kconfig
+++ b/test/self/Kconfig
@@ -35,6 +35,7 @@ config SELFTEST_ENABLE_ALL
 	select SELFTEST_ENVIRONMENT_VARIABLES if ENVIRONMENT_VARIABLES
 	imply SELFTEST_FS_RAMFS
 	imply SELFTEST_TFTP
+	imply SELFTEST_JSON
 	help
 	  Selects all self-tests compatible with current configuration
 
@@ -64,4 +65,8 @@ config SELFTEST_FS_RAMFS
 	bool "ramfs selftest"
 	depends on FS_RAMFS
 
+config SELFTEST_JSON
+	bool "JSON selftest"
+	depends on JSMN
+
 endif
diff --git a/test/self/Makefile b/test/self/Makefile
index c7c729cba0e0..4d2c0374c9c3 100644
--- a/test/self/Makefile
+++ b/test/self/Makefile
@@ -7,3 +7,4 @@ obj-$(CONFIG_SELFTEST_PROGRESS_NOTIFIER) += progress-notifier.o
 obj-$(CONFIG_SELFTEST_OF_MANIPULATION) += of_manipulation.o of_manipulation.dtb.o
 obj-$(CONFIG_SELFTEST_ENVIRONMENT_VARIABLES) += envvar.o
 obj-$(CONFIG_SELFTEST_FS_RAMFS) += ramfs.o
+obj-$(CONFIG_SELFTEST_JSON) += json.o
diff --git a/test/self/json.c b/test/self/json.c
new file mode 100644
index 000000000000..54323cf4350c
--- /dev/null
+++ b/test/self/json.c
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <common.h>
+#include <bselftest.h>
+#include <jsmn.h>
+
+BSELFTEST_GLOBALS();
+
+static const jsmntok_t*
+__json_expect(const char *json, const jsmntok_t *token, const char **lookup,
+	      jsmntype_t expected_type, const char *expected_value)
+{
+	bool success = true;
+
+	total_tests++;
+
+	if (token->type != expected_type) {
+		failed_tests++;
+		printf("%pJP: type mismatch: got %d, but %d expected\n",
+		       lookup, token->type, expected_type);
+		success = false;
+	}
+
+	if (!expected_value)
+		goto out;
+
+	total_tests++;
+
+	if (!jsmn_eq(expected_value, json, token)) {
+		failed_tests++;
+		printf("%pJP: string mismatch: got %.*s, but %s expected\n",
+		       lookup, (int)(token->end - token->start),
+		       &json[token->start], expected_value);
+		success = false;
+	}
+
+out:
+	return success ? token : NULL;
+}
+
+static const jsmntok_t*
+json_expect(const char *json, const jsmntok_t *tokens, const char **lookup,
+	    jsmntype_t expected_type, const char *expected_value)
+{
+	const jsmntok_t *token;
+
+	total_tests++;
+
+	token = jsmn_locate(lookup, json, tokens);
+	if (!token) {
+		failed_tests++;
+		printf("%pJP: couldn't be located\n", lookup);
+		return NULL;
+	}
+
+	return __json_expect(json, token, lookup, expected_type, expected_value);
+}
+
+#define JP(...)	(const char *[]) { __VA_ARGS__, NULL }
+
+/* Wonky indentation is intended */
+static const char json[] =
+"{\n"
+"	\"null\" :null,\"number\" : 0x42,\n"
+"	\"object\": {\n"
+"		\"a\": \"b\",\n"
+"		\"C\": \"dE\","
+"		\"e\": \"\"\n"
+"	},"
+"	\"array\": [ \"1\",\"2\",\t\t\"3\"],\n"
+"	\"boolean\": true,\n"
+"\"string\": \"Hello World\n\"}\n";
+
+static void test_json(void)
+{
+	ssize_t token_count;
+	const jsmntok_t *token;
+	jsmntok_t *tokens;
+	jsmn_parser parser;
+	char *string;
+	int ret;
+
+	total_tests++;
+
+	jsmn_init(&parser);
+
+	/* Figure out how many tokens we need. */
+	ret = jsmn_parse(&parser, json, sizeof(json), NULL, 0);
+	if (ret < 0) {
+		printf("failed to determine number of tokens: %d\n", ret);
+		failed_tests++;
+		return;
+	}
+
+	token_count = ret;
+
+	/* `token_count` is strictly less than `length` which is strictly less
+	 * than CONFIG_SYS_EEPROM_SIZE (or 8K), so we should never overflow an
+	 * int here.
+	 */
+	tokens = kmalloc_array(token_count, sizeof(jsmntok_t), GFP_KERNEL);
+	if (WARN_ON(!tokens))
+		return;
+
+	total_tests++;
+
+	jsmn_init(&parser);
+	ret = jsmn_parse(&parser, json, sizeof(json), tokens, token_count);
+	if (ret < 0) {
+		printf("failed to parse JSON with tokens: %d\n", ret);
+		failed_tests++;
+		goto out;
+	}
+
+	json_expect(json, tokens, JP("null"), JSMN_PRIMITIVE, "null");
+	json_expect(json, tokens, JP("number"), JSMN_PRIMITIVE, "0x42");
+	json_expect(json, tokens, JP("object"), JSMN_OBJECT, NULL);
+	json_expect(json, tokens, JP("object", "a"), JSMN_STRING, "b");
+	json_expect(json, tokens, JP("object", "C"), JSMN_STRING, "dE");
+	json_expect(json, tokens, JP("object", "e"), JSMN_STRING, "");
+
+	token = json_expect(json, tokens, JP("array"), JSMN_ARRAY, NULL);
+
+	token = jsmn_skip_value(token);
+	__json_expect(json, token, JP("boolean"), JSMN_STRING, "boolean");
+
+	token = jsmn_skip_value(token);
+	__json_expect(json, token, JP("boolean"), JSMN_PRIMITIVE, "true");
+
+	string = jsmn_strcpy(JP("string"), json, tokens);
+	if (WARN_ON(!string))
+		return;
+
+	total_tests++;
+	if (strcmp(string, "Hello World\n")) {
+		failed_tests++;
+		printf("%pJP: string mismatch\n", JP("string"));
+	}
+
+	free(string);
+out:
+	free(tokens);
+}
+bselftest(parser, test_json);
-- 
2.30.2




^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 0/4] lib: add jsmn JSON parser
  2023-01-10  8:49 [PATCH 0/4] lib: add jsmn JSON parser Ahmad Fatoum
                   ` (3 preceding siblings ...)
  2023-01-10  8:49 ` [PATCH 4/4] test: self: add json parser test Ahmad Fatoum
@ 2023-01-10 15:10 ` Sascha Hauer
  4 siblings, 0 replies; 6+ messages in thread
From: Sascha Hauer @ 2023-01-10 15:10 UTC (permalink / raw)
  To: Ahmad Fatoum; +Cc: barebox

On Tue, Jan 10, 2023 at 09:49:26AM +0100, Ahmad Fatoum wrote:
> Board code I have needs to read out factory data encoded into JSON blob
> on EEPROM. Import JSMN, a minimal JSON parser written in C.
> 
> I don't know if the board code will be upstreamed in future, but for
> now, add a selftest that exercises the API. As JSON could be useful
> elsewhere as well, I think it's good to have it in-tree.
> 
> Ahmad Fatoum (4):
>   lib: add jsmn JSON parser support
>   lib: extend jsmn with simple JSONPath lookup helpers
>   vsprintf: implement %pJP for printing JSONPaths
>   test: self: add json parser test

Applied, thanks

Sascha

> 
>  include/bselftest.h |   3 +-
>  include/jsmn.h      | 148 ++++++++++++++
>  lib/Kconfig         |   5 +
>  lib/Makefile        |   1 +
>  lib/jsmn.c          | 457 ++++++++++++++++++++++++++++++++++++++++++++
>  lib/vsprintf.c      |  44 +++++
>  test/self/Kconfig   |   5 +
>  test/self/Makefile  |   1 +
>  test/self/json.c    | 146 ++++++++++++++
>  test/self/printf.c  |  17 ++
>  10 files changed, 826 insertions(+), 1 deletion(-)
>  create mode 100644 include/jsmn.h
>  create mode 100644 lib/jsmn.c
>  create mode 100644 test/self/json.c
> 
> -- 
> 2.30.2
> 
> 
> 

-- 
Pengutronix e.K.                           |                             |
Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |



^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2023-01-10 15:12 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-01-10  8:49 [PATCH 0/4] lib: add jsmn JSON parser Ahmad Fatoum
2023-01-10  8:49 ` [PATCH 1/4] lib: add jsmn JSON parser support Ahmad Fatoum
2023-01-10  8:49 ` [PATCH 2/4] lib: extend jsmn with simple JSONPath lookup helpers Ahmad Fatoum
2023-01-10  8:49 ` [PATCH 3/4] vsprintf: implement %pJP for printing JSONPaths Ahmad Fatoum
2023-01-10  8:49 ` [PATCH 4/4] test: self: add json parser test Ahmad Fatoum
2023-01-10 15:10 ` [PATCH 0/4] lib: add jsmn JSON parser Sascha Hauer

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox