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