1 /*
2 * bomtool/main.c
3 * main() routine, printer functions
4 *
5 * SPDX-License-Identifier: pkgconf
6 *
7 * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
8 * pkgconf authors (see AUTHORS).
9 *
10 * Permission to use, copy, modify, and/or distribute this software for any
11 * purpose with or without fee is hereby granted, provided that the above
12 * copyright notice and this permission notice appear in all copies.
13 *
14 * This software is provided 'as is' and without any warranty, express or
15 * implied. In no event shall the authors be liable for any damages arising
16 * from the use of this software.
17 */
18
19 #include <ctype.h>
20 #include <time.h>
21
22 #include "libpkgconf/config.h"
23 #include <libpkgconf/stdinc.h>
24 #include <libpkgconf/libpkgconf.h>
25 #include "getopt_long.h"
26
27 #define PKG_VERSION (((uint64_t) 1) << 1)
28 #define PKG_ABOUT (((uint64_t) 1) << 2)
29 #define PKG_HELP (((uint64_t) 1) << 3)
30 #define PKG_OUTPUT (((uint64_t) 1) << 4)
31 #define PKG_DEFINE_VARIABLE (((uint64_t) 1) << 5)
32 #define PKG_CREATION_TIME (((uint64_t) 1) << 6)
33
34 static const char *spdx_version = "SPDX-2.2";
35 static const char *bom_license = "CC0-1.0";
36 static const char *document_ref = "SPDXRef-DOCUMENT";
37 static const char *creation_time = NULL;
38
39 static pkgconf_client_t pkg_client;
40 static uint64_t want_flags;
41 static int maximum_traverse_depth = 2000;
42 static FILE *error_msgout = NULL;
43 static FILE *sbom_out = NULL;
44
45 #define OUTPUT_OR_RET(client, f, fmt, ...) \
46 do { \
47 if (!pkgconf_output_file_fmt((f), (fmt), ##__VA_ARGS__)) { \
48 pkgconf_error((client), "bomtool: Could not output to file: %s", strerror(errno)); \
49 return; \
50 } \
51 } while (0)
52
53 #define OUTPUT_OR_RET_FALSE(client, f, fmt, ...) \
54 do { \
55 if (!pkgconf_output_file_fmt((f), (fmt), ##__VA_ARGS__)) { \
56 pkgconf_error((client), "bomtool: Could not output to file: %s", strerror(errno)); \
57 return false; \
58 } \
59 } while (0)
60
61 static const char *
environ_lookup_handler(const pkgconf_client_t * client,const char * key)62 environ_lookup_handler(const pkgconf_client_t *client, const char *key)
63 {
64 (void) client;
65
66 return getenv(key);
67 }
68
69 static bool
error_handler(const char * msg,const pkgconf_client_t * client,void * data)70 error_handler(const char *msg, const pkgconf_client_t *client, void *data)
71 {
72 (void) client;
73 (void) data;
74 OUTPUT_OR_RET_FALSE(client, error_msgout, "%s", msg);
75 return true;
76 }
77
78 static const char *
sbom_spdx_identity(pkgconf_pkg_t * pkg)79 sbom_spdx_identity(pkgconf_pkg_t *pkg)
80 {
81 static char buf[PKGCONF_ITEM_SIZE];
82 size_t i, o;
83
84 /* Sanitize the package ID: only letters, numbers, dot (.) and dash (-)
85 * are allowed.
86 */
87 for (i = 0, o = 0; i < strlen(pkg->id) && o < sizeof(buf); i++, o++) {
88 char c = pkg->id[i];
89 if (c == '-' || c == '.' || isalnum(c))
90 buf[o] = c;
91 else {
92 snprintf(buf + o, sizeof(buf) - o, "C%02x", c);
93 o += 2;
94 }
95 }
96 snprintf(buf + o, sizeof(buf) - o, "C40%s", pkg->version);
97 /* ^^^ 0x40 is the at sign (@) */
98 return buf;
99 }
100
101 static char *
sbom_name(pkgconf_pkg_t * world)102 sbom_name(pkgconf_pkg_t *world)
103 {
104 pkgconf_buffer_t name = PKGCONF_BUFFER_INITIALIZER;
105 pkgconf_node_t *node;
106
107 pkgconf_buffer_append(&name, "SBOM-SPDX");
108
109 PKGCONF_FOREACH_LIST_ENTRY(world->required.head, node)
110 {
111 pkgconf_dependency_t *dep = node->data;
112 pkgconf_pkg_t *match = dep->match;
113
114 if ((dep->flags & PKGCONF_PKG_DEPF_QUERY) != PKGCONF_PKG_DEPF_QUERY)
115 continue;
116
117 if (!dep->match)
118 continue;
119
120 pkgconf_buffer_append_fmt(&name, "-%s", sbom_spdx_identity(match));
121 }
122
123 return pkgconf_buffer_freeze(&name);
124 }
125
126 static bool
write_sbom_header(pkgconf_client_t * client,pkgconf_pkg_t * world)127 write_sbom_header(pkgconf_client_t *client, pkgconf_pkg_t *world)
128 {
129 time_t t;
130 struct tm *tm;
131 char buf[21];
132
133 OUTPUT_OR_RET_FALSE(client, sbom_out, "SPDXVersion: %s\n", spdx_version);
134 OUTPUT_OR_RET_FALSE(client, sbom_out, "DataLicense: %s\n", bom_license);
135 OUTPUT_OR_RET_FALSE(client, sbom_out, "SPDXID: %s\n", document_ref);
136
137 char *docname = sbom_name(world);
138 if (!docname)
139 {
140 pkgconf_error(client, "write_sbom_header: out of memory");
141 return false;
142 }
143
144 if (!pkgconf_output_file_fmt(sbom_out, "DocumentName: %s\n", docname))
145 {
146 free(docname);
147 return false;
148 }
149
150 free(docname);
151
152 OUTPUT_OR_RET_FALSE(client, sbom_out, "DocumentNamespace: https://spdx.org/spdxdocs/bomtool\n");
153 OUTPUT_OR_RET_FALSE(client, sbom_out, "Creator: Tool: bomtool\n");
154
155 if (creation_time != NULL)
156 {
157 OUTPUT_OR_RET_FALSE(client, sbom_out, "Created: %s\n", creation_time);
158 }
159 else
160 {
161 const char *source_date_epoch = getenv("SOURCE_DATE_EPOCH");
162
163 if (source_date_epoch != NULL && *source_date_epoch != '\0')
164 t = (time_t) strtoll(source_date_epoch, NULL, 10);
165 else
166 t = time(NULL);
167
168 tm = gmtime(&t);
169 strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", tm);
170 OUTPUT_OR_RET_FALSE(client, sbom_out, "Created: %s\n", buf);
171 }
172
173 OUTPUT_OR_RET_FALSE(client, sbom_out, "\n\n");
174
175 return true;
176 }
177
178 static const char *
sbom_identity(pkgconf_pkg_t * pkg)179 sbom_identity(pkgconf_pkg_t *pkg)
180 {
181 static char buf[PKGCONF_ITEM_SIZE];
182
183 snprintf(buf, sizeof buf, "%s@%s", pkg->id, pkg->version);
184
185 return buf;
186 }
187
188 static bool
write_copyright_lines(pkgconf_client_t * client,const pkgconf_list_t * copyright_lines)189 write_copyright_lines(pkgconf_client_t *client, const pkgconf_list_t *copyright_lines)
190 {
191 const pkgconf_node_t *node;
192
193 if (copyright_lines->head == NULL) {
194 OUTPUT_OR_RET_FALSE(client, sbom_out, "PackageCopyrightText: NOASSERTION\n");
195 return true;
196 }
197
198 OUTPUT_OR_RET_FALSE(client, sbom_out, "PackageCopyrightText: <text>");
199
200 PKGCONF_FOREACH_LIST_ENTRY(copyright_lines->head, node)
201 {
202 const pkgconf_bufferset_t *set = node->data;
203 OUTPUT_OR_RET_FALSE(client, sbom_out, "%s%s", pkgconf_buffer_str_or_empty(&set->buffer), node->prev != NULL ? "\n" : "");
204 }
205
206 OUTPUT_OR_RET_FALSE(client, sbom_out, "</text>\n");
207
208 return true;
209 }
210
211 static void
write_sbom_package(pkgconf_client_t * client,pkgconf_pkg_t * pkg,void * unused)212 write_sbom_package(pkgconf_client_t *client, pkgconf_pkg_t *pkg, void *unused)
213 {
214 pkgconf_buffer_t license_buf = PKGCONF_BUFFER_INITIALIZER;
215 (void) client;
216 (void) unused;
217
218 if (pkg->flags & PKGCONF_PKG_PROPF_VIRTUAL)
219 return;
220
221 OUTPUT_OR_RET(client, sbom_out, "##### Package: %s\n\n", sbom_identity(pkg));
222 OUTPUT_OR_RET(client, sbom_out, "PackageName: %s\n", sbom_identity(pkg));
223 OUTPUT_OR_RET(client, sbom_out, "SPDXID: SPDXRef-Package-%s\n", sbom_spdx_identity(pkg));
224 OUTPUT_OR_RET(client, sbom_out, "PackageVersion: %s\n", pkg->version);
225 OUTPUT_OR_RET(client, sbom_out, "PackageDownloadLocation: NOASSERTION\n");
226
227 /* NOASSERTION is not a valide value for PackageVerificationCode. It
228 * expect 40 lowercase hexadecimal digits.
229 */
230 #if 0
231 OUTPUT_OR_RET(client, sbom_out, "PackageVerificationCode: NOASSERTION\n");
232 #endif
233
234 /* XXX: What about projects? */
235 if (pkg->maintainer != NULL)
236 OUTPUT_OR_RET(client, sbom_out, "PackageSupplier: Person: %s\n", pkg->maintainer);
237
238 if (pkg->url != NULL)
239 OUTPUT_OR_RET(client, sbom_out, "PackageHomePage: %s\n", pkg->url);
240
241 if (pkg->license.head != NULL)
242 {
243 pkgconf_license_render(client, &pkg->license, &license_buf);
244 bool ret = pkgconf_output_file_fmt(sbom_out, "PackageLicenseDeclared: %s\n", pkgconf_buffer_str_or_empty(&license_buf));
245 int errno_save = errno;
246 pkgconf_buffer_finalize(&license_buf);
247 if (!ret)
248 {
249 pkgconf_error(client, "bomtool: could not output to file: %s", strerror(errno_save));
250 return;
251 }
252 }
253 else
254 OUTPUT_OR_RET(client, sbom_out, "PackageLicenseDeclared: NOASSERTION\n");
255 OUTPUT_OR_RET(client, sbom_out, "PackageLicenseConcluded: NOASSERTION\n");
256
257 if (!write_copyright_lines(client, &pkg->copyright))
258 return;
259
260 if (pkg->description != NULL)
261 OUTPUT_OR_RET(client, sbom_out, "PackageSummary: <text>%s</text>\n", pkg->description);
262
263 if (pkg->source != NULL)
264 OUTPUT_OR_RET(client, sbom_out, "PackageDownloadLocation: %s\n", pkg->source);
265 else
266 OUTPUT_OR_RET(client, sbom_out, "PackageDownloadLocation: NOASSERTION\n");
267
268 OUTPUT_OR_RET(client, sbom_out, "\n\n");
269 }
270
271 static void
write_sbom_relationships(pkgconf_client_t * client,pkgconf_pkg_t * pkg,void * unused)272 write_sbom_relationships(pkgconf_client_t *client, pkgconf_pkg_t *pkg, void *unused)
273 {
274 (void) client;
275 (void) unused;
276
277 char baseref[PKGCONF_ITEM_SIZE];
278 pkgconf_node_t *node;
279
280 if (pkg->flags & PKGCONF_PKG_PROPF_VIRTUAL)
281 return;
282
283 snprintf(baseref, sizeof baseref, "SPDXRef-Package-%s", sbom_spdx_identity(pkg));
284
285 PKGCONF_FOREACH_LIST_ENTRY(pkg->required.head, node)
286 {
287 pkgconf_dependency_t *dep = node->data;
288 pkgconf_pkg_t *match = dep->match;
289
290 if (!dep->match)
291 continue;
292
293 OUTPUT_OR_RET(client, sbom_out, "Relationship: %s DEPENDS_ON SPDXRef-Package-%s\n", baseref, sbom_spdx_identity(match));
294 OUTPUT_OR_RET(client, sbom_out, "Relationship: SPDXRef-Package-%s DEPENDENCY_OF %s\n", sbom_spdx_identity(match), baseref);
295 }
296
297 PKGCONF_FOREACH_LIST_ENTRY(pkg->requires_private.head, node)
298 {
299 pkgconf_dependency_t *dep = node->data;
300 pkgconf_pkg_t *match = dep->match;
301
302 if (!dep->match)
303 continue;
304
305 OUTPUT_OR_RET(client, sbom_out, "Relationship: %s DEPENDS_ON SPDXRef-Package-%s\n", baseref, sbom_spdx_identity(match));
306 OUTPUT_OR_RET(client, sbom_out, "Relationship: SPDXRef-Package-%s DEV_DEPENDENCY_OF %s\n", sbom_spdx_identity(match), baseref);
307 }
308
309 if (pkg->required.head != NULL || pkg->requires_private.head != NULL)
310 OUTPUT_OR_RET(client, sbom_out, "\n\n");
311 }
312
313 static bool
generate_sbom_from_world(pkgconf_client_t * client,pkgconf_pkg_t * world)314 generate_sbom_from_world(pkgconf_client_t *client, pkgconf_pkg_t *world)
315 {
316 int eflag;
317 pkgconf_node_t *node;
318
319 if (!write_sbom_header(client, world))
320 return false;
321
322 eflag = pkgconf_pkg_traverse(client, world, write_sbom_package, NULL, maximum_traverse_depth, 0);
323 if (eflag != PKGCONF_PKG_ERRF_OK)
324 return false;
325
326 eflag = pkgconf_pkg_traverse(client, world, write_sbom_relationships, NULL, maximum_traverse_depth, 0);
327 if (eflag != PKGCONF_PKG_ERRF_OK)
328 return false;
329
330 PKGCONF_FOREACH_LIST_ENTRY(world->required.head, node)
331 {
332 pkgconf_dependency_t *dep = node->data;
333 pkgconf_pkg_t *match = dep->match;
334
335 if (!dep->match)
336 continue;
337
338 OUTPUT_OR_RET_FALSE(client, sbom_out, "Relationship: %s DESCRIBES SPDXRef-Package-%s\n", document_ref, sbom_spdx_identity(match));
339 }
340
341 return true;
342 }
343
344 static int
version(void)345 version(void)
346 {
347 printf("bomtool %s\n", PACKAGE_VERSION);
348 return EXIT_SUCCESS;
349 }
350
351 static int
about(void)352 about(void)
353 {
354 printf("bomtool (%s %s)\n", PACKAGE_NAME, PACKAGE_VERSION);
355 printf("Copyright (c) 2011-2026 pkgconf authors (see AUTHORS in documentation directory)\n\n");
356 printf("Permission to use, copy, modify, and/or distribute this software for any\n");
357 printf("purpose with or without fee is hereby granted, provided that the above\n");
358 printf("copyright notice and this permission notice appear in all copies.\n\n");
359 printf("This software is provided 'as is' and without any warranty, express or\n");
360 printf("implied. In no event shall the authors be liable for any damages arising\n");
361 printf("from the use of this software.\n\n");
362 printf("Report bugs at <%s>.\n", PACKAGE_BUGREPORT);
363 return EXIT_SUCCESS;
364 }
365
366 static int
usage(void)367 usage(void)
368 {
369 printf("usage: bomtool [--flags] [modules]\n");
370
371 printf("\nbasic options:\n\n");
372
373 printf(" --help this message\n");
374 printf(" --about print bomtool version and license to stdout\n");
375 printf(" --version print bomtool version to stdout\n");
376 printf(" --output FILE output SBOM text to FILE\n");
377 printf(" --define-variable=varname=value define variable 'varname' as 'value'\n");
378 printf(" --creation-time Use string as creation time (Should be in ISO8601 format) [default: current time]\n");
379
380 return EXIT_SUCCESS;
381 }
382
383 int
main(int argc,char * argv[])384 main(int argc, char *argv[])
385 {
386 int ret = EXIT_SUCCESS;
387 pkgconf_list_t pkgq = PKGCONF_LIST_INITIALIZER;
388 unsigned int want_client_flags = PKGCONF_PKG_PKGF_SEARCH_PRIVATE;
389 pkgconf_cross_personality_t *personality = pkgconf_cross_personality_default();
390 pkgconf_pkg_t world = {
391 .id = "virtual:world",
392 .realname = "virtual world package",
393 .flags = PKGCONF_PKG_PROPF_STATIC | PKGCONF_PKG_PROPF_VIRTUAL,
394 };
395
396 error_msgout = stderr;
397 sbom_out = stdout;
398
399 struct pkg_option options[] = {
400 { "version", no_argument, &want_flags, PKG_VERSION, },
401 { "about", no_argument, &want_flags, PKG_ABOUT, },
402 { "help", no_argument, &want_flags, PKG_HELP, },
403 { "output", required_argument, NULL, PKG_OUTPUT, },
404 { "define-variable", required_argument, NULL, PKG_DEFINE_VARIABLE, },
405 { "creation-time", required_argument, NULL, PKG_CREATION_TIME, },
406 { NULL, 0, NULL, 0 }
407 };
408
409 while ((ret = pkg_getopt_long_only(argc, argv, "", options, NULL)) != -1)
410 {
411 switch (ret)
412 {
413 case PKG_OUTPUT:
414 sbom_out = fopen(pkg_optarg, "w");
415 if (sbom_out == NULL)
416 {
417 pkgconf_output_file_fmt(stderr, "unable to open %s: %s\n", pkg_optarg, strerror(errno));
418 return EXIT_FAILURE;
419 }
420
421 break;
422 case PKG_DEFINE_VARIABLE:
423 pkgconf_tuple_define_global(&pkg_client, pkg_optarg);
424 break;
425 case PKG_CREATION_TIME:
426 creation_time = pkg_optarg;
427 break;
428 case '?':
429 case ':':
430 return EXIT_FAILURE;
431 default:
432 break;
433 }
434 }
435
436 pkgconf_client_init(&pkg_client, error_handler, NULL, personality, NULL, environ_lookup_handler);
437
438 /* we have determined what features we want most likely. in some cases, we override later. */
439 pkgconf_client_set_flags(&pkg_client, want_client_flags);
440
441 /* at this point, want_client_flags should be set, so build the dir list */
442 pkgconf_client_dir_list_build(&pkg_client, personality);
443
444 if ((want_flags & PKG_ABOUT) == PKG_ABOUT)
445 return about();
446
447 if ((want_flags & PKG_VERSION) == PKG_VERSION)
448 return version();
449
450 if ((want_flags & PKG_HELP) == PKG_HELP)
451 return usage();
452
453 /* Join the remaining arguments into a single query string, as the main
454 * pkgconf CLI does, and let the dependency parser handle module names,
455 * comparison operators and versions.
456 */
457 pkgconf_buffer_t queryparams = PKGCONF_BUFFER_INITIALIZER;
458
459 while (pkg_optind < argc && argv[pkg_optind] != NULL)
460 {
461 if (pkgconf_buffer_len(&queryparams) > 0)
462 pkgconf_buffer_push_byte(&queryparams, ' ');
463
464 pkgconf_buffer_append(&queryparams, argv[pkg_optind]);
465 pkg_optind++;
466 }
467
468 if (pkgconf_buffer_len(&queryparams) > 0)
469 pkgconf_queue_push(&pkgq, pkgconf_buffer_str(&queryparams));
470
471 pkgconf_buffer_finalize(&queryparams);
472
473 if (pkgq.head == NULL)
474 {
475 pkgconf_output_file_fmt(stderr, "Please specify at least one package name on the command line.\n");
476 ret = EXIT_FAILURE;
477 goto out;
478 }
479
480 ret = EXIT_SUCCESS;
481
482 if (!pkgconf_queue_solve(&pkg_client, &pkgq, &world, maximum_traverse_depth))
483 {
484 ret = EXIT_FAILURE;
485 goto out;
486 }
487
488 if (!generate_sbom_from_world(&pkg_client, &world))
489 {
490 ret = EXIT_FAILURE;
491 goto out;
492 }
493
494 out:
495 if (sbom_out != stdout)
496 fclose(sbom_out);
497
498 pkgconf_solution_free(&pkg_client, &world);
499 pkgconf_queue_free(&pkgq);
500 pkgconf_cross_personality_deinit(personality);
501 pkgconf_client_deinit(&pkg_client);
502
503 return ret;
504 }
505