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::{Path, PathBuf};
8
9use anyhow::Result;
10
11use crate::parsers::parse_sbom_str;
12use crate::pipeline::{exit_codes, read_input};
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: &Path,
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    // Shared '-'-aware input path (same as view/validate): supports piping an
34    // SBOM via stdin and gives contextful errors instead of a raw ENOENT.
35    let raw_json = read_input(file)?;
36    let mut sbom = parse_sbom_str(&raw_json)?;
37
38    if preserve {
39        emit::preserve_source_json(&raw_json, &mut sbom);
40    }
41
42    let (output, report) = match emit::emit(&sbom, emit_target) {
43        Ok(result) => result,
44        Err(EmitError::Unsupported(fmt)) => {
45            eprintln!("error: emitting to {fmt} is not yet implemented.");
46            return Ok(exit_codes::ERROR);
47        }
48        Err(e) => return Err(e.into()),
49    };
50
51    // Fidelity report always goes to stderr so it never pollutes piped output.
52    if !quiet {
53        eprint!("{}", report.render());
54        if report.is_lossy() {
55            eprintln!(
56                "  Note: conversion is lossy ({} field type(s) dropped). Re-run with --preserve to retain format-specific blocks where possible.",
57                report.dropped_count()
58            );
59        }
60    }
61
62    match output_file {
63        Some(path) => {
64            std::fs::write(path, &output)?;
65            if !quiet {
66                eprintln!("Converted SBOM written to {}", path.display());
67            }
68        }
69        None => {
70            println!("{output}");
71        }
72    }
73
74    Ok(exit_codes::SUCCESS)
75}