diff --git a/build/error.yaml b/build/error.yaml index 25cfc1f3c..4ef84bf65 100644 --- a/build/error.yaml +++ b/build/error.yaml @@ -86,6 +86,9 @@ path-close: 97 # Unable to get info for a file file-info: 98 +# Invalid JSON format. Eventually this should be a child of format error and share the same code +json-format: 99 + # This error should not be thrown directly -- it serves as a parent for the C errors runtime: 122 diff --git a/doc/xml/release.xml b/doc/xml/release.xml index 0c0fee0c5..0096f8ea6 100644 --- a/doc/xml/release.xml +++ b/doc/xml/release.xml @@ -53,6 +53,10 @@

Add uint64 variant type and supporting conversion functions.

+ +

Add basic C JSON parser.

+
+

Allow Buffer object used size to be different than allocated size. Add functions to manage used size and remaining size and update automatically when possible.

diff --git a/lib/pgBackRest/Common/ExceptionAuto.pm b/lib/pgBackRest/Common/ExceptionAuto.pm index 2fbf0b707..45ab39eca 100644 --- a/lib/pgBackRest/Common/ExceptionAuto.pm +++ b/lib/pgBackRest/Common/ExceptionAuto.pm @@ -165,6 +165,8 @@ use constant ERROR_PATH_CLOSE => 97; push @EXPORT, qw(ERROR_PATH_CLOSE); use constant ERROR_FILE_INFO => 98; push @EXPORT, qw(ERROR_FILE_INFO); +use constant ERROR_JSON_FORMAT => 99; +push @EXPORT, qw(ERROR_JSON_FORMAT); use constant ERROR_RUNTIME => 122; push @EXPORT, qw(ERROR_RUNTIME); use constant ERROR_INVALID => 123; diff --git a/src/Makefile b/src/Makefile index 1d02e2143..8a96fe563 100644 --- a/src/Makefile +++ b/src/Makefile @@ -214,6 +214,9 @@ common/type/buffer.o: common/type/buffer.c common/assert.h common/debug.h common common/type/convert.o: common/type/convert.c common/assert.h common/debug.h common/error.auto.h common/error.h common/logLevel.h common/stackTrace.h common/type/convert.h $(CC) $(CFLAGS) -c common/type/convert.c -o common/type/convert.o +common/type/json.o: common/type/json.c common/debug.h common/log.h common/logLevel.h common/stackTrace.h common/type/convert.h common/type/json.h common/type/keyValue.h common/type/variantList.h + $(CC) $(CFLAGS) -c common/type/json.c -o common/type/json.o + common/type/keyValue.o: common/type/keyValue.c common/debug.h common/error.auto.h common/error.h common/logLevel.h common/memContext.h common/stackTrace.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/list.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h $(CC) $(CFLAGS) -c common/type/keyValue.c -o common/type/keyValue.o diff --git a/src/common/error.auto.c b/src/common/error.auto.c index 4ec079df6..eeb97031d 100644 --- a/src/common/error.auto.c +++ b/src/common/error.auto.c @@ -80,6 +80,7 @@ ERROR_DEFINE( 95, CipherError, RuntimeError); ERROR_DEFINE( 96, ParamInvalidError, RuntimeError); ERROR_DEFINE( 97, PathCloseError, RuntimeError); ERROR_DEFINE( 98, FileInfoError, RuntimeError); +ERROR_DEFINE( 99, JsonFormatError, RuntimeError); ERROR_DEFINE(122, RuntimeError, RuntimeError); ERROR_DEFINE(123, InvalidError, RuntimeError); ERROR_DEFINE(124, UnhandledError, RuntimeError); @@ -163,6 +164,7 @@ static const ErrorType *errorTypeList[] = &ParamInvalidError, &PathCloseError, &FileInfoError, + &JsonFormatError, &RuntimeError, &InvalidError, &UnhandledError, diff --git a/src/common/error.auto.h b/src/common/error.auto.h index 697ea8b4d..1785e9362 100644 --- a/src/common/error.auto.h +++ b/src/common/error.auto.h @@ -82,6 +82,7 @@ ERROR_DECLARE(CipherError); ERROR_DECLARE(ParamInvalidError); ERROR_DECLARE(PathCloseError); ERROR_DECLARE(FileInfoError); +ERROR_DECLARE(JsonFormatError); ERROR_DECLARE(RuntimeError); ERROR_DECLARE(InvalidError); ERROR_DECLARE(UnhandledError); diff --git a/src/common/type/json.c b/src/common/type/json.c new file mode 100644 index 000000000..b6900d1ad --- /dev/null +++ b/src/common/type/json.c @@ -0,0 +1,111 @@ +/*********************************************************************************************************************************** +Convert JSON to/from KeyValue +***********************************************************************************************************************************/ +#include + +#include "common/debug.h" +#include "common/log.h" +#include "common/type/json.h" + +/*********************************************************************************************************************************** +Convert JSON to KeyValue object + +Currently this function is only intended to convert the limited types that are included in info files. More types will be added as +needed. Since this function is only intended to read internally-generated JSON it is assumed to be well-formed with no extraneous +whitespace. +***********************************************************************************************************************************/ +KeyValue *jsonToKv(const String *json) +{ + FUNCTION_DEBUG_BEGIN(logLevelTrace); + FUNCTION_DEBUG_PARAM(STRING, json); + + FUNCTION_TEST_ASSERT(json != NULL); + FUNCTION_DEBUG_END(); + + KeyValue *result = kvNew(); + + MEM_CONTEXT_TEMP_BEGIN() + { + // We'll examine the string byte by byte + const char *jsonC = strPtr(json); + unsigned int jsonPos = 0; + + // Consume the initial delimiter + if (jsonC[jsonPos] == '[') + THROW(JsonFormatError, "arrays not supported"); + else if (jsonC[jsonPos] != '{') + THROW_FMT(JsonFormatError, "expected '{' but found '%c'", jsonC[jsonPos]); + + // Start parsing key/value pairs + do + { + Variant *value = NULL; + + jsonPos++; + + // Parse the key which should always be quoted + if (jsonC[jsonPos] != '"') + THROW_FMT(JsonFormatError, "expected '\"' but found '%c'", jsonC[jsonPos]); + + unsigned int keyBeginPos = ++jsonPos; + + while (jsonC[jsonPos] != '"' && jsonPos < strSize(json) - 1) + jsonPos++; + + if (jsonC[jsonPos] != '"') + THROW_FMT(JsonFormatError, "expected '\"' but found '%c'", jsonC[jsonPos]); + + String *key = strNewN(jsonC + keyBeginPos, jsonPos - keyBeginPos); + + if (strSize(key) == 0) + THROW(JsonFormatError, "zero-length key not allowed"); + + if (jsonC[++jsonPos] != ':') + THROW_FMT(JsonFormatError, "expected ':' but found '%c'", jsonC[jsonPos]); + + unsigned int valueBeginPos = ++jsonPos; + + // The value appears to be a string + if (jsonC[jsonPos] == '"') + { + valueBeginPos++; + jsonPos++; + + while (jsonC[jsonPos] != '"' && jsonPos < strSize(json) - 1) + jsonPos++; + + if (jsonC[jsonPos] != '"') + THROW_FMT(JsonFormatError, "expected '\"' but found '%c'", jsonC[jsonPos]); + + value = varNewStr(strNewN(jsonC + valueBeginPos, jsonPos - valueBeginPos)); + + jsonPos++; + } + + // The value appears to be a number + else if (isdigit(jsonC[jsonPos])) + { + while (isdigit(jsonC[jsonPos]) && jsonPos < strSize(json) - 1) + jsonPos++; + + String *valueStr = strNewN(jsonC + valueBeginPos, jsonPos - valueBeginPos); + + value = varNewUInt64(cvtZToUInt64(strPtr(valueStr))); + } + + // Else not sure what it is. Currently booleans and nulls will error. + else + THROW(JsonFormatError, "unknown value type"); + + kvPut(result, varNewStr(key), value); + } + while (jsonC[jsonPos] == ','); + + // Look for end delimiter + if (jsonC[jsonPos] != '}') + THROW_FMT(JsonFormatError, "expected '}' but found '%c'", jsonC[jsonPos]); + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_DEBUG_RESULT(KEY_VALUE, result); +} diff --git a/src/common/type/json.h b/src/common/type/json.h new file mode 100644 index 000000000..3c1b69404 --- /dev/null +++ b/src/common/type/json.h @@ -0,0 +1,14 @@ +/*********************************************************************************************************************************** +Convert JSON to/from KeyValue +***********************************************************************************************************************************/ +#ifndef COMMON_TYPE_JSON_H +#define COMMON_TYPE_JSON_H + +#include "common/type/keyValue.h" + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +KeyValue *jsonToKv(const String *json); + +#endif diff --git a/src/perl/embed.auto.c b/src/perl/embed.auto.c index 2ec645f4c..0eac3b5f0 100644 --- a/src/perl/embed.auto.c +++ b/src/perl/embed.auto.c @@ -5046,6 +5046,8 @@ static const EmbeddedModule embeddedModule[] = "push @EXPORT, qw(ERROR_PATH_CLOSE);\n" "use constant ERROR_FILE_INFO => 98;\n" "push @EXPORT, qw(ERROR_FILE_INFO);\n" + "use constant ERROR_JSON_FORMAT => 99;\n" + "push @EXPORT, qw(ERROR_JSON_FORMAT);\n" "use constant ERROR_RUNTIME => 122;\n" "push @EXPORT, qw(ERROR_RUNTIME);\n" "use constant ERROR_INVALID => 123;\n" diff --git a/test/define.yaml b/test/define.yaml index 1d4559e27..2e8d965f8 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -185,6 +185,13 @@ unit: coverage: common/type/variantList: full + # ---------------------------------------------------------------------------------------------------------------------------- + - name: type-json + total: 1 + + coverage: + common/type/json: full + # ---------------------------------------------------------------------------------------------------------------------------- - name: type-key-value total: 2 diff --git a/test/src/module/common/typeJsonTest.c b/test/src/module/common/typeJsonTest.c new file mode 100644 index 000000000..186c4d0cf --- /dev/null +++ b/test/src/module/common/typeJsonTest.c @@ -0,0 +1,50 @@ +/*********************************************************************************************************************************** +Test Convert JSON to/from KeyValue +***********************************************************************************************************************************/ + +/*********************************************************************************************************************************** +Test Run +***********************************************************************************************************************************/ +void +testRun(void) +{ + FUNCTION_HARNESS_VOID(); + + // ***************************************************************************************************************************** + if (testBegin("jsonToKv()")) + { + TEST_ERROR(jsonToKv(strNew("[")), JsonFormatError, "arrays not supported"); + TEST_ERROR(jsonToKv(strNew("<")), JsonFormatError, "expected '{' but found '<'"); + TEST_ERROR(jsonToKv(strNew("{>")), JsonFormatError, "expected '\"' but found '>'"); + TEST_ERROR(jsonToKv(strNew("{\"\"")), JsonFormatError, "zero-length key not allowed"); + TEST_ERROR(jsonToKv(strNew("{\"key1\"")), JsonFormatError, "expected ':' but found '"); + TEST_ERROR(jsonToKv(strNew("{\"key1'")), JsonFormatError, "expected '\"' but found '''"); + TEST_ERROR(jsonToKv(strNew("{\"key1\":t")), JsonFormatError, "unknown value type"); + TEST_ERROR(jsonToKv(strNew("{\"key1\":123")), JsonFormatError, "expected '}' but found '3'"); + TEST_ERROR(jsonToKv(strNew("{\"key1\":123>")), JsonFormatError, "expected '}' but found '>'"); + TEST_ERROR(jsonToKv(strNew("{\"key1\":\"123'}")), JsonFormatError, "expected '\"' but found '}'"); + + // ------------------------------------------------------------------------------------------------------------------------- + KeyValue *kv = NULL; + + TEST_ASSIGN(kv, jsonToKv(strNew("{\"key1\":123}")), "single integer value"); + TEST_RESULT_UINT(varUInt64(kvGet(kv, varNewStr(strNew("key1")))), 123, " check integer"); + + TEST_ASSIGN(kv, jsonToKv(strNew("{\"key1\":\"value1\"}")), "single string value"); + TEST_RESULT_STR(strPtr(varStr(kvGet(kv, varNewStr(strNew("key1"))))), "value1", " check string"); + + TEST_ASSIGN( + kv, + jsonToKv( + strNew( + "{\"db-catalog-version\":201409291,\"db-control-version\":942,\"db-system-id\":6116111691796124355," + "\"db-version\":\"9.4\"}")), + "multiple values"); + TEST_RESULT_UINT(varUInt64(kvGet(kv, varNewStr(strNew("db-catalog-version")))), 201409291, " check integer"); + TEST_RESULT_UINT(varUInt64(kvGet(kv, varNewStr(strNew("db-control-version")))), 942, " check integer"); + TEST_RESULT_UINT(varUInt64(kvGet(kv, varNewStr(strNew("db-system-id")))), 6116111691796124355, " check integer"); + TEST_RESULT_STR(strPtr(varStr(kvGet(kv, varNewStr(strNew("db-version"))))), "9.4", " check string"); + } + + FUNCTION_HARNESS_RESULT_VOID(); +}