summaryrefslogtreecommitdiff
path: root/cli/spdxtool
diff options
context:
space:
mode:
Diffstat (limited to 'cli/spdxtool')
-rw-r--r--cli/spdxtool/core.c729
-rw-r--r--cli/spdxtool/core.h77
-rw-r--r--cli/spdxtool/main.c425
-rw-r--r--cli/spdxtool/serialize.c467
-rw-r--r--cli/spdxtool/serialize.h539
-rw-r--r--cli/spdxtool/simplelicensing.c113
-rw-r--r--cli/spdxtool/simplelicensing.h36
-rw-r--r--cli/spdxtool/software.c447
-rw-r--r--cli/spdxtool/software.h37
-rw-r--r--cli/spdxtool/util.c331
-rw-r--r--cli/spdxtool/util.h130
11 files changed, 3331 insertions, 0 deletions
diff --git a/cli/spdxtool/core.c b/cli/spdxtool/core.c
new file mode 100644
index 000000000000..5cf7f10e81e1
--- /dev/null
+++ b/cli/spdxtool/core.c
@@ -0,0 +1,729 @@
+/*
+ * 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
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "util.h"
+#include "serialize.h"
+#include "core.h"
+#include "software.h"
+#include "simplelicensing.h"
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_core_agent_t *spdxtool_core_agent_new(pkgconf_client_t *client, const char *creation_info_id, const char *name)
+ *
+ * Create new /Core/Agent struct
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param char *creation_info_id: CreationInfo spdxId
+ * :param char *name: Name of agent
+ * :return: NULL if some problem occurs and Agent struct if not
+ */
+spdxtool_core_agent_t *
+spdxtool_core_agent_new(pkgconf_client_t *client, const char *creation_info_id, const char *name)
+{
+ if (!client || !creation_info_id || !name)
+ return NULL;
+
+ spdxtool_core_agent_t *agent = calloc(1, sizeof(spdxtool_core_agent_t));
+ if (!agent)
+ goto err;
+
+ agent->type = "Agent";
+
+ char *spdx_id_name = strdup(name);
+ if (!spdx_id_name)
+ goto err;
+
+ spdxtool_util_string_correction(spdx_id_name);
+
+ agent->spdx_id = spdxtool_util_get_spdx_id_string(client, agent->type, spdx_id_name);
+ free(spdx_id_name);
+
+ agent->creation_info = strdup(creation_info_id);
+ agent->name = strdup(name);
+
+ if (!agent->spdx_id || !agent->creation_info || !agent->name)
+ goto err;
+
+ return agent;
+
+err:
+ pkgconf_error(client, "spdxtool_core_agent_new: out of memory");
+ spdxtool_core_agent_free(agent);
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_core_agent_free(spdxtool_core_agent_t *agent)
+ *
+ * Free /Core/Agent struct
+ *
+ * :param spdxtool_core_agent_t *agent: Agent struct to be freed.
+ * :return: nothing
+ */
+void
+spdxtool_core_agent_free(spdxtool_core_agent_t *agent)
+{
+ if (!agent)
+ return;
+
+ free(agent->creation_info);
+ free(agent->spdx_id);
+ free(agent->name);
+
+ free(agent);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_core_agent_to_object(pkgconf_client_t *client, const spdxtool_core_agent_t *agent)
+ *
+ * Serialize /Core/Agent struct to a JSON value tree.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const spdxtool_core_agent_t *agent: Agent struct to be serialized.
+ * :return: spdxtool_serialize_value_t * representing the Agent object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_core_agent_to_object(pkgconf_client_t *client, const spdxtool_core_agent_t *agent)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", agent->type) &&
+ spdxtool_serialize_object_add_string(object_list, "creationInfo", agent->creation_info) &&
+ spdxtool_serialize_object_add_string(object_list, "spdxId", agent->spdx_id) &&
+ spdxtool_serialize_object_add_string(object_list, "name", agent->name)))
+ {
+ goto err;
+ }
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_core_agent_to_object: out of memory");
+
+ spdxtool_serialize_object_list_free(object_list);
+ return ret;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_core_creation_info_t *spdxtool_core_creation_info_new(pkgconf_client_t *client, const char *agent_id, const char *id, const char *time)
+ *
+ * Create new /Core/CreationInfo struct
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const char *agent_id: Agent spdxId
+ * :param const char *id: Id for creation info
+ * :param const char *time: If NULL current time is used if not then
+ * this time string is used. Time string should be
+ * in ISO8601 format: YYYY-MM-DDTHH:MM:SSZ
+ * :return: NULL if some problem occurs and CreationInfo struct if not
+ */
+spdxtool_core_creation_info_t *
+spdxtool_core_creation_info_new(pkgconf_client_t *client, const char *agent_id, const char *id, const char *time)
+{
+ if (!client || !agent_id || !id)
+ return NULL;
+
+ spdxtool_core_creation_info_t *creation = calloc(1, sizeof(spdxtool_core_creation_info_t));
+ if (!creation)
+ goto err;
+
+ creation->type = "CreationInfo";
+ creation->created_using = "pkgconf spdxtool";
+ creation->id = strdup(id);
+ creation->created = time ? strdup(time) : spdxtool_util_get_current_iso8601_time();
+ creation->created_by = strdup(agent_id);
+ creation->spec_version = strdup(spdxtool_util_get_spdx_version(client));
+
+ if (!creation->id || !creation->created || !creation->created_by || !creation->spec_version)
+ goto err;
+
+ return creation;
+
+err:
+ pkgconf_error(client, "spdxtool_core_creation_info_new: out of memory");
+ spdxtool_core_creation_info_free(creation);
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_core_creation_info_free(spdxtool_core_creation_info_t *creation)
+ *
+ * Free /Core/CreationInfo struct
+ *
+ * :param spdxtool_core_creation_info_t *creation: CreationInfo struct to be freed.
+ * :return: nothing
+ */
+void
+spdxtool_core_creation_info_free(spdxtool_core_creation_info_t *creation)
+{
+ if (!creation)
+ return;
+
+ free(creation->id);
+ free(creation->created);
+ free(creation->created_by);
+ free(creation->spec_version);
+
+ free(creation);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_core_creation_info_to_object(pkgconf_client_t *client, const spdxtool_core_creation_info_t *creation)
+ *
+ * Serialize /Core/CreationInfo struct to a JSON value tree.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const spdxtool_core_creation_info_t *creation: CreationInfo struct to be serialized.
+ * :return: spdxtool_serialize_value_t * representing the CreationInfo object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_core_creation_info_to_object(pkgconf_client_t *client, const spdxtool_core_creation_info_t *creation)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ spdxtool_serialize_array_t *created_by = spdxtool_serialize_array_new();
+ if (!created_by)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_string(created_by, creation->created_by))
+ {
+ spdxtool_serialize_array_free(created_by);
+ goto err;
+ }
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", creation->type) &&
+ spdxtool_serialize_object_add_string(object_list, "@id", creation->id) &&
+ spdxtool_serialize_object_add_string(object_list, "created", creation->created) &&
+ spdxtool_serialize_object_add_array(object_list, "createdBy", created_by) &&
+ spdxtool_serialize_object_add_string(object_list, "specVersion", creation->spec_version)))
+ {
+ goto err;
+ }
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_core_creation_info_to_object: out of memory");
+
+ spdxtool_serialize_object_list_free(object_list);
+ return ret;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_core_creation_info_t *spdxtool_core_creation_info_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *agent)
+ *
+ * Create new /Core/SpdxDocument struct
+ * In SPDX Lite SBOM there can be only one SpdxDocument
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const char *spdx_id: Id of this SpdxDocument
+ * :param const char *creation_id: Id for creation info
+ * :param const char *agent_id: Agent for this document
+ * :return: NULL if some problem occurs and SpdxDocument struct if not
+ */
+spdxtool_core_spdx_document_t *
+spdxtool_core_spdx_document_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *agent_id)
+{
+ if (!client || !spdx_id || !creation_id || !agent_id)
+ return NULL;
+
+ spdxtool_core_spdx_document_t *spdx = calloc(1, sizeof(spdxtool_core_spdx_document_t));
+ if (!spdx)
+ goto err;
+
+ spdx->type = "SpdxDocument";
+ spdx->spdx_id = strdup(spdx_id);
+ spdx->agent = strdup(agent_id);
+ spdx->creation_info = strdup(creation_id);
+
+ if (!spdx->spdx_id || !spdx->agent || !spdx->creation_info)
+ goto err;
+
+ return spdx;
+
+err:
+ pkgconf_error(client, "spdxtool_core_spdx_document_new: out of memory");
+ spdxtool_core_spdx_document_free(spdx);
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_core_spdx_document_free(spdxtool_core_spdx_document_t *spdx)
+ *
+ * Free /Core/SpdxDocument struct
+ *
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct to be freed.
+ * :return: nothing
+ */
+void
+spdxtool_core_spdx_document_free(spdxtool_core_spdx_document_t *spdx)
+{
+ pkgconf_node_t *iter = NULL, *iter_next = NULL;
+
+ if (!spdx)
+ return;
+
+ free(spdx->spdx_id);
+ free(spdx->creation_info);
+ free(spdx->agent);
+
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(spdx->rootElement.head, iter_next, iter)
+ {
+ spdxtool_software_sbom_t *sbom = iter->data;
+ spdxtool_software_sbom_free(sbom);
+ free(iter);
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(spdx->element.head, iter_next, iter)
+ {
+ free(iter->data);
+ free(iter);
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(spdx->licenses.head, iter_next, iter)
+ {
+ spdxtool_simplelicensing_license_expression_t *expression = iter->data;
+ spdxtool_simplelicensing_licenseExpression_free(expression);
+ free(iter);
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(spdx->relationships.head, iter_next, iter)
+ {
+ spdxtool_core_relationship_t *relationship = iter->data;
+ spdxtool_core_relationship_free(relationship);
+ free(iter);
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY_SAFE(spdx->packages.head, iter_next, iter)
+ {
+ free(iter);
+ }
+
+ free(spdx);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_core_spdx_document_to_object(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx)
+ *
+ * Serialize /Core/SpdxDocument struct to a JSON value tree. This function
+ * should be called after all SBOMs and packages have been serialized so that
+ * the document's element and rootElement lists are fully populated.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct to be serialized.
+ * :return: spdxtool_serialize_value_t * representing the SpdxDocument object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_core_spdx_document_to_object(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = NULL;
+ spdxtool_serialize_array_t *root_element_array = NULL;
+ spdxtool_serialize_array_t *element_array = NULL;
+
+ object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ root_element_array = spdxtool_serialize_array_new();
+ if (!root_element_array)
+ goto err;
+
+ pkgconf_node_t *iter = NULL;
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->rootElement.head, iter)
+ {
+ spdxtool_software_sbom_t *sbom = iter->data;
+ if (!spdxtool_serialize_array_add_string(root_element_array, sbom->spdx_id))
+ goto err;
+ }
+
+ element_array = spdxtool_serialize_array_new();
+ if (!element_array)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_string(element_array, spdx->agent))
+ goto err;
+
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->element.head, iter)
+ {
+ char *element_id = iter->data;
+ if (!spdxtool_serialize_array_add_string(element_array, element_id))
+ goto err;
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->rootElement.head, iter)
+ {
+ spdxtool_software_sbom_t *sbom = iter->data;
+ char *pkg_spdx_id = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "spdxId");
+ if (!pkg_spdx_id)
+ goto err;
+
+ bool ok = spdxtool_serialize_array_add_string(element_array, sbom->spdx_id) &&
+ spdxtool_serialize_array_add_string(element_array, pkg_spdx_id);
+ free(pkg_spdx_id);
+
+ if (!ok)
+ goto err;
+ }
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", spdx->type) &&
+ spdxtool_serialize_object_add_string(object_list, "creationInfo", spdx->creation_info) &&
+ spdxtool_serialize_object_add_string(object_list, "spdxId", spdx->spdx_id) &&
+ spdxtool_serialize_object_add_array(object_list, "rootElement", root_element_array) &&
+ spdxtool_serialize_object_add_array(object_list, "element", element_array)))
+ {
+ goto err;
+ }
+
+ root_element_array = NULL;
+ element_array = NULL;
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_core_spdx_document_to_object: out of memory");
+
+ spdxtool_serialize_object_list_free(object_list);
+ spdxtool_serialize_array_free(root_element_array);
+ spdxtool_serialize_array_free(element_array);
+ return ret;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: bool spdxtool_core_spdx_document_is_license(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *license)
+ *
+ * Find out if specific license is already there.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct being used.
+ * :param const char *license: SPDX name of license
+ * :return: true is license is there and false if not
+ */
+bool
+spdxtool_core_spdx_document_is_license(pkgconf_client_t *client, const spdxtool_core_spdx_document_t *spdx, const char *license)
+{
+ pkgconf_node_t *iter = NULL;
+ spdxtool_simplelicensing_license_expression_t *expression = NULL;
+
+ (void) client;
+
+ if (!license || !spdx)
+ {
+ return false;
+ }
+
+ PKGCONF_FOREACH_LIST_ENTRY(spdx->licenses.head, iter)
+ {
+ expression = iter->data;
+ if (!strcmp(expression->license_expression, license))
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: bool spdxtool_core_spdx_document_add_license(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *license)
+ *
+ * Add license to SpdxDocument and make sure that specific license is not already there.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct being used.
+ * :param const char *license: SPDX name of license
+ * :return: true on success, false on failure
+ */
+bool
+spdxtool_core_spdx_document_add_license(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *license)
+{
+ if (!license || !spdx)
+ return false;
+
+ if (spdxtool_core_spdx_document_is_license(client, spdx, license))
+ return true;
+
+ pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
+ if (!node)
+ {
+ pkgconf_error(client, "spdxtool_core_spdx_document_add_license: out of memory");
+ return false;
+ }
+
+ spdxtool_simplelicensing_license_expression_t *expression = spdxtool_simplelicensing_licenseExpression_new(client, license);
+ if (!expression)
+ {
+ free(node);
+ return false;
+ }
+
+ pkgconf_node_insert_tail(node, expression, &spdx->licenses);
+ if (!spdxtool_core_spdx_document_add_element(client, spdx, expression->spdx_id))
+ return false;
+
+ return true;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: bool spdxtool_core_spdx_document_add_element(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *element)
+ *
+ * Add element spdxId to SpdxDocument
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct being used.
+ * :param char *element: spdxId of element
+ * :return: true on success, false on failure
+ */
+bool
+spdxtool_core_spdx_document_add_element(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *element)
+{
+ if (!element || !spdx)
+ return false;
+
+ pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
+ if (!node)
+ {
+ pkgconf_error(client, "spdxtool_core_spdx_document_add_element: out of memory");
+ return false;
+ }
+
+ char *nelement = strdup(element);
+ if (!nelement)
+ {
+ pkgconf_error(client, "spdxtool_core_spdx_document_add_element: out of memory");
+ free(node);
+ return false;
+ }
+
+ pkgconf_node_insert_tail(node, nelement, &spdx->element);
+ return true;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: bool spdxtool_core_spdx_document_add_relationship(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, spdxtool_core_relationship_t *relationship)
+ *
+ * Add relationship rel to SpdxDocument
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct being used.
+ * :param spdxtool_core_relationship_t *relationship: relationship to add.
+ * :return: true on success, false on failure
+ */
+bool
+spdxtool_core_spdx_document_add_relationship(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, spdxtool_core_relationship_t *relationship)
+{
+ if (!client || !spdx || !relationship)
+ return false;
+
+ pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
+ if (!node)
+ {
+ pkgconf_error(client, "spdxtool_core_spdx_document_add_relationship: out of memory");
+ return false;
+ }
+
+ pkgconf_node_insert_tail(node, relationship, &spdx->relationships);
+ return true;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: bool spdxtool_core_spdx_document_add_package(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, pkgconf_pkg_t *pkg)
+ *
+ * Register a package with the SpdxDocument for later serialization. The document
+ * does not take ownership of the package pointer; the package must outlive the
+ * document and will not be freed by spdxtool_core_spdx_document_free.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct to register the package with.
+ * :param pkgconf_pkg_t *pkg: Package to register. Ownership is NOT transferred.
+ * :return: true on success, false on failure
+ */
+bool
+spdxtool_core_spdx_document_add_package(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, pkgconf_pkg_t *pkg)
+{
+ if (!client || !spdx || !pkg)
+ return false;
+
+ pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
+ if (!node)
+ {
+ pkgconf_error(client, "spdxtool_core_spdx_document_add_package: out of memory");
+ return false;
+ }
+
+ pkgconf_node_insert_tail(node, pkg, &spdx->packages);
+ return true;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_core_relationship_t *spdxtool_core_relationship_new(pkgconf_client_t *client, const char *creation_info_id, const char *spdx_id, const char *from, const char *to, const char *relationship_type)
+ *
+ * Create new /Core/Relationship struct
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const char *creation_id: Id for creation info
+ * :param const char *spdx_id: Id of this SpdxDocument
+ * :param const char *from: from spdxId
+ * :param const char *to: to spdxId
+ * :param const char *relationship_type: These can be found on SPDX documentation
+ * :return: NULL if some problem occurs and SpdxDocument struct if not
+ */
+spdxtool_core_relationship_t *
+spdxtool_core_relationship_new(pkgconf_client_t *client, const char *creation_info_id, const char *spdx_id, const char *from, pkgconf_list_t *to, const char *relationship_type)
+{
+ if (!client || !creation_info_id || !spdx_id || !from || !to || !relationship_type)
+ return NULL;
+
+ spdxtool_core_relationship_t *relationship = calloc(1, sizeof(spdxtool_core_relationship_t));
+ if (!relationship)
+ goto err;
+
+ relationship->type = "Relationship";
+ relationship->creation_info = strdup(creation_info_id);
+ relationship->spdx_id = strdup(spdx_id);
+ relationship->from = strdup(from);
+ relationship->to = to;
+ relationship->relationship_type = strdup(relationship_type);
+
+ if (!relationship->creation_info || !relationship->spdx_id || !relationship->from || !relationship->relationship_type)
+ goto err;
+
+ return relationship;
+
+err:
+ pkgconf_error(client, "spdxtool_core_relationship_new: out of memory");
+ spdxtool_core_relationship_free(relationship);
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_core_relationship_free(spdxtool_core_relationship_t *relationship)
+ *
+ * Free /Core/Relationship struct
+ *
+ * :param spdxtool_core_relationship_t *relationship: Relationship struct to be freed.
+ * :return: nothing
+ */
+void
+spdxtool_core_relationship_free(spdxtool_core_relationship_t *relationship)
+{
+ if (!relationship)
+ return;
+
+ free(relationship->spdx_id);
+ free(relationship->creation_info);
+ free(relationship->from);
+ pkgconf_license_free(relationship->to);
+ free(relationship->to);
+ free(relationship->relationship_type);
+
+ free(relationship);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_core_relationship_to_object(pkgconf_client_t *client, const spdxtool_core_relationship_t *relationship)
+ *
+ * Serialize /Core/Relationship struct to a JSON value tree.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const spdxtool_core_relationship_t *relationship: Relationship struct to be serialized.
+ * :return: spdxtool_serialize_value_t * representing the Relationship object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_core_relationship_to_object(pkgconf_client_t *client, const spdxtool_core_relationship_t *relationship)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ spdxtool_serialize_array_t *to = spdxtool_serialize_array_new();
+ if (!to)
+ goto err;
+
+ pkgconf_node_t *node = NULL;
+ PKGCONF_FOREACH_LIST_ENTRY(relationship->to->head, node)
+ {
+ const pkgconf_license_t *license = node->data;
+ if (!spdxtool_serialize_array_add_string(to, license->data))
+ {
+ spdxtool_serialize_array_free(to);
+ goto err;
+ }
+ }
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", relationship->type) &&
+ spdxtool_serialize_object_add_string(object_list, "creationInfo", relationship->creation_info) &&
+ spdxtool_serialize_object_add_string(object_list, "spdxId", relationship->spdx_id) &&
+ spdxtool_serialize_object_add_string(object_list, "from", relationship->from) &&
+ spdxtool_serialize_object_add_array(object_list, "to", to) &&
+ spdxtool_serialize_object_add_string(object_list, "relationshipType", relationship->relationship_type)))
+ {
+ goto err;
+ }
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_core_relationship_to_object: out of memory");
+
+ spdxtool_serialize_object_list_free(object_list);
+ return ret;
+}
diff --git a/cli/spdxtool/core.h b/cli/spdxtool/core.h
new file mode 100644
index 000000000000..0cbb1f4b3d51
--- /dev/null
+++ b/cli/spdxtool/core.h
@@ -0,0 +1,77 @@
+/*
+ * 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
+ */
+
+#ifndef CLI__SPDXTOOL__CORE_H
+#define CLI__SPDXTOOL__CORE_H
+
+#include <stdlib.h>
+#include "util.h"
+#include "serialize.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+spdxtool_core_agent_t *
+spdxtool_core_agent_new(pkgconf_client_t *client, const char *creation_id, const char *name);
+
+void
+spdxtool_core_agent_free(spdxtool_core_agent_t *agent);
+
+spdxtool_serialize_value_t *
+spdxtool_core_agent_to_object(pkgconf_client_t *client, const spdxtool_core_agent_t *agent);
+
+spdxtool_core_creation_info_t *
+spdxtool_core_creation_info_new(pkgconf_client_t *client, const char *agent_id, const char *id, const char *time);
+
+void
+spdxtool_core_creation_info_free(spdxtool_core_creation_info_t *creation);
+
+spdxtool_serialize_value_t *
+spdxtool_core_creation_info_to_object(pkgconf_client_t *client, const spdxtool_core_creation_info_t *creation);
+
+spdxtool_core_spdx_document_t *
+spdxtool_core_spdx_document_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *agent_id);
+
+bool
+spdxtool_core_spdx_document_is_license(pkgconf_client_t *client, const spdxtool_core_spdx_document_t *spdx, const char *license);
+
+bool
+spdxtool_core_spdx_document_add_license(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *license);
+
+bool
+spdxtool_core_spdx_document_add_relationship(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, spdxtool_core_relationship_t *relationship);
+
+bool
+spdxtool_core_spdx_document_add_package(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, pkgconf_pkg_t *pkg);
+
+bool
+spdxtool_core_spdx_document_add_element(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx, const char *element);
+
+void
+spdxtool_core_spdx_document_free(spdxtool_core_spdx_document_t *spdx);
+
+spdxtool_serialize_value_t *
+spdxtool_core_spdx_document_to_object(pkgconf_client_t *client, spdxtool_core_spdx_document_t *spdx);
+
+spdxtool_core_relationship_t *
+spdxtool_core_relationship_new(pkgconf_client_t *client, const char *creation_info_id, const char *spdx_id, const char *from, pkgconf_list_t *to, const char *relationship_type);
+
+void
+spdxtool_core_relationship_free(spdxtool_core_relationship_t *relationship);
+
+spdxtool_serialize_value_t *
+spdxtool_core_relationship_to_object(pkgconf_client_t *client, const spdxtool_core_relationship_t *relationship);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/cli/spdxtool/main.c b/cli/spdxtool/main.c
new file mode 100644
index 000000000000..70cdce2dd891
--- /dev/null
+++ b/cli/spdxtool/main.c
@@ -0,0 +1,425 @@
+/*
+ * 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
+ */
+
+#include "libpkgconf/config.h"
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include "getopt_long.h"
+#include "util.h"
+#include "core.h"
+#include "software.h"
+#include "serialize.h"
+#include "simplelicensing.h"
+
+#define PKG_VERSION (((uint64_t) 1) << 1)
+#define PKG_ABOUT (((uint64_t) 1) << 2)
+#define PKG_HELP (((uint64_t) 1) << 3)
+
+static const char *spdx_version = "3.0.1";
+static const char *bom_license = "CC0-1.0";
+static const char *xsd_any_url_default_base = "https://github.com/pkgconf/pkgconf";
+static const char *xsd_any_uri_default_base = "github.com:pkgconf:pkgconf";
+static int maximum_traverse_depth = 2000;
+
+static pkgconf_client_t pkg_client;
+static uint64_t want_flags;
+static size_t maximum_package_count = 0;
+// static int maximum_traverse_depth = 2000;
+static FILE *error_msgout = NULL;
+static FILE *sbom_out = NULL;
+
+static const char *
+environ_lookup_handler(const pkgconf_client_t *client, const char *key)
+{
+ (void) client;
+
+ return getenv(key);
+}
+
+static bool
+error_handler(const char *msg, const pkgconf_client_t *client, void *data)
+{
+ (void) client;
+ (void) data;
+ if (!pkgconf_output_file_fmt(error_msgout, "%s", msg))
+ {
+ pkgconf_error(client, "spdxtool: Could not output error message: %s", strerror(errno));
+ return false;
+ }
+ return true;
+}
+
+// NOTE: this function is passed to pkgconf_pkg_traverse
+static void
+generate_spdx_package(pkgconf_client_t *client, pkgconf_pkg_t *pkg, void *ptr)
+{
+ spdxtool_core_spdx_document_t *document = (spdxtool_core_spdx_document_t *)ptr;
+ pkgconf_node_t *node = NULL;
+ spdxtool_software_sbom_t *sbom = NULL;
+ char *package_spdx = NULL;
+ char *spdx_id_string = NULL;
+ char sep = spdxtool_util_get_uri_separator(client);
+
+ if (pkg->flags & PKGCONF_PKG_PROPF_VIRTUAL)
+ return;
+
+ spdx_id_string = spdxtool_util_get_spdx_id_string(client, "software_Sbom", pkg->id);
+ if (!spdx_id_string)
+ goto err;
+
+ sbom = spdxtool_software_sbom_new(client, spdx_id_string, document->creation_info, "build");
+ free(spdx_id_string);
+ spdx_id_string = NULL;
+ if (!sbom)
+ goto err;
+
+ sbom->spdx_document = document;
+ sbom->rootElement = pkg;
+
+ package_spdx = spdxtool_util_get_spdx_id_string(client, "Package", pkg->id);
+ if (!package_spdx)
+ goto err;
+
+ pkgconf_tuple_add(client, &pkg->vars, "spdxId", package_spdx, false, 0);
+ free(package_spdx);
+ package_spdx = NULL;
+
+ pkgconf_tuple_add(client, &pkg->vars, "creationInfo", document->creation_info, false, 0);
+ pkgconf_tuple_add(client, &pkg->vars, "agent", document->agent, false, 0);
+
+ if (pkg->license.head != NULL)
+ {
+ pkgconf_buffer_t spdx_id_buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append_fmt(&spdx_id_buf, "%s%chasDeclaredLicense", pkg->id, sep);
+ char *spdx_id_name = pkgconf_buffer_freeze(&spdx_id_buf);
+ if (!spdx_id_name)
+ goto err;
+
+ package_spdx = spdxtool_util_get_spdx_id_string(client, "Relationship", spdx_id_name);
+ free(spdx_id_name);
+ if (!package_spdx)
+ goto err;
+
+ pkgconf_tuple_add(client, &pkg->vars, "hasDeclaredLicense", package_spdx, false, 0);
+ free(package_spdx);
+ package_spdx = NULL;
+
+ pkgconf_buffer_t concluded_buf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_append_fmt(&concluded_buf, "%s%chasConcludedLicense", pkg->id, sep);
+ spdx_id_name = pkgconf_buffer_freeze(&concluded_buf);
+ if (!spdx_id_name)
+ goto err;
+
+ package_spdx = spdxtool_util_get_spdx_id_string(client, "Relationship", spdx_id_name);
+ free(spdx_id_name);
+ if (!package_spdx)
+ goto err;
+
+ pkgconf_tuple_add(client, &pkg->vars, "hasConcludedLicense", package_spdx, false, 0);
+ free(package_spdx);
+ package_spdx = NULL;
+
+ PKGCONF_FOREACH_LIST_ENTRY(pkg->license.head, node)
+ {
+ const pkgconf_license_t *license = node->data;
+ if (license->type == PKGCONF_LICENSE_EXPRESSION)
+ {
+ if (!spdxtool_core_spdx_document_add_license(client, document, license->data))
+ goto err;
+ }
+ }
+ }
+
+ node = calloc(1, sizeof(pkgconf_node_t));
+ if (!node)
+ goto err;
+
+ pkgconf_node_insert_tail(node, sbom, &document->rootElement);
+ return;
+
+err:
+ pkgconf_error(client, "generate_spdx_package: failed for %s", pkg->id);
+ free(package_spdx);
+ free(spdx_id_string);
+ spdxtool_software_sbom_free(sbom);
+}
+
+static bool
+generate_spdx(pkgconf_client_t *client, pkgconf_pkg_t *world, const char *creation_time, const char *creation_id, const char *agent_name)
+{
+ const char *agent_name_string = agent_name ? agent_name : "Default";
+ const char *creation_id_string = creation_id ? creation_id : "_:creationinfo_1";
+
+ spdxtool_core_agent_t *agent = spdxtool_core_agent_new(client, creation_id_string, agent_name_string);
+ if (!agent)
+ {
+ pkgconf_error(client, "Could not create agent struct");
+ return false;
+ }
+
+ spdxtool_core_creation_info_t *creation = spdxtool_core_creation_info_new(client, agent->spdx_id, creation_id_string, creation_time);
+ if (!creation)
+ {
+ pkgconf_error(client, "Could not create creation info struct");
+ spdxtool_core_agent_free(agent);
+ return false;
+ }
+
+ char *spdx_id_int = spdxtool_util_get_spdx_id_int(client, "spdxDocument");
+ spdxtool_core_spdx_document_t *document = spdxtool_core_spdx_document_new(client, spdx_id_int, creation_id_string, agent->spdx_id);
+ free(spdx_id_int);
+ if (!document)
+ {
+ pkgconf_error(client, "Could not create document");
+ spdxtool_core_creation_info_free(creation);
+ spdxtool_core_agent_free(agent);
+ return false;
+ }
+
+ int eflag = pkgconf_pkg_traverse(client, world, generate_spdx_package, document, maximum_traverse_depth, 0);
+ if (eflag != PKGCONF_PKG_ERRF_OK)
+ {
+ spdxtool_core_spdx_document_free(document);
+ spdxtool_core_creation_info_free(creation);
+ spdxtool_core_agent_free(agent);
+ return false;
+ }
+
+ spdxtool_serialize_value_t *root = spdxtool_serialize_sbom(client, agent, creation, document);
+ if (!root)
+ {
+ spdxtool_core_spdx_document_free(document);
+ spdxtool_core_creation_info_free(creation);
+ spdxtool_core_agent_free(agent);
+ return false;
+ }
+
+ pkgconf_buffer_t buffer = PKGCONF_BUFFER_INITIALIZER;
+ spdxtool_serialize_value_to_buf(&buffer, root, 0);
+ spdxtool_serialize_value_free(root);
+
+ bool ret = pkgconf_output_file_fmt(sbom_out, "%s\n", pkgconf_buffer_str(&buffer));
+ pkgconf_buffer_finalize(&buffer);
+
+ spdxtool_core_spdx_document_free(document);
+ spdxtool_core_creation_info_free(creation);
+ spdxtool_core_agent_free(agent);
+
+ if (!ret)
+ pkgconf_error(client, "spdxtool: Could not output to file: %s", strerror(errno));
+ return ret;
+}
+
+static int
+version(void)
+{
+ printf("spdxtool %s\n", PACKAGE_VERSION);
+ return EXIT_SUCCESS;
+}
+
+static int
+about(void)
+{
+ printf("spdxtool (%s %s)\n\n", PACKAGE_NAME, PACKAGE_VERSION);
+ printf("SPDX-License-Identifier: BSD-2-Clause\n\n");
+ printf("Copyright (c) 2025 The FreeBSD Foundation\n\n");
+ printf("Portions of this software were developed by\n");
+ printf("Tuukka Pasanen <tuukka.pasanen@ilmi.fi> under sponsorship from\n");
+ printf("the FreeBSD Foundation\n\n");
+ printf("Report bugs at <%s>.\n", PACKAGE_BUGREPORT);
+ return EXIT_SUCCESS;
+}
+
+static int
+usage(void)
+{
+ printf("usage: spdxtool [modules]\n");
+
+ printf("\nOptions:\n");
+
+ printf(" --agent-name Set agent name [default: 'Default']\n");
+ printf(" --creation-time Use string as creation time (Should be in ISO8601 format) [default: current time]\n");
+ printf(" --creation-id Use string as creation id [default: '_:creationinfo_1']\n");
+ printf(" --help this message\n");
+ printf(" --about print bomtool version and license to stdout\n");
+ printf(" --version print bomtool version to stdout\n");
+ printf(" --output FILE output SBOM data to file\n");
+ printf(" --spdx-base-id URL Uset string as base of SPDX ids [default: %s]\n", xsd_any_uri_default_base);
+ printf(" --use-uri Use URIs not URLs as SPDX id");
+ printf(" --define-variable=varname=value define variable global 'varname' as 'value'\n");
+
+ return EXIT_SUCCESS;
+}
+
+int
+main(int argc, char *argv[])
+{
+ int ret = EXIT_SUCCESS;
+ pkgconf_list_t pkgq = PKGCONF_LIST_INITIALIZER;
+ unsigned int want_client_flags = PKGCONF_PKG_PKGF_SEARCH_PRIVATE;
+ pkgconf_cross_personality_t *personality = pkgconf_cross_personality_default();
+ char *creation_time = NULL;
+ char *creation_id = NULL;
+ char *agent_name = NULL;
+ char world_id[] = "virtual:world";
+ char world_realname[] = "virtual world package";
+ const char *spdx_id_base = xsd_any_url_default_base;
+ bool colon_sep = false;
+ pkgconf_pkg_t world =
+ {
+ .id = world_id,
+ .realname = world_realname,
+ .flags = PKGCONF_PKG_PROPF_STATIC | PKGCONF_PKG_PROPF_VIRTUAL,
+ };
+
+ error_msgout = stderr;
+ sbom_out = stdout;
+
+ struct pkg_option options[] =
+ {
+ { "agent-name", required_argument, NULL, 100, },
+ { "creation-time", required_argument, NULL, 101, },
+ { "creation-id", required_argument, NULL, 102, },
+ { "version", no_argument, &want_flags, PKG_VERSION, },
+ { "about", no_argument, &want_flags, PKG_ABOUT, },
+ { "help", no_argument, &want_flags, PKG_HELP, },
+ { "output", required_argument, NULL, 103, },
+ { "spdx-base-id", required_argument, NULL, 104, },
+ { "use-uri", no_argument, NULL, 105, },
+ { "define-variable", required_argument, NULL, 106, },
+ { NULL, 0, NULL, 0 }
+ };
+
+ while ((ret = pkg_getopt_long_only(argc, argv, "", options, NULL)) != -1)
+ {
+ switch (ret)
+ {
+ case 100:
+ agent_name = pkg_optarg;
+ break;
+ case 101:
+ creation_time = pkg_optarg;
+ break;
+ case 102:
+ creation_id = pkg_optarg;
+ break;
+ case 103:
+ sbom_out = fopen(pkg_optarg, "w");
+ if (sbom_out == NULL)
+ {
+ pkgconf_output_file_fmt(stderr, "unable to open %s: %s\n", pkg_optarg, strerror(errno));
+ return EXIT_FAILURE;
+ }
+ break;
+ case 104:
+ spdx_id_base = pkg_optarg;
+ break;
+ case 105:
+ // If SPDX id base have not been altered use default
+ if (!strcmp(spdx_id_base, xsd_any_url_default_base))
+ spdx_id_base = xsd_any_uri_default_base;
+ colon_sep = true;
+ break;
+ case 106:
+ pkgconf_tuple_define_global(&pkg_client, pkg_optarg);
+ break;
+ case '?':
+ case ':':
+ return EXIT_FAILURE;
+ default:
+ break;
+ }
+ }
+
+ pkgconf_client_init(&pkg_client, error_handler, NULL, personality, NULL, environ_lookup_handler);
+
+ /* we have determined what features we want most likely. in some cases, we override later. */
+ pkgconf_client_set_flags(&pkg_client, want_client_flags);
+
+ /* at this point, want_client_flags should be set, so build the dir list */
+ pkgconf_client_dir_list_build(&pkg_client, personality);
+
+
+ if ((want_flags & PKG_ABOUT) == PKG_ABOUT)
+ return about();
+
+ if ((want_flags & PKG_VERSION) == PKG_VERSION)
+ return version();
+
+ if ((want_flags & PKG_HELP) == PKG_HELP)
+ return usage();
+
+ while (1)
+ {
+ const char *package = argv[pkg_optind];
+
+ if (package == NULL)
+ break;
+
+ /* check if there is a limit to the number of packages allowed to be included, if so and we have hit
+ * the limit, stop adding packages to the queue.
+ */
+ if (maximum_package_count > 0 && pkgq.length > maximum_package_count)
+ break;
+
+ while (isspace((unsigned char)package[0]))
+ package++;
+
+ /* skip empty packages */
+ if (package[0] == '\0')
+ {
+ pkg_optind++;
+ continue;
+ }
+
+ if (argv[pkg_optind + 1] == NULL || !PKGCONF_IS_OPERATOR_CHAR(*(argv[pkg_optind + 1])))
+ {
+ pkgconf_queue_push(&pkgq, package);
+ pkg_optind++;
+ }
+ else
+ {
+ char packagebuf[PKGCONF_BUFSIZE];
+
+ snprintf(packagebuf, sizeof packagebuf, "%s %s %s", package, argv[pkg_optind + 1], argv[pkg_optind + 2]);
+ pkg_optind += 3;
+
+ pkgconf_queue_push(&pkgq, packagebuf);
+ }
+ }
+
+ if (!pkgconf_queue_solve(&pkg_client, &pkgq, &world, maximum_traverse_depth))
+ {
+ ret = EXIT_FAILURE;
+ goto out;
+ }
+
+ spdxtool_util_set_uri_root(&pkg_client, spdx_id_base);
+ spdxtool_util_set_uri_separator_colon(&pkg_client, colon_sep);
+ spdxtool_util_set_spdx_license(&pkg_client, bom_license);
+ spdxtool_util_set_spdx_version(&pkg_client, spdx_version);
+
+ if (!generate_spdx(&pkg_client, &world, creation_time, creation_id, agent_name))
+ {
+ ret = EXIT_FAILURE;
+ goto out;
+ }
+
+ ret = EXIT_SUCCESS;
+
+out:
+ pkgconf_solution_free(&pkg_client, &world);
+ pkgconf_queue_free(&pkgq);
+ pkgconf_cross_personality_deinit(personality);
+ pkgconf_client_deinit(&pkg_client);
+
+ return ret;
+}
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;
+}
diff --git a/cli/spdxtool/serialize.h b/cli/spdxtool/serialize.h
new file mode 100644
index 000000000000..e04b7f38b741
--- /dev/null
+++ b/cli/spdxtool/serialize.h
@@ -0,0 +1,539 @@
+/*
+ * 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"
+
+#ifndef CLI__SPDXTOOL__SERIALIZE_H
+#define CLI__SPDXTOOL__SERIALIZE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum spdxtool_serialize_type_
+{
+ SPDXTOOL_SERIALIZE_TYPE_STRING, // JSON string type
+ SPDXTOOL_SERIALIZE_TYPE_INT, // JSON number type (int)
+ SPDXTOOL_SERIALIZE_TYPE_BOOL, // JSON bool type
+ SPDXTOOL_SERIALIZE_TYPE_NULL, // JSON null type
+ SPDXTOOL_SERIALIZE_TYPE_OBJECT, // JSON object type
+ SPDXTOOL_SERIALIZE_TYPE_ARRAY // JSON array type
+} spdxtool_serialize_type_t;
+
+typedef struct spdxtool_serialize_value_ {
+ spdxtool_serialize_type_t type;
+ union {
+ char *s;
+ int i;
+ bool b;
+ struct spdxtool_serialize_object_list_ *o;
+ struct spdxtool_serialize_array_ *a;
+ } value;
+} spdxtool_serialize_value_t;
+
+typedef struct spdxtool_serialize_object_ {
+ char *key;
+ spdxtool_serialize_value_t *value;
+} spdxtool_serialize_object_t;
+
+typedef struct spdxtool_serialize_object_list_ {
+ pkgconf_list_t entries;
+} spdxtool_serialize_object_list_t;
+
+typedef struct spdxtool_serialize_array_ {
+ pkgconf_list_t items;
+} spdxtool_serialize_array_t;
+
+bool
+spdxtool_serialize_value_to_buf(pkgconf_buffer_t *buffer, spdxtool_serialize_value_t *value, unsigned int indent);
+
+spdxtool_serialize_value_t *
+spdxtool_serialize_value_dup(const spdxtool_serialize_value_t *value);
+
+spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_take(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_value_t* value);
+
+spdxtool_serialize_object_list_t *
+spdxtool_serialize_object_list_new(void);
+
+spdxtool_serialize_array_t *
+spdxtool_serialize_array_new(void);
+
+spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_take(spdxtool_serialize_array_t *array, spdxtool_serialize_value_t* value);
+
+void
+spdxtool_serialize_value_free(spdxtool_serialize_value_t *value);
+
+void
+spdxtool_serialize_object_list_free(spdxtool_serialize_object_list_t *object_list);
+
+void
+spdxtool_serialize_object_free(spdxtool_serialize_object_t *object);
+
+void
+spdxtool_serialize_array_free(spdxtool_serialize_array_t *array);
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t * spdxtool_serialize_value_string(const char *s)
+ *
+ * Construct a JSON string value. The string is copied internally.
+ * If this return value is not stolen, it must be freed with spdxtool_serialize_value_free().
+ *
+ * :param const char *s: String to copy. May be NULL, in which case the value holds NULL.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_STRING.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_value_string(const char *s)
+{
+ if (!s)
+ return NULL;
+
+ char *sv = strdup(s);
+ if (!sv)
+ return NULL;
+
+ spdxtool_serialize_value_t *value = calloc(1, sizeof(spdxtool_serialize_value_t));
+ if (!value)
+ {
+ free(sv);
+ return NULL;
+ }
+
+ value->type = SPDXTOOL_SERIALIZE_TYPE_STRING;
+ value->value.s = sv;
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_value_int(int d)
+ *
+ * Construct a JSON integer value.
+ * If this return value is not stolen, it must be freed with spdxtool_serialize_value_free().
+ *
+ * :param int d: int value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_INT.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_value_int(int i)
+{
+ spdxtool_serialize_value_t *value = calloc(1, sizeof(spdxtool_serialize_value_t));
+ if (!value)
+ return NULL;
+
+ value->type = SPDXTOOL_SERIALIZE_TYPE_INT;
+ value->value.i = i;
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_value_bool(bool b)
+ *
+ * Construct a JSON boolean value.
+ * If this return value is not stolen, it must be freed with spdxtool_serialize_value_free().
+ *
+ * :param bool b: Boolean value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_BOOL.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_value_bool(bool b)
+{
+ spdxtool_serialize_value_t *value = calloc(1, sizeof(spdxtool_serialize_value_t));
+ if (!value)
+ return NULL;
+
+ value->type = SPDXTOOL_SERIALIZE_TYPE_BOOL;
+ value->value.b = b;
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_value_null(void)
+ *
+ * Construct a JSON null value.
+ * If this return value is not stolen, it must be freed with spdxtool_serialize_value_free().
+ *
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_NULL.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_value_null(void)
+{
+ spdxtool_serialize_value_t *value = calloc(1, sizeof(spdxtool_serialize_value_t));
+ if (!value)
+ return NULL;
+
+ value->type = SPDXTOOL_SERIALIZE_TYPE_NULL;
+ return value;
+}
+
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_value_object(spdxtool_serialize_object_list_t *object_list)
+ *
+ * Construct a JSON object value wrapping an existing object list.
+ * The returned value takes ownership of the object list.
+ * If this return value is not stolen, it must be freed with spdxtool_serialize_value_free().
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to wrap. Ownership transfers to the returned value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_OBJECT.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_value_object(spdxtool_serialize_object_list_t *object_list)
+{
+ spdxtool_serialize_value_t *value = calloc(1, sizeof(spdxtool_serialize_value_t));
+ if (!value)
+ return NULL;
+
+ value->type = SPDXTOOL_SERIALIZE_TYPE_OBJECT;
+ value->value.o = object_list;
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_value_array(spdxtool_serialize_array_t *array)
+ *
+ * Construct a JSON array value wrapping an existing array.
+ * The returned value takes ownership of the array.
+ * If this return value is not stolen, it must be freed with spdxtool_serialize_value_free().
+ *
+ * :param spdxtool_serialize_array_t *array: Array to wrap. Ownership transfers to the returned value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_ARRAY.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_value_array(spdxtool_serialize_array_t *array)
+{
+ spdxtool_serialize_value_t *value = calloc(1, sizeof(spdxtool_serialize_value_t));
+ if (!value)
+ return NULL;
+
+ value->type = SPDXTOOL_SERIALIZE_TYPE_ARRAY;
+ value->value.a = array;
+ return value;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_serialize_object_add_string(spdxtool_serialize_object_list_t *object_list, const char *key, const char *value)
+ *
+ * Add a string key-value pair to a JSON object. The string is copied internally.
+ * Unconditionally adds the key even if value is NULL.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :param const char *value: String value to copy.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_STRING, located in the object.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_string(spdxtool_serialize_object_list_t *object_list, const char *key, const char *value)
+{
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_string(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_string_opt(spdxtool_serialize_object_list_t *object_list, const char *key, const char *value)
+ *
+ * Add a string key-value pair to a JSON object only if value is non-NULL.
+ * Use this for optional fields that should be omitted entirely when absent.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :param const char *value: String value to copy, or NULL to skip.
+ * :return: If value is set: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_STRING, located in the object.
+ * This object is not owned by the caller.
+ * If value is not set: NULL.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_string_opt(spdxtool_serialize_object_list_t *object_list, const char *key, const char *value)
+{
+ if (value)
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_string(value));
+
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_int(spdxtool_serialize_object_list_t *object_list, const char *key, int value)
+ *
+ * Add a int key-value pair to a JSON object.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :param int value: Integer value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_INT, located in the object.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_int(spdxtool_serialize_object_list_t *object_list, const char *key, int value)
+{
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_int(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_bool(spdxtool_serialize_object_list_t *object_list, const char *key, bool value)
+ *
+ * Add a boolean key-value pair to a JSON object.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :param bool value: Boolean value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_BOOL, located in the object.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_bool(spdxtool_serialize_object_list_t *object_list, const char *key, bool value)
+{
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_bool(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_null(spdxtool_serialize_object_list_t *object_list, const char *key)
+ *
+ * Add a null key-value pair to a JSON object.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_NULL, located in the object.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_null(spdxtool_serialize_object_list_t *object_list, const char *key)
+{
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_null());
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_object(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_object_list_t *value)
+ *
+ * Add an object key-value pair to a JSON object.
+ * This takes ownership of the object in value unconditionally, freeing on failure.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :param spdxtool_serialize_object_list_t *value: Object value to add.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_OBJECT, located in the object.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_object(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_object_list_t *value)
+{
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_object(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_object_add_array(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_array_t *value)
+ *
+ * Add an array key-value pair to a JSON object.
+ * This takes ownership of the array in value unconditionally, freeing on failure.
+ *
+ * :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
+ * :param const char *key: Key string.
+ * :param spdxtool_serialize_array_t *value: Array value to add.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_ARRAY, located in the object.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_object_add_array(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_array_t *value)
+{
+ return spdxtool_serialize_object_add_take(object_list, key, spdxtool_serialize_value_array(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_string(spdxtool_serialize_array_t *array, const char *value)
+ *
+ * Append a string value to a JSON array. The string is copied internally.
+ * Unconditionally appends even if value is NULL.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to append to.
+ * :param const char *value: String value to copy.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_STRING, located in the array.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_string(spdxtool_serialize_array_t *array, const char *value)
+{
+ return spdxtool_serialize_array_add_take(array, spdxtool_serialize_value_string(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_string_opt(spdxtool_serialize_array_t *a, const char *value)
+ *
+ * Append a string value to a JSON array only if value is non-NULL.
+ * Use this for optional array entries that should be omitted when absent.
+ *
+ * :param spdxtool_serialize_array_t *a: Array to append to.
+ * :param const char *value: String value to copy, or NULL to skip.
+ * :return: If value is set: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_STRING, located in the array.
+ * This object is not owned by the caller.
+ * If value is not set: NULL.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_string_opt(spdxtool_serialize_array_t *array, const char *value)
+{
+ if (value)
+ return spdxtool_serialize_array_add_take(array, spdxtool_serialize_value_string(value));
+
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_int(spdxtool_serialize_array_t *array, int value)
+ *
+ * Append a int value to a JSON array.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to append to.
+ * :param int value: integer value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_INT, located in the array.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_int(spdxtool_serialize_array_t *array, int value)
+{
+ return spdxtool_serialize_array_add_take(array, spdxtool_serialize_value_int(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_bool(spdxtool_serialize_array_t *array, bool value)
+ *
+ * Append a boolean value to a JSON array.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to append to.
+ * :param bool value: Boolean value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_BOOL, located in the array.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_bool(spdxtool_serialize_array_t *array, bool value)
+{
+ return spdxtool_serialize_array_add_take(array, spdxtool_serialize_value_bool(value));
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_null(spdxtool_serialize_array_t *array)
+ *
+ * Append a null value to a JSON array.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to append to.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_NULL, located in the array.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_null(spdxtool_serialize_array_t *array)
+{
+ return spdxtool_serialize_array_add_take(array, spdxtool_serialize_value_null());
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_object(spdxtool_serialize_array_t *array, spdxtool_serialize_object_list_t *value)
+ *
+ * Append an object value to a JSON array.
+ * This takes ownership of the object in value unconditionally, freeing on failure.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to append to.
+ * :param spdxtool_serialize_object_list_t *value: Object value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_OBJECT, located in the array.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_object(spdxtool_serialize_array_t *array, spdxtool_serialize_object_list_t *value)
+{
+ if (!value)
+ return NULL;
+
+ spdxtool_serialize_value_t *ret = spdxtool_serialize_value_object(value);
+ if (!ret)
+ {
+ // Since we take possession of the pointer unconditionally, clean up.
+ spdxtool_serialize_object_list_free(value);
+ return NULL;
+ }
+
+ return spdxtool_serialize_array_add_take(array, ret);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_array(spdxtool_serialize_array_t *array, spdxtool_serialize_array_t *value)
+ *
+ * Append an array value to a JSON array.
+ * This takes ownership of the array in value unconditionally, freeing on failure.
+ *
+ * :param spdxtool_serialize_array_t *array: Array to append to.
+ * :param spdxtool_serialize_array_t *value: Array value.
+ * :return: spdxtool_serialize_value_t * of type SPDXTOOL_SERIALIZE_TYPE_ARRAY, located in the array.
+ * This object is not owned by the caller.
+ */
+static inline spdxtool_serialize_value_t *
+spdxtool_serialize_array_add_array(spdxtool_serialize_array_t *array, spdxtool_serialize_array_t *value)
+{
+ if (!value)
+ return NULL;
+
+ spdxtool_serialize_value_t *ret = spdxtool_serialize_value_array(value);
+ if (!ret)
+ {
+ // Since we take possession of the pointer unconditionally, clean up.
+ spdxtool_serialize_array_free(value);
+ return NULL;
+ }
+
+ return spdxtool_serialize_array_add_take(array, ret);
+}
+
+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);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/cli/spdxtool/simplelicensing.c b/cli/spdxtool/simplelicensing.c
new file mode 100644
index 000000000000..a9823b9b3b6c
--- /dev/null
+++ b/cli/spdxtool/simplelicensing.c
@@ -0,0 +1,113 @@
+/*
+ * 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
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "util.h"
+#include "serialize.h"
+#include "software.h"
+#include "core.h"
+#include "simplelicensing.h"
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_simplelicensing_license_expression_t *spdxtool_simplelicensing_licenseExpression_new(pkgconf_client_t *client, char *license)
+ *
+ * Create new /SimpleLicensing/SimpleLicensingText struct
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param char *license: SPDX name of license
+ * :return: NULL if some problem occurs and SimpleLicensingText struct if not
+ */
+spdxtool_simplelicensing_license_expression_t *
+spdxtool_simplelicensing_licenseExpression_new(pkgconf_client_t *client, const char *license)
+{
+ if (!client || !license)
+ return NULL;
+
+ spdxtool_simplelicensing_license_expression_t *expression = calloc(1, sizeof(spdxtool_simplelicensing_license_expression_t));
+ if (!expression)
+ goto err;
+
+ expression->type = "simplelicensing_LicenseExpression";
+ expression->license_expression = strdup(license);
+ expression->spdx_id = spdxtool_util_get_spdx_id_string(client, expression->type, license);
+
+ if (!expression->license_expression || !expression->spdx_id)
+ goto err;
+
+ return expression;
+
+err:
+ pkgconf_error(client, "spdxtool_simplelicensing_licenseExpression_new: out of memory");
+ spdxtool_simplelicensing_licenseExpression_free(expression);
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_simplelicensing_licenseExpression_free(spdxtool_simplelicensing_license_expression_t *expression)
+ *
+ * Free /SimpleLicensing/SimpleLicensingText struct
+ *
+ * :param spdxtool_simplelicensing_license_expression_t *expression: SimpleLicensingText struct to be freed.
+ * :return: nothing
+ */
+void
+spdxtool_simplelicensing_licenseExpression_free(spdxtool_simplelicensing_license_expression_t *expression)
+{
+ if(!expression)
+ return;
+
+ free(expression->spdx_id);
+ free(expression->license_expression);
+
+ free(expression);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_simplelicensing_licenseExpression_to_object(const char *creation_info, const spdxtool_simplelicensing_license_expression_t *expression)
+ *
+ * Serialize /SimpleLicensing/LicenseExpression struct to a JSON value tree.
+ *
+ * :param const char *creation_info: The creationInfo ID string to embed in the object.
+ * :param const spdxtool_simplelicensing_license_expression_t *expression: LicenseExpression struct to be serialized.
+ * :return: spdxtool_serialize_value_t * representing the LicenseExpression object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_simplelicensing_licenseExpression_to_object(pkgconf_client_t *client, const char *creation_info, const spdxtool_simplelicensing_license_expression_t *expression)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", "simplelicensing_LicenseExpression") &&
+ spdxtool_serialize_object_add_string(object_list, "creationInfo", creation_info) &&
+ spdxtool_serialize_object_add_string(object_list, "spdxId", expression->spdx_id) &&
+ spdxtool_serialize_object_add_string(object_list, "simplelicensing_licenseExpression", expression->license_expression)))
+ {
+ goto err;
+ }
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_simplelicensing_licenseExpression_to_object: out of memory");
+
+ spdxtool_serialize_object_list_free(object_list);
+ return ret;
+}
diff --git a/cli/spdxtool/simplelicensing.h b/cli/spdxtool/simplelicensing.h
new file mode 100644
index 000000000000..328136c620c7
--- /dev/null
+++ b/cli/spdxtool/simplelicensing.h
@@ -0,0 +1,36 @@
+/*
+ * 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
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include "util.h"
+
+#ifndef CLI__SPDXTOOL__SIMPLELICENSING_H
+#define CLI__SPDXTOOL__SIMPLELICENSING_H
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+spdxtool_simplelicensing_license_expression_t *
+spdxtool_simplelicensing_licenseExpression_new(pkgconf_client_t *client, const char *license);
+
+void
+spdxtool_simplelicensing_licenseExpression_free(spdxtool_simplelicensing_license_expression_t *expression);
+
+spdxtool_serialize_value_t *
+spdxtool_simplelicensing_licenseExpression_to_object(pkgconf_client_t *client, const char *creation_info, const spdxtool_simplelicensing_license_expression_t *expression);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/cli/spdxtool/software.c b/cli/spdxtool/software.c
new file mode 100644
index 000000000000..4e9712ba6942
--- /dev/null
+++ b/cli/spdxtool/software.c
@@ -0,0 +1,447 @@
+/*
+ * 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
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <libpkgconf/libpkgconf.h>
+#include "util.h"
+#include "serialize.h"
+#include "software.h"
+#include "core.h"
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_software_sbom_t *spdxtool_software_sbom_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *sbom_type)
+ *
+ * Create new /Software/Sbom struct
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param const char *spdx_id: spdxId for this SBOM element
+ * :param const char *creation_id: id for CreationInfo
+ * :param const char *sbom_type: Sbom types can be found SPDX documention
+ * :return: NULL if some problem occurs and Sbom struct if not
+ */
+spdxtool_software_sbom_t *
+spdxtool_software_sbom_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *sbom_type)
+{
+ if (!client || !spdx_id || !creation_id || !sbom_type)
+ return NULL;
+
+ spdxtool_software_sbom_t *sbom = calloc(1, sizeof(spdxtool_software_sbom_t));
+ if (!sbom)
+ goto err;
+
+ sbom->type = "software_Sbom";
+ sbom->spdx_id = strdup(spdx_id);
+ sbom->creation_info = strdup(creation_id);
+ sbom->sbom_type = strdup(sbom_type);
+
+ if (!sbom->spdx_id || !sbom->creation_info || !sbom->sbom_type)
+ goto err;
+
+ return sbom;
+
+err:
+ pkgconf_error(client, "spdxtool_software_sbom_new: out of memory");
+ spdxtool_software_sbom_free(sbom);
+ return NULL;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_software_sbom_free(spdxtool_software_sbom_t *sbom)
+ *
+ * Free /Software/Sbom struct
+ *
+ * :param spdxtool_software_sbom_t *sbom: Sbom struct to be freed.
+ * :return: nothing
+ */
+void
+spdxtool_software_sbom_free(spdxtool_software_sbom_t *sbom)
+{
+ if(!sbom)
+ return;
+
+ free(sbom->spdx_id);
+ free(sbom->creation_info);
+ free(sbom->sbom_type);
+
+ free(sbom);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_software_sbom_to_object(pkgconf_client_t *client, spdxtool_software_sbom_t *sbom)
+ *
+ * Serialize /Software/Sbom struct to a JSON value tree. As a side effect,
+ * the package associated with the SBOM's rootElement is registered on the
+ * document via spdxtool_core_spdx_document_add_package, and relationship
+ * element IDs are registered via spdxtool_core_spdx_document_add_element.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param spdxtool_software_sbom_t *sbom: Sbom struct to be serialized.
+ * :return: spdxtool_serialize_value_t * representing the Sbom object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_software_sbom_to_object(pkgconf_client_t *client, spdxtool_software_sbom_t *sbom)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = NULL;
+ spdxtool_serialize_array_t *sbom_type_array = NULL;
+ spdxtool_serialize_array_t *root_element_array = NULL;
+ spdxtool_serialize_array_t *element_array = NULL;
+ char *spdx_id = NULL;
+
+ char sep = spdxtool_util_get_uri_separator(client);
+
+ spdx_id = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "spdxId");
+ if (!spdx_id)
+ goto err;
+
+ object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ sbom_type_array = spdxtool_serialize_array_new();
+ if (!sbom_type_array)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_string(sbom_type_array, sbom->sbom_type))
+ goto err;
+
+ root_element_array = spdxtool_serialize_array_new();
+ if (!root_element_array)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_string(root_element_array, spdx_id))
+ goto err;
+
+ element_array = spdxtool_serialize_array_new();
+ if (!element_array)
+ goto err;
+
+ pkgconf_node_t *node = NULL;
+ PKGCONF_FOREACH_LIST_ENTRY(sbom->rootElement->required.head, node)
+ {
+ pkgconf_dependency_t *dep = node->data;
+ pkgconf_pkg_t *match = dep->match;
+ pkgconf_buffer_t relationship_buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append_fmt(&relationship_buf, "%s%cdependsOn%c%s", sbom->rootElement->id, sep, sep, match->id);
+ char *relationship_str = pkgconf_buffer_freeze(&relationship_buf);
+ if (!relationship_str)
+ goto err;
+
+ char *spdx_id_relation = spdxtool_util_get_spdx_id_string(client, "Relationship", relationship_str);
+ free(relationship_str);
+ if (!spdx_id_relation)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_string(element_array, spdx_id_relation))
+ {
+ free(spdx_id_relation);
+ goto err;
+ }
+
+ if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, spdx_id_relation))
+ {
+ free(spdx_id_relation);
+ goto err;
+ }
+
+ free(spdx_id_relation);
+ }
+
+ char *value = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "hasDeclaredLicense");
+ if (value)
+ {
+ if (!spdxtool_serialize_array_add_string(element_array, value))
+ {
+ free(value);
+ goto err;
+ }
+
+ if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, value))
+ {
+ free(value);
+ goto err;
+ }
+
+ free(value);
+ }
+
+ value = spdxtool_util_tuple_lookup(client, &sbom->rootElement->vars, "hasConcludedLicense");
+ if (value)
+ {
+ if (!spdxtool_serialize_array_add_string(element_array, value))
+ {
+ free(value);
+ goto err;
+ }
+
+ if (!spdxtool_core_spdx_document_add_element(client, sbom->spdx_document, value))
+ {
+ free(value);
+ goto err;
+ }
+
+ free(value);
+ }
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", sbom->type) &&
+ spdxtool_serialize_object_add_string(object_list, "creationInfo", sbom->creation_info) &&
+ spdxtool_serialize_object_add_string(object_list, "spdxId", sbom->spdx_id)))
+ {
+ goto err;
+ }
+
+ if (!spdxtool_serialize_object_add_array(object_list, "software_sbomType", sbom_type_array))
+ goto err;
+ sbom_type_array = NULL;
+
+ if (!spdxtool_serialize_object_add_array(object_list, "rootElement", root_element_array))
+ goto err;
+ root_element_array = NULL;
+
+ if (!spdxtool_serialize_object_add_array(object_list, "element", element_array))
+ goto err;
+ element_array = NULL;
+
+ if (!spdxtool_core_spdx_document_add_package(client, sbom->spdx_document, sbom->rootElement))
+ goto err;
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_software_sbom_to_object: out of memory");
+
+ free(spdx_id);
+ spdxtool_serialize_object_list_free(object_list);
+ spdxtool_serialize_array_free(sbom_type_array);
+ spdxtool_serialize_array_free(root_element_array);
+ spdxtool_serialize_array_free(element_array);
+ return ret;
+}
+
+static bool
+serialize_copyright_lines_to_object(spdxtool_serialize_object_list_t *object_list, const pkgconf_list_t *copyright_lines)
+{
+ pkgconf_buffer_t copyright_buf = PKGCONF_BUFFER_INITIALIZER;
+ const pkgconf_node_t *node;
+
+ if (copyright_lines->head == NULL)
+ return spdxtool_serialize_object_add_string(object_list, "software_copyrightText", "NOASSERTION") != NULL;
+
+ PKGCONF_FOREACH_LIST_ENTRY(copyright_lines->head, node)
+ {
+ const pkgconf_bufferset_t *set = node->data;
+ pkgconf_buffer_join(&copyright_buf, '\n', pkgconf_buffer_str_or_empty(&set->buffer), NULL);
+ }
+
+ bool ok = spdxtool_serialize_object_add_string(object_list, "software_copyrightText", pkgconf_buffer_str_or_empty(&copyright_buf)) != NULL;
+ pkgconf_buffer_finalize(&copyright_buf);
+ return ok;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: spdxtool_serialize_value_t *spdxtool_software_package_to_object(pkgconf_client_t *client, pkgconf_pkg_t *pkg, spdxtool_core_spdx_document_t *spdx)
+ *
+ * Serialize /Software/Package struct to a JSON value tree. As a side effect,
+ * any license and dependency relationships generated during serialization are
+ * added to the document via spdxtool_core_spdx_document_add_relationship.
+ *
+ * :param pkgconf_client_t *client: The pkgconf client being accessed.
+ * :param pkgconf_pkg_t *pkg: Package struct to be serialized.
+ * :param spdxtool_core_spdx_document_t *spdx: SpdxDocument to which generated relationships are added.
+ * :return: spdxtool_serialize_value_t * representing the Package object.
+ */
+spdxtool_serialize_value_t *
+spdxtool_software_package_to_object(pkgconf_client_t *client, pkgconf_pkg_t *pkg, spdxtool_core_spdx_document_t *spdx)
+{
+ spdxtool_serialize_value_t *ret = NULL;
+ spdxtool_serialize_object_list_t *object_list = NULL;
+ spdxtool_serialize_array_t *originated_by = NULL;
+ char *creation_info = NULL;
+ char *spdx_id = NULL;
+ char *agent = NULL;
+ char *spdx_id_license = NULL;
+ pkgconf_list_t relations = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t *cpy_relations = NULL;
+ pkgconf_node_t *node = NULL;
+ char sep = spdxtool_util_get_uri_separator(client);
+
+ creation_info = spdxtool_util_tuple_lookup(client, &pkg->vars, "creationInfo");
+ spdx_id = spdxtool_util_tuple_lookup(client, &pkg->vars, "spdxId");
+ agent = spdxtool_util_tuple_lookup(client, &pkg->vars, "agent");
+
+ if (!creation_info || !spdx_id || !agent)
+ goto err;
+
+ object_list = spdxtool_serialize_object_list_new();
+ if (!object_list)
+ goto err;
+
+ originated_by = spdxtool_serialize_array_new();
+ if (!originated_by)
+ goto err;
+
+ if (!spdxtool_serialize_array_add_string(originated_by, agent))
+ goto err;
+
+ if (!(spdxtool_serialize_object_add_string(object_list, "type", "software_Package") &&
+ spdxtool_serialize_object_add_string(object_list, "creationInfo", creation_info) &&
+ spdxtool_serialize_object_add_string(object_list, "spdxId", spdx_id) &&
+ spdxtool_serialize_object_add_string(object_list, "name", pkg->realname)))
+ {
+ goto err;
+ }
+
+ if (!spdxtool_serialize_object_add_array(object_list, "originatedBy", originated_by))
+ goto err;
+ originated_by = NULL;
+
+ if (!serialize_copyright_lines_to_object(object_list, &pkg->copyright))
+ goto err;
+
+ if (!spdxtool_serialize_object_add_string(object_list, "software_homePage",
+ pkg->url ? pkg->url : ""))
+ {
+ goto err;
+ }
+
+ if (!spdxtool_serialize_object_add_string(object_list, "software_downloadLocation",
+ pkg->source ? pkg->source : ""))
+ {
+ goto err;
+ }
+
+ if (!spdxtool_serialize_object_add_string(object_list, "software_packageVersion", pkg->version))
+ goto err;
+
+ PKGCONF_FOREACH_LIST_ENTRY(pkg->license.head, node)
+ {
+ const pkgconf_license_t *license = node->data;
+ if (license->type == PKGCONF_LICENSE_EXPRESSION)
+ {
+ spdx_id_license = spdxtool_util_get_spdx_id_string(client, "simplelicensing_LicenseExpression", license->data);
+ if (!spdx_id_license)
+ goto err;
+
+ pkgconf_license_insert(client, &relations, PKGCONF_LICENSE_UNKNOWN, spdx_id_license);
+ free(spdx_id_license);
+ spdx_id_license = NULL;
+ }
+ }
+
+ char *tuple_license = spdxtool_util_tuple_lookup(client, &pkg->vars, "hasDeclaredLicense");
+ if (tuple_license)
+ {
+ cpy_relations = calloc(1, sizeof(pkgconf_list_t));
+ if (!cpy_relations)
+ {
+ free(tuple_license);
+ goto err;
+ }
+
+ pkgconf_license_copy_list(client, cpy_relations, &relations);
+ spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, tuple_license, spdx_id, cpy_relations, "hasDeclaredLicense");
+ free(tuple_license);
+ if (!relationship)
+ goto err;
+ if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
+ goto err;
+ cpy_relations = NULL;
+ }
+
+ tuple_license = spdxtool_util_tuple_lookup(client, &pkg->vars, "hasConcludedLicense");
+ if (tuple_license)
+ {
+ cpy_relations = calloc(1, sizeof(pkgconf_list_t));
+ if (!cpy_relations)
+ {
+ free(tuple_license);
+ goto err;
+ }
+
+ pkgconf_license_copy_list(client, cpy_relations, &relations);
+ spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, tuple_license, spdx_id, cpy_relations, "hasConcludedLicense");
+ free(tuple_license);
+ if (!relationship)
+ goto err;
+ if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
+ goto err;
+ cpy_relations = NULL;
+ }
+ pkgconf_license_free(&relations);
+
+ PKGCONF_FOREACH_LIST_ENTRY(pkg->required.head, node)
+ {
+ pkgconf_dependency_t *dep = node->data;
+ pkgconf_pkg_t *match = dep->match;
+ pkgconf_buffer_t relationship_buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append_fmt(&relationship_buf, "%s%cdependsOn%c%s", pkg->id, sep, sep, match->id);
+ char *relationship_str = pkgconf_buffer_freeze(&relationship_buf);
+ if (!relationship_str)
+ goto err;
+
+ char *spdx_id_relation = spdxtool_util_get_spdx_id_string(client, "Relationship", relationship_str);
+ free(relationship_str);
+ if (!spdx_id_relation)
+ goto err;
+
+ char *spdx_id_package = spdxtool_util_get_spdx_id_string(client, "Package", match->id);
+ if (!spdx_id_package)
+ {
+ free(spdx_id_relation);
+ goto err;
+ }
+
+ cpy_relations = calloc(1, sizeof(pkgconf_list_t));
+ if (!cpy_relations)
+ {
+ free(spdx_id_relation);
+ free(spdx_id_package);
+ goto err;
+ }
+
+ pkgconf_license_insert(client, cpy_relations, PKGCONF_LICENSE_UNKNOWN, spdx_id_package);
+ spdxtool_core_relationship_t *relationship = spdxtool_core_relationship_new(client, creation_info, spdx_id_relation, spdx_id, cpy_relations, "dependsOn");
+ free(spdx_id_relation);
+ free(spdx_id_package);
+ if (!relationship)
+ goto err;
+ if (!spdxtool_core_spdx_document_add_relationship(client, spdx, relationship))
+ goto err;
+ cpy_relations = NULL;
+ }
+
+ ret = spdxtool_serialize_value_object(object_list);
+ object_list = NULL;
+
+err:
+ if (!ret)
+ pkgconf_error(client, "spdxtool_software_package_to_object: out of memory");
+
+ free(creation_info);
+ free(spdx_id);
+ free(agent);
+ free(spdx_id_license);
+ spdxtool_serialize_object_list_free(object_list);
+ spdxtool_serialize_array_free(originated_by);
+ return ret;
+}
diff --git a/cli/spdxtool/software.h b/cli/spdxtool/software.h
new file mode 100644
index 000000000000..d18710cb0d1b
--- /dev/null
+++ b/cli/spdxtool/software.h
@@ -0,0 +1,37 @@
+/*
+ * 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
+ */
+
+#ifndef CLI__SPDXTOOL_SOFTWARE_H
+#define CLI__SPDXTOOL_SOFTWARE_H
+
+#include <stdlib.h>
+#include "util.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+spdxtool_software_sbom_t *
+spdxtool_software_sbom_new(pkgconf_client_t *client, const char *spdx_id, const char *creation_id, const char *sbom_type);
+
+void
+spdxtool_software_sbom_free(spdxtool_software_sbom_t *sbom);
+
+spdxtool_serialize_value_t *
+spdxtool_software_package_to_object(pkgconf_client_t *client, pkgconf_pkg_t *pkg, spdxtool_core_spdx_document_t *doc);
+
+spdxtool_serialize_value_t *
+spdxtool_software_sbom_to_object(pkgconf_client_t *client, spdxtool_software_sbom_t *sbom);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/cli/spdxtool/util.c b/cli/spdxtool/util.c
new file mode 100644
index 000000000000..e47bf0a1e61f
--- /dev/null
+++ b/cli/spdxtool/util.c
@@ -0,0 +1,331 @@
+/*
+ * 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
+ */
+
+#include <libpkgconf/libpkgconf.h>
+#include <libpkgconf/stdinc.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <ctype.h>
+#include "util.h"
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_util_set_key(pkgconf_client_t *client, const char *key, const char *key_value, const char *key_default)
+ *
+ * Set key wit default value. Default value is used if key is NULL
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param const char *key: Key to be preserved
+ * :param const char *key_value: Value for key
+ * :param const char *key_default: Default value if key_value is NULL
+ * :return: nothing
+ */
+void
+spdxtool_util_set_key(pkgconf_client_t *client, const char *key, const char *key_value, const char *key_default)
+{
+ PKGCONF_TRACE(client, "set uri_root to: %s", key_value != NULL ? key_value : key_default);
+ pkgconf_tuple_add_global(client, key, key_value != NULL ? key_value : key_default);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_util_set_uri_root(pkgconf_client_t *client, const char *uri_root)
+ *
+ * Set URI/URL root for spdxId. Type for this is 'xsd:anyURI' which means
+ * it can they may be absolute or relative.
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param const char *uri_root: URI root.
+ * :return: nothing
+ */
+void
+spdxtool_util_set_uri_root(pkgconf_client_t *client, const char *uri_root)
+{
+ spdxtool_util_set_key(client, "spdx_uri_root", uri_root, "https://github.com/pkgconf/pkgconf");
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: const char *spdxtool_util_get_uri_root(pkgconf_client_t *client)
+ *
+ * Get current URI
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :return: URI or NULL
+ */
+const char *
+spdxtool_util_get_uri_root(pkgconf_client_t *client)
+{
+ return pkgconf_tuple_find_global(client, "spdx_uri_root");
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_util_set_uri_separator_colon(pkgconf_client_t *client, bool is_colon)
+ *
+ * when using URI rather than URL change separator from slash '/' to colon ':'.
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param bool is_colon If true use colon ':' is separator and otherwise slash '/'
+ * :return: nothing
+ */
+void
+spdxtool_util_set_uri_separator_colon(pkgconf_client_t *client, bool is_colon)
+{
+ if (is_colon == true)
+ client->flags |= SPDXTOOL_SEPARATOR_COLON;
+ else
+ client->flags = client->flags & ~SPDXTOOL_SEPARATOR_COLON;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: char spdxtool_util_get_uri_separator(pkgconf_client_t *client)
+ *
+ * Get separator char
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :return: Colon ':' or slash '/'
+ */
+char
+spdxtool_util_get_uri_separator(pkgconf_client_t *client)
+{
+ if (client->flags & SPDXTOOL_SEPARATOR_COLON)
+ return ':';
+
+ return '/';
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_util_set_spdx_version(pkgconf_client_t *client, const char *spdx_version)
+ *
+ * Set current SPDX SBOM Spec version. If not set it's 3.0.1
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param char* spdx_version: SPDX specification version.
+ * :return: nothing
+ */
+void
+spdxtool_util_set_spdx_version(pkgconf_client_t *client, const char *spdx_version)
+{
+ spdxtool_util_set_key(client, "spdx_version", spdx_version, "3.0.1");
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: const char *spdxtool_util_get_spdx_version(pkgconf_client_t *client)
+ *
+ * Get SPDX SBOM specification version in use
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :return: Spec version
+ */
+const char *
+spdxtool_util_get_spdx_version(pkgconf_client_t *client)
+{
+ return pkgconf_tuple_find_global(client, "spdx_version");
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: void spdxtool_util_set_spdx_license(pkgconf_client_t *client, const char *spdx_license)
+ *
+ * Under which license SBOM is released. License string should be in
+ * SPDX style. Something like: FreeBSD-DOC, CC0-1.0 or MIT
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param const char *spdx_license: SPDX compatible license
+ * :return: nothing
+ */
+void
+spdxtool_util_set_spdx_license(pkgconf_client_t *client, const char *spdx_license)
+{
+ spdxtool_util_set_key(client, "spdx_license", spdx_license, "CC0-1.0");
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: const char *spdxtool_util_get_spdx_license(pkgconf_client_t *client)
+ *
+ * Get license which SBOM is release in
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :return: SPDX license
+ */
+const char *
+spdxtool_util_get_spdx_license(pkgconf_client_t *client)
+{
+ return pkgconf_tuple_find_global(client, "spdx_license");
+}
+
+static size_t last_id = 0;
+
+/*
+ * !doc
+ *
+ * .. c:function:: char *spdxtool_util_get_spdx_id_int(pkgconf_client_t *client, char *part)
+ *
+ * Get spdxId with current URI.
+ * URI is lookg like https://test/part/1
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param char *part: Addition subdir part before number
+ * :return: URI
+ */
+char *
+spdxtool_util_get_spdx_id_int(pkgconf_client_t *client, const char *part)
+{
+ const char *global_xsd_any_uri = spdxtool_util_get_uri_root(client);
+ char sep = spdxtool_util_get_uri_separator(client);
+ pkgconf_buffer_t current_uri = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_join(&current_uri, sep, global_xsd_any_uri, part, NULL);
+ pkgconf_buffer_append_fmt(&current_uri, "%c" SIZE_FMT_SPECIFIER, sep, ++last_id);
+
+ return pkgconf_buffer_freeze(&current_uri);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: char *spdxtool_util_get_spdx_id_string(pkgconf_client_t *client, const char *part, const char *string_id)
+ *
+ * Get string id URI
+ * looks something like: https://test/part/string_id
+ *
+ * :param pkgconf_client_t* client: The pkgconf client being accessed.
+ * :param const char* part: subdir part of URI.
+ * :param const char* string_id: String ID.
+ * :return: URI
+ */
+char *
+spdxtool_util_get_spdx_id_string(pkgconf_client_t *client, const char *part, const char *string_id)
+{
+ const char *global_xsd_any_uri = spdxtool_util_get_uri_root(client);
+ char sep = spdxtool_util_get_uri_separator(client);
+ pkgconf_buffer_t current_uri = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_join(&current_uri, sep, global_xsd_any_uri, part, string_id, NULL);
+
+ return pkgconf_buffer_freeze(&current_uri);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: char *spdxtool_util_get_iso8601_time(time_t *wanted_time)
+ *
+ * Get ISO8601 time string
+ *
+ * :param time_t *wanted_time: Time to be converted
+ * :return: Time string in ISO8601 format
+ */
+char *
+spdxtool_util_get_iso8601_time(time_t *wanted_time)
+{
+ char buf[PKGCONF_ITEM_SIZE];
+
+ if (!wanted_time)
+ return NULL;
+
+ struct tm *tm_info = gmtime(wanted_time);
+ if (!tm_info)
+ return NULL;
+
+ /* ISO8601 time with Z at the end */
+ strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%SZ", tm_info);
+ return strdup(buf);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: char *spdxtool_util_get_current_iso8601_time(void)
+ *
+ * Get ISO8601 current timestamp
+ *
+ * :return: Time string in ISO8601 format
+ */
+char *
+spdxtool_util_get_current_iso8601_time(void)
+{
+ time_t now;
+ time(&now);
+ return spdxtool_util_get_iso8601_time(&now);
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: char *spdxtool_util_string_correction(char *str)
+ *
+ * Lowercase string and change spaces to '_'
+ *
+ * :param char *str: String to be converted
+ * :return: Converted string
+ */
+char *
+spdxtool_util_string_correction(char *str)
+{
+ char *ptr = str;
+ /* Lowecase string and make spaces '_' */
+ for ( ; *ptr; ++ptr)
+ {
+ *ptr = (char) tolower(*ptr);
+ if(isspace((unsigned char) *ptr))
+ {
+ *ptr = '_';
+ }
+ }
+
+ return str;
+}
+
+/*
+ * !doc
+ *
+ * .. c:function:: char *spdxtool_util_tuple_lookup(pkgconf_client_t *client, pkgconf_list_t *vars, const char *key)
+ *
+ * Lowercase string and change spaces to '_'
+ *
+ * :param pkgconf_client_t *client: client to use for the lookup
+ * :param pkgconf_list_t *vars: locally-scoped vars to look through
+ * :param char *key: key to lookup
+ * :return: Converted string
+ */
+char *
+spdxtool_util_tuple_lookup(pkgconf_client_t *client, pkgconf_list_t *vars, const char *key)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_eval_ctx_t ctx = {
+ .client = client,
+ .vars = vars,
+ };
+
+ pkgconf_variable_t *v = pkgconf_bytecode_eval_lookup_var(&ctx, key, strlen(key));
+ if (v == NULL)
+ return NULL;
+
+ if (!pkgconf_variable_eval(client, vars, v, &buf, NULL))
+ return NULL;
+
+ return pkgconf_buffer_freeze(&buf);
+}
diff --git a/cli/spdxtool/util.h b/cli/spdxtool/util.h
new file mode 100644
index 000000000000..be86d6e154d7
--- /dev/null
+++ b/cli/spdxtool/util.h
@@ -0,0 +1,130 @@
+/*
+ * 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
+ */
+
+#ifndef CLI__SPDXTOOL__UTIL_H
+#define CLI__SPDXTOOL__UTIL_H
+
+#include <libpkgconf/libpkgconf.h>
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define SPDXTOOL_SEPARATOR_COLON 0x80
+
+typedef struct spdxtool_core_agent_
+{
+ char *spdx_id;
+ const char *type;
+ char *creation_info;
+ char *name;
+} spdxtool_core_agent_t;
+
+typedef struct spdxtool_core_creation_info_
+{
+ char *id;
+ const char *type;
+ char *created;
+ char *created_by;
+ const char *created_using;
+ char *spec_version;
+} spdxtool_core_creation_info_t;
+
+typedef struct spdxtool_core_spdx_document
+{
+ const char *type;
+ char *spdx_id;
+ char *creation_info;
+ char *agent;
+ pkgconf_list_t licenses;
+ pkgconf_list_t element;
+ pkgconf_list_t rootElement;
+ pkgconf_list_t relationships;
+ pkgconf_list_t packages;
+} spdxtool_core_spdx_document_t;
+
+typedef struct spdxtool_software_sbom_
+{
+ char *spdx_id;
+ const char *type;
+ char *creation_info;
+ char *sbom_type;
+ spdxtool_core_spdx_document_t *spdx_document;
+ pkgconf_pkg_t *rootElement;
+} spdxtool_software_sbom_t;
+
+typedef struct spdxtool_simplelicensing_license_expression_
+{
+ const char *type;
+ char *spdx_id;
+ char *license_expression;
+} spdxtool_simplelicensing_license_expression_t;
+
+typedef struct spdxtool_core_relationship_
+{
+ const char *type;
+ char *spdx_id;
+ char *creation_info;
+ char *from;
+ pkgconf_list_t *to;
+ char *relationship_type;
+} spdxtool_core_relationship_t;
+
+void
+spdxtool_util_set_key(pkgconf_client_t *client, const char *key, const char *key_value, const char *key_default);
+
+void
+spdxtool_util_set_uri_root(pkgconf_client_t *client, const char *uri_root);
+
+void
+spdxtool_util_set_uri_separator_colon(pkgconf_client_t *client, bool sep);
+
+char
+spdxtool_util_get_uri_separator(pkgconf_client_t *client);
+
+const char *
+spdxtool_util_get_uri_root(pkgconf_client_t *client);
+
+void
+spdxtool_util_set_spdx_version(pkgconf_client_t *client, const char *spdx_version);
+
+const char *
+spdxtool_util_get_spdx_version(pkgconf_client_t *client);
+
+void
+spdxtool_util_set_spdx_license(pkgconf_client_t *client, const char *spdx_license);
+
+const char *
+spdxtool_util_get_spdx_license(pkgconf_client_t *client);
+
+char *
+spdxtool_util_get_spdx_id_int(pkgconf_client_t *client, const char *part);
+
+char *
+spdxtool_util_get_spdx_id_string(pkgconf_client_t *client, const char *part, const char *string_id);
+
+char *
+spdxtool_util_get_iso8601_time(time_t *wanted_time);
+
+char *
+spdxtool_util_get_current_iso8601_time(void);
+
+char *
+spdxtool_util_string_correction(char *str);
+
+char *
+spdxtool_util_tuple_lookup(pkgconf_client_t *client, pkgconf_list_t *vars, const char *key);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif