diff --git a/include/open62541/types.h b/include/open62541/types.h index 18384fe9399..7648e082d30 100644 --- a/include/open62541/types.h +++ b/include/open62541/types.h @@ -200,6 +200,9 @@ UA_String_fromChars(const char *src) UA_FUNC_ATTR_WARN_UNUSED_RESULT; UA_Boolean UA_EXPORT UA_String_isEmpty(const UA_String *s); +UA_StatusCode +UA_String_append(UA_String *s, const UA_String s2); + UA_EXPORT extern const UA_String UA_STRING_NULL; /** @@ -1321,6 +1324,8 @@ typedef struct { UA_Boolean unquotedKeys; /* Don't print quotes around object element keys */ UA_Boolean stringNodeIds; /* String encoding for NodeIds, like "ns=1;i=42" */ + UA_Boolean stringQualifiedNames; /* String QualifiedNames, like "0:name" */ + UA_Boolean stringLocalizedText; /* String LocalizedText, like "en:text" */ } UA_EncodeJsonOptions; /* Returns the number of bytes the value src takes in json encoding. Returns diff --git a/include/open62541/util.h b/include/open62541/util.h index 47ab4662cc4..e5abc794dbe 100644 --- a/include/open62541/util.h +++ b/include/open62541/util.h @@ -260,8 +260,17 @@ UA_readNumberWithBase(const UA_Byte *buf, size_t buflen, * extended escaping are ``,()[] \t\n\v\f\r``. * * This documentation always states whether "and-escaping" or the - * "extended-and-escaping is used. - * + * "extended-and-escaping is used. */ + +UA_StatusCode +UA_String_escape(const UA_String str, UA_String *out, + UA_Boolean extended); + +/* In-situ, modifies (and maybe frees) the string */ +void +UA_String_unescape(UA_String *str); + +/** * .. _relativepath: * * RelativePath Expressions diff --git a/src/ua_types_encoding_json.c b/src/ua_types_encoding_json.c index 687a4e270da..be980530528 100644 --- a/src/ua_types_encoding_json.c +++ b/src/ua_types_encoding_json.c @@ -749,7 +749,7 @@ ENCODE_JSON(NodeId) { if(ctx->stringNodeIds) { UA_String out = UA_STRING_NULL; ret |= UA_NodeId_print(src, &out); - ret |= encodeJsonJumpTable[UA_DATATYPEKIND_STRING](ctx, &out, NULL); + ret |= ENCODE_DIRECT_JSON(&out, String); UA_String_clear(&out); return ret; } @@ -766,18 +766,17 @@ ENCODE_JSON(NodeId) { /* For the non-reversible encoding, the field is the NamespaceUri * associated with the NamespaceIndex, encoded as a JSON string. * A NamespaceIndex of 1 is always encoded as a JSON number. */ + ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); if(src->namespaceIndex == 1) { - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); ret |= ENCODE_DIRECT_JSON(&src->namespaceIndex, UInt16); } else { - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); - /* Check if Namespace given and in range */ if(src->namespaceIndex < ctx->namespacesSize && ctx->namespaces != NULL) { UA_String namespaceEntry = ctx->namespaces[src->namespaceIndex]; ret |= ENCODE_DIRECT_JSON(&namespaceEntry, String); } else { - return UA_STATUSCODE_BADNOTFOUND; + /* If not found, print the identifier */ + ret |= ENCODE_DIRECT_JSON(&src->namespaceIndex, UInt16); } } } @@ -793,7 +792,7 @@ ENCODE_JSON(ExpandedNodeId) { if(ctx->stringNodeIds) { UA_String out = UA_STRING_NULL; ret |= UA_ExpandedNodeId_print(src, &out); - ret |= encodeJsonJumpTable[UA_DATATYPEKIND_STRING](ctx, &out, NULL); + ret |= ENCODE_DIRECT_JSON(&out, String); UA_String_clear(&out); return ret; } @@ -807,16 +806,15 @@ ENCODE_JSON(ExpandedNodeId) { if(ctx->useReversible) { /* Reversible Case */ + ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); if(src->namespaceUri.data) { /* If the NamespaceUri is specified it is encoded as a JSON string * in this field */ - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); ret |= ENCODE_DIRECT_JSON(&src->namespaceUri, String); } else if(src->nodeId.namespaceIndex > 0) { /* If the NamespaceUri is not specified, the NamespaceIndex is * encoded. Encoded as a JSON number for the reversible encoding. * Omitted if the NamespaceIndex equals 0. */ - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); ret |= ENCODE_DIRECT_JSON(&src->nodeId.namespaceIndex, UInt16); } @@ -834,20 +832,20 @@ ENCODE_JSON(ExpandedNodeId) { * NamespaceUri associated with the NamespaceIndex encoded as a JSON * string. A NamespaceIndex of 1 is always encoded as a JSON number. */ + ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); if(src->namespaceUri.data) { - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); ret |= ENCODE_DIRECT_JSON(&src->namespaceUri, String); } else { if(src->nodeId.namespaceIndex == 1) { - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); ret |= ENCODE_DIRECT_JSON(&src->nodeId.namespaceIndex, UInt16); } else { /* Check if Namespace given and in range */ - if(src->nodeId.namespaceIndex >= ctx->namespacesSize || !ctx->namespaces) - return UA_STATUSCODE_BADNOTFOUND; - UA_String namespaceEntry = ctx->namespaces[src->nodeId.namespaceIndex]; - ret |= writeJsonKey(ctx, UA_JSONKEY_NAMESPACE); - ret |= ENCODE_DIRECT_JSON(&namespaceEntry, String); + if(src->nodeId.namespaceIndex < ctx->namespacesSize && ctx->namespaces != NULL) { + UA_String namespaceEntry = ctx->namespaces[src->nodeId.namespaceIndex]; + ret |= ENCODE_DIRECT_JSON(&namespaceEntry, String); + } else { + ret |= ENCODE_DIRECT_JSON(&src->nodeId.namespaceIndex, UInt16); + } } } @@ -869,8 +867,26 @@ ENCODE_JSON(ExpandedNodeId) { /* LocalizedText */ ENCODE_JSON(LocalizedText) { + status ret = UA_STATUSCODE_GOOD; + + if(ctx->stringLocalizedText) { + UA_String out = UA_STRING_NULL; + ret |= writeChar(ctx, '"'); + if(src->locale.length > 0) { + ret |= UA_String_escape(src->locale, &out, false); + ret |= writeChars(ctx, (char*)out.data, out.length); + UA_String_clear(&out); + ret |= writeChar(ctx, ':'); + } + ret |= UA_String_escape(src->text, &out, false); + ret |= writeChars(ctx, (char*)out.data, out.length); + UA_String_clear(&out); + ret |= writeChar(ctx, '"'); + return ret; + } + if(ctx->useReversible) { - status ret = writeJsonObjStart(ctx); + ret = writeJsonObjStart(ctx); ret |= writeJsonKey(ctx, UA_JSONKEY_LOCALE); ret |= ENCODE_DIRECT_JSON(&src->locale, String); ret |= writeJsonKey(ctx, UA_JSONKEY_TEXT); @@ -884,7 +900,22 @@ ENCODE_JSON(LocalizedText) { } ENCODE_JSON(QualifiedName) { - status ret = writeJsonObjStart(ctx); + status ret = UA_STATUSCODE_GOOD; + if(ctx->stringQualifiedNames) { + ret |= writeChar(ctx, '"'); + if(src->namespaceIndex > 0) { + ret |= ENCODE_DIRECT_JSON(&src->namespaceIndex, UInt16); + ret |= writeChar(ctx, ':'); + } + UA_String out = UA_STRING_NULL; + ret |= UA_String_escape(src->name, &out, false); + ret |= writeChars(ctx, (char*)out.data, out.length); + UA_String_clear(&out); + ret |= writeChar(ctx, '"'); + return ret; + } + + ret = writeJsonObjStart(ctx); ret |= writeJsonKey(ctx, UA_JSONKEY_NAME); ret |= ENCODE_DIRECT_JSON(&src->name, String); @@ -898,12 +929,10 @@ ENCODE_JSON(QualifiedName) { * NamespaceIndex portion of the QualifiedName is encoded as JSON string * unless the NamespaceIndex is 1 or if NamespaceUri is unknown. In * these cases, the NamespaceIndex is encoded as a JSON number. */ + ret |= writeJsonKey(ctx, UA_JSONKEY_URI); if(src->namespaceIndex == 1) { - ret |= writeJsonKey(ctx, UA_JSONKEY_URI); ret |= ENCODE_DIRECT_JSON(&src->namespaceIndex, UInt16); } else { - ret |= writeJsonKey(ctx, UA_JSONKEY_URI); - /* Check if Namespace given and in range */ if(src->namespaceIndex < ctx->namespacesSize && ctx->namespaces != NULL) { UA_String namespaceEntry = ctx->namespaces[src->namespaceIndex]; @@ -1318,6 +1347,8 @@ UA_encodeJson(const void *src, const UA_DataType *type, UA_ByteString *outBuf, ctx.prettyPrint = options->prettyPrint; ctx.unquotedKeys = options->unquotedKeys; ctx.stringNodeIds = options->stringNodeIds; + ctx.stringQualifiedNames = options->stringQualifiedNames; + ctx.stringLocalizedText = options->stringLocalizedText; } /* Encode */ @@ -2145,13 +2176,6 @@ DECODE_JSON(StatusCode) { return UInt32_decodeJson(ctx, dst, NULL); } -static status -VariantDimension_decodeJson(ParseCtx *ctx, void *dst, const UA_DataType *type) { - (void) type; - const UA_DataType *dimType = &UA_TYPES[UA_TYPES_UINT32]; - return Array_decodeJson(ctx, (void**)dst, dimType); -} - /* Get type type encoded by the ExtensionObject at ctx->index. * Returns NULL if that fails (type unknown or otherwise). */ static const UA_DataType * @@ -2314,6 +2338,73 @@ Array_decodeJsonUnwrapExtensionObject(ParseCtx *ctx, void **dst, const UA_DataTy return UA_STATUSCODE_GOOD; } +static status +decodeVariantBodyWithType(ParseCtx *ctx, UA_Variant *dst, size_t bodyIndex, + size_t *dimIndex, const UA_DataType *type) { + /* Value is an array? */ + UA_Boolean isArray = (ctx->tokens[bodyIndex].type == CJ5_TOKEN_ARRAY); + + /* TODO: Handling of null-arrays (length -1) needs to be clarified + * + * if(tokenIsNull(ctx, bodyIndex)) { + * isArray = true; + * dst->arrayLength = 0; + * } */ + + /* No array but has dimension -> error */ + if(!isArray && dimIndex) + return UA_STATUSCODE_BADDECODINGERROR; + + /* Get the datatype of the content. The type must be a builtin data type. + * All not-builtin types are wrapped in an ExtensionObject. */ + if(type->typeKind > UA_DATATYPEKIND_DIAGNOSTICINFO) + return UA_STATUSCODE_BADDECODINGERROR; + + /* A variant cannot contain a variant. But it can contain an array of + * variants */ + if(type->typeKind == UA_DATATYPEKIND_VARIANT && !isArray) + return UA_STATUSCODE_BADDECODINGERROR; + + ctx->depth++; + ctx->index = bodyIndex; + + /* Decode an array */ + status res = UA_STATUSCODE_GOOD; + if(isArray) { + /* Try to unwrap ExtensionObjects in the array. + * The members must all have the same type. */ + const UA_DataType *unwrapType = NULL; + if(type == &UA_TYPES[UA_TYPES_EXTENSIONOBJECT] && + (unwrapType = getArrayUnwrapType(ctx, bodyIndex))) { + res = Array_decodeJsonUnwrapExtensionObject(ctx, &dst->data, unwrapType); + } else { + dst->type = type; + res = Array_decodeJson(ctx, &dst->data, type); + } + + /* Decode array dimensions */ + if(dimIndex) { + ctx->index = *dimIndex; + res |= Array_decodeJson(ctx, (void**)&dst->arrayDimensions, &UA_TYPES[UA_TYPES_UINT32]); + } + ctx->depth--; + return res; + } + + /* Decode a value wrapped in an ExtensionObject */ + if(type->typeKind == UA_DATATYPEKIND_EXTENSIONOBJECT) + return Variant_decodeJsonUnwrapExtensionObject(ctx, dst, NULL); + + /* Allocate Memory for Body */ + dst->data = UA_new(type); + if(!dst->data) + return UA_STATUSCODE_BADOUTOFMEMORY; + + /* Decode the body */ + dst->type = type; + return decodeJsonJumpTable[type->typeKind](ctx, dst, type); +} + DECODE_JSON(Variant) { CHECK_NULL_SKIP; /* Treat null as an empty variant */ CHECK_OBJECT; @@ -2343,8 +2434,8 @@ DECODE_JSON(Variant) { /* Set the type */ UA_NodeId typeNodeId = UA_NODEID_NUMERIC(0, (UA_UInt32)idType); - dst->type = UA_findDataTypeWithCustom(&typeNodeId, ctx->customTypes); - if(!dst->type) + type = UA_findDataTypeWithCustom(&typeNodeId, ctx->customTypes); + if(!type) return UA_STATUSCODE_BADDECODINGERROR; /* Search for body */ @@ -2353,79 +2444,15 @@ DECODE_JSON(Variant) { if(ret != UA_STATUSCODE_GOOD) return UA_STATUSCODE_BADDECODINGERROR; - /* Value is an array? */ - UA_Boolean isArray = (ctx->tokens[bodyIndex].type == CJ5_TOKEN_ARRAY); - - /* TODO: Handling of null-arrays (length -1) needs to be clarified - * - * if(tokenIsNull(ctx, bodyIndex)) { - * isArray = true; - * dst->arrayLength = 0; - * } */ - - /* Has the variant dimension? */ - UA_Boolean hasDimension = false; + /* Search for the dimensions */ + size_t *dimPtr = NULL; size_t dimIndex = 0; ret = lookAheadForKey(ctx, UA_JSONKEY_DIMENSION, &dimIndex); - if(ret == UA_STATUSCODE_GOOD) - hasDimension = (ctx->tokens[dimIndex].size > 0); - - /* No array but has dimension -> error */ - if(!isArray && hasDimension) - return UA_STATUSCODE_BADDECODINGERROR; - - /* Get the datatype of the content. The type must be a builtin data type. - * All not-builtin types are wrapped in an ExtensionObject. */ - if(dst->type->typeKind > UA_DATATYPEKIND_DIAGNOSTICINFO) - return UA_STATUSCODE_BADDECODINGERROR; - - /* A variant cannot contain a variant. But it can contain an array of - * variants */ - if(dst->type->typeKind == UA_DATATYPEKIND_VARIANT && !isArray) - return UA_STATUSCODE_BADDECODINGERROR; - - /* Decode an array */ - if(isArray) { - DecodeEntry entries[3] = { - {UA_JSONKEY_TYPE, NULL, NULL, false, NULL}, - {UA_JSONKEY_BODY, &dst->data, (decodeJsonSignature)Array_decodeJson, false, dst->type}, - {UA_JSONKEY_DIMENSION, &dst->arrayDimensions, VariantDimension_decodeJson, false, NULL} - }; - - /* Try to unwrap ExtensionObjects in the array. - * The members must all have the same type. */ - if(dst->type == &UA_TYPES[UA_TYPES_EXTENSIONOBJECT]) { - const UA_DataType *unwrapType = getArrayUnwrapType(ctx, bodyIndex); - if(unwrapType) { - dst->type = unwrapType; - entries[1].type = unwrapType; - entries[1].function = (decodeJsonSignature) - Array_decodeJsonUnwrapExtensionObject; - } - } - - return decodeFields(ctx, entries, (hasDimension) ? 3 : 2); - } - - /* Decode a value wrapped in an ExtensionObject */ - if(dst->type->typeKind == UA_DATATYPEKIND_EXTENSIONOBJECT) { - DecodeEntry entries[2] = { - {UA_JSONKEY_TYPE, NULL, NULL, false, NULL}, - {UA_JSONKEY_BODY, dst, Variant_decodeJsonUnwrapExtensionObject, false, NULL} - }; - return decodeFields(ctx, entries, 2); - } + if(ret == UA_STATUSCODE_GOOD && ctx->tokens[dimIndex].size > 0) + dimPtr = &dimIndex; - /* Allocate Memory for Body */ - dst->data = UA_new(dst->type); - if(!dst->data) - return UA_STATUSCODE_BADOUTOFMEMORY; - - DecodeEntry entries[2] = { - {UA_JSONKEY_TYPE, NULL, NULL, false, NULL}, - {UA_JSONKEY_BODY, dst->data, NULL, false, dst->type} - }; - return decodeFields(ctx, entries, 2); + /* Decode the body */ + return decodeVariantBodyWithType(ctx, dst, bodyIndex, dimPtr, type); } DECODE_JSON(DataValue) { diff --git a/src/ua_types_encoding_json.h b/src/ua_types_encoding_json.h index 825c0da31ed..2cf3a6f0524 100644 --- a/src/ua_types_encoding_json.h +++ b/src/ua_types_encoding_json.h @@ -38,6 +38,8 @@ typedef struct { UA_Boolean prettyPrint; UA_Boolean unquotedKeys; UA_Boolean stringNodeIds; + UA_Boolean stringQualifiedNames; + UA_Boolean stringLocalizedText; } CtxJson; UA_StatusCode writeJsonObjStart(CtxJson *ctx); diff --git a/src/util/ua_util.c b/src/util/ua_util.c index 3c34af8ced9..999e767e026 100644 --- a/src/util/ua_util.c +++ b/src/util/ua_util.c @@ -748,6 +748,21 @@ UA_String_escapeAppend(UA_String *s, const UA_String s2, UA_Boolean extended) { return UA_STATUSCODE_GOOD; } +UA_StatusCode +UA_String_escape(const UA_String str, UA_String *out, + UA_Boolean extended) { + *out = UA_STRING_NULL; + return UA_String_escapeAppend(out, str, extended); +} + +void +UA_String_unescape(UA_String *str) { + char *out = unescape((char*)str->data, (char*)str->data + str->length); + str->length = (UA_Byte*)out - str->data; + if(str->length == 0) + str->data = NULL; +} + #ifdef UA_ENABLE_PARSING static UA_StatusCode diff --git a/src/util/ua_util_internal.h b/src/util/ua_util_internal.h index dadc2aa8161..a7d819e4a28 100644 --- a/src/util/ua_util_internal.h +++ b/src/util/ua_util_internal.h @@ -73,9 +73,6 @@ find_unescaped(char *pos, const char *end, UA_Boolean extended); UA_StatusCode UA_String_escapeAppend(UA_String *s, const UA_String s2, UA_Boolean extended); -UA_StatusCode -UA_String_append(UA_String *s, const UA_String s2); - /* Case insensitive lookup. Returns UA_ATTRIBUTEID_INVALID if not found. */ UA_AttributeId UA_AttributeId_fromName(const UA_String name); diff --git a/tests/check_types_builtin_json.c b/tests/check_types_builtin_json.c index f90833c2924..ef5ff2c730e 100644 --- a/tests/check_types_builtin_json.c +++ b/tests/check_types_builtin_json.c @@ -5421,14 +5421,14 @@ START_TEST(UA_JsonHelper) { ck_assert_int_eq(writeJsonArrStart(&ctx), UA_STATUSCODE_BADENCODINGLIMITSEXCEEDED); ck_assert_int_eq(writeJsonObjStart(&ctx), UA_STATUSCODE_BADENCODINGLIMITSEXCEEDED); ck_assert_int_eq(writeJsonObjEnd(&ctx), UA_STATUSCODE_BADENCODINGLIMITSEXCEEDED); - ck_assert_int_eq(writeJsonArrEnd(&ctx), UA_STATUSCODE_BADENCODINGLIMITSEXCEEDED); + ck_assert_int_eq(writeJsonArrEnd(&ctx, NULL), UA_STATUSCODE_BADENCODINGLIMITSEXCEEDED); ctx.calcOnly = true; ctx.end = (const UA_Byte*)(uintptr_t)SIZE_MAX; ck_assert_int_eq(writeJsonArrStart(&ctx), UA_STATUSCODE_GOOD); ck_assert_int_eq(writeJsonObjStart(&ctx), UA_STATUSCODE_GOOD); ck_assert_int_eq(writeJsonObjEnd(&ctx), UA_STATUSCODE_GOOD); - ck_assert_int_eq(writeJsonArrEnd(&ctx), UA_STATUSCODE_GOOD); + ck_assert_int_eq(writeJsonArrEnd(&ctx, NULL), UA_STATUSCODE_GOOD); } END_TEST