Skip to main content

torsh_package/
diagnostics.rs

1//! Package diagnostics and analysis
2//!
3//! This module provides comprehensive diagnostics for packages, including
4//! health checks, integrity verification, and issue detection.
5
6use 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/// Package diagnostic report
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DiagnosticReport {
16    /// Overall package health score (0-100)
17    pub health_score: u8,
18    /// Package health status
19    pub status: HealthStatus,
20    /// List of detected issues
21    pub issues: Vec<DiagnosticIssue>,
22    /// Package statistics
23    pub statistics: PackageStatistics,
24    /// Metadata validation results
25    pub metadata_validation: ValidationResult,
26    /// Resource validation results
27    pub resource_validation: Vec<ResourceValidation>,
28    /// Security assessment
29    pub security: SecurityAssessment,
30}
31
32/// Package health status
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum HealthStatus {
35    /// Package is healthy (score >= 90)
36    Healthy,
37    /// Package has minor issues (score 70-89)
38    Warning,
39    /// Package has significant issues (score 50-69)
40    Degraded,
41    /// Package has critical issues (score < 50)
42    Critical,
43}
44
45/// A diagnostic issue found in the package
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DiagnosticIssue {
48    /// Issue severity
49    pub severity: IssueSeverity,
50    /// Issue category
51    pub category: IssueCategory,
52    /// Issue description
53    pub description: String,
54    /// Recommended action
55    pub recommendation: String,
56    /// Affected resources or components
57    pub affected: Vec<String>,
58}
59
60/// Issue severity levels
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
62pub enum IssueSeverity {
63    /// Informational
64    Info,
65    /// Low severity
66    Low,
67    /// Medium severity
68    Medium,
69    /// High severity
70    High,
71    /// Critical severity
72    Critical,
73}
74
75/// Issue categories
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub enum IssueCategory {
78    /// Metadata issues
79    Metadata,
80    /// Resource issues
81    Resource,
82    /// Dependency issues
83    Dependency,
84    /// Security issues
85    Security,
86    /// Performance issues
87    Performance,
88    /// Compatibility issues
89    Compatibility,
90}
91
92/// Package statistics
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct PackageStatistics {
95    /// Total package size in bytes
96    pub total_size: u64,
97    /// Number of resources
98    pub resource_count: usize,
99    /// Number of dependencies
100    pub dependency_count: usize,
101    /// Largest resource size
102    pub largest_resource_size: u64,
103    /// Smallest resource size
104    pub smallest_resource_size: u64,
105    /// Average resource size
106    pub average_resource_size: u64,
107    /// Resource type distribution
108    pub resource_types: HashMap<String, usize>,
109}
110
111/// Validation result
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ValidationResult {
114    /// Whether validation passed
115    pub passed: bool,
116    /// Validation messages
117    pub messages: Vec<String>,
118}
119
120/// Resource validation result
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ResourceValidation {
123    /// Resource name
124    pub name: String,
125    /// Whether validation passed
126    pub valid: bool,
127    /// Validation issues
128    pub issues: Vec<String>,
129}
130
131/// Security assessment
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SecurityAssessment {
134    /// Whether package is signed
135    pub is_signed: bool,
136    /// Whether package is encrypted
137    pub is_encrypted: bool,
138    /// Security issues found
139    pub issues: Vec<String>,
140    /// Security score (0-100)
141    pub security_score: u8,
142}
143
144/// Package diagnostics analyzer
145pub struct PackageDiagnostics {
146    /// Strict validation mode
147    pub strict_mode: bool,
148    /// Check security features
149    pub check_security: bool,
150    /// Check performance issues
151    pub check_performance: bool,
152}
153
154impl PackageDiagnostics {
155    /// Create a new diagnostics analyzer
156    pub fn new() -> Self {
157        Self {
158            strict_mode: false,
159            check_security: true,
160            check_performance: true,
161        }
162    }
163
164    /// Run comprehensive diagnostics on a package
165    pub fn diagnose(&self, package: &Package) -> Result<DiagnosticReport> {
166        let mut issues = Vec::new();
167
168        // Validate metadata
169        let metadata_validation = self.validate_metadata(package, &mut issues)?;
170
171        // Validate resources
172        let resource_validation = self.validate_resources(package, &mut issues);
173
174        // Check security
175        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        // Check performance
187        if self.check_performance {
188            self.check_performance_issues(package, &mut issues);
189        }
190
191        // Calculate statistics
192        let statistics = self.calculate_statistics(package);
193
194        // Calculate health score
195        let health_score = self.calculate_health_score(&issues, &security);
196        let status = Self::determine_status(health_score);
197
198        // Sort issues by severity
199        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    /// Validate package metadata
213    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        // Validate package name and version
222        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    /// Validate resources
245    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            // Validate resource path
257            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            // Check for excessively large resources
271            if resource.size() > 100 * 1024 * 1024 {
272                // 100 MB
273                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    /// Assess package security
301    fn assess_security(
302        &self,
303        package: &Package,
304        issues: &mut Vec<DiagnosticIssue>,
305    ) -> SecurityAssessment {
306        let mut security_issues = Vec::new();
307
308        // Check if package is signed
309        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        // Check for encryption
323        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        // Check for weak checksums
330        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        // Calculate security score (0-100)
348        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    /// Check for performance issues
368    fn check_performance_issues(&self, package: &Package, issues: &mut Vec<DiagnosticIssue>) {
369        // Check for large uncompressed resources
370        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                // > 10 MB uncompressed
376                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        // Check for excessive number of small resources
392        let small_resources: Vec<_> = package
393            .resources()
394            .iter()
395            .filter(|(_, resource)| resource.size() < 1024) // < 1 KB
396            .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        // Check for deep dependency trees
415        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    /// Calculate package statistics
430    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    /// Calculate overall health score
453    fn calculate_health_score(
454        &self,
455        issues: &[DiagnosticIssue],
456        security: &SecurityAssessment,
457    ) -> u8 {
458        let mut score = 100u8;
459
460        // Deduct points for issues
461        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        // Factor in security score
473        let weighted_score = (score as f64 * 0.7 + security.security_score as f64 * 0.3) as u8;
474
475        weighted_score
476    }
477
478    /// Determine health status from score
479    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}