xref: /freebsd/contrib/pkgconf/cli/spdxtool/serialize.c (revision 592efe252472a3385acf36b1f49ecf710a7f3d9c)
1 /*
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  *​ Copyright (c) 2025 The FreeBSD Foundation
5  *​
6  *​ Portions of this software were developed by
7  * Tuukka Pasanen <tuukka.pasanen@ilmi.fi> under sponsorship from
8  * the FreeBSD Foundation
9  *​
10  *​ Copyright (C) 2026 Elizabeth Ashford.
11  */
12 
13 #include <stdlib.h>
14 #include <string.h>
15 #include "util.h"
16 #include "core.h"
17 #include "software.h"
18 #include "simplelicensing.h"
19 #include "serialize.h"
20 
21 static void
serialize_escape_string(pkgconf_buffer_t * buffer,const char * s)22 serialize_escape_string(pkgconf_buffer_t *buffer, const char *s)
23 {
24 	for (const char *p = s; *p; p++)
25 	{
26 		switch (*p)
27 		{
28 		case '\"':
29 			pkgconf_buffer_append(buffer, "\\\"");
30 			break;
31 		case '\\':
32 			pkgconf_buffer_append(buffer, "\\\\");
33 			break;
34 		case '\b':
35 			pkgconf_buffer_append(buffer, "\\b");
36 			break;
37 		case '\f':
38 			pkgconf_buffer_append(buffer, "\\f");
39 			break;
40 		case '\n':
41 			pkgconf_buffer_append(buffer, "\\n");
42 			break;
43 		case '\r':
44 			pkgconf_buffer_append(buffer, "\\r");
45 			break;
46 		case '\t':
47 			pkgconf_buffer_append(buffer, "\\t");
48 			break;
49 		default:
50 			if ((unsigned char) *p < 0x20)
51 				pkgconf_buffer_append_fmt(buffer, "\\u%04x", (unsigned int)(unsigned char) *p);
52 			else
53 				pkgconf_buffer_push_byte(buffer, *p);
54 		}
55 	}
56 }
57 
58 static inline void
serialize_add_indent(pkgconf_buffer_t * buffer,unsigned int level)59 serialize_add_indent(pkgconf_buffer_t *buffer, unsigned int level)
60 {
61 	for (; level; level--)
62 		pkgconf_buffer_append(buffer, "    ");
63 }
64 
65 /*
66  * !doc
67  *
68  * .. c:function:: void spdxtool_serialize_value_to_buf(pkgconf_buffer_t *buffer, spdxtool_serialize_value_t *value, unsigned int indent)
69  *
70  *    Serialize the given JSON to the buffer
71  *
72  *    :param pkgconf_buffer_t *buffer: Buffer to add to.
73  *    :param spdxtool_serialize_value *value: Value to serialize.
74  *    :param unsigned int indent: Indent level
75  *    :return: true on success, false on failure
76  */
77 bool
spdxtool_serialize_value_to_buf(pkgconf_buffer_t * buffer,spdxtool_serialize_value_t * value,unsigned int indent)78 spdxtool_serialize_value_to_buf(pkgconf_buffer_t *buffer, spdxtool_serialize_value_t *value, unsigned int indent)
79 {
80 	if (!buffer || !value)
81 		return false;
82 
83 	switch(value->type) {
84 		case SPDXTOOL_SERIALIZE_TYPE_STRING:
85 			pkgconf_buffer_push_byte(buffer, '"');
86 			serialize_escape_string(buffer, value->value.s ? value->value.s : "");
87 			pkgconf_buffer_push_byte(buffer, '"');
88 			break;
89 		case SPDXTOOL_SERIALIZE_TYPE_INT:
90 			pkgconf_buffer_append_fmt(buffer, "%d", value->value.i);
91 			break;
92 		case SPDXTOOL_SERIALIZE_TYPE_BOOL:
93 			pkgconf_buffer_append(buffer, value->value.b ? "true" : "false");
94 			break;
95 		case SPDXTOOL_SERIALIZE_TYPE_NULL:
96 			pkgconf_buffer_append(buffer, "null");
97 			break;
98 		case SPDXTOOL_SERIALIZE_TYPE_OBJECT:
99 		{
100 			pkgconf_node_t *iter;
101 			pkgconf_buffer_push_byte(buffer, '{');
102 			pkgconf_buffer_push_byte(buffer, '\n');
103 
104 			PKGCONF_FOREACH_LIST_ENTRY(value->value.o->entries.head, iter)
105 			{
106 				spdxtool_serialize_object_t *entry = iter->data;
107 				serialize_add_indent(buffer, indent + 1);
108 				pkgconf_buffer_append_fmt(buffer, "\"%s\": ", entry->key);
109 				spdxtool_serialize_value_to_buf(buffer, entry->value, indent + 1);
110 				if (iter->next)
111 					pkgconf_buffer_push_byte(buffer, ',');
112 				pkgconf_buffer_push_byte(buffer, '\n');
113 			}
114 
115 			serialize_add_indent(buffer, indent);
116 			pkgconf_buffer_push_byte(buffer, '}');
117 			break;
118 		}
119 		case SPDXTOOL_SERIALIZE_TYPE_ARRAY:
120 		{
121 			pkgconf_node_t *iter;
122 			pkgconf_buffer_push_byte(buffer, '[');
123 			pkgconf_buffer_push_byte(buffer, '\n');
124 
125 			PKGCONF_FOREACH_LIST_ENTRY(value->value.a->items.head, iter)
126 			{
127 				spdxtool_serialize_value_t *entry = iter->data;
128 				serialize_add_indent(buffer, indent + 1);
129 				spdxtool_serialize_value_to_buf(buffer, entry, indent + 1);
130 				if (iter->next)
131 					pkgconf_buffer_push_byte(buffer, ',');
132 				pkgconf_buffer_push_byte(buffer, '\n');
133 			}
134 			serialize_add_indent(buffer, indent);
135 			pkgconf_buffer_push_byte(buffer, ']');
136 			break;
137 		}
138 	}
139 
140 	return true;
141 }
142 
143 /*
144  * !doc
145  *
146  * .. 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)
147  *
148  *    Add a key-value pair to a JSON object list. The key is copied internally.
149  *    The object list takes ownership of the value.
150  *
151  *    :param spdxtool_serialize_object_list_t *object_list: Object list to add to.
152  *    :param const char *key: Key string, copied internally.
153  *    :param spdxtool_serialize_value_t *value: Value to associate with the key. Ownership transfers to the object list.
154  *    :return: The value added, not owned by the caller.
155  */
156 spdxtool_serialize_value_t *
spdxtool_serialize_object_add_take(spdxtool_serialize_object_list_t * object_list,const char * key,spdxtool_serialize_value_t * value)157 spdxtool_serialize_object_add_take(spdxtool_serialize_object_list_t *object_list, const char *key, spdxtool_serialize_value_t *value)
158 {
159 	if (!object_list || !value)
160 	{
161 		spdxtool_serialize_value_free(value);
162 		return NULL;
163 	}
164 
165 	pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
166 	spdxtool_serialize_object_t *object = calloc(1, sizeof(spdxtool_serialize_object_t));
167 	char *keycopy = key ? strdup(key) : strdup("");
168 	if (!node || !object || !keycopy)
169 	{
170 		free(node);
171 		free(keycopy);
172 		/* object->key/value are not assigned yet; free the struct itself */
173 		free(object);
174 		spdxtool_serialize_value_free(value);
175 		return NULL;
176 	}
177 
178 	object->key = keycopy;
179 	object->value = value;
180 	pkgconf_node_insert_tail(node, object, &object_list->entries);
181 	return value;
182 }
183 
184 /*
185  * !doc
186  *
187  * .. c:function:: spdxtool_serialize_value_t *spdxtool_serialize_array_add_take(spdxtool_serialize_array_t *array, spdxtool_serialize_value_t value)
188  *
189  *    Add a value to a JSON array. The array takes ownership of the value.
190  *
191  *    :param spdxtool_serialize_array_t *array: Array to add to.
192  *    :param spdxtool_serialize_value_t value: Value to append. Ownership transfers to the array.
193  *    :return: The value added, not owned by the caller.
194  */
195 spdxtool_serialize_value_t *
spdxtool_serialize_array_add_take(spdxtool_serialize_array_t * array,spdxtool_serialize_value_t * value)196 spdxtool_serialize_array_add_take(spdxtool_serialize_array_t *array, spdxtool_serialize_value_t *value)
197 {
198 	if (!array)
199 	{
200 		// Taking value, so free
201 		spdxtool_serialize_value_free(value);
202 		return NULL;
203 	}
204 
205 	pkgconf_node_t *node = calloc(1, sizeof(pkgconf_node_t));
206 	if (!node)
207 	{
208 		spdxtool_serialize_value_free(value);
209 		return NULL;
210 	}
211 
212 	pkgconf_node_insert_tail(node, value, &array->items);
213 	return value;
214 }
215 
216 /*
217  * !doc
218  *
219  * .. c:function:: spdxtool_serialize_object_list_t *spdxtool_serialize_object_list_new(void)
220  *
221  *    Allocate and initialize a new empty JSON object list.
222  *
223  *    :return: Pointer to a new spdxtool_serialize_object_list_t, or NULL on allocation failure.
224  */
225 spdxtool_serialize_object_list_t *
spdxtool_serialize_object_list_new(void)226 spdxtool_serialize_object_list_new(void)
227 {
228 	return calloc(1, sizeof(spdxtool_serialize_object_list_t));
229 }
230 
231 /*
232  * !doc
233  *
234  * .. c:function:: spdxtool_serialize_array_t *spdxtool_serialize_array_new(void)
235  *
236  *    Allocate and initialize a new empty JSON array.
237  *
238  *    :return: Pointer to a new spdxtool_serialize_array_t, or NULL on allocation failure.
239  */
240 spdxtool_serialize_array_t *
spdxtool_serialize_array_new(void)241 spdxtool_serialize_array_new(void)
242 {
243 	return calloc(1, sizeof(spdxtool_serialize_array_t));
244 }
245 
246 /*
247  * !doc
248  *
249  * .. c:function:: void spdxtool_serialize_value_free(spdxtool_serialize_value_t *value)
250  *
251  *    Free all resources owned by a JSON value. For strings, frees the string.
252  *    For objects and arrays, recursively frees all children. The value pointer
253  *    itself is not freed as it is assumed to be stack-allocated.
254  *
255  *    :param spdxtool_serialize_value_t *value: Value to free. May be NULL.
256  *    :return: nothing
257  */
258 void
spdxtool_serialize_value_free(spdxtool_serialize_value_t * value)259 spdxtool_serialize_value_free(spdxtool_serialize_value_t *value)
260 {
261 	if (!value)
262 		return;
263 
264 	switch (value->type)
265 	{
266 		case SPDXTOOL_SERIALIZE_TYPE_STRING:
267 			free(value->value.s);
268 			break;
269 		case SPDXTOOL_SERIALIZE_TYPE_ARRAY:
270 			spdxtool_serialize_array_free(value->value.a);
271 			break;
272 		case SPDXTOOL_SERIALIZE_TYPE_OBJECT:
273 			spdxtool_serialize_object_list_free(value->value.o);
274 			break;
275 		default:
276 			// Nothing to do
277 			break;
278 	}
279 
280 	free(value);
281 }
282 
283 /*
284  * !doc
285  *
286  * .. c:function:: void spdxtool_serialize_object_free(spdxtool_serialize_object_t *object)
287  *
288  *    Free a JSON object entry, including its key string and owned value.
289  *    The object pointer itself is not freed by this function.
290  *
291  *    :param spdxtool_serialize_object_t *object: Object entry to free. May be NULL.
292  *    :return: nothing
293  */
294 void
spdxtool_serialize_object_free(spdxtool_serialize_object_t * object)295 spdxtool_serialize_object_free(spdxtool_serialize_object_t *object)
296 {
297 	if (!object)
298 		return;
299 
300 	free(object->key);
301 	spdxtool_serialize_value_free(object->value);
302 }
303 
304 /*
305  * !doc
306  *
307  * .. c:function:: void spdxtool_serialize_object_list_free(spdxtool_serialize_object_list_t *object_list)
308  *
309  *    Free a JSON object list and all of its entries, including their keys and values.
310  *
311  *    :param spdxtool_serialize_object_list_t *object_list: Object list to free. May be NULL.
312  *    :return: nothing
313  */
314 void
spdxtool_serialize_object_list_free(spdxtool_serialize_object_list_t * object_list)315 spdxtool_serialize_object_list_free(spdxtool_serialize_object_list_t *object_list)
316 {
317 	if (!object_list)
318 		return;
319 
320 	pkgconf_node_t *iter_next = NULL, *iter = NULL;
321 	PKGCONF_FOREACH_LIST_ENTRY_SAFE(object_list->entries.head, iter_next, iter)
322 	{
323 		spdxtool_serialize_object_t *object = iter->data;
324 		spdxtool_serialize_object_free(object);
325 		free(object);
326 		free(iter);
327 	}
328 
329 	free(object_list);
330 }
331 
332 /*
333  * !doc
334  *
335  * .. c:function:: void spdxtool_serialize_array_free(spdxtool_serialize_array_t *array)
336  *
337  *    Free a JSON array and all of its elements.
338  *
339  *    :param spdxtool_serialize_array_t *array: Array to free. May be NULL.
340  *    :return: nothing
341  */
342 void
spdxtool_serialize_array_free(spdxtool_serialize_array_t * array)343 spdxtool_serialize_array_free(spdxtool_serialize_array_t *array)
344 {
345 	if (!array)
346 		return;
347 
348 	pkgconf_node_t *iter_next = NULL, *iter = NULL;
349 	PKGCONF_FOREACH_LIST_ENTRY_SAFE(array->items.head, iter_next, iter)
350 	{
351 		spdxtool_serialize_value_t *value = iter->data;
352 		spdxtool_serialize_value_free(value);
353 		free(iter);
354 	}
355 
356 	free(array);
357 }
358 
359 /*
360  * !doc
361  *
362  * .. 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)
363  *
364  *    Serialize a complete SPDX SBOM document to a JSON-LD value tree. Iterates
365  *    all SBOMs, packages, relationships, and license expressions registered on
366  *    the document. The SpdxDocument object is emitted last to ensure all element
367  *    IDs have been populated by prior iteration. This function must be called
368  *    after pkgconf_pkg_traverse has completed so that all packages and their
369  *    dependencies are registered on spdx.
370  *
371  *    :param pkgconf_client_t *client: The pkgconf client being accessed.
372  *    :param spdxtool_core_agent_t *agent: Agent struct to include in the document.
373  *    :param spdxtool_core_creation_info_t *creation: CreationInfo struct to include in the document.
374  *    :param spdxtool_core_spdx_document_t *spdx: SpdxDocument struct containing all registered SBOMs, packages, relationships, and licenses.
375  *    :return: spdxtool_serialize_value_t * representing the complete JSON-LD document, or a null string value on allocation failure.
376  */
377 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)378 spdxtool_serialize_sbom(pkgconf_client_t *client, spdxtool_core_agent_t *agent, spdxtool_core_creation_info_t *creation, spdxtool_core_spdx_document_t *spdx)
379 {
380 	const char *errstr = "out of memory";
381 	spdxtool_serialize_value_t *ret = NULL;
382 	spdxtool_serialize_array_t *graph = NULL;
383 	spdxtool_serialize_object_list_t *root = spdxtool_serialize_object_list_new();
384 	if (!root)
385 		goto err;
386 
387 	if (!spdxtool_serialize_object_add_string(root, "@context", "https://spdx.org/rdf/3.0.1/spdx-context.jsonld"))
388 		goto err;
389 
390 	graph = spdxtool_serialize_array_new();
391 	if (!graph)
392 		goto err;
393 
394 	if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_agent_to_object(client, agent)))
395 		goto err;
396 
397 	if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_creation_info_to_object(client, creation)))
398 		goto err;
399 
400 	pkgconf_node_t *iter = NULL;
401 	PKGCONF_FOREACH_LIST_ENTRY(spdx->maintainers.head, iter)
402 	{
403 		spdxtool_core_agent_t *maintainer = iter->data;
404 		if (!maintainer)
405 		{
406 			errstr = "maintainers list corrupted";
407 			goto err;
408 		}
409 		if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_agent_to_object(client, maintainer)))
410 			goto err;
411 	}
412 
413 	PKGCONF_FOREACH_LIST_ENTRY(spdx->licenses.head, iter)
414 	{
415 		spdxtool_simplelicensing_license_expression_t *expression = iter->data;
416 		if (!expression)
417 		{
418 			errstr = "licenses list corrupted";
419 			goto err;
420 		}
421 		if (!spdxtool_serialize_array_add_take(graph, spdxtool_simplelicensing_licenseExpression_to_object(client, spdx->creation_info, expression)))
422 			goto err;
423 	}
424 
425 	PKGCONF_FOREACH_LIST_ENTRY(spdx->rootElement.head, iter)
426 	{
427 		spdxtool_software_sbom_t *current_sbom = iter->data;
428 		if (!current_sbom)
429 		{
430 			errstr = "sbom list corrupted";
431 			goto err;
432 		}
433 		if (!spdxtool_serialize_array_add_take(graph, spdxtool_software_sbom_to_object(client, current_sbom)))
434 			goto err;
435 	}
436 
437 	PKGCONF_FOREACH_LIST_ENTRY(spdx->packages.head, iter)
438 	{
439 		pkgconf_pkg_t *pkg = iter->data;
440 		if (!pkg)
441 		{
442 			errstr = "pkg list corrupted";
443 			goto err;
444 		}
445 		if (!spdxtool_serialize_array_add_take(graph, spdxtool_software_package_to_object(client, pkg, spdx)))
446 			goto err;
447 	}
448 
449 	PKGCONF_FOREACH_LIST_ENTRY(spdx->relationships.head, iter)
450 	{
451 		spdxtool_core_relationship_t *relationship = iter->data;
452 		if (!relationship)
453 		{
454 			errstr = "relationship list corrupted";
455 			goto err;
456 		}
457 		if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_relationship_to_object(client, relationship)))
458 			goto err;
459 	}
460 
461 	// SpdxDocument last — spdx->element must be fully populated first
462 	if (!spdxtool_serialize_array_add_take(graph, spdxtool_core_spdx_document_to_object(client, spdx)))
463 		goto err;
464 
465 	bool ok = spdxtool_serialize_object_add_array(root, "@graph", graph);
466 	graph = NULL;
467 	if (!ok)
468 		goto err;
469 
470 	ret = spdxtool_serialize_value_object(root);
471 	root = NULL;
472 
473 err:
474 	if (!ret)
475 		pkgconf_error(client, "spdxtool_serialize_sbom: %s", errstr);
476 
477 	spdxtool_serialize_object_list_free(root);
478 	spdxtool_serialize_array_free(graph);
479 	return ret;
480 }
481