1#[cfg(feature = "enrichment")]
7mod cache;
8mod convert;
9mod cra_docs;
10mod cra_standards_watch;
11mod diff;
12#[cfg(feature = "enrichment")]
13mod enrich;
14mod license_check;
15mod merge;
16mod multi;
17mod quality;
18mod query;
19mod tailor;
20mod validate;
21mod verify;
22mod vex;
23mod view;
24mod watch;
25
26#[cfg(feature = "enrichment")]
27pub use cache::{CacheAction, run_cache};
28pub use convert::run_convert;
29pub use cra_docs::{run_cra_docs, run_cra_docs_with_force};
30pub use cra_standards_watch::{
31 OnlineProbe, TrackedStandard, WatchOutputFormat, cra_catalogue, probe_cra_standards,
32 run_cra_standards_watch,
33};
34pub use diff::run_diff;
35#[cfg(feature = "enrichment")]
36pub use enrich::run_enrich;
37pub use license_check::run_license_check;
38pub use merge::run_merge;
39pub use multi::{run_diff_multi, run_matrix, run_timeline};
40pub use quality::{QUALITY_OUTPUT_FORMATS, run_quality};
41pub use query::{QueryFilter, run_query};
42pub use tailor::run_tailor;
43pub use validate::{VALIDATE_OUTPUT_FORMATS, run_validate};
44pub use verify::{VerifyAction, run_verify};
45pub use vex::{VexAction, VexExportFormat, run_vex};
46pub use view::run_view;
47pub use watch::run_watch;
48
49pub use crate::config::{DiffConfig, ViewConfig};
51
52pub(crate) fn ensure_output_format_supported(
60 command: &str,
61 requested: crate::reports::ReportFormat,
62 supported: &[crate::reports::ReportFormat],
63) -> anyhow::Result<()> {
64 if supported.contains(&requested) {
65 return Ok(());
66 }
67 anyhow::bail!(
68 "output format '{requested}' is not supported by `sbom-tools {command}`; \
69 supported formats: {}",
70 supported
71 .iter()
72 .map(std::string::ToString::to_string)
73 .collect::<Vec<_>>()
74 .join(", ")
75 )
76}
77
78pub(crate) fn load_cra_sidecar(
87 explicit: Option<&std::path::Path>,
88 sbom_path: &std::path::Path,
89) -> anyhow::Result<Option<crate::model::CraSidecarMetadata>> {
90 match explicit {
91 Some(p) => crate::model::CraSidecarMetadata::from_file(p)
92 .map(Some)
93 .map_err(|e| anyhow::anyhow!("Failed to load CRA sidecar from {}: {e}", p.display())),
94 None => crate::model::CraSidecarMetadata::discover_for_sbom(sbom_path)
95 .map_err(|e| anyhow::anyhow!("Failed to load auto-discovered CRA sidecar: {e}")),
96 }
97}
98
99pub(crate) fn parse_cra_product_class(
107 value: Option<&str>,
108) -> anyhow::Result<Option<crate::model::CraProductClass>> {
109 value
110 .map(|s| {
111 crate::model::CraProductClass::parse_cli_strict(s)
112 .map_err(|e| anyhow::anyhow!("invalid --cra-product-class: {e}"))
113 })
114 .transpose()
115}
116
117pub(crate) fn parse_as_of(raw: &str) -> anyhow::Result<chrono::DateTime<chrono::Utc>> {
125 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(raw) {
126 return Ok(dt.with_timezone(&chrono::Utc));
127 }
128 if let Ok(ndt) = raw.parse::<chrono::NaiveDateTime>() {
129 return Ok(ndt.and_utc());
130 }
131 if let Ok(d) = raw.parse::<chrono::NaiveDate>() {
132 return Ok(d.and_hms_opt(0, 0, 0).expect("midnight is valid").and_utc());
133 }
134 anyhow::bail!(
135 "invalid --as-of {raw:?}: expected an RFC 3339 datetime \
136 (e.g. 2027-01-01T00:00:00Z), an offset-less datetime taken as UTC \
137 (e.g. 2027-01-01T00:00:00), or a date YYYY-MM-DD (midnight UTC)"
138 )
139}
140
141#[cfg(test)]
142mod tests {
143 use chrono::{TimeZone, Utc};
144
145 #[test]
146 fn parse_as_of_accepts_rfc3339_with_offset() {
147 let t = super::parse_as_of("2027-01-01T02:00:00+02:00").unwrap();
148 assert_eq!(t, Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap());
149 }
150
151 #[test]
152 fn parse_as_of_accepts_offsetless_datetime_as_utc() {
153 let t = super::parse_as_of("2027-01-01T12:30:00").unwrap();
156 assert_eq!(t, Utc.with_ymd_and_hms(2027, 1, 1, 12, 30, 0).unwrap());
157 }
158
159 #[test]
160 fn parse_as_of_accepts_bare_date_as_midnight_utc() {
161 let t = super::parse_as_of("2027-01-01").unwrap();
162 assert_eq!(t, Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap());
163 }
164
165 #[test]
166 fn parse_as_of_error_names_every_accepted_form() {
167 let err = super::parse_as_of("not-a-date").unwrap_err().to_string();
168 assert!(err.contains("not-a-date"));
169 assert!(err.contains("RFC 3339"), "must name RFC 3339: {err}");
170 assert!(
171 err.contains("offset-less") && err.contains("UTC"),
172 "must explain the offset-less form: {err}"
173 );
174 assert!(err.contains("YYYY-MM-DD"), "must name the date form: {err}");
175 }
176
177 #[test]
178 fn parse_cra_product_class_is_strict_and_lists_valid_values() {
179 assert_eq!(
180 super::parse_cra_product_class(Some("critical")).unwrap(),
181 Some(crate::model::CraProductClass::Critical)
182 );
183 assert_eq!(super::parse_cra_product_class(None).unwrap(), None);
184 let err = super::parse_cra_product_class(Some("critcal"))
185 .unwrap_err()
186 .to_string();
187 assert!(err.contains("critcal"), "must name the bad value: {err}");
188 for valid in [
189 "default",
190 "important-class-1",
191 "important-class-2",
192 "critical",
193 ] {
194 assert!(err.contains(valid), "must list '{valid}': {err}");
195 }
196 }
197}