1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use torsh_core::error::Result;
9
10use crate::package::Package;
11use crate::utils::validate_package_metadata;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DiagnosticReport {
16 pub health_score: u8,
18 pub status: HealthStatus,
20 pub issues: Vec<DiagnosticIssue>,
22 pub statistics: PackageStatistics,
24 pub metadata_validation: ValidationResult,
26 pub resource_validation: Vec<ResourceValidation>,
28 pub security: SecurityAssessment,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum HealthStatus {
35 Healthy,
37 Warning,
39 Degraded,
41 Critical,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DiagnosticIssue {
48 pub severity: IssueSeverity,
50 pub category: IssueCategory,
52 pub description: String,
54 pub recommendation: String,
56 pub affected: Vec<String>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
62pub enum IssueSeverity {
63 Info,
65 Low,
67 Medium,
69 High,
71 Critical,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub enum IssueCategory {
78 Metadata,
80 Resource,
82 Dependency,
84 Security,
86 Performance,
88 Compatibility,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct PackageStatistics {
95 pub total_size: u64,
97 pub resource_count: usize,
99 pub dependency_count: usize,
101 pub largest_resource_size: u64,
103 pub smallest_resource_size: u64,
105 pub average_resource_size: u64,
107 pub resource_types: HashMap<String, usize>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ValidationResult {
114 pub passed: bool,
116 pub messages: Vec<String>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ResourceValidation {
123 pub name: String,
125 pub valid: bool,
127 pub issues: Vec<String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SecurityAssessment {
134 pub is_signed: bool,
136 pub is_encrypted: bool,
138 pub issues: Vec<String>,
140 pub security_score: u8,
142}
143
144pub struct PackageDiagnostics {
146 pub strict_mode: bool,
148 pub check_security: bool,
150 pub check_performance: bool,
152}
153
154impl PackageDiagnostics {
155 pub fn new() -> Self {
157 Self {
158 strict_mode: false,
159 check_security: true,
160 check_performance: true,
161 }
162 }
163
164 pub fn diagnose(&self, package: &Package) -> Result<DiagnosticReport> {
166 let mut issues = Vec::new();
167
168 let metadata_validation = self.validate_metadata(package, &mut issues)?;
170
171 let resource_validation = self.validate_resources(package, &mut issues);
173
174 let security = if self.check_security {
176 self.assess_security(package, &mut issues)
177 } else {
178 SecurityAssessment {
179 is_signed: false,
180 is_encrypted: false,
181 issues: Vec::new(),
182 security_score: 50,
183 }
184 };
185
186 if self.check_performance {
188 self.check_performance_issues(package, &mut issues);
189 }
190
191 let statistics = self.calculate_statistics(package);
193
194 let health_score = self.calculate_health_score(&issues, &security);
196 let status = Self::determine_status(health_score);
197
198 issues.sort_by(|a, b| b.severity.cmp(&a.severity));
200
201 Ok(DiagnosticReport {
202 health_score,
203 status,
204 issues,
205 statistics,
206 metadata_validation,
207 resource_validation,
208 security,
209 })
210 }
211
212 fn validate_metadata(
214 &self,
215 package: &Package,
216 issues: &mut Vec<DiagnosticIssue>,
217 ) -> Result<ValidationResult> {
218 let mut messages = Vec::new();
219 let mut passed = true;
220
221 let name = package.name();
223 let version = package.get_version();
224
225 match validate_package_metadata(name, version, None) {
226 Ok(_) => messages.push("Package metadata is valid".to_string()),
227 Err(e) => {
228 passed = false;
229 messages.push(format!("Metadata validation failed: {}", e));
230 issues.push(DiagnosticIssue {
231 severity: IssueSeverity::High,
232 category: IssueCategory::Metadata,
233 description: format!("Invalid package metadata: {}", e),
234 recommendation: "Fix package name or version to comply with standards"
235 .to_string(),
236 affected: vec!["metadata".to_string()],
237 });
238 }
239 }
240
241 Ok(ValidationResult { passed, messages })
242 }
243
244 fn validate_resources(
246 &self,
247 package: &Package,
248 issues: &mut Vec<DiagnosticIssue>,
249 ) -> Vec<ResourceValidation> {
250 let mut validations = Vec::new();
251
252 for (name, resource) in package.resources() {
253 let mut resource_issues = Vec::new();
254 let mut valid = true;
255
256 if let Err(e) = crate::utils::validate_resource_path(name) {
258 valid = false;
259 resource_issues.push(format!("Invalid path: {}", e));
260
261 issues.push(DiagnosticIssue {
262 severity: IssueSeverity::High,
263 category: IssueCategory::Resource,
264 description: format!("Resource '{}' has invalid path", name),
265 recommendation: "Ensure resource paths do not contain special characters or path traversal sequences".to_string(),
266 affected: vec![name.clone()],
267 });
268 }
269
270 if resource.size() > 100 * 1024 * 1024 {
272 resource_issues.push("Resource size exceeds 100 MB".to_string());
274
275 issues.push(DiagnosticIssue {
276 severity: IssueSeverity::Medium,
277 category: IssueCategory::Performance,
278 description: format!(
279 "Resource '{}' is very large ({} bytes)",
280 name,
281 resource.size()
282 ),
283 recommendation:
284 "Consider compressing the resource or splitting into smaller chunks"
285 .to_string(),
286 affected: vec![name.clone()],
287 });
288 }
289
290 validations.push(ResourceValidation {
291 name: name.clone(),
292 valid,
293 issues: resource_issues,
294 });
295 }
296
297 validations
298 }
299
300 fn assess_security(
302 &self,
303 package: &Package,
304 issues: &mut Vec<DiagnosticIssue>,
305 ) -> SecurityAssessment {
306 let mut security_issues = Vec::new();
307
308 let is_signed = package.metadata().signature.is_some();
310
311 if !is_signed {
312 issues.push(DiagnosticIssue {
313 severity: IssueSeverity::Medium,
314 category: IssueCategory::Security,
315 description: "Package is not digitally signed".to_string(),
316 recommendation: "Sign the package to ensure authenticity and integrity using the security module".to_string(),
317 affected: vec!["package".to_string()],
318 });
319 security_issues.push("Package is not signed".to_string());
320 }
321
322 let is_encrypted = package.resources().values().any(|resource| {
324 resource.metadata.get("encryption").map_or(false, |v| {
325 v == "true" || v.starts_with("aes") || v.starts_with("chacha")
326 })
327 });
328
329 let has_weak_checksums = package.resources().values().any(|resource| {
331 !resource.metadata.contains_key("sha256") && !resource.metadata.contains_key("sha512")
332 });
333
334 if has_weak_checksums {
335 issues.push(DiagnosticIssue {
336 severity: IssueSeverity::Low,
337 category: IssueCategory::Security,
338 description: "Some resources lack strong checksums".to_string(),
339 recommendation:
340 "Add SHA-256 or SHA-512 checksums to all resources for integrity verification"
341 .to_string(),
342 affected: vec!["resources".to_string()],
343 });
344 security_issues.push("Missing strong checksums".to_string());
345 }
346
347 let mut security_score = 100;
349 if !is_signed {
350 security_score -= 30;
351 }
352 if !is_encrypted {
353 security_score -= 15;
354 }
355 if has_weak_checksums {
356 security_score -= 5;
357 }
358
359 SecurityAssessment {
360 is_signed,
361 is_encrypted,
362 issues: security_issues,
363 security_score: security_score.max(0),
364 }
365 }
366
367 fn check_performance_issues(&self, package: &Package, issues: &mut Vec<DiagnosticIssue>) {
369 for (name, resource) in package.resources() {
371 let size = resource.size();
372 let is_compressed = resource.is_compressed();
373
374 if size > 10 * 1024 * 1024 && !is_compressed {
375 issues.push(DiagnosticIssue {
377 severity: IssueSeverity::Medium,
378 category: IssueCategory::Performance,
379 description: format!(
380 "Resource '{}' is large ({} bytes) and uncompressed",
381 name, size
382 ),
383 recommendation:
384 "Enable compression to reduce package size and improve download times"
385 .to_string(),
386 affected: vec![name.clone()],
387 });
388 }
389 }
390
391 let small_resources: Vec<_> = package
393 .resources()
394 .iter()
395 .filter(|(_, resource)| resource.size() < 1024) .map(|(name, _)| name.clone())
397 .collect();
398
399 if small_resources.len() > 100 {
400 issues.push(DiagnosticIssue {
401 severity: IssueSeverity::Low,
402 category: IssueCategory::Performance,
403 description: format!(
404 "Package contains {} very small resources (< 1 KB each)",
405 small_resources.len()
406 ),
407 recommendation:
408 "Consider bundling small resources into larger files to reduce overhead"
409 .to_string(),
410 affected: small_resources,
411 });
412 }
413
414 let dependency_count = package.metadata().dependencies.len();
416 if dependency_count > 50 {
417 issues.push(DiagnosticIssue {
418 severity: IssueSeverity::Medium,
419 category: IssueCategory::Performance,
420 description: format!("Package has {} dependencies", dependency_count),
421 recommendation:
422 "Review dependencies and consider reducing the dependency tree depth"
423 .to_string(),
424 affected: vec!["dependencies".to_string()],
425 });
426 }
427 }
428
429 fn calculate_statistics(&self, package: &Package) -> PackageStatistics {
431 let total_size: usize = package.resources().values().map(|r| r.size()).sum();
432 let resource_count = package.resources().len();
433 let dependency_count = package.metadata().dependencies.len();
434 let largest_resource_size = package
435 .resources()
436 .values()
437 .map(|r| r.size())
438 .max()
439 .unwrap_or(0) as u64;
440
441 PackageStatistics {
442 total_size: total_size as u64,
443 resource_count,
444 dependency_count,
445 largest_resource_size,
446 smallest_resource_size: 0,
447 average_resource_size: 0,
448 resource_types: HashMap::new(),
449 }
450 }
451
452 fn calculate_health_score(
454 &self,
455 issues: &[DiagnosticIssue],
456 security: &SecurityAssessment,
457 ) -> u8 {
458 let mut score = 100u8;
459
460 for issue in issues {
462 let deduction = match issue.severity {
463 IssueSeverity::Critical => 25,
464 IssueSeverity::High => 15,
465 IssueSeverity::Medium => 8,
466 IssueSeverity::Low => 3,
467 IssueSeverity::Info => 0,
468 };
469 score = score.saturating_sub(deduction);
470 }
471
472 let weighted_score = (score as f64 * 0.7 + security.security_score as f64 * 0.3) as u8;
474
475 weighted_score
476 }
477
478 fn determine_status(score: u8) -> HealthStatus {
480 match score {
481 90..=100 => HealthStatus::Healthy,
482 70..=89 => HealthStatus::Warning,
483 50..=69 => HealthStatus::Degraded,
484 _ => HealthStatus::Critical,
485 }
486 }
487}
488
489impl Default for PackageDiagnostics {
490 fn default() -> Self {
491 Self::new()
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_diagnostics_creation() {
501 let diagnostics = PackageDiagnostics::new();
502 assert!(!diagnostics.strict_mode);
503 assert!(diagnostics.check_security);
504 assert!(diagnostics.check_performance);
505 }
506
507 #[test]
508 fn test_health_status() {
509 assert_eq!(
510 PackageDiagnostics::determine_status(95),
511 HealthStatus::Healthy
512 );
513 assert_eq!(
514 PackageDiagnostics::determine_status(80),
515 HealthStatus::Warning
516 );
517 assert_eq!(
518 PackageDiagnostics::determine_status(60),
519 HealthStatus::Degraded
520 );
521 assert_eq!(
522 PackageDiagnostics::determine_status(40),
523 HealthStatus::Critical
524 );
525 }
526
527 #[test]
528 fn test_issue_severity_ordering() {
529 assert!(IssueSeverity::Critical > IssueSeverity::High);
530 assert!(IssueSeverity::High > IssueSeverity::Medium);
531 assert!(IssueSeverity::Medium > IssueSeverity::Low);
532 assert!(IssueSeverity::Low > IssueSeverity::Info);
533 }
534
535 #[test]
536 fn test_diagnostic_issue_creation() {
537 let issue = DiagnosticIssue {
538 severity: IssueSeverity::High,
539 category: IssueCategory::Security,
540 description: "Test issue".to_string(),
541 recommendation: "Fix it".to_string(),
542 affected: vec!["test".to_string()],
543 };
544
545 assert_eq!(issue.severity, IssueSeverity::High);
546 assert_eq!(issue.category, IssueCategory::Security);
547 }
548
549 #[test]
550 fn test_security_assessment() {
551 let assessment = SecurityAssessment {
552 is_signed: true,
553 is_encrypted: false,
554 issues: Vec::new(),
555 security_score: 100,
556 };
557
558 assert!(assessment.is_signed);
559 assert!(!assessment.is_encrypted);
560 assert_eq!(assessment.security_score, 100);
561 }
562
563 #[test]
564 fn test_validation_result() {
565 let result = ValidationResult {
566 passed: true,
567 messages: vec!["All checks passed".to_string()],
568 };
569
570 assert!(result.passed);
571 assert_eq!(result.messages.len(), 1);
572 }
573}