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