use std::path::Path;
use csaf_models::csaf_document::CsafDocument;
use crate::error::{CsafError, Result};
use crate::fs::DataDir;
use crate::storage::CsafStorage;
use crate::validation;
#[derive(Debug, Default)]
pub struct ImportResult {
pub imported: usize,
pub skipped: usize,
pub errors: Vec<String>,
}
pub fn import_directory(
import_dir: impl AsRef<Path>,
storage: &CsafStorage,
) -> Result<ImportResult> {
let import_dir = import_dir.as_ref();
if !import_dir.exists() {
return Err(CsafError::Import(format!(
"Import directory does not exist: {}",
import_dir.display()
)));
}
let data_dir = DataDir::open(import_dir)?;
let mut result = ImportResult::default();
for rel in data_dir.walk_files()? {
if !rel
.rsplit_once('.')
.is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("json"))
{
continue;
}
let filename = rel.rsplit('/').next().unwrap_or(rel.as_str());
if filename == "provider-metadata.json" {
continue;
}
match import_single_file(&data_dir, &rel, storage) {
Ok(()) => {
result.imported += 1;
},
Err(e) => {
let msg = format!("{rel}: {e}");
tracing::warn!(path = %rel, error = %e, "Skipping file");
result.errors.push(msg);
result.skipped += 1;
},
}
}
tracing::info!(
imported = result.imported,
skipped = result.skipped,
errors = result.errors.len(),
"Import completed"
);
Ok(result)
}
fn import_single_file(dir: &DataDir, rel: &str, storage: &CsafStorage) -> Result<()> {
let content = dir.read_to_string(rel)?;
let doc: CsafDocument =
serde_json::from_str(&content).map_err(|e| CsafError::Import(e.to_string()))?;
let errors = validation::validate(&doc);
let hard_errors: Vec<_> = errors
.iter()
.filter(|e| e.severity == validation::Severity::Error)
.collect();
if !hard_errors.is_empty() {
let messages: Vec<String> = hard_errors.iter().map(|e| e.message.clone()).collect();
return Err(CsafError::Validation(messages.join("; ")));
}
#[cfg(feature = "oasis-validator")]
if let Ok(findings) = crate::oasis::validate_oasis_json(&content) {
let oasis_errors = findings
.iter()
.filter(|e| e.severity == validation::Severity::Error)
.count();
if oasis_errors > 0 {
tracing::warn!(
tracking_id = doc.tracking_id(),
oasis_errors,
"Imported advisory has OASIS conformance errors (admitted; \
a re-export would be refused until they are fixed)"
);
}
}
storage.put_document(&doc)?;
tracing::info!(
tracking_id = doc.tracking_id(),
path = %rel,
"Imported CSAF document"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_import_test_directory() {
let storage = CsafStorage::open_temp().expect("open failed");
let test_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test/csaf");
let result = import_directory(&test_dir, &storage).expect("import failed");
assert!(
result.imported >= 15,
"Expected at least 15 imports, got {}",
result.imported
);
assert_eq!(
result.skipped, 0,
"No files should be skipped: {:?}",
result.errors
);
}
#[test]
fn test_import_nonexistent_directory() {
let storage = CsafStorage::open_temp().expect("open failed");
let result = import_directory(Path::new("/nonexistent/path"), &storage);
assert!(result.is_err());
}
#[test]
fn test_import_empty_directory() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let storage = CsafStorage::open_temp().expect("open failed");
let result = import_directory(dir.path(), &storage).expect("import failed");
assert_eq!(result.imported, 0);
assert_eq!(result.skipped, 0);
}
#[test]
fn test_import_invalid_json() {
let dir = tempfile::tempdir().expect("tmpdir failed");
std::fs::write(dir.path().join("bad.json"), "not valid json").expect("write failed");
let storage = CsafStorage::open_temp().expect("open failed");
let result = import_directory(dir.path(), &storage).expect("import failed");
assert_eq!(result.imported, 0);
assert_eq!(result.skipped, 1);
assert!(!result.errors.is_empty());
}
#[test]
fn test_import_skips_provider_metadata() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let meta = include_str!("../../../test/csaf/provider-metadata.json");
std::fs::write(dir.path().join("provider-metadata.json"), meta).expect("write failed");
let storage = CsafStorage::open_temp().expect("open failed");
let result = import_directory(dir.path(), &storage).expect("import failed");
assert_eq!(result.imported, 0);
assert_eq!(result.skipped, 0);
}
}