aboutsummaryrefslogtreecommitdiff
path: root/cli/spdxtool/serialize.c
diff options
context:
space:
mode:
Diffstat (limited to 'cli/spdxtool/serialize.c')
-rw-r--r--cli/spdxtool/serialize.c467
1 files changed, 467 insertions, 0 deletions
diff --git a/cli/spdxtool/serialize.c b/cli/spdxtool/serialize.c
new file mode 100644
index 000000000000..69ef3a70a3b1
--- /dev/null
+++ b/cli/spdxtool/serialize.c
@@ -0,0 +1,467 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ *​ Copyright (c) 2025 The FreeBSD Foundation
+ *​
+ *​ Portions of this software were developed by
+ * Tuukka Pasanen <tuukka.pasanen@ilmi.fi> under sponsorship from
+ * the FreeBSD Foundation
+ *​
+ *​ Copyright (C) 2026 Elizabeth Ashford.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "util.h"
+#include "core.h"
+#include "software.h"
+#include "simplelicensing.h"
+#include "serialize.h"
+
+static void
+serialize_escape_string(pkgconf_buffer_t *buffer, const char *s)
+{
+ for (const char *p = s; *p; p++)
+ {
+ switch (*p)
+ {
+ case '\"':
+ pkgconf_buffer_append(buffer, "\\\"");
+ break;
+ case '\\':
+ pkgconf_buffer_append(buffer, "\\\\");
+ break;
+ case '\b':
+ pkgconf_buffer_append(buffer, "\\b");
+ break;
+ case '\f':
+ pkgconf_buffer_append(buffer, "\\f");
+ break;
+ case '\n':
+ pkgconf_buffer_append(buffer, "\\n");
+ break;
+ case '\r':
+ pkgconf_buffer_append(buffer, "\\r");
+ break;
+ case '\t':
+ pkgconf_buffer_append(buffer, "\\t");
+ break;
+ default:
+ if (*p < 0x20)
+ pkgconf_buffer_append_fmt(buffer, "\\u%04x", (unsigned int)*p);
+ else
+ pkgconf_buffer_push_byte(buffer, *p);
+ }
+ }
+}
+
+static inline void
+serialize_add_indent(pkgconf_buffer_t *buffer, unsigned int level)
+{
+ for (; level; level--)
+ pkgconf_buffer_append(buffer, " ");
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_serialize_value_to_buf(pkgconf_buffer_t *buffer, spdxtool_serialize_value_t *value, unsigned int indent)
+ *
+ * Serialize the given JSON to the buffer
+ *
+ * :param pkgconf_buffer_t *buffer: Buffer to add to.
+ * :param spdxtool_serialize_value *value: Value to serialize.
+ * :param unsigned int indent: Indent level
+ * :return: true on success, false on failure
+ */
+bool
+spdxtool_serialize_value_to_buf(pkgconf_buffer_t *buffer, spdxtool_serialize_value_t *value, unsigned int indent)
+{
+ if (!buffer || !value)
+ return false;
+
+ switch(value->type) {
+ case SPDXTOOL_SERIALIZE_TYPE_STRING:
+ pkgconf_buffer_push_byte(buffer, '"');
+ serialize_escape_string(buffer, value->value.s ? value->value.s : "");
+ pkgconf_buffer_push_byte(buffer, '"');
+ break;
+ case SPDXTOOL_SERIALIZE_TYPE_INT:
+ pkgconf_buffer_append_fmt(buffer, "%d", value->value.i);
+ break;
+ case SPDXTOOL_SERIALIZE_TYPE_BOOL:
+ pkgconf_buffer_append(buffer, value->value.b ? "true" : "false");
+ break;
+ case SPDXTOOL_SERIALIZE_TYPE_NULL:
+ pkgconf_buffer_append(buffer, "null");
+ break;
+ case SPDXTOOL_SERIALIZE_TYPE_OBJECT:
+ {
+ pkgconf_node_t *iter;
+ pkgconf_buffer_push_byte(buffer, '{');
+ pkgconf_buffer_push_byte(buffer, '\n');
+
+ PKGCONF_FOREACH_LIST_ENTRY(value->value.o->entries.head, iter)
+ {
+ spdxtool_serialize_object_t *entry = iter->data;
+ serialize_add_indent(buffer, indent + 1);
+ pkgconf_buffer_append_fmt(buffer, "\"%s\": ", entry->key);
+ spdxtool_serialize_value_to_buf(buffer, entry->value, indent + 1);
+ if (iter->next)
+ pkgconf_buffer_push_byte(buffer, ',');
+ pkgconf_buffer_push_byte(buffer, '\n');
+ }
+
+ serialize_add_indent(buffer, indent);
+ pkgconf_buffer_push_byte(buffer, '}');
+ break;
+ }
+ case SPDXTOOL_SERIALIZE_TYPE_ARRAY:
+ {
+ pkgconf_node_t *iter;
+ pkgconf_buffer_push_byte(buffer, '[');
+ pkgconf_buffer_push_byte(buffer, '\n');
+
+ PKGCONF_FOREACH_LIST_ENTRY(value->value.a->items.head, iter)
+ {
+ spdxtool_serialize_value_t *entry = iter->data;
+ serialize_add_indent(buffer, indent + 1);
+ spdxtool_serialize_value_to_buf(buffer, entry, indent + 1);
+ if (iter->next)
+ pkgconf_buffer_push_byte(buffer, ',');
+ pkgconf_buffer_push_byte(buffer, '\n');
+ }
+ serialize_add_indent(buffer, indent);
+ pkgconf_buffer_push_byte(buffer, ']');
+ break;
+ }
+ }
+
+ return true;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_take(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_value_t *value)
+ *
+ * Add a key-value pair to a JSON object list. The key is copied internally.
+ * The object list takes ownership of the value.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string, copied internally.
+ * :param spdxtool_serialize_value_t *value: Value to associate with the key. Ownership transfers to the object list.
+ * :return: The value added, not owned by the caller.
+ */
+spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_take(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_value_t *value)
+{
+ if (!object_list || !value)
+ {
+ spdxtool_serialize_value_free(value);
+ return NULL;
+ }
+
+ pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
+ spdxtool_serialize_object_t *object = calloc(1, sizeof(spdxtool_serialize_object_t));
+ char *keycopy = key ? strdup(key) : strdup("");
+ if (!node || !object || !keycopy)
+ {
+ free(node);
+ free(keycopy);
+ spdxtool_serialize_object_free(object);
+ spdxtool_serialize_value_free(value);
+ return NULL;
+ }
+
+ object->key = keycopy;
+ object->value = value;
+ pkgconf_node_insert_tail(node, object, &object_list->entries);
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_take(spdxtool_serialize_array_t *array, spdxtool_serialize_value_t value)
+ *
+ * Add a value to a JSON array. The array takes ownership of the value.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to add to.
+ * :param spdxtool_serialize_value_t value: Value to append. Ownership transfers to the array.
+ * :return: The value added, not owned by the caller.
+ */
+spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_take(spdxtool_serialize_array_t *array, spdxtool_serialize_value_t *value)
+{
+ if (!array)
+ {
+ // Taking value, so free
+ spdxtool_serialize_value_free(value);
+ return NULL;
+ }
+
+ pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
+ if (!node)
+ {
+ spdxtool_serialize_value_free(value);
+ return NULL;
+ }
+
+ pkgconf_node_insert_tail(node, value, &array->items);
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_object_list_t *spdxtool_serialize_object_list_new(void)
+ *
+ * Allocate and initialize a new empty JSON object list.
+ *
+ * :return: Pointer to a new spdxtool_serialize_object_list_t, or NULL on allocation failure.
+ */
+spdxtool_serialize_object_list_t *
+spdxtool_serialize_object_list_new(void)
+{
+ return calloc(1, sizeof(spdxtool_serialize_object_list_t));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_array_t *spdxtool_serialize_array_new(void)
+ *
+ * Allocate and initialize a new empty JSON array.
+ *
+ * :return: Pointer to a new spdxtool_serialize_array_t, or NULL on allocation failure.
+ */
+spdxtool_serialize_array_t *
+spdxtool_serialize_array_new(void)
+{
+ return calloc(1, sizeof(spdxtool_serialize_array_t));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_serialize_value_free(spdxtool_serialize_value_t *value)
+ *
+ * Free all resources owned by a JSON value. For strings, frees the string.
+ * For objects and arrays, recursively frees all children. The value pointer
+ * itself is not freed as it is assumed to be stack-allocated.
+ *
+ * :param spdxtool_serialize_value_t *value: Value to free. May be NULL.
+ * :return: nothing
+ */
+void
+spdxtool_serialize_value_free(spdxtool_serialize_value_t *value)
+{
+ if (!value)
+ return;
+
+ switch (value->type)
+ {
+ case SPDXTOOL_SERIALIZE_TYPE_STRING:
+ free(value->value.s);
+ break;
+ case SPDXTOOL_SERIALIZE_TYPE_ARRAY:
+ spdxtool_serialize_array_free(value->value.a);
+ break;
+ case SPDXTOOL_SERIALIZE_TYPE_OBJECT:
+ spdxtool_serialize_object_list_free(value->value.o);
+ break;
+ default:
+ // Nothing to do
+ break;
+ }
+
+ free(value);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_serialize_object_free(spdxtool_serialize_object_t *object)
+ *
+ * Free a JSON object entry, including its key string and owned value.
+ * The object pointer itself is not freed by this function.
+ *
+ * :param spdxtool_serialize_object_t *object: Object entry to free. May be NULL.
+ * :return: nothing
+ */
+void
+spdxtool_serialize_object_free(spdxtool_serialize_object_t *object)
+{
+ if (!object)
+ return;
+
+ free(object->key);
+ spdxtool_serialize_value_free(object->value);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_serialize_object_list_free(spdxtool_serialize_object_list_t *object_list)
+ *
+ * Free a JSON object list and all of its entries, including their keys and values.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to free. May be NULL.
+ * :return: nothing
+ */
+void
+spdxtool_serialize_object_list_free(spdxtool_serialize_object_list_t *object_list)
+{
+ if (!object_list)
+ return;
+
+ pkgconf_node_t *iter_next = NULL, *iter = NULL;
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(object_list->entries.head, iter_next, iter)
+ {
+ spdxtool_serialize_object_t *object = iter->data;
+ spdxtool_serialize_object_free(object);
+ free(object);
+ free(iter);
+ }
+
+ free(object_list);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_serialize_array_free(spdxtool_serialize_array_t *array)
+ *
+ * Free a JSON array and all of its elements.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to free. May be NULL.
+ * :return: nothing
+ */
+void
+spdxtool_serialize_array_free(spdxtool_serialize_array_t *array)
+{
+ if (!array)
+ return;
+
+ pkgconf_node_t *iter_next = NULL, *iter = NULL;
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(array->items.head, iter_next, iter)
+ {
+ spdxtool_serialize_value_t *value = iter->data;
+ spdxtool_serialize_value_free(value);
+ free(iter);
+ }
+
+ free(array);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_sbom(pkgconf_client_t *client, spdxtool_core_agent_t *agent, spdxtool_core_creation_info_t *creation, spdxtool_core_spdx_document_t *spdx)
+ *
+ * Serialize a complete SPDX SBOM document to a JSON-LD value tree. Iterates
+ * all SBOMs, packages, relationships, and license expressions registered on
+ * the document. The SpdxDocument object is emitted last to ensure all element
+ * IDs have been populated by prior iteration. This function must be called
+ * after pkgconf_pkg_traverse has completed so that all packages and their
+ * dependencies are registered on spdx.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_agent_t *agent: Agent struct to include in the document.
+ * :param spdxtool_core_creation_info_t *creation: CreationInfo struct to include in the document.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct containing all registered SBOMs, packages, relationships, and licenses.
+ * :return: spdxtool_serialize_value_t * representing the complete JSON-LD document, or a null string value on allocation failure.
+ */
+spdxtool_serialize_value_t *
+spdxtool_serialize_sbom(pkgconf_client_t *client, spdxtool_core_agent_t *agent, spdxtool_core_creation_info_t *creation, spdxtool_core_spdx_document_t *spdx)
+{
+ const char *errstr = "out of memory";
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_array_t *graph = NULL;
+ spdxtool_serialize_object_list_t *root = spdxtool_serialize_object_list_new();
+ if (!root)
+ goto err;
+
+ if (!spdxtool_serialize_object_add_string(root, "@context", "https://spdx.org/rdf/3.0.1/spdx-context.jsonld"))
+ goto err;
+
+ graph = spdxtool_serialize_array_new();
+ if (!graph)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_agent_to_object(client, agent)))
+ goto err;
+
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_creation_info_to_object(client, creation)))
+ goto err;
+
+ pkgconf_node_t *iter = NULL;
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->licenses.head, iter)
+ {
+ spdxtool_simplelicensing_license_expression_t *expression = iter->data;
+ if (!expression)
+ {
+ errstr = "licenses list corrupted";
+ goto err;
+ }
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_simplelicensing_licenseExpression_to_object(client, spdx->creation_info, expression)))
+ goto err;
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->rootElement.head, iter)
+ {
+ spdxtool_software_sbom_t *current_sbom = iter->data;
+ if (!current_sbom)
+ {
+ errstr = "sbom list corrupted";
+ goto err;
+ }
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_software_sbom_to_object(client, current_sbom)))
+ goto err;
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->packages.head, iter)
+ {
+ pkgconf_pkg_t *pkg = iter->data;
+ if (!pkg)
+ {
+ errstr = "pkg list corrupted";
+ goto err;
+ }
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_software_package_to_object(client, pkg, spdx)))
+ goto err;
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->relationships.head, iter)
+ {
+ spdxtool_core_relationship_t *relationship = iter->data;
+ if (!relationship)
+ {
+ errstr = "relationship list corrupted";
+ goto err;
+ }
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_relationship_to_object(client, relationship)))
+ goto err;
+ }
+
+ // SpdxDocument last — spdx->element must be fully populated first
+ if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_spdx_document_to_object(client, spdx)))
+ goto err;
+
+ bool ok = spdxtool_serialize_object_add_array(root, "@graph", graph);
+ graph = NULL;
+ if (!ok)
+ goto err;
+
+ ret = spdxtool_serialize_value_object(root);
+ root = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_serialize_sbom: %s", errstr);
+
+ spdxtool_serialize_object_list_free(root);
+ spdxtool_serialize_array_free(graph);
+ return ret;
+}