memscope_rs/cli/commands/html_from_json/
json_file_discovery.rs1use std::error::Error;
7use std::fmt;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
13pub struct JsonFileConfig {
14 pub suffix: &'static str,
16 pub description: &'static str,
18 pub required: bool,
20 pub max_size_mb: Option<usize>,
22}
23
24impl JsonFileConfig {
25 pub fn new(suffix: &'static str, description: &'static str) -> Self {
27 Self {
28 suffix,
29 description,
30 required: false,
31 max_size_mb: Some(100), }
33 }
34
35 pub fn required(mut self) -> Self {
37 self.required = true;
38 self
39 }
40
41 pub fn max_size_mb(mut self, size: usize) -> Self {
43 self.max_size_mb = Some(size);
44 self
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct JsonFileInfo {
51 pub path: PathBuf,
53 pub config: JsonFileConfig,
55 pub size_bytes: u64,
57 pub is_readable: bool,
59}
60
61#[derive(Debug)]
63pub struct JsonDiscoveryResult {
64 pub found_files: Vec<JsonFileInfo>,
66 pub missing_required: Vec<JsonFileConfig>,
68 pub oversized_files: Vec<JsonFileInfo>,
70 pub unreadable_files: Vec<JsonFileInfo>,
72 pub total_size_bytes: u64,
74}
75
76#[derive(Debug)]
78pub enum JsonDiscoveryError {
79 DirectoryNotFound(String),
81 MissingRequiredFiles(Vec<String>),
83 FilesTooLarge(Vec<String>),
85 IoError(std::io::Error),
87}
88
89impl fmt::Display for JsonDiscoveryError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 JsonDiscoveryError::DirectoryNotFound(dir) => {
93 write!(f, "Directory not found or not accessible: {dir}")
94 }
95 JsonDiscoveryError::MissingRequiredFiles(files) => {
96 write!(f, "Missing required JSON files: {files:?}")
97 }
98 JsonDiscoveryError::FilesTooLarge(files) => {
99 write!(f, "Files exceed size limit: {files:?}")
100 }
101 JsonDiscoveryError::IoError(err) => {
102 write!(f, "IO error during file discovery: {err}")
103 }
104 }
105 }
106}
107
108impl Error for JsonDiscoveryError {}
109
110pub struct JsonFileDiscovery {
112 input_dir: String,
114 base_name: String,
116}
117
118impl JsonFileDiscovery {
119 pub fn new(input_dir: String, base_name: String) -> Self {
121 Self {
122 input_dir,
123 base_name,
124 }
125 }
126
127 pub fn get_default_file_configs() -> Vec<JsonFileConfig> {
129 vec![
130 JsonFileConfig::new("memory_analysis", "Memory Analysis").required(),
131 JsonFileConfig::new("lifetime", "Lifecycle Analysis"),
132 JsonFileConfig::new("unsafe_ffi", "Unsafe/FFI Analysis"),
133 JsonFileConfig::new("performance", "Performance Metrics"),
134 JsonFileConfig::new("complex_types", "Complex Types Analysis"),
135 JsonFileConfig::new("security_violations", "Security Violations"),
136 JsonFileConfig::new("variable_relationships", "Variable Relationships"),
137 ]
138 }
139
140 pub fn discover_files(&self) -> Result<JsonDiscoveryResult, JsonDiscoveryError> {
142 let input_path = Path::new(&self.input_dir);
144 if !input_path.exists() || !input_path.is_dir() {
145 return Err(JsonDiscoveryError::DirectoryNotFound(
146 self.input_dir.clone(),
147 ));
148 }
149
150 let file_configs = Self::get_default_file_configs();
151 let mut found_files = Vec::new();
152 let mut missing_required = Vec::new();
153 let mut oversized_files = Vec::new();
154 let mut unreadable_files = Vec::new();
155 let mut total_size_bytes = 0u64;
156
157 tracing::info!("🔍 Discovering JSON files in directory: {}", self.input_dir);
158 tracing::info!("🏷️ Using base name pattern: {}", self.base_name);
159
160 for config in file_configs {
161 match self.find_file_for_config(&config) {
162 Ok(Some(file_info)) => {
163 if let Some(max_size_mb) = config.max_size_mb {
165 let max_bytes = (max_size_mb * 1024 * 1024) as u64;
166 if file_info.size_bytes > max_bytes {
167 tracing::info!(
168 "⚠️ File {} ({:.1} MB) exceeds size limit ({} MB)",
169 file_info.path.display(),
170 file_info.size_bytes as f64 / 1024.0 / 1024.0,
171 max_size_mb
172 );
173 oversized_files.push(file_info);
174 continue;
175 }
176 }
177
178 if !file_info.is_readable {
180 tracing::info!("⚠️ File {} is not readable", file_info.path.display());
181 unreadable_files.push(file_info);
182 continue;
183 }
184
185 total_size_bytes += file_info.size_bytes;
186 tracing::info!(
187 "✅ Found {}: {} ({:.1} KB)",
188 config.description,
189 file_info.path.display(),
190 file_info.size_bytes as f64 / 1024.0
191 );
192 found_files.push(file_info);
193 }
194 Ok(None) => {
195 if config.required {
196 tracing::info!(
197 "❌ Required file not found: {}_{}*.json",
198 self.base_name,
199 config.suffix
200 );
201 missing_required.push(config);
202 } else {
203 tracing::info!(
204 "⚠️ Optional file not found: {}_{}*.json (skipping)",
205 self.base_name,
206 config.suffix
207 );
208 }
209 }
210 Err(e) => {
211 tracing::info!("❌ Error searching for {}: {}", config.description, e);
212 if config.required {
213 missing_required.push(config);
214 }
215 }
216 }
217 }
218
219 tracing::info!("📊 Discovery Summary:");
221 tracing::info!(" Files found: {}", found_files.len());
222 tracing::info!(
223 " Total size: {:.1} MB",
224 total_size_bytes as f64 / 1024.0 / 1024.0
225 );
226 tracing::info!(" Missing required: {}", missing_required.len());
227 tracing::info!(" Oversized files: {}", oversized_files.len());
228 tracing::info!(" Unreadable files: {}", unreadable_files.len());
229
230 if !missing_required.is_empty() {
232 let missing_names: Vec<String> = missing_required
233 .iter()
234 .map(|config| format!("{}_{}", self.base_name, config.suffix))
235 .collect();
236 return Err(JsonDiscoveryError::MissingRequiredFiles(missing_names));
237 }
238
239 if !oversized_files.is_empty() {
240 let oversized_names: Vec<String> = oversized_files
241 .iter()
242 .map(|file| file.path.to_string_lossy().to_string())
243 .collect();
244 return Err(JsonDiscoveryError::FilesTooLarge(oversized_names));
245 }
246
247 Ok(JsonDiscoveryResult {
248 found_files,
249 missing_required,
250 oversized_files,
251 unreadable_files,
252 total_size_bytes,
253 })
254 }
255
256 fn find_file_for_config(
258 &self,
259 config: &JsonFileConfig,
260 ) -> Result<Option<JsonFileInfo>, std::io::Error> {
261 let exact_path = format!(
263 "{}/{}_{}.json",
264 self.input_dir, self.base_name, config.suffix
265 );
266 if let Ok(file_info) = self.create_file_info(&exact_path, config) {
267 return Ok(Some(file_info));
268 }
269
270 let entries = fs::read_dir(&self.input_dir)?;
272 for entry in entries {
273 let entry = entry?;
274 let path = entry.path();
275
276 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
277 if file_name.contains(config.suffix) && file_name.ends_with(".json") {
278 if let Ok(file_info) = self.create_file_info(&path.to_string_lossy(), config) {
279 return Ok(Some(file_info));
280 }
281 }
282 }
283 }
284
285 Ok(None)
286 }
287
288 fn create_file_info(
290 &self,
291 file_path: &str,
292 config: &JsonFileConfig,
293 ) -> Result<JsonFileInfo, std::io::Error> {
294 let path = PathBuf::from(file_path);
295 let metadata = fs::metadata(&path)?;
296 let size_bytes = metadata.len();
297
298 let is_readable = fs::File::open(&path).is_ok();
300
301 Ok(JsonFileInfo {
302 path,
303 config: config.clone(),
304 size_bytes,
305 is_readable,
306 })
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use std::fs;
314 use tempfile::TempDir;
315
316 #[test]
317 fn test_json_file_config_creation() {
318 let config = JsonFileConfig::new("test", "Test File");
319 assert_eq!(config.suffix, "test");
320 assert_eq!(config.description, "Test File");
321 assert!(!config.required);
322 assert_eq!(config.max_size_mb, Some(100));
323 }
324
325 #[test]
326 fn test_json_file_config_required() {
327 let config = JsonFileConfig::new("test", "Test File").required();
328 assert!(config.required);
329 }
330
331 #[test]
332 fn test_json_file_config_max_size() {
333 let config = JsonFileConfig::new("test", "Test File").max_size_mb(50);
334 assert_eq!(config.max_size_mb, Some(50));
335 }
336
337 #[test]
338 fn test_directory_not_found() {
339 let discovery = JsonFileDiscovery::new("/nonexistent/path".to_string(), "test".to_string());
340 let result = discovery.discover_files();
341 assert!(matches!(
342 result,
343 Err(JsonDiscoveryError::DirectoryNotFound(_))
344 ));
345 }
346
347 #[test]
348 fn test_discover_files_with_temp_dir() {
349 let temp_dir = TempDir::new().expect("Failed to get test value");
350 let temp_path = temp_dir
351 .path()
352 .to_str()
353 .expect("Failed to convert path to string");
354
355 let test_file_path = format!("{temp_path}/test_memory_analysis.json");
357 fs::write(&test_file_path, r#"{"test": "data"}"#).expect("Failed to write test file");
358
359 let discovery = JsonFileDiscovery::new(temp_path.to_string(), "test".to_string());
360 let result = discovery
361 .discover_files()
362 .expect("Failed to discover files");
363
364 assert!(!result.found_files.is_empty());
365 assert_eq!(result.found_files[0].config.suffix, "memory_analysis");
366 }
367}