Skip to main content

sbom_tools/cli/
convert.rs

1//! CLI handler for the `convert` command.
2//!
3//! Cross-format SBOM conversion: parses any supported input format and emits a
4//! single target format from the canonical model. A fidelity report is written
5//! to stderr describing synthesized and dropped fields (honest about lossiness).
6
7use std::path::PathBuf;
8
9use anyhow::Result;
10
11use crate::parsers::parse_sbom;
12use crate::pipeline::exit_codes;
13use crate::serialization::emit::{self, EmitError, EmitTarget};
14
15/// Run the convert command.
16///
17/// `target` is the raw `--to` value (e.g. "cyclonedx"). When `preserve` is set,
18/// each component's verbatim source JSON is captured before emission so
19/// format-specific blocks the canonical model can't fully reconstruct
20/// (`cryptoProperties`, `evidence`) are spliced back where present.
21pub fn run_convert(
22    file: &PathBuf,
23    target: &str,
24    output_file: Option<&PathBuf>,
25    preserve: bool,
26    quiet: bool,
27) -> Result<i32> {
28    let Some(emit_target) = EmitTarget::parse(target) else {
29        eprintln!("error: unknown conversion target '{target}'. Supported: cyclonedx, spdx.");
30        return Ok(exit_codes::ERROR);
31    };
32
33    let raw_json = std::fs::read_to_string(file)?;
34    let mut sbom = parse_sbom(file)?;
35
36    if preserve {
37        emit::preserve_source_json(&raw_json, &mut sbom);
38    }
39
40    let (output, report) = match emit::emit(&sbom, emit_target) {
41        Ok(result) => result,
42        Err(EmitError::Unsupported(fmt)) => {
43            eprintln!("error: emitting to {fmt} is not yet implemented.");
44            return Ok(exit_codes::ERROR);
45        }
46        Err(e) => return Err(e.into()),
47    };
48
49    // Fidelity report always goes to stderr so it never pollutes piped output.
50    if !quiet {
51        eprint!("{}", report.render());
52        if report.is_lossy() {
53            eprintln!(
54                "  Note: conversion is lossy ({} field type(s) dropped). Re-run with --preserve to retain format-specific blocks where possible.",
55                report.dropped_count()
56            );
57        }
58    }
59
60    match output_file {
61        Some(path) => {
62            std::fs::write(path, &output)?;
63            if !quiet {
64                eprintln!("Converted SBOM written to {}", path.display());
65            }
66        }
67        None => {
68            println!("{output}");
69        }
70    }
71
72    Ok(exit_codes::SUCCESS)
73}