libs/utils/src/properties_encoding.c (350 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "celix_properties.h"
#include "celix_properties_private.h"
#include "celix_err.h"
#include "celix_stdlib_cleanup.h"
#include "celix_utils.h"
#include "celix_array_list_encoding_private.h"
#include "celix_json_utils_private.h"
#include <assert.h>
#include <jansson.h>
#include <math.h>
#include <string.h>
#define CELIX_PROPERTIES_JSONPATH_SEPARATOR '.'
static celix_status_t
celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags);
static celix_status_t celix_properties_arrayEntryValueToJson(const char* key,
const celix_properties_entry_t* entry,
int flags,
json_t** out) {
*out = NULL;
int encodeArrayFlags = 0;
if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) {
encodeArrayFlags |= CELIX_ARRAY_LIST_ENCODE_ERROR_ON_EMPTY_ARRAYS;
}
if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) {
encodeArrayFlags |= CELIX_ARRAY_LIST_ENCODE_ERROR_ON_NAN_INF;
}
json_auto_t* array = NULL;
celix_status_t status = celix_arrayList_encodeToJson(entry->typed.arrayValue, encodeArrayFlags, &array);
if (status != CELIX_SUCCESS) {
celix_err_pushf("Failed to encode array list for key %s.", key);
return status;
}
if (json_array_size(array) == 0) {//allow empty arrays, but treat as unset property
return CELIX_SUCCESS;
}
*out = celix_steal_ptr(array);
return CELIX_SUCCESS;
}
static celix_status_t
celix_properties_entryValueToJson(const char* key, const celix_properties_entry_t* entry, int flags, json_t** out) {
*out = NULL;
switch (entry->valueType) {
case CELIX_PROPERTIES_VALUE_TYPE_STRING:
*out = json_string(entry->value);
break;
case CELIX_PROPERTIES_VALUE_TYPE_LONG:
*out = json_integer(entry->typed.longValue);
break;
case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE:
if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) {
if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) {
celix_err_pushf("Invalid NaN or Inf in key '%s'.", key);
return CELIX_ILLEGAL_ARGUMENT;
}
return CELIX_SUCCESS; // ignore NaN and Inf
}
*out = json_real(entry->typed.doubleValue);
break;
case CELIX_PROPERTIES_VALUE_TYPE_BOOL:
*out = json_boolean(entry->typed.boolValue);
break;
case CELIX_PROPERTIES_VALUE_TYPE_VERSION:
return celix_utils_versionToJson(entry->typed.versionValue, out);
case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST:
return celix_properties_arrayEntryValueToJson(key, entry, flags, out);
default:
// LCOV_EXCL_START
celix_err_pushf("Unexpected properties entry type %d.", entry->valueType);
return CELIX_ILLEGAL_ARGUMENT;
// LCOV_EXCL_STOP
}
if (!*out) {
celix_err_pushf("Failed to create json value for key '%s'.", key);
return ENOMEM;
}
return CELIX_SUCCESS;
}
static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const char* key, json_t* obj, int flags) {
if (!value) {
// ignore unset values
return CELIX_SUCCESS;
}
json_t* field = json_object_get(obj, key);
if (field) {
if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
celix_err_pushf("Invalid key collision. key '%s' already exists.", key);
json_decref(value);
return CELIX_ILLEGAL_ARGUMENT;
}
}
int rc = json_object_set_new(obj, key, value);
if (rc != 0) {
celix_err_push("Failed to set json object");
return ENOMEM;
}
return CELIX_SUCCESS;
}
static celix_status_t celix_properties_addPropertiesEntryFlatToJson(const celix_properties_entry_t* entry,
const char* key,
json_t* root,
int flags) {
json_t* value;
celix_status_t status = celix_properties_entryValueToJson(key, entry, flags, &value);
status = CELIX_DO_IF(status, celix_properties_addJsonValueToJson(value, key, root, flags));
return status;
}
static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_properties_entry_t* entry,
const char* key,
json_t* root,
int flags) {
json_t* jsonObj = root;
const char* fieldName = key;
const char* slash = strchr(key, CELIX_PROPERTIES_JSONPATH_SEPARATOR);
while (slash) {
char buf[64];
char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)(slash - fieldName), fieldName);
celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, name);
if (!name) {
celix_err_push("Failed to create name string");
return ENOMEM;
}
json_t* subObj = json_object_get(jsonObj, name);
if (subObj && !json_is_object(subObj)) {
if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
celix_err_pushf("Invalid key collision. Key '%s' already exists.", name);
return CELIX_ILLEGAL_ARGUMENT;
}
return CELIX_SUCCESS;
}
if (!subObj) {
subObj = json_object();
if (!subObj) {
celix_err_push("Failed to create json object");
return ENOMEM;
}
int rc = json_object_set_new(jsonObj, name, subObj);
if (rc != 0) {
celix_err_push("Failed to set json object");
return ENOMEM;
}
}
jsonObj = subObj;
fieldName = slash + 1;
slash = strchr(fieldName, CELIX_PROPERTIES_JSONPATH_SEPARATOR);
}
json_t* value;
celix_status_t status = celix_properties_entryValueToJson(fieldName, entry, flags, &value);
if (status != CELIX_SUCCESS) {
return status;
}
return celix_properties_addJsonValueToJson(value, fieldName, jsonObj, flags);
}
celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) {
json_auto_t* root = json_object();
if (!root) {
celix_err_push("Failed to create json object");
return ENOMEM;
}
if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE)) {
//no encoding flags set, default to flat
encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT_STYLE;
}
CELIX_PROPERTIES_ITERATE(properties, iter) {
celix_status_t status;
if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) {
status = celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, encodeFlags);
} else {
assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE);
status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags);
}
if (status != CELIX_SUCCESS) {
return status;
}
}
size_t jsonFlags = JSON_COMPACT;
if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) {
jsonFlags = JSON_INDENT(2);
}
int rc = json_dumpf(root, stream, jsonFlags);
if (rc != 0) {
celix_err_push("Failed to dump json object to stream.");
return CELIX_FILE_IO_EXCEPTION;
}
return CELIX_SUCCESS;
}
celix_status_t celix_properties_save(const celix_properties_t* properties, const char* filename, int encodeFlags) {
FILE* stream = fopen(filename, "w");
if (!stream) {
celix_err_pushf("Failed to open file %s.", filename);
return CELIX_FILE_IO_EXCEPTION;
}
celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags);
int rc = fclose(stream);
if (rc != 0) {
celix_err_pushf("Failed to close file %s: %s", filename, strerror(errno));
return CELIX_FILE_IO_EXCEPTION;
}
return status;
}
celix_status_t celix_properties_saveToString(const celix_properties_t* properties, int encodeFlags, char** out) {
*out = NULL;
celix_autofree char* buffer = NULL;
size_t size = 0;
FILE* stream = open_memstream(&buffer, &size);
if (!stream) {
celix_err_push("Failed to open memstream.");
return ENOMEM;
}
celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags);
(void)fclose(stream);
if (!buffer || status != CELIX_SUCCESS) {
if (!buffer || status == CELIX_FILE_IO_EXCEPTION) {
return ENOMEM; // Using memstream as stream, return ENOMEM instead of CELIX_FILE_IO_EXCEPTION
}
return status;
}
*out = celix_steal_ptr(buffer);
return CELIX_SUCCESS;
}
static celix_status_t
celix_properties_decodeArray(celix_properties_t* props, const char* key, const json_t* jsonArray, int flags) {
int decodeArrayFlags = 0;
if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) {
decodeArrayFlags |= CELIX_ARRAY_LIST_DECODE_ERROR_ON_EMPTY_ARRAYS;
}
if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS) {
decodeArrayFlags |= CELIX_ARRAY_LIST_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS;
}
celix_autoptr(celix_array_list_t) array = NULL;
celix_status_t status = celix_arrayList_decodeFromJson(jsonArray, decodeArrayFlags, &array);
if (status != CELIX_SUCCESS) {
celix_err_pushf("Failed to decode array list for key '%s'.", key);
return status;
}
if (array == NULL) {
// ignore empty arrays or mixed type arrays
return CELIX_SUCCESS;
}
return celix_properties_assignArrayList(props, key, celix_steal_ptr(array));
}
static celix_status_t
celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags) {
if (strncmp(key, "", 1) == 0) {
if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) {
celix_err_push("Key cannot be empty.");
return CELIX_ILLEGAL_ARGUMENT;
}
}
if (!json_is_object(jsonValue) && celix_properties_hasKey(props, key) &&
(flags & CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS)) {
celix_err_pushf("Invalid key collision. Key '%s' already exists.", key);
return CELIX_ILLEGAL_ARGUMENT;
}
celix_status_t status = CELIX_SUCCESS;
if (celix_utils_isVersionJsonString(jsonValue)) {
celix_version_t* version;
status = celix_utils_jsonToVersion(jsonValue, &version);
status = CELIX_DO_IF(status, celix_properties_assignVersion(props, key, version));
} else if (json_is_string(jsonValue)) {
status = celix_properties_setString(props, key, json_string_value(jsonValue));
} else if (json_is_integer(jsonValue)) {
status = celix_properties_setLong(props, key, json_integer_value(jsonValue));
} else if (json_is_real(jsonValue)) {
status = celix_properties_setDouble(props, key, json_real_value(jsonValue));
} else if (json_is_boolean(jsonValue)) {
status = celix_properties_setBool(props, key, json_boolean_value(jsonValue));
} else if (json_is_object(jsonValue)) {
const char* fieldName;
json_t* fieldValue;
json_object_foreach(jsonValue, fieldName, fieldValue) {
char buf[64];
char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s%c%s", key, CELIX_PROPERTIES_JSONPATH_SEPARATOR, fieldName);
celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey);
if (!combinedKey) {
celix_err_push("Failed to create sub key.");
return ENOMEM;
}
status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags);
if (status != CELIX_SUCCESS) {
return status;
}
}
return CELIX_SUCCESS;
} else if (json_is_array(jsonValue)) {
status = celix_properties_decodeArray(props, key, jsonValue, flags);
} else if (json_is_null(jsonValue)) {
if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) {
celix_err_pushf("Invalid null value for key '%s'.", key);
return CELIX_ILLEGAL_ARGUMENT;
}
// ignore null values
return CELIX_SUCCESS;
} else {
// LCOV_EXCL_START
celix_err_pushf("Unexpected json value type for key '%s'.", key);
return CELIX_ILLEGAL_ARGUMENT;
// LCOV_EXCL_STOP
}
return status;
}
static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) {
*out = NULL;
if (!json_is_object(obj)) {
celix_err_push("Expected json object.");
return CELIX_ILLEGAL_ARGUMENT;
}
celix_autoptr(celix_properties_t) props = celix_properties_create();
if (!props) {
return ENOMEM;
}
const char* key;
json_t* value;
json_object_foreach(obj, key, value) {
celix_status_t status = celix_properties_decodeValue(props, key, value, flags);
if (status != CELIX_SUCCESS) {
return status;
}
}
*out = celix_steal_ptr(props);
return CELIX_SUCCESS;
}
celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) {
json_error_t jsonError;
size_t jsonFlags = 0;
if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) {
jsonFlags = JSON_REJECT_DUPLICATES;
}
json_auto_t* root = json_loadf(stream, jsonFlags, &jsonError);
if (!root) {
celix_err_pushf("Failed to parse json from %s:%i:%i: %s.",
jsonError.source,
jsonError.line,
jsonError.column,
jsonError.text);
return celix_utils_jsonErrorToStatus(json_error_code(&jsonError));
}
return celix_properties_decodeFromJson(root, decodeFlags, out);
}
celix_status_t celix_properties_load(const char* filename, int decodeFlags, celix_properties_t** out) {
FILE* stream = fopen(filename, "r");
if (!stream) {
celix_err_pushf("Failed to open file %s.", filename);
return CELIX_FILE_IO_EXCEPTION;
}
celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out);
fclose(stream);
return status;
}
celix_status_t celix_properties_loadFromString(const char* input, int decodeFlags, celix_properties_t** out) {
FILE* stream = fmemopen((void*)input, strlen(input), "r");
if (!stream) {
celix_err_push("Failed to open memstream.");
return ENOMEM;
}
celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out);
fclose(stream);
return status;
}