/*
* This file is part of Cockpit.
*
* Copyright (C) 2014 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
#include "config.h"
#include "cockpitjson.h"
#include
#include
gboolean
cockpit_json_get_int (JsonObject *object,
const gchar *name,
gint64 defawlt,
gint64 *value)
{
JsonNode *node;
node = json_object_get_member (object, name);
if (!node)
{
if (value)
*value = defawlt;
return TRUE;
}
else if (json_node_get_value_type (node) == G_TYPE_INT64 ||
json_node_get_value_type (node) == G_TYPE_DOUBLE)
{
if (value)
*value = json_node_get_int (node);
return TRUE;
}
else
{
return FALSE;
}
}
gboolean
cockpit_json_get_double (JsonObject *object,
const gchar *name,
gdouble defawlt,
gdouble *value)
{
JsonNode *node;
node = json_object_get_member (object, name);
if (!node)
{
if (value)
*value = defawlt;
return TRUE;
}
else if (json_node_get_value_type (node) == G_TYPE_INT64 ||
json_node_get_value_type (node) == G_TYPE_DOUBLE)
{
if (value)
*value = json_node_get_double (node);
return TRUE;
}
else
{
return FALSE;
}
}
gboolean
cockpit_json_get_bool (JsonObject *object,
const gchar *name,
gboolean defawlt,
gboolean *value)
{
JsonNode *node;
node = json_object_get_member (object, name);
if (!node)
{
if (value)
*value = defawlt;
return TRUE;
}
else if (json_node_get_value_type (node) == G_TYPE_BOOLEAN)
{
if (value)
*value = json_node_get_boolean (node);
return TRUE;
}
else
{
return FALSE;
}
}
gboolean
cockpit_json_get_string (JsonObject *options,
const gchar *name,
const gchar *defawlt,
const gchar **value)
{
JsonNode *node;
node = json_object_get_member (options, name);
if (!node)
{
if (value)
*value = defawlt;
return TRUE;
}
else if (json_node_get_value_type (node) == G_TYPE_STRING)
{
if (value)
*value = json_node_get_string (node);
return TRUE;
}
else
{
return FALSE;
}
}
gboolean
cockpit_json_get_array (JsonObject *options,
const gchar *name,
JsonArray *defawlt,
JsonArray **value)
{
JsonNode *node;
node = json_object_get_member (options, name);
if (!node)
{
if (value)
*value = defawlt;
return TRUE;
}
else if (json_node_get_node_type (node) == JSON_NODE_ARRAY)
{
if (value)
*value = json_node_get_array (node);
return TRUE;
}
else
{
return FALSE;
}
}
gboolean
cockpit_json_get_object (JsonObject *options,
const gchar *member,
JsonObject *defawlt,
JsonObject **value)
{
JsonNode *node;
node = json_object_get_member (options, member);
if (!node)
{
if (value)
*value = defawlt;
return TRUE;
}
else if (json_node_get_node_type (node) == JSON_NODE_OBJECT)
{
if (value)
*value = json_node_get_object (node);
return TRUE;
}
else
{
return FALSE;
}
}
/**
* cockpit_json_get_strv:
* @options: the json object
* @member: the member name
* @defawlt: defawlt value
* @value: returned value
*
* Gets a string array member from a JSON object. Validates
* that the member is an array, and that all elements in the
* array are strings. If these fail, then will return %FALSE.
*
* If @member does not exist in @options, returns the values
* in @defawlt.
*
* The returned value in @value should be freed with g_free()
* but the actual strings are owned by the JSON object.
*
* Returns: %FALSE if invalid member, %TRUE if valid or missing.
*/
gboolean
cockpit_json_get_strv (JsonObject *options,
const gchar *member,
const gchar **defawlt,
gchar ***value)
{
gboolean valid = FALSE;
JsonArray *array;
JsonNode *node;
guint length, i;
gchar **val = NULL;
node = json_object_get_member (options, member);
if (!node)
{
if (defawlt)
val = g_memdup (defawlt, sizeof (gchar *) * (g_strv_length ((gchar **)defawlt) + 1));
valid = TRUE;
}
else if (json_node_get_node_type (node) == JSON_NODE_ARRAY)
{
valid = TRUE;
array = json_node_get_array (node);
length = json_array_get_length (array);
val = g_new (gchar *, length + 1);
for (i = 0; i < length; i++)
{
node = json_array_get_element (array, i);
if (json_node_get_value_type (node) == G_TYPE_STRING)
{
val[i] = (gchar *)json_node_get_string (node);
}
else
{
valid = FALSE;
break;
}
}
val[length] = NULL;
}
if (valid && value)
*value = val;
else
g_free (val);
return valid;
}
gboolean
cockpit_json_get_null (JsonObject *object,
const gchar *member,
gboolean *present)
{
JsonNode *node;
node = json_object_get_member (object, member);
if (!node)
{
if (present)
*present = FALSE;
return TRUE;
}
else if (json_node_get_node_type (node) == JSON_NODE_NULL)
{
if (present)
*present = TRUE;
return TRUE;
}
else
{
return FALSE;
}
}
gboolean
cockpit_json_equal_object (JsonObject *previous,
JsonObject *current)
{
const gchar *name = NULL;
gboolean ret = TRUE;
GList *names;
GList *l;
names = json_object_get_members (previous);
names = g_list_concat (names, json_object_get_members (current));
names = g_list_sort (names, (GCompareFunc)strcmp);
for (l = names; l != NULL; l = g_list_next (l))
{
if (name && g_str_equal (name, l->data))
continue;
name = l->data;
if (!cockpit_json_equal (json_object_get_member (previous, name),
json_object_get_member (current, name)))
{
ret = FALSE;
break;
}
}
g_list_free (names);
return ret;
}
static gboolean
cockpit_json_equal_array (JsonArray *previous,
JsonArray *current)
{
guint len_previous;
guint len_current;
guint i;
len_previous = json_array_get_length (previous);
len_current = json_array_get_length (current);
if (len_previous != len_current)
return FALSE;
/* Look for something that has changed */
for (i = 0; i < len_previous; i++)
{
if (!cockpit_json_equal (json_array_get_element (previous, i),
json_array_get_element (current, i)))
return FALSE;
}
return TRUE;
}
/**
* cockpit_json_equal:
* @previous: first JSON thing or %NULL
* @current: second JSON thing or %NULL
*
* Compares whether two JSON nodes are equal or not. Accepts
* %NULL for either parameter, and if both are %NULL is equal.
*
* The keys of objects do not have to be in the same order.
*
* If nodes have different types or value types then equality
* is FALSE.
*
* Returns: whether equal or not
*/
gboolean
cockpit_json_equal (JsonNode *previous,
JsonNode *current)
{
JsonNodeType type = 0;
GType gtype = 0;
if (previous == current)
return TRUE;
if (!previous || !current)
return FALSE;
type = json_node_get_node_type (previous);
if (type != json_node_get_node_type (current))
return FALSE;
if (type == JSON_NODE_VALUE)
{
gtype = json_node_get_value_type (previous);
if (gtype != json_node_get_value_type (current))
return FALSE;
}
/* Now compare values */
switch (type)
{
case JSON_NODE_OBJECT:
return cockpit_json_equal_object (json_node_get_object (previous),
json_node_get_object (current));
case JSON_NODE_ARRAY:
return cockpit_json_equal_array (json_node_get_array (previous),
json_node_get_array (current));
case JSON_NODE_NULL:
return TRUE;
case JSON_NODE_VALUE:
if (gtype == G_TYPE_INT64)
return json_node_get_int (previous) == json_node_get_int (current);
else if (gtype == G_TYPE_DOUBLE)
return json_node_get_double (previous) == json_node_get_double (current);
else if (gtype == G_TYPE_BOOLEAN)
return json_node_get_boolean (previous) == json_node_get_boolean (current);
else if (gtype == G_TYPE_STRING)
return g_strcmp0 (json_node_get_string (previous), json_node_get_string (current)) == 0;
else
return TRUE;
default:
return FALSE;
}
}
/**
* cockpit_json_override:
* @target: a JSON object
* @override: a JSON object to override from
*
* Override the values in @target with the members in @override.
* Any members of @override are set on @target. If both contain
* objects for a given member, then these are overridden in turn.
*
* Any members that are set to null in the @override will be
* removed from the @target.
*/
void
cockpit_json_patch (JsonObject *target,
JsonObject *override)
{
GList *l, *members;
JsonNode *node, *other;
members = json_object_get_members (override);
for (l = members; l != NULL; l = g_list_next (l))
{
node = json_object_get_member (override, l->data);
if (JSON_NODE_HOLDS_NULL (node))
{
json_object_remove_member (target, l->data);
continue;
}
else if (JSON_NODE_HOLDS_OBJECT (node))
{
other = json_object_get_member (target, l->data);
if (other && JSON_NODE_HOLDS_OBJECT (other))
{
cockpit_json_patch (json_node_get_object (other),
json_node_get_object (node));
continue;
}
}
json_object_set_member (target, l->data, json_node_copy (node));
}
g_list_free (members);
}
/**
* cockpit_json_int_hash:
* @v: pointer to a gint64
*
* Hash a pointer to a gint64. This is like g_int_hash()
* but for gint64.
*
* Returns: the hash
*/
guint
cockpit_json_int_hash (gconstpointer v)
{
return (guint)*((const guint64 *)v);
}
/**
* cockpit_json_int_equal:
* @v1: pointer to a gint64
* @v2: pointer to a gint64
*
* Compare pointers to a gint64. This is like g_int_equal()
* but for gint64.
*
* Returns: the hash
*/
gboolean
cockpit_json_int_equal (gconstpointer v1,
gconstpointer v2)
{
return *((const guint64 *)v1) == *((const guint64 *)v2);
}
/**
* cockpit_json_parse:
* @data: string data to parse
* @length: length of @data or -1
* @error: optional location to return an error
*
* Parses JSON into a JsonNode.
*
* Returns: (transfer full): the parsed node or %NULL
*/
JsonNode *
cockpit_json_parse (const gchar *data,
gssize length,
GError **error)
{
static GPrivate cached_parser = G_PRIVATE_INIT (g_object_unref);
JsonParser *parser;
JsonNode *root;
JsonNode *ret;
parser = g_private_get (&cached_parser);
if (parser == NULL)
{
parser = json_parser_new ();
g_private_set (&cached_parser, parser);
}
/*
* HACK: Workaround for the fact that json-glib did not utf-8
* validate its data until 0.99.2
*/
#ifdef COCKPIT_JSON_GLIB_NEED_UTF8_VALIDATE
if (!g_utf8_validate (data, length, NULL))
{
GError *local_error = NULL;
g_set_error_literal (&local_error, JSON_PARSER_ERROR,
JSON_PARSER_ERROR_INVALID_DATA,
"JSON data must be UTF-8 encoded");
g_signal_emit_by_name (parser, "error", 0, error);
g_propagate_error (error, local_error);
return NULL;
}
#endif
if (json_parser_load_from_data (parser, data, length, error))
{
root = json_parser_get_root (parser);
if (root == NULL)
{
g_set_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE,
"JSON data was empty");
ret = NULL;
}
else
{
ret = json_node_copy (root);
/*
* HACK: JsonParser doesn't give us a way to clear the parser
* and remove memory sitting around until the next parse, so
* we clear it like this.
*
* https://bugzilla.gnome.org/show_bug.cgi?id=728951
*/
if (JSON_NODE_HOLDS_OBJECT (root))
json_node_take_object (root, json_object_new ());
else if (JSON_NODE_HOLDS_ARRAY (root))
json_node_take_array (root, json_array_new ());
}
}
else
{
ret = NULL;
}
return ret;
}
/**
* cockpit_json_parse_object:
* @data: string data to parse
* @length: length of @data or -1
* @error: optional location to return an error
*
* Parses JSON GBytes into a JsonObject. This is a helper function
* combining cockpit_json_parse(), json_node_get_type() and
* json_node_get_object().
*
* Returns: (transfer full): the parsed object or %NULL
*/
JsonObject *
cockpit_json_parse_object (const gchar *data,
gssize length,
GError **error)
{
JsonNode *node;
JsonObject *object;
node = cockpit_json_parse (data, length, error);
if (!node)
return NULL;
if (json_node_get_node_type (node) != JSON_NODE_OBJECT)
{
object = NULL;
g_set_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_UNKNOWN, "Not a JSON object");
}
else
{
object = json_node_dup_object (node);
}
json_node_free (node);
return object;
}
/**
* cockpit_json_parse_bytes:
* @data: data to parse
* @error: optional location to return an error
*
* Parses JSON GBytes into a JsonObject. This is a helper function
* combining cockpit_json_parse(), json_node_get_type() and
* json_node_get_object().
*
* Returns: (transfer full): the parsed object or %NULL
*/
JsonObject *
cockpit_json_parse_bytes (GBytes *data,
GError **error)
{
gsize length = g_bytes_get_size (data);
if (length == 0)
{
g_set_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE,
"JSON data was empty");
return NULL;
}
return cockpit_json_parse_object (g_bytes_get_data (data, NULL), length, error);
}
/**
* cockpit_json_write_bytes:
* @object: object to write
*
* Encode a JsonObject to a GBytes.
*
* Returns: (transfer full): the encoded data
*/
GBytes *
cockpit_json_write_bytes (JsonObject *object)
{
gchar *data;
gsize length;
data = cockpit_json_write_object (object, &length);
return g_bytes_new_take (data, length);
}
/**
* cockpit_json_write_object:
* @object: object to write
* @length: optionally a location to return the length
*
* Encode a JsonObject to a string.
*
* Returns: (transfer full): the encoded data
*/
gchar *
cockpit_json_write_object (JsonObject *object,
gsize *length)
{
JsonNode *node;
gchar *ret;
node = json_node_new (JSON_NODE_OBJECT);
json_node_set_object (node, object);
ret = cockpit_json_write (node, length);
json_node_free (node);
return ret;
}
/*
* HACK: JsonGenerator is completely borked, so we've copied it\
* here until we can rely on a fixed version.
*
* https://bugzilla.gnome.org/show_bug.cgi?id=727593
*/
static gchar *dump_value (const gchar *name,
JsonNode *node,
gsize *length);
static gchar *dump_array (const gchar *name,
JsonArray *array,
gsize *length);
static gchar *dump_object (const gchar *name,
JsonObject *object,
gsize *length);
static gchar *
json_strescape (const gchar *str)
{
const gchar *p;
const gchar *end;
GString *output;
gsize len;
len = strlen (str);
end = str + len;
output = g_string_sized_new (len);
for (p = str; p < end; p++)
{
if (*p == '\\' || *p == '"')
{
g_string_append_c (output, '\\');
g_string_append_c (output, *p);
}
else if ((*p > 0 && *p < 0x1f) || *p == 0x7f)
{
switch (*p)
{
case '\b':
g_string_append (output, "\\b");
break;
case '\f':
g_string_append (output, "\\f");
break;
case '\n':
g_string_append (output, "\\n");
break;
case '\r':
g_string_append (output, "\\r");
break;
case '\t':
g_string_append (output, "\\t");
break;
default:
g_string_append_printf (output, "\\u00%02x", (guint)*p);
break;
}
}
else
{
g_string_append_c (output, *p);
}
}
return g_string_free (output, FALSE);
}
static gchar *
dump_value (const gchar *name,
JsonNode *node,
gsize *length)
{
GString *buffer;
GType type;
buffer = g_string_new ("");
if (name)
g_string_append_printf (buffer, "\"%s\":", name);
type = json_node_get_value_type (node);
if (type == G_TYPE_INT64)
{
g_string_append_printf (buffer, "%" G_GINT64_FORMAT, json_node_get_int (node));
}
else if (type == G_TYPE_DOUBLE)
{
gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
gdouble d = json_node_get_double (node);
if (fpclassify (d) == FP_NAN || fpclassify (d) == FP_INFINITE)
{
g_string_append (buffer, "null");
}
else
{
g_string_append (buffer, g_ascii_dtostr (buf, sizeof (buf), d));
}
}
else if (type == G_TYPE_BOOLEAN)
{
g_string_append (buffer, json_node_get_boolean (node) ? "true" : "false");
}
else if (type == G_TYPE_STRING)
{
gchar *tmp;
tmp = json_strescape (json_node_get_string (node));
g_string_append_c (buffer, '"');
g_string_append (buffer, tmp);
g_string_append_c (buffer, '"');
g_free (tmp);
}
else
{
g_return_val_if_reached (NULL);
}
if (length)
*length = buffer->len;
return g_string_free (buffer, FALSE);
}
static gchar *
dump_array (const gchar *name,
JsonArray *array,
gsize *length)
{
guint array_len = json_array_get_length (array);
guint i;
GString *buffer;
buffer = g_string_new ("");
if (name)
g_string_append_printf (buffer, "\"%s\":", name);
g_string_append_c (buffer, '[');
for (i = 0; i < array_len; i++)
{
JsonNode *cur = json_array_get_element (array, i);
gchar *value;
switch (JSON_NODE_TYPE (cur))
{
case JSON_NODE_NULL:
g_string_append (buffer, "null");
break;
case JSON_NODE_VALUE:
value = dump_value (NULL, cur, NULL);
g_string_append (buffer, value);
g_free (value);
break;
case JSON_NODE_ARRAY:
value = dump_array (NULL, json_node_get_array (cur), NULL);
g_string_append (buffer, value);
g_free (value);
break;
case JSON_NODE_OBJECT:
value = dump_object (NULL, json_node_get_object (cur), NULL);
g_string_append (buffer, value);
g_free (value);
break;
}
if ((i + 1) != array_len)
g_string_append_c (buffer, ',');
}
g_string_append_c (buffer, ']');
if (length)
*length = buffer->len;
return g_string_free (buffer, FALSE);
}
static gchar *
dump_object (const gchar *name,
JsonObject *object,
gsize *length)
{
GList *members, *l;
GString *buffer;
buffer = g_string_new ("");
if (name)
g_string_append_printf (buffer, "\"%s\":", name);
g_string_append_c (buffer, '{');
members = json_object_get_members (object);
for (l = members; l != NULL; l = l->next)
{
const gchar *member_name = l->data;
gchar *escaped_name = json_strescape (member_name);
JsonNode *cur = json_object_get_member (object, member_name);
gchar *value;
switch (JSON_NODE_TYPE (cur))
{
case JSON_NODE_NULL:
g_string_append_printf (buffer, "\"%s\":null", escaped_name);
break;
case JSON_NODE_VALUE:
value = dump_value (escaped_name, cur, NULL);
g_string_append (buffer, value);
g_free (value);
break;
case JSON_NODE_ARRAY:
value = dump_array (escaped_name, json_node_get_array (cur), NULL);
g_string_append (buffer, value);
g_free (value);
break;
case JSON_NODE_OBJECT:
value = dump_object (escaped_name, json_node_get_object (cur), NULL);
g_string_append (buffer, value);
g_free (value);
break;
}
if (l->next != NULL)
g_string_append_c (buffer, ',');
g_free (escaped_name);
}
g_list_free (members);
g_string_append_c (buffer, '}');
if (length)
*length = buffer->len;
return g_string_free (buffer, FALSE);
}
/**
* cockpit_json_write:
* @node: the node to encode
* @length: optional place to return length
*
* Encode a JsonNode to a string.
*
* Returns: (transfer full): the encoded string
*/
gchar *
cockpit_json_write (JsonNode *node,
gsize *length)
{
gchar *retval = NULL;
if (!node)
{
if (length)
*length = 0;
return NULL;
}
switch (JSON_NODE_TYPE (node))
{
case JSON_NODE_ARRAY:
retval = dump_array (NULL, json_node_get_array (node), length);
break;
case JSON_NODE_OBJECT:
retval = dump_object (NULL, json_node_get_object (node), length);
break;
case JSON_NODE_NULL:
retval = g_strdup ("null");
if (length)
*length = 4;
break;
case JSON_NODE_VALUE:
retval = dump_value (NULL, node, length);
break;
}
return retval;
}
JsonObject *
cockpit_json_from_hash_table (GHashTable *hash_table,
const gchar **fields)
{
JsonObject *block = NULL;
gint i;
const gchar *value;
if (hash_table)
{
block = json_object_new ();
for (i = 0; fields[i] != NULL; i++)
{
value = g_hash_table_lookup (hash_table, fields[i]);
if (value)
json_object_set_string_member (block, fields[i], value);
else
json_object_set_null_member (block, fields[i]);
}
}
return block;
}
GHashTable *
cockpit_json_to_hash_table (JsonObject *object,
const gchar **fields)
{
gint i;
GHashTable *hash_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
for (i = 0; fields[i] != NULL; i++)
{
const gchar *value;
if (!cockpit_json_get_string (object, fields[i], NULL, &value))
continue;
if (value)
g_hash_table_insert (hash_table, g_strdup (fields[i]), g_strdup (value));
}
return hash_table;
}