use std::io::Write;
use anyhow::Result;
use serde::Serialize;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use tokmd_types::{ExportData, FileKind, RedactMode};
use super::redact_rows;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CycloneDxBom {
bom_format: &'static str,
spec_version: &'static str,
serial_number: String,
version: u32,
metadata: CycloneDxMetadata,
components: Vec<CycloneDxComponent>,
}
#[derive(Debug, Clone, Serialize)]
struct CycloneDxMetadata {
timestamp: String,
tools: Vec<CycloneDxTool>,
}
#[derive(Debug, Clone, Serialize)]
struct CycloneDxTool {
vendor: &'static str,
name: &'static str,
version: String,
}
#[derive(Debug, Clone, Serialize)]
struct CycloneDxComponent {
#[serde(rename = "type")]
ty: &'static str,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
group: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
properties: Vec<CycloneDxProperty>,
}
#[derive(Debug, Clone, Serialize)]
struct CycloneDxProperty {
name: String,
value: String,
}
pub(super) fn write_export_cyclonedx<W: Write>(
out: &mut W,
export: &ExportData,
redact: RedactMode,
) -> Result<()> {
write_export_cyclonedx_impl(out, export, redact, None, None)
}
pub(super) fn write_export_cyclonedx_impl<W: Write>(
out: &mut W,
export: &ExportData,
redact: RedactMode,
serial_number: Option<String>,
timestamp: Option<String>,
) -> Result<()> {
let timestamp = timestamp.unwrap_or_else(|| {
OffsetDateTime::now_utc()
.format(&Rfc3339)
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
});
let components: Vec<CycloneDxComponent> = redact_rows(&export.rows, redact)
.map(|row| {
let mut properties = vec![
CycloneDxProperty {
name: "tokmd:lang".to_string(),
value: row.lang.clone(),
},
CycloneDxProperty {
name: "tokmd:code".to_string(),
value: row.code.to_string(),
},
CycloneDxProperty {
name: "tokmd:comments".to_string(),
value: row.comments.to_string(),
},
CycloneDxProperty {
name: "tokmd:blanks".to_string(),
value: row.blanks.to_string(),
},
CycloneDxProperty {
name: "tokmd:lines".to_string(),
value: row.lines.to_string(),
},
CycloneDxProperty {
name: "tokmd:bytes".to_string(),
value: row.bytes.to_string(),
},
CycloneDxProperty {
name: "tokmd:tokens".to_string(),
value: row.tokens.to_string(),
},
];
if row.kind == FileKind::Child {
properties.push(CycloneDxProperty {
name: "tokmd:kind".to_string(),
value: "child".to_string(),
});
}
CycloneDxComponent {
ty: "file",
name: row.path.clone(),
group: if row.module.is_empty() {
None
} else {
Some(row.module.clone())
},
properties,
}
})
.collect();
let bom = CycloneDxBom {
bom_format: "CycloneDX",
spec_version: "1.6",
serial_number: serial_number
.unwrap_or_else(|| format!("urn:uuid:{}", uuid::Uuid::new_v4())),
version: 1,
metadata: CycloneDxMetadata {
timestamp,
tools: vec![CycloneDxTool {
vendor: "tokmd",
name: "tokmd",
version: env!("CARGO_PKG_VERSION").to_string(),
}],
},
components,
};
writeln!(out, "{}", serde_json::to_string_pretty(&bom)?)?;
Ok(())
}