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