use std::path::Path;
use csaf_models::csaf_document::CsafDocument;
use crate::error::{CsafError, Result};
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: &Path, storage: &CsafStorage) -> Result<ImportResult> {
if !import_dir.exists() {
return Err(CsafError::Import(format!(
"Import directory does not exist: {}",
import_dir.display()
)));
}
let mut result = ImportResult::default();
import_recursive(import_dir, storage, &mut result)?;
tracing::info!(
imported = result.imported,
skipped = result.skipped,
errors = result.errors.len(),
"Import completed"
);
Ok(result)
}
#[allow(clippy::cognitive_complexity)]
fn import_recursive(dir: &Path, storage: &CsafStorage, result: &mut ImportResult) -> Result<()> {
let entries = std::fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
import_recursive(&path, storage, result)?;
continue;
}
if path.extension().is_some_and(|e| e == "json") {
if path
.file_name()
.is_some_and(|f| f == "provider-metadata.json")
{
continue;
}
match import_single_file(&path, storage) {
Ok(()) => {
result.imported += 1;
},
Err(e) => {
let msg = format!("{}: {e}", path.display());
tracing::warn!(path = %path.display(), error = %e, "Skipping file");
result.errors.push(msg);
result.skipped += 1;
},
}
}
}
Ok(())
}
fn import_single_file(path: &Path, storage: &CsafStorage) -> Result<()> {
let content = std::fs::read_to_string(path)?;
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("; ")));
}
storage.put_document(&doc)?;
tracing::info!(
tracking_id = doc.tracking_id(),
path = %path.display(),
"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);
}
}