#[cfg(feature = "enrichment")]
mod cache;
mod convert;
mod cra_docs;
mod cra_standards_watch;
mod diff;
#[cfg(feature = "enrichment")]
mod enrich;
mod license_check;
mod merge;
mod multi;
mod quality;
mod query;
mod tailor;
mod validate;
mod verify;
mod vex;
mod view;
mod watch;
#[cfg(feature = "enrichment")]
pub use cache::{CacheAction, run_cache};
pub use convert::run_convert;
pub use cra_docs::{run_cra_docs, run_cra_docs_with_force};
pub use cra_standards_watch::{
OnlineProbe, TrackedStandard, WatchOutputFormat, cra_catalogue, probe_cra_standards,
run_cra_standards_watch,
};
pub use diff::run_diff;
#[cfg(feature = "enrichment")]
pub use enrich::run_enrich;
pub use license_check::run_license_check;
pub use merge::run_merge;
pub use multi::{run_diff_multi, run_matrix, run_timeline};
pub use quality::{QUALITY_OUTPUT_FORMATS, run_quality};
pub use query::{QueryFilter, run_query};
pub use tailor::run_tailor;
pub use validate::{VALIDATE_OUTPUT_FORMATS, run_validate};
pub use verify::{VerifyAction, run_verify};
pub use vex::{VexAction, VexExportFormat, run_vex};
pub use view::run_view;
pub use watch::run_watch;
pub use crate::config::{DiffConfig, ViewConfig};
pub(crate) fn ensure_output_format_supported(
command: &str,
requested: crate::reports::ReportFormat,
supported: &[crate::reports::ReportFormat],
) -> anyhow::Result<()> {
if supported.contains(&requested) {
return Ok(());
}
anyhow::bail!(
"output format '{requested}' is not supported by `sbom-tools {command}`; \
supported formats: {}",
supported
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
pub(crate) fn load_cra_sidecar(
explicit: Option<&std::path::Path>,
sbom_path: &std::path::Path,
) -> anyhow::Result<Option<crate::model::CraSidecarMetadata>> {
match explicit {
Some(p) => crate::model::CraSidecarMetadata::from_file(p)
.map(Some)
.map_err(|e| anyhow::anyhow!("Failed to load CRA sidecar from {}: {e}", p.display())),
None => crate::model::CraSidecarMetadata::discover_for_sbom(sbom_path)
.map_err(|e| anyhow::anyhow!("Failed to load auto-discovered CRA sidecar: {e}")),
}
}
pub(crate) fn parse_cra_product_class(
value: Option<&str>,
) -> anyhow::Result<Option<crate::model::CraProductClass>> {
value
.map(|s| {
crate::model::CraProductClass::parse_cli_strict(s)
.map_err(|e| anyhow::anyhow!("invalid --cra-product-class: {e}"))
})
.transpose()
}
pub(crate) fn parse_as_of(raw: &str) -> anyhow::Result<chrono::DateTime<chrono::Utc>> {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(raw) {
return Ok(dt.with_timezone(&chrono::Utc));
}
if let Ok(ndt) = raw.parse::<chrono::NaiveDateTime>() {
return Ok(ndt.and_utc());
}
if let Ok(d) = raw.parse::<chrono::NaiveDate>() {
return Ok(d.and_hms_opt(0, 0, 0).expect("midnight is valid").and_utc());
}
anyhow::bail!(
"invalid --as-of {raw:?}: expected an RFC 3339 datetime \
(e.g. 2027-01-01T00:00:00Z), an offset-less datetime taken as UTC \
(e.g. 2027-01-01T00:00:00), or a date YYYY-MM-DD (midnight UTC)"
)
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
#[test]
fn parse_as_of_accepts_rfc3339_with_offset() {
let t = super::parse_as_of("2027-01-01T02:00:00+02:00").unwrap();
assert_eq!(t, Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap());
}
#[test]
fn parse_as_of_accepts_offsetless_datetime_as_utc() {
let t = super::parse_as_of("2027-01-01T12:30:00").unwrap();
assert_eq!(t, Utc.with_ymd_and_hms(2027, 1, 1, 12, 30, 0).unwrap());
}
#[test]
fn parse_as_of_accepts_bare_date_as_midnight_utc() {
let t = super::parse_as_of("2027-01-01").unwrap();
assert_eq!(t, Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap());
}
#[test]
fn parse_as_of_error_names_every_accepted_form() {
let err = super::parse_as_of("not-a-date").unwrap_err().to_string();
assert!(err.contains("not-a-date"));
assert!(err.contains("RFC 3339"), "must name RFC 3339: {err}");
assert!(
err.contains("offset-less") && err.contains("UTC"),
"must explain the offset-less form: {err}"
);
assert!(err.contains("YYYY-MM-DD"), "must name the date form: {err}");
}
#[test]
fn parse_cra_product_class_is_strict_and_lists_valid_values() {
assert_eq!(
super::parse_cra_product_class(Some("critical")).unwrap(),
Some(crate::model::CraProductClass::Critical)
);
assert_eq!(super::parse_cra_product_class(None).unwrap(), None);
let err = super::parse_cra_product_class(Some("critcal"))
.unwrap_err()
.to_string();
assert!(err.contains("critcal"), "must name the bad value: {err}");
for valid in [
"default",
"important-class-1",
"important-class-2",
"critical",
] {
assert!(err.contains(valid), "must list '{valid}': {err}");
}
}
}