use crate::lib_utils::errors::Vex2PdfError;
use cyclonedx_bom::errors::{BomError, JsonReadError, XmlReadError};
use cyclonedx_bom::prelude::Bom;
use log::warn;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
pub const fn get_version_info() -> &'static str {
concat!(
"vex2pdf ",
env!("CARGO_PKG_VERSION"),
" - CycloneDX (VEX) to PDF Converter\n",
"Copyright (c) 2025 Salem B. - MIT Or Apache 2.0 License"
)
}
pub(crate) fn parse_vex_xml<P: AsRef<Path>>(path: P) -> Result<Bom, Box<dyn Error>> {
let content = fs::read(path)?;
match Bom::parse_from_xml_v1_5(&content[..]) {
Ok(bom) => Ok(bom),
Err(err) => match &err {
XmlReadError::InvalidNamespaceError {
expected_namespace,
actual_namespace,
} => {
if let Some(actual) = actual_namespace {
if actual.contains("1.6") {
print_downgrade_warning();
let xml_str = std::string::String::from_utf8_lossy(&content);
let modified_xml = xml_str.replace(actual, expected_namespace);
return Ok(Bom::parse_from_xml_v1_5(modified_xml.as_bytes())?);
}
}
Err(Box::new(err))
}
_ => Err(Box::new(err)),
},
}
}
pub(crate) fn parse_vex_json<P: AsRef<Path>>(path: P) -> Result<Bom, Box<dyn Error>> {
let content = fs::read(path)?;
match Bom::parse_from_json(&content[..]) {
Ok(bom) => Ok(bom),
Err(err) => match err {
JsonReadError::BomError { error } => {
match error {
BomError::UnsupportedSpecVersion(version) if version == "1.6" => {
let mut json_value: serde_json::Value = serde_json::from_slice(&content)?;
print_downgrade_warning();
json_value["specVersion"] = serde_json::Value::String("1.5".to_string());
Ok(Bom::parse_json_value(json_value)?)
}
_ => Err(JsonReadError::BomError { error }.into()),
}
}
_ => Err(err.into()),
},
}
}
fn print_downgrade_warning() {
warn!("");
warn!("NOTE: Downgrading CycloneDX BOM from spec version 1.6 to 1.5");
warn!("Reason: Current implementation does not yet fully support spec version 1.6");
warn!("Warning: This compatibility mode only works for BOMs that don't utilize 1.6-specific fields");
warn!(" Processing will fail if 1.6-specific fields are encountered");
warn!("");
}
pub fn get_output_pdf_path(
dest_dir: Option<&Path>,
file_path: &Path,
) -> Result<PathBuf, Vex2PdfError> {
let file_stem = file_path
.file_stem()
.ok_or_else(|| Vex2PdfError::InvalidFileStem(file_path.to_path_buf()))?;
let pdf_name = format!("{}.pdf", file_stem.to_string_lossy());
match dest_dir {
None => Ok(file_path.with_file_name(pdf_name)),
Some(out_dir) if out_dir.is_file() => {
Err(Vex2PdfError::InvalidOutputDir(out_dir.to_path_buf()))
}
Some(out_dir) => Ok(out_dir.join(&pdf_name)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile;
use tempfile::env::temp_dir;
use tempfile::NamedTempFile;
#[test]
fn test_path_build_no_dest_dir() {
let path = PathBuf::from("/tmp/file.json");
assert_eq!(
get_output_pdf_path(None, path.as_path()).unwrap(),
PathBuf::from("/tmp/file.pdf")
);
}
#[test]
fn test_path_build_with_dest_dir() {
let path = PathBuf::from("/tmp/file.json");
assert_eq!(
get_output_pdf_path(
Some(PathBuf::from("/tmp/test_path").as_path()),
path.as_path()
)
.unwrap(),
PathBuf::from("/tmp/test_path/file.pdf")
);
assert_eq!(
get_output_pdf_path(Some(PathBuf::from("/output/dir").as_path()), path.as_path())
.unwrap(),
PathBuf::from("/output/dir/file.pdf")
);
}
#[test]
fn test_path_build_real_dir_pdf() {
let path = PathBuf::from("/tmp/file.json");
let new_dest_path = temp_dir();
assert_eq!(
get_output_pdf_path(None, path.as_path()).unwrap(),
PathBuf::from("/tmp/file.pdf")
);
assert_eq!(
get_output_pdf_path(Some(new_dest_path.as_path()), path.as_path()).unwrap(),
new_dest_path.join("file.pdf")
);
}
#[test]
fn test_path_build_file_as_dest_dir_fails() {
let path = PathBuf::from("/tmp/file.json");
let fake_file = NamedTempFile::new().unwrap();
let result = get_output_pdf_path(Some(fake_file.path()), path.as_path());
assert!(result.is_err());
}
}