1use std::path::Path;
7
8use csaf_models::csaf_document::CsafDocument;
9
10use crate::error::{CsafError, Result};
11use crate::storage::CsafStorage;
12use crate::validation;
13
14#[derive(Debug, Default)]
16pub struct ImportResult {
17 pub imported: usize,
19 pub skipped: usize,
21 pub errors: Vec<String>,
23}
24
25pub fn import_directory(import_dir: &Path, storage: &CsafStorage) -> Result<ImportResult> {
34 if !import_dir.exists() {
35 return Err(CsafError::Import(format!(
36 "Import directory does not exist: {}",
37 import_dir.display()
38 )));
39 }
40
41 let mut result = ImportResult::default();
42 import_recursive(import_dir, storage, &mut result)?;
43
44 tracing::info!(
45 imported = result.imported,
46 skipped = result.skipped,
47 errors = result.errors.len(),
48 "Import completed"
49 );
50
51 Ok(result)
52}
53
54#[allow(clippy::cognitive_complexity)]
62fn import_recursive(dir: &Path, storage: &CsafStorage, result: &mut ImportResult) -> Result<()> {
63 let entries = std::fs::read_dir(dir)?;
64
65 for entry in entries {
66 let entry = entry?;
67 let path = entry.path();
68
69 if path.is_dir() {
70 import_recursive(&path, storage, result)?;
71 continue;
72 }
73
74 if path.extension().is_some_and(|e| e == "json") {
75 if path
77 .file_name()
78 .is_some_and(|f| f == "provider-metadata.json")
79 {
80 continue;
81 }
82
83 match import_single_file(&path, storage) {
84 Ok(()) => {
85 result.imported += 1;
86 },
87 Err(e) => {
88 let msg = format!("{}: {e}", path.display());
89 tracing::warn!(path = %path.display(), error = %e, "Skipping file");
90 result.errors.push(msg);
91 result.skipped += 1;
92 },
93 }
94 }
95 }
96
97 Ok(())
98}
99
100fn import_single_file(path: &Path, storage: &CsafStorage) -> Result<()> {
102 let content = std::fs::read_to_string(path)?;
103 let doc: CsafDocument =
104 serde_json::from_str(&content).map_err(|e| CsafError::Import(e.to_string()))?;
105
106 let errors = validation::validate(&doc);
108 let hard_errors: Vec<_> = errors
109 .iter()
110 .filter(|e| e.severity == validation::Severity::Error)
111 .collect();
112
113 if !hard_errors.is_empty() {
114 let messages: Vec<String> = hard_errors.iter().map(|e| e.message.clone()).collect();
115 return Err(CsafError::Validation(messages.join("; ")));
116 }
117
118 storage.put_document(&doc)?;
119
120 tracing::info!(
121 tracking_id = doc.tracking_id(),
122 path = %path.display(),
123 "Imported CSAF document"
124 );
125
126 Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_import_test_directory() {
135 let storage = CsafStorage::open_temp().expect("open failed");
136 let test_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test/csaf");
137
138 let result = import_directory(&test_dir, &storage).expect("import failed");
139
140 assert!(
141 result.imported >= 15,
142 "Expected at least 15 imports, got {}",
143 result.imported
144 );
145 assert_eq!(
146 result.skipped, 0,
147 "No files should be skipped: {:?}",
148 result.errors
149 );
150 }
151
152 #[test]
153 fn test_import_nonexistent_directory() {
154 let storage = CsafStorage::open_temp().expect("open failed");
155 let result = import_directory(Path::new("/nonexistent/path"), &storage);
156 assert!(result.is_err());
157 }
158
159 #[test]
160 fn test_import_empty_directory() {
161 let dir = tempfile::tempdir().expect("tmpdir failed");
162 let storage = CsafStorage::open_temp().expect("open failed");
163
164 let result = import_directory(dir.path(), &storage).expect("import failed");
165 assert_eq!(result.imported, 0);
166 assert_eq!(result.skipped, 0);
167 }
168
169 #[test]
170 fn test_import_invalid_json() {
171 let dir = tempfile::tempdir().expect("tmpdir failed");
172 std::fs::write(dir.path().join("bad.json"), "not valid json").expect("write failed");
173
174 let storage = CsafStorage::open_temp().expect("open failed");
175 let result = import_directory(dir.path(), &storage).expect("import failed");
176
177 assert_eq!(result.imported, 0);
178 assert_eq!(result.skipped, 1);
179 assert!(!result.errors.is_empty());
180 }
181
182 #[test]
183 fn test_import_skips_provider_metadata() {
184 let dir = tempfile::tempdir().expect("tmpdir failed");
185 let meta = include_str!("../../../test/csaf/provider-metadata.json");
186 std::fs::write(dir.path().join("provider-metadata.json"), meta).expect("write failed");
187
188 let storage = CsafStorage::open_temp().expect("open failed");
189 let result = import_directory(dir.path(), &storage).expect("import failed");
190
191 assert_eq!(result.imported, 0);
192 assert_eq!(result.skipped, 0);
193 }
194}