⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/
statistics.rs

1//! Package Statistics, Update Notifications, and Security Advisories
2//!
3//! Tracks package installation metrics, detects available updates by comparing
4//! installed vs available package versions, and checks installed packages
5//! against security advisories.
6//!
7//! NOTE: Many types in this module are forward declarations for user-space
8//! APIs. They will be exercised when user-space process execution is
9//! functional. See TODO(user-space) markers for specific activation points.
10
11// User-space API forward declarations -- see NOTE above
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16#[cfg(feature = "alloc")]
17use alloc::{collections::BTreeMap, string::String, vec::Vec};
18
19#[cfg(feature = "alloc")]
20use super::{PackageMetadata, Version};
21
22// ---------------------------------------------------------------------------
23// Package Statistics
24// ---------------------------------------------------------------------------
25
26/// Per-package usage and installation statistics.
27#[cfg(feature = "alloc")]
28#[derive(Debug, Clone)]
29pub struct PackageStats {
30    /// Number of times this package has been installed
31    pub install_count: u64,
32    /// Timestamp of the most recent installation (seconds since epoch)
33    pub last_installed: u64,
34    /// Timestamp of the most recent update (seconds since epoch)
35    pub last_updated: u64,
36    /// Total number of times the package has been downloaded
37    pub total_downloads: u64,
38}
39
40#[cfg(feature = "alloc")]
41impl PackageStats {
42    /// Create a new zeroed statistics entry.
43    pub fn new() -> Self {
44        Self {
45            install_count: 0,
46            last_installed: 0,
47            last_updated: 0,
48            total_downloads: 0,
49        }
50    }
51}
52
53#[cfg(feature = "alloc")]
54impl Default for PackageStats {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60/// Collects and queries per-package statistics.
61#[cfg(feature = "alloc")]
62pub struct StatsCollector {
63    /// Per-package statistics keyed by package name.
64    stats: BTreeMap<String, PackageStats>,
65}
66
67#[cfg(feature = "alloc")]
68impl StatsCollector {
69    /// Create a new empty stats collector.
70    pub fn new() -> Self {
71        Self {
72            stats: BTreeMap::new(),
73        }
74    }
75
76    /// Record a package installation event.
77    pub fn record_install(&mut self, package_name: &str, timestamp: u64) {
78        let entry = self
79            .stats
80            .entry(String::from(package_name))
81            .or_insert_with(PackageStats::new);
82        entry.install_count += 1;
83        entry.last_installed = timestamp;
84    }
85
86    /// Record a package update event.
87    pub fn record_update(&mut self, package_name: &str, timestamp: u64) {
88        let entry = self
89            .stats
90            .entry(String::from(package_name))
91            .or_insert_with(PackageStats::new);
92        entry.last_updated = timestamp;
93    }
94
95    /// Record a package download event.
96    pub fn record_download(&mut self, package_name: &str) {
97        let entry = self
98            .stats
99            .entry(String::from(package_name))
100            .or_insert_with(PackageStats::new);
101        entry.total_downloads += 1;
102    }
103
104    /// Retrieve statistics for a specific package, if any.
105    pub fn get_stats(&self, package_name: &str) -> Option<&PackageStats> {
106        self.stats.get(package_name)
107    }
108
109    /// Return the top `n` most-installed packages sorted by install count
110    /// (descending).
111    pub fn get_most_installed(&self, n: usize) -> Vec<(&str, u64)> {
112        let mut entries: Vec<(&str, u64)> = self
113            .stats
114            .iter()
115            .map(|(name, s)| (name.as_str(), s.install_count))
116            .collect();
117        entries.sort_by(|a, b| b.1.cmp(&a.1));
118        entries.truncate(n);
119        entries
120    }
121
122    /// Return the total number of tracked packages.
123    pub fn total_packages(&self) -> usize {
124        self.stats.len()
125    }
126}
127
128#[cfg(feature = "alloc")]
129impl Default for StatsCollector {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135// ---------------------------------------------------------------------------
136// Update Notifications
137// ---------------------------------------------------------------------------
138
139/// Notification that an installed package has a newer version available.
140#[cfg(feature = "alloc")]
141#[derive(Debug, Clone)]
142pub struct UpdateNotification {
143    /// Package name
144    pub package: String,
145    /// Currently installed version
146    pub current_version: Version,
147    /// Version available in the repository
148    pub available_version: Version,
149    /// Whether this update addresses a security vulnerability
150    pub is_security: bool,
151    /// Summary of changes in the available version
152    pub changelog: String,
153}
154
155/// Compare installed packages against available packages and return
156/// notifications for packages that have newer versions.
157///
158/// A notification is flagged as a security update if the available
159/// package's description contains "security" (case-insensitive).
160#[cfg(feature = "alloc")]
161pub fn check_for_updates(
162    installed: &[PackageMetadata],
163    available: &[PackageMetadata],
164) -> Vec<UpdateNotification> {
165    let mut notifications = Vec::new();
166
167    // Build a lookup map from available packages by name, keeping the
168    // highest version for each name.
169    let mut available_map: BTreeMap<&str, &PackageMetadata> = BTreeMap::new();
170    for pkg in available {
171        let entry = available_map.entry(pkg.name.as_str()).or_insert(pkg);
172        if pkg.version > entry.version {
173            *entry = pkg;
174        }
175    }
176
177    for inst in installed {
178        if let Some(avail) = available_map.get(inst.name.as_str()) {
179            if avail.version > inst.version {
180                // Heuristic: flag as security if description mentions "security"
181                let desc_lower = avail.description.as_bytes();
182                let is_security = contains_ignore_case(desc_lower, b"security");
183
184                notifications.push(UpdateNotification {
185                    package: inst.name.clone(),
186                    current_version: inst.version.clone(),
187                    available_version: avail.version.clone(),
188                    is_security,
189                    changelog: avail.description.clone(),
190                });
191            }
192        }
193    }
194
195    notifications
196}
197
198/// Case-insensitive substring search in byte slices.
199#[cfg(feature = "alloc")]
200fn contains_ignore_case(haystack: &[u8], needle: &[u8]) -> bool {
201    if needle.is_empty() || needle.len() > haystack.len() {
202        return needle.is_empty();
203    }
204    for i in 0..=(haystack.len() - needle.len()) {
205        let mut matched = true;
206        for j in 0..needle.len() {
207            if to_ascii_lower(haystack[i + j]) != to_ascii_lower(needle[j]) {
208                matched = false;
209                break;
210            }
211        }
212        if matched {
213            return true;
214        }
215    }
216    false
217}
218
219/// Convert a single ASCII byte to lowercase.
220#[cfg(feature = "alloc")]
221fn to_ascii_lower(b: u8) -> u8 {
222    if b.is_ascii_uppercase() {
223        b + 32
224    } else {
225        b
226    }
227}
228
229// ---------------------------------------------------------------------------
230// Security Advisories
231// ---------------------------------------------------------------------------
232
233/// Severity level for a security advisory.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
235pub enum AdvisorySeverity {
236    /// Low impact
237    Low,
238    /// Medium impact
239    Medium,
240    /// High impact
241    High,
242    /// Critical impact -- immediate action recommended
243    Critical,
244}
245
246/// A security advisory describing a vulnerability that affects one or more
247/// packages.
248#[cfg(feature = "alloc")]
249#[derive(Debug, Clone)]
250pub struct SecurityAdvisory {
251    /// Advisory identifier (e.g. "VSA-2026-001")
252    pub id: String,
253    /// Names of packages affected by this advisory
254    pub affected_packages: Vec<String>,
255    /// Severity of the vulnerability
256    pub severity: AdvisorySeverity,
257    /// Human-readable description of the vulnerability
258    pub description: String,
259    /// Version that fixes the vulnerability, if known
260    pub fixed_version: Option<Version>,
261}
262
263/// Check installed packages against a list of security advisories.
264///
265/// Returns all advisories that affect at least one installed package (matched
266/// by name).
267#[cfg(feature = "alloc")]
268pub fn check_advisories(
269    installed: &[PackageMetadata],
270    advisories: &[SecurityAdvisory],
271) -> Vec<SecurityAdvisory> {
272    let installed_names: Vec<&str> = installed.iter().map(|p| p.name.as_str()).collect();
273
274    advisories
275        .iter()
276        .filter(|adv| {
277            adv.affected_packages
278                .iter()
279                .any(|name| installed_names.contains(&name.as_str()))
280        })
281        .cloned()
282        .collect()
283}