xref: /freebsd/contrib/pkgconf/cli/bomtool/main.c (revision a259b98fa211ed87bfee58c575de4e2de94ee0fa)
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 *
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
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 *
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 *
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
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 *
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
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
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
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
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
345 version(void)
346 {
347 	printf("bomtool %s\n", PACKAGE_VERSION);
348 	return EXIT_SUCCESS;
349 }
350 
351 static int
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
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
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