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

veridian_kernel/pkg/
repository.rs

1//! Package Repository Management
2//!
3//! Implements HTTP-based package repository fetching for VeridianOS package
4//! manager. Uses the network stack for actual HTTP communication.
5
6// Phase 4 (package ecosystem) -- repository fetching is defined but not yet
7// wired to the network stack.
8
9use alloc::{string::String, vec::Vec};
10
11use super::{Dependency, PackageId, PackageMetadata, Version};
12
13/// HTTP request type
14#[derive(Debug, Clone, Copy)]
15#[allow(dead_code)] // Network API stub -- used when network stack is available
16enum HttpMethod {
17    Get,
18    Head,
19}
20
21/// HTTP response
22#[derive(Debug)]
23pub struct HttpResponse {
24    status_code: u16,
25    headers: Vec<(String, String)>,
26    body: Vec<u8>,
27}
28
29impl HttpResponse {
30    fn new() -> Self {
31        Self {
32            status_code: 0,
33            headers: Vec::new(),
34            body: Vec::new(),
35        }
36    }
37
38    fn is_success(&self) -> bool {
39        (200..300).contains(&self.status_code)
40    }
41}
42
43/// HTTP client for repository communication
44pub struct HttpClient {
45    /// Base URL for the repository
46    base_url: String,
47    /// Connection timeout in milliseconds
48    timeout_ms: u32,
49    /// User agent string
50    user_agent: String,
51}
52
53impl HttpClient {
54    fn new(base_url: String) -> Self {
55        Self {
56            base_url,
57            timeout_ms: 30000, // 30 second default timeout
58            user_agent: String::from("VeridianOS-PackageManager/1.0"),
59        }
60    }
61
62    /// Perform HTTP GET request
63    fn get(&self, path: &str) -> Result<HttpResponse, HttpError> {
64        self.request(HttpMethod::Get, path, None)
65    }
66
67    /// Perform HTTP request
68    fn request(
69        &self,
70        method: HttpMethod,
71        path: &str,
72        _body: Option<&[u8]>,
73    ) -> Result<HttpResponse, HttpError> {
74        // Build full URL
75        let url = self.build_url(path);
76
77        // Parse URL components
78        let (host, port, request_path) = self.parse_url(&url)?;
79
80        // Build HTTP request
81        let request = self.build_request(method, &host, &request_path);
82
83        // Connect and send request via network stack
84        let response_data = self.send_request(&host, port, &request)?;
85
86        // Parse HTTP response
87        self.parse_response(&response_data)
88    }
89
90    fn build_url(&self, path: &str) -> String {
91        let mut url = self.base_url.clone();
92        if !url.ends_with('/') && !path.starts_with('/') {
93            url.push('/');
94        }
95        url.push_str(path);
96        url
97    }
98
99    fn parse_url(&self, url: &str) -> Result<(String, u16, String), HttpError> {
100        // Strip protocol prefix
101        let url = url
102            .strip_prefix("https://")
103            .or_else(|| url.strip_prefix("http://"))
104            .unwrap_or(url);
105
106        // Split host and path
107        let (host_port, path) = match url.find('/') {
108            Some(idx) => (&url[..idx], &url[idx..]),
109            None => (url, "/"),
110        };
111
112        // Parse host and port
113        let (host, port) = match host_port.find(':') {
114            Some(idx) => {
115                let port_str = &host_port[idx + 1..];
116                let port = port_str.parse::<u16>().unwrap_or(443);
117                (&host_port[..idx], port)
118            }
119            None => {
120                // Default to HTTPS port for security
121                (host_port, 443)
122            }
123        };
124
125        Ok((String::from(host), port, String::from(path)))
126    }
127
128    fn build_request(&self, method: HttpMethod, host: &str, path: &str) -> Vec<u8> {
129        let method_str = match method {
130            HttpMethod::Get => "GET",
131            HttpMethod::Head => "HEAD",
132        };
133
134        let request = alloc::format!(
135            "{} {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: {}\r\nAccept: application/octet-stream, \
136             application/json, */*\r\nConnection: close\r\n\r\n",
137            method_str,
138            path,
139            host,
140            self.user_agent
141        );
142
143        request.into_bytes()
144    }
145
146    fn send_request(&self, host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, HttpError> {
147        // Use network stack to establish TCP connection
148        use crate::net::{
149            socket::{Socket, SocketDomain, SocketProtocol, SocketType},
150            Ipv4Address, SocketAddr,
151        };
152
153        // Resolve hostname to IP (simplified - in production would use DNS)
154        let ip_addr = self.resolve_hostname(host)?;
155
156        // Create TCP socket
157        let mut socket = Socket::new(SocketDomain::Inet, SocketType::Stream, SocketProtocol::Tcp)
158            .map_err(|_| HttpError::ConnectionFailed)?;
159
160        // Connect to server
161        let addr = SocketAddr::v4(Ipv4Address(ip_addr), port);
162        socket
163            .connect(addr)
164            .map_err(|_| HttpError::ConnectionFailed)?;
165
166        // Set receive timeout via socket options
167        socket.options.recv_timeout_ms = Some(self.timeout_ms as u64);
168
169        // Send HTTP request
170        socket
171            .send(request, 0)
172            .map_err(|_| HttpError::NetworkError)?;
173
174        // Receive response
175        let mut response = Vec::new();
176        let mut buf = [0u8; 4096];
177
178        loop {
179            match socket.recv(&mut buf, 0) {
180                Ok(0) => break, // Connection closed
181                Ok(n) => response.extend_from_slice(&buf[..n]),
182                Err(_) => break,
183            }
184
185            // Safety limit - max 64MB response
186            if response.len() > 64 * 1024 * 1024 {
187                return Err(HttpError::ResponseTooLarge);
188            }
189        }
190
191        // Close socket
192        let _ = socket.close();
193
194        Ok(response)
195    }
196
197    fn resolve_hostname(&self, host: &str) -> Result<[u8; 4], HttpError> {
198        // Simple hostname resolution
199        // In production, would use DNS resolver
200
201        // Check for IP address format first
202        let parts: Vec<&str> = host.split('.').collect();
203        if parts.len() == 4 {
204            let mut ip = [0u8; 4];
205            let mut valid = true;
206            for (i, part) in parts.iter().enumerate() {
207                match part.parse::<u8>() {
208                    Ok(byte) => ip[i] = byte,
209                    Err(_) => {
210                        valid = false;
211                        break;
212                    }
213                }
214            }
215            if valid {
216                return Ok(ip);
217            }
218        }
219
220        // For now, return localhost for unresolved hostnames
221        // Real implementation would use DNS
222        Ok([127, 0, 0, 1])
223    }
224
225    fn parse_response(&self, data: &[u8]) -> Result<HttpResponse, HttpError> {
226        let mut response = HttpResponse::new();
227
228        // Find header/body separator
229        let header_end = self
230            .find_header_end(data)
231            .ok_or(HttpError::InvalidResponse)?;
232        let header_data = &data[..header_end];
233        let body_start = header_end + 4; // Skip \r\n\r\n
234
235        // Parse status line
236        let header_str =
237            core::str::from_utf8(header_data).map_err(|_| HttpError::InvalidResponse)?;
238        let mut lines = header_str.lines();
239
240        let status_line = lines.next().ok_or(HttpError::InvalidResponse)?;
241        let parts: Vec<&str> = status_line.split_whitespace().collect();
242        if parts.len() < 2 {
243            return Err(HttpError::InvalidResponse);
244        }
245        response.status_code = parts[1].parse().unwrap_or(0);
246
247        // Parse headers
248        for line in lines {
249            if let Some((key, value)) = line.split_once(':') {
250                response
251                    .headers
252                    .push((String::from(key.trim()), String::from(value.trim())));
253            }
254        }
255
256        // Extract body
257        if body_start < data.len() {
258            response.body = data[body_start..].to_vec();
259        }
260
261        Ok(response)
262    }
263
264    fn find_header_end(&self, data: &[u8]) -> Option<usize> {
265        for i in 0..data.len().saturating_sub(3) {
266            if &data[i..i + 4] == b"\r\n\r\n" {
267                return Some(i);
268            }
269        }
270        None
271    }
272}
273
274/// HTTP errors
275#[derive(Debug)]
276pub enum HttpError {
277    ConnectionFailed,
278    InvalidResponse,
279    ResponseTooLarge,
280    Timeout,
281    NetworkError,
282}
283
284/// Package repository
285#[derive(Debug, Clone)]
286pub struct Repository {
287    /// Repository name
288    pub name: String,
289    /// Repository URL
290    pub url: String,
291    /// Is repository trusted
292    pub trusted: bool,
293    /// Package index cache
294    package_cache: Vec<PackageMetadata>,
295    /// Last update timestamp (Unix time)
296    last_updated: u64,
297}
298
299impl Repository {
300    pub fn new(name: String, url: String, trusted: bool) -> Self {
301        Self {
302            name,
303            url,
304            trusted,
305            package_cache: Vec::new(),
306            last_updated: 0,
307        }
308    }
309
310    /// Fetch package list from repository via HTTP/HTTPS
311    pub fn fetch_package_list(&self) -> Vec<PackageMetadata> {
312        let client = HttpClient::new(self.url.clone());
313
314        // Fetch package index file
315        match client.get("packages.json") {
316            Ok(response) if response.is_success() => self.parse_package_index(&response.body),
317            Ok(_response) => {
318                // HTTP error - return cached packages if available
319                crate::println!("[PKG] Repository {} returned error, using cache", self.name);
320                self.package_cache.clone()
321            }
322            Err(_e) => {
323                // Network error - return cached packages
324                crate::println!("[PKG] Failed to connect to repository {}", self.name);
325                self.package_cache.clone()
326            }
327        }
328    }
329
330    /// Parse package index JSON
331    fn parse_package_index(&self, data: &[u8]) -> Vec<PackageMetadata> {
332        let mut packages = Vec::new();
333
334        // Parse JSON package list (simplified JSON parser)
335        let json_str = match core::str::from_utf8(data) {
336            Ok(s) => s,
337            Err(_) => return packages,
338        };
339
340        // Simple JSON parser for package array
341        // Format: [{"name":"pkg1","version":"1.0.0",...},...]
342        if !json_str.starts_with('[') {
343            return packages;
344        }
345
346        // Split into package objects
347        let mut depth = 0;
348        let mut obj_start = 0;
349        let chars: Vec<char> = json_str.chars().collect();
350
351        for (i, &c) in chars.iter().enumerate() {
352            match c {
353                '{' => {
354                    if depth == 1 {
355                        obj_start = i;
356                    }
357                    depth += 1;
358                }
359                '}' => {
360                    depth -= 1;
361                    if depth == 1 {
362                        let obj_str: String = chars[obj_start..=i].iter().collect();
363                        if let Some(pkg) = self.parse_package_object(&obj_str) {
364                            packages.push(pkg);
365                        }
366                    }
367                }
368                _ => {}
369            }
370        }
371
372        packages
373    }
374
375    /// Parse single package JSON object
376    fn parse_package_object(&self, json: &str) -> Option<PackageMetadata> {
377        // Extract fields from JSON object
378        let name = self.extract_json_string(json, "name")?;
379        let version_str = self.extract_json_string(json, "version")?;
380        let author = self.extract_json_string(json, "author").unwrap_or_default();
381        let description = self
382            .extract_json_string(json, "description")
383            .unwrap_or_default();
384        let license = self
385            .extract_json_string(json, "license")
386            .unwrap_or_default();
387
388        // Parse version
389        let version = self.parse_version(&version_str)?;
390
391        // Parse dependencies array
392        let dependencies = self.extract_dependencies(json);
393        let conflicts = self.extract_conflicts(json);
394
395        Some(PackageMetadata {
396            name,
397            version,
398            author,
399            description,
400            license,
401            dependencies,
402            conflicts,
403        })
404    }
405
406    fn extract_json_string(&self, json: &str, key: &str) -> Option<String> {
407        let pattern = alloc::format!("\"{}\":\"", key);
408        let start = json.find(&pattern)? + pattern.len();
409        let end = json[start..].find('"')? + start;
410        Some(String::from(&json[start..end]))
411    }
412
413    fn parse_version(&self, version_str: &str) -> Option<Version> {
414        let parts: Vec<&str> = version_str.split('.').collect();
415        if parts.len() >= 3 {
416            let major = parts[0].parse().ok()?;
417            let minor = parts[1].parse().ok()?;
418            let patch = parts[2].parse().ok()?;
419            Some(Version::new(major, minor, patch))
420        } else {
421            None
422        }
423    }
424
425    fn extract_dependencies(&self, json: &str) -> Vec<Dependency> {
426        let mut deps = Vec::new();
427
428        // Find dependencies array
429        if let Some(start) = json.find("\"dependencies\":[") {
430            let arr_start = start + "\"dependencies\":[".len();
431            if let Some(arr_end) = json[arr_start..].find(']') {
432                let arr_str = &json[arr_start..arr_start + arr_end];
433
434                // Parse each dependency object
435                for dep_obj in arr_str.split("},{") {
436                    let dep_str = dep_obj.trim_matches(|c| c == '{' || c == '}');
437                    if let (Some(name), Some(version)) = (
438                        self.extract_json_string(dep_str, "name"),
439                        self.extract_json_string(dep_str, "version"),
440                    ) {
441                        deps.push(Dependency {
442                            name,
443                            version_req: version,
444                        });
445                    }
446                }
447            }
448        }
449
450        deps
451    }
452
453    fn extract_conflicts(&self, json: &str) -> Vec<String> {
454        let mut conflicts = Vec::new();
455
456        // Find conflicts array
457        if let Some(start) = json.find("\"conflicts\":[") {
458            let arr_start = start + "\"conflicts\":[".len();
459            if let Some(arr_end) = json[arr_start..].find(']') {
460                let arr_str = &json[arr_start..arr_start + arr_end];
461
462                // Parse each conflict string
463                for conflict in arr_str.split(',') {
464                    let name = conflict.trim().trim_matches('"');
465                    if !name.is_empty() {
466                        conflicts.push(String::from(name));
467                    }
468                }
469            }
470        }
471
472        conflicts
473    }
474
475    /// Download package by ID via HTTP/HTTPS
476    pub fn download_package(&self, package_id: &PackageId) -> Option<Vec<u8>> {
477        let client = HttpClient::new(self.url.clone());
478
479        // Construct package download path
480        let path = alloc::format!("packages/{}.vpkg", package_id);
481
482        match client.get(&path) {
483            Ok(response) if response.is_success() => {
484                crate::println!(
485                    "[PKG] Downloaded {} ({} bytes)",
486                    package_id,
487                    response.body.len()
488                );
489                Some(response.body)
490            }
491            Ok(_response) => {
492                #[cfg(target_arch = "x86_64")]
493                crate::println!(
494                    "[PKG] Failed to download {}: HTTP {}",
495                    package_id,
496                    _response.status_code
497                );
498                None
499            }
500            Err(_e) => {
501                crate::println!("[PKG] Network error downloading {}", package_id);
502                None
503            }
504        }
505    }
506
507    /// Check if package exists in repository
508    pub fn has_package(&self, package_id: &PackageId) -> bool {
509        self.package_cache.iter().any(|p| &p.name == package_id)
510    }
511
512    /// Get cached package metadata
513    pub fn get_package(&self, package_id: &PackageId) -> Option<&PackageMetadata> {
514        self.package_cache.iter().find(|p| &p.name == package_id)
515    }
516
517    /// Invalidate cache
518    pub fn invalidate_cache(&mut self) {
519        self.package_cache.clear();
520        self.last_updated = 0;
521    }
522
523    /// Check if cache is stale (older than 1 hour)
524    pub fn is_cache_stale(&self, current_time: u64) -> bool {
525        current_time.saturating_sub(self.last_updated) > 3600
526    }
527}
528
529impl Default for Repository {
530    fn default() -> Self {
531        Self::new(
532            String::from("default"),
533            String::from("https://packages.veridian.org"),
534            true,
535        )
536    }
537}
538
539// ============================================================================
540// Repository Index
541// ============================================================================
542
543/// Server-side repository metadata index.
544///
545/// Describes all packages available in a repository, signed for integrity.
546/// Serialized as a simple JSON-like format for transmission.
547pub struct RepositoryIndex {
548    /// Index format version
549    pub version: u32,
550    /// Timestamp when the index was generated (seconds since epoch)
551    pub generated_at: u64,
552    /// Package entries in the index
553    pub entries: Vec<RepositoryIndexEntry>,
554    /// Ed25519 signature over the serialized entries
555    pub signature: Vec<u8>,
556}
557
558/// A single entry in the repository index.
559#[derive(Debug, Clone)]
560pub struct RepositoryIndexEntry {
561    /// Package name
562    pub name: String,
563    /// Package version string
564    pub version: String,
565    /// SHA-256 hash of the .vpkg file
566    pub hash: [u8; 32],
567    /// Size of the .vpkg file in bytes
568    pub size: u64,
569    /// Package description
570    pub description: String,
571    /// License identifier
572    pub license: String,
573}
574
575impl RepositoryIndex {
576    /// Create a new empty index.
577    pub fn new() -> Self {
578        Self {
579            version: 1,
580            generated_at: crate::arch::timer::get_timestamp_secs(),
581            entries: Vec::new(),
582            signature: Vec::new(),
583        }
584    }
585
586    /// Generate a repository index from a list of package metadata.
587    pub fn generate(packages: &[super::PackageMetadata]) -> Self {
588        let mut index = Self::new();
589        for pkg in packages {
590            index.entries.push(RepositoryIndexEntry {
591                name: pkg.name.clone(),
592                version: alloc::format!(
593                    "{}.{}.{}",
594                    pkg.version.major,
595                    pkg.version.minor,
596                    pkg.version.patch
597                ),
598                hash: [0u8; 32], // Hash populated when package file is available
599                size: 0,
600                description: pkg.description.clone(),
601                license: pkg.license.clone(),
602            });
603        }
604        index
605    }
606
607    /// Serialize the index to bytes (simple JSON format).
608    pub fn to_bytes(&self) -> Vec<u8> {
609        let mut buf = Vec::new();
610        buf.extend_from_slice(b"{\"version\":");
611        let version_str = alloc::format!("{}", self.version);
612        buf.extend_from_slice(version_str.as_bytes());
613        buf.extend_from_slice(b",\"generated_at\":");
614        let ts_str = alloc::format!("{}", self.generated_at);
615        buf.extend_from_slice(ts_str.as_bytes());
616        buf.extend_from_slice(b",\"packages\":[");
617        for (i, entry) in self.entries.iter().enumerate() {
618            if i > 0 {
619                buf.push(b',');
620            }
621            let entry_json = alloc::format!(
622                "{{\"name\":\"{}\",\"version\":\"{}\",\"size\":{},\"description\":\"{}\",\"\
623                 license\":\"{}\"}}",
624                entry.name,
625                entry.version,
626                entry.size,
627                entry.description,
628                entry.license
629            );
630            buf.extend_from_slice(entry_json.as_bytes());
631        }
632        buf.extend_from_slice(b"]}");
633        buf
634    }
635
636    /// Verify the Ed25519 signature over the index data.
637    ///
638    /// Uses `crate::crypto::asymmetric::Ed25519` for real verification.
639    pub fn verify_signature(&self, public_key: &[u8]) -> bool {
640        if self.signature.is_empty() || public_key.is_empty() {
641            return false;
642        }
643
644        let content = self.to_bytes();
645
646        // Use real Ed25519 verification
647        let sig = match crate::crypto::asymmetric::Signature::from_bytes(&self.signature) {
648            Ok(s) => s,
649            Err(_) => return false,
650        };
651        let vk = match crate::crypto::asymmetric::VerifyingKey::from_bytes(public_key) {
652            Ok(v) => v,
653            Err(_) => return false,
654        };
655
656        matches!(vk.verify(&content, &sig), Ok(true))
657    }
658}
659
660impl Default for RepositoryIndex {
661    fn default() -> Self {
662        Self::new()
663    }
664}
665
666// ============================================================================
667// Mirror Management
668// ============================================================================
669
670/// Status of a mirror.
671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
672pub enum MirrorStatus {
673    /// Mirror is available and responding
674    Online,
675    /// Mirror is not responding
676    Offline,
677    /// Mirror status is unknown (not yet checked)
678    Unknown,
679}
680
681/// Metadata about a repository mirror.
682#[derive(Debug, Clone)]
683pub struct MirrorMetadata {
684    /// Mirror URL
685    pub url: String,
686    /// Priority (lower = preferred)
687    pub priority: u32,
688    /// Geographic region hint
689    pub region: String,
690    /// Timestamp of last successful sync
691    pub last_sync: u64,
692    /// Current mirror status
693    pub status: MirrorStatus,
694}
695
696/// Manages multiple mirrors for a repository, providing failover.
697pub struct MirrorManager {
698    /// Available mirrors sorted by priority
699    mirrors: Vec<MirrorMetadata>,
700}
701
702impl MirrorManager {
703    /// Create a new mirror manager.
704    pub fn new() -> Self {
705        Self {
706            mirrors: Vec::new(),
707        }
708    }
709
710    /// Add a mirror to the manager.
711    pub fn add_mirror(&mut self, mirror: MirrorMetadata) {
712        self.mirrors.push(mirror);
713        // Keep sorted by priority (lower first)
714        self.mirrors.sort_by_key(|m| m.priority);
715    }
716
717    /// Remove a mirror by URL.
718    pub fn remove_mirror(&mut self, url: &str) -> bool {
719        if let Some(pos) = self.mirrors.iter().position(|m| m.url == url) {
720            self.mirrors.remove(pos);
721            true
722        } else {
723            false
724        }
725    }
726
727    /// Select the best available mirror.
728    ///
729    /// Returns the highest-priority mirror that is not offline.
730    /// Falls back to the first mirror if all are offline.
731    pub fn select_best_mirror(&self) -> Option<&MirrorMetadata> {
732        // Prefer online mirrors by priority, fall back to any mirror
733        self.mirrors
734            .iter()
735            .find(|m| m.status != MirrorStatus::Offline)
736            .or(self.mirrors.first())
737    }
738
739    /// Mark a mirror as offline after a failed connection.
740    pub fn mark_offline(&mut self, url: &str) {
741        if let Some(mirror) = self.mirrors.iter_mut().find(|m| m.url == url) {
742            mirror.status = MirrorStatus::Offline;
743        }
744    }
745
746    /// Mark a mirror as online after a successful connection.
747    pub fn mark_online(&mut self, url: &str) {
748        if let Some(mirror) = self.mirrors.iter_mut().find(|m| m.url == url) {
749            mirror.status = MirrorStatus::Online;
750            mirror.last_sync = crate::arch::timer::get_timestamp_secs();
751        }
752    }
753
754    /// List all mirrors.
755    pub fn list_mirrors(&self) -> &[MirrorMetadata] {
756        &self.mirrors
757    }
758
759    /// Return the number of mirrors.
760    pub fn mirror_count(&self) -> usize {
761        self.mirrors.len()
762    }
763}
764
765impl Default for MirrorManager {
766    fn default() -> Self {
767        Self::new()
768    }
769}
770
771// ============================================================================
772// Repository Configuration
773// ============================================================================
774
775/// Configuration for multi-repository management.
776pub struct RepositoryConfig {
777    /// Configured repositories
778    repositories: Vec<RepositoryEntry>,
779}
780
781/// A single repository entry in the configuration.
782#[derive(Debug, Clone)]
783pub struct RepositoryEntry {
784    /// Repository name
785    pub name: String,
786    /// Repository URL
787    pub url: String,
788    /// Whether this repository is enabled
789    pub enabled: bool,
790    /// Whether this repository is trusted
791    pub trusted: bool,
792    /// Priority (lower = checked first)
793    pub priority: u32,
794    /// Mirror manager for this repository
795    pub mirrors: Vec<MirrorMetadata>,
796}
797
798impl RepositoryConfig {
799    /// Create a new empty configuration.
800    pub fn new() -> Self {
801        Self {
802            repositories: Vec::new(),
803        }
804    }
805
806    /// Add a repository.
807    pub fn add_repository(&mut self, entry: RepositoryEntry) {
808        self.repositories.push(entry);
809        self.repositories.sort_by_key(|r| r.priority);
810    }
811
812    /// Remove a repository by name.
813    pub fn remove_repository(&mut self, name: &str) -> bool {
814        if let Some(pos) = self.repositories.iter().position(|r| r.name == name) {
815            self.repositories.remove(pos);
816            true
817        } else {
818            false
819        }
820    }
821
822    /// Enable a repository.
823    pub fn enable_repository(&mut self, name: &str) -> bool {
824        if let Some(repo) = self.repositories.iter_mut().find(|r| r.name == name) {
825            repo.enabled = true;
826            true
827        } else {
828            false
829        }
830    }
831
832    /// Disable a repository.
833    pub fn disable_repository(&mut self, name: &str) -> bool {
834        if let Some(repo) = self.repositories.iter_mut().find(|r| r.name == name) {
835            repo.enabled = false;
836            true
837        } else {
838            false
839        }
840    }
841
842    /// List all enabled repositories.
843    pub fn enabled_repositories(&self) -> Vec<&RepositoryEntry> {
844        self.repositories.iter().filter(|r| r.enabled).collect()
845    }
846
847    /// List all repositories.
848    pub fn all_repositories(&self) -> &[RepositoryEntry] {
849        &self.repositories
850    }
851
852    /// Get a repository by name.
853    pub fn get_repository(&self, name: &str) -> Option<&RepositoryEntry> {
854        self.repositories.iter().find(|r| r.name == name)
855    }
856}
857
858impl Default for RepositoryConfig {
859    fn default() -> Self {
860        Self::new()
861    }
862}
863
864// ============================================================================
865// Repository Access Control
866// ============================================================================
867
868/// Policy governing who may upload packages to a repository.
869#[derive(Debug, Clone, Copy, PartialEq, Eq)]
870pub enum UploadPolicy {
871    /// Anyone can upload packages.
872    Open,
873    /// Only uploaders whose Ed25519 public key fingerprint is in the
874    /// allowed list may upload.
875    Restricted,
876    /// No uploads are accepted.
877    Closed,
878}
879
880/// Controls which uploaders are permitted to push packages to a repository.
881#[derive(Debug, Clone)]
882pub struct AccessControl {
883    /// SHA-256 fingerprints of allowed Ed25519 public keys.
884    allowed_uploaders: Vec<[u8; 32]>,
885    /// Current upload policy.
886    pub upload_policy: UploadPolicy,
887}
888
889impl AccessControl {
890    /// Create a new access control with the given policy.
891    pub fn new(policy: UploadPolicy) -> Self {
892        Self {
893            allowed_uploaders: Vec::new(),
894            upload_policy: policy,
895        }
896    }
897
898    /// Register an uploader by their Ed25519 public key fingerprint.
899    pub fn add_uploader(&mut self, key_fingerprint: [u8; 32]) {
900        if !self.allowed_uploaders.contains(&key_fingerprint) {
901            self.allowed_uploaders.push(key_fingerprint);
902        }
903    }
904
905    /// Remove an uploader. Returns `true` if the fingerprint was present.
906    pub fn remove_uploader(&mut self, key_fingerprint: &[u8; 32]) -> bool {
907        if let Some(pos) = self
908            .allowed_uploaders
909            .iter()
910            .position(|fp| fp == key_fingerprint)
911        {
912            self.allowed_uploaders.remove(pos);
913            true
914        } else {
915            false
916        }
917    }
918
919    /// Verify that an upload is authorized and properly signed.
920    ///
921    /// Checks the upload policy, uploader identity (for `Restricted`), and
922    /// Ed25519 signature over the package data.
923    pub fn verify_upload(
924        &self,
925        package_data: &[u8],
926        signature: &[u8],
927        uploader_key: &[u8],
928    ) -> Result<(), crate::error::KernelError> {
929        use crate::error::KernelError;
930
931        // Policy gate
932        match self.upload_policy {
933            UploadPolicy::Open => { /* skip identity check */ }
934            UploadPolicy::Closed => {
935                return Err(KernelError::PermissionDenied {
936                    operation: "upload package",
937                });
938            }
939            UploadPolicy::Restricted => {
940                let fingerprint = crate::crypto::hash::sha256(uploader_key);
941                if !self.allowed_uploaders.contains(fingerprint.as_bytes()) {
942                    return Err(KernelError::PermissionDenied {
943                        operation: "upload package",
944                    });
945                }
946            }
947        }
948
949        // Signature verification
950        let sig = crate::crypto::asymmetric::Signature::from_bytes(signature).map_err(|_| {
951            KernelError::PermissionDenied {
952                operation: "upload package",
953            }
954        })?;
955        let vk =
956            crate::crypto::asymmetric::VerifyingKey::from_bytes(uploader_key).map_err(|_| {
957                KernelError::PermissionDenied {
958                    operation: "upload package",
959                }
960            })?;
961
962        match vk.verify(package_data, &sig) {
963            Ok(true) => Ok(()),
964            _ => Err(KernelError::PermissionDenied {
965                operation: "upload package",
966            }),
967        }
968    }
969}
970
971impl Default for AccessControl {
972    fn default() -> Self {
973        Self::new(UploadPolicy::Restricted)
974    }
975}
976
977// ============================================================================
978// Package Security Scanning
979// ============================================================================
980
981/// Classification of a malware detection pattern.
982#[derive(Debug, Clone, PartialEq, Eq)]
983pub enum PatternType {
984    /// File path pattern (e.g. accessing sensitive system files).
985    SuspiciousPath,
986    /// Requesting dangerous capabilities.
987    ExcessiveCapability,
988    /// File matches a known malware hash.
989    KnownBadHash,
990}
991
992/// Severity level for security findings.
993#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
994pub enum Severity {
995    /// Low-risk finding.
996    Low,
997    /// Medium-risk finding.
998    Medium,
999    /// High-risk finding.
1000    High,
1001    /// Critical-risk finding.
1002    Critical,
1003}
1004
1005/// A pattern used to detect suspicious content in a package.
1006#[derive(Debug, Clone)]
1007pub struct MalwarePattern {
1008    /// What kind of pattern this is.
1009    pub pattern_type: PatternType,
1010    /// Human-readable description.
1011    pub description: String,
1012    /// Severity if matched.
1013    pub severity: Severity,
1014    /// The actual pattern to match (path substring, capability name, or hex
1015    /// hash).
1016    pub pattern: String,
1017}
1018
1019/// A security finding produced by the scanner.
1020#[derive(Debug, Clone)]
1021pub struct MalwareFinding {
1022    /// Severity of the finding.
1023    pub severity: Severity,
1024    /// Description of the issue.
1025    pub description: String,
1026    /// Which file triggered the match.
1027    pub file_path: String,
1028    /// The pattern that was matched.
1029    pub pattern_matched: String,
1030}
1031
1032/// Scans packages for suspicious paths, excessive capabilities, and known-bad
1033/// hashes.
1034#[derive(Debug, Clone)]
1035pub struct SecurityScanner {
1036    /// Registered detection patterns.
1037    patterns: Vec<MalwarePattern>,
1038}
1039
1040impl SecurityScanner {
1041    /// Create a new scanner pre-loaded with default suspicious patterns.
1042    pub fn new() -> Self {
1043        let mut scanner = Self {
1044            patterns: Vec::new(),
1045        };
1046        scanner.add_default_patterns();
1047        scanner
1048    }
1049
1050    /// Populate the scanner with well-known suspicious patterns.
1051    pub fn add_default_patterns(&mut self) {
1052        // High-severity sensitive file paths
1053        let sensitive_paths = [
1054            "/etc/shadow",
1055            "/etc/passwd",
1056            "/dev/mem",
1057            "/dev/kmem",
1058            "/proc/kcore",
1059        ];
1060        for path in &sensitive_paths {
1061            self.patterns.push(MalwarePattern {
1062                pattern_type: PatternType::SuspiciousPath,
1063                description: alloc::format!("Access to sensitive path: {}", path),
1064                severity: Severity::High,
1065                pattern: String::from(*path),
1066            });
1067        }
1068
1069        // Medium-severity capability requests
1070        let dangerous_caps = ["CAP_SYS_ADMIN", "CAP_NET_RAW", "CAP_SYS_PTRACE"];
1071        for cap in &dangerous_caps {
1072            self.patterns.push(MalwarePattern {
1073                pattern_type: PatternType::ExcessiveCapability,
1074                description: alloc::format!("Excessive capability request: {}", cap),
1075                severity: Severity::Medium,
1076                pattern: String::from(*cap),
1077            });
1078        }
1079
1080        // Medium-severity permission patterns
1081        let perm_patterns = ["setuid", "world-writable"];
1082        for pat in &perm_patterns {
1083            self.patterns.push(MalwarePattern {
1084                pattern_type: PatternType::SuspiciousPath,
1085                description: alloc::format!("Suspicious permission pattern: {}", pat),
1086                severity: Severity::Medium,
1087                pattern: String::from(*pat),
1088            });
1089        }
1090    }
1091
1092    /// Register an additional detection pattern.
1093    pub fn add_pattern(&mut self, pattern: MalwarePattern) {
1094        self.patterns.push(pattern);
1095    }
1096
1097    /// Scan a list of file paths against `SuspiciousPath` patterns.
1098    pub fn scan_package_paths(&self, file_paths: &[&str]) -> Vec<MalwareFinding> {
1099        let mut findings = Vec::new();
1100        for path in file_paths {
1101            for pat in &self.patterns {
1102                if pat.pattern_type != PatternType::SuspiciousPath {
1103                    continue;
1104                }
1105                if path.contains(pat.pattern.as_str()) {
1106                    findings.push(MalwareFinding {
1107                        severity: pat.severity,
1108                        description: pat.description.clone(),
1109                        file_path: String::from(*path),
1110                        pattern_matched: pat.pattern.clone(),
1111                    });
1112                }
1113            }
1114        }
1115        findings
1116    }
1117
1118    /// Scan requested capabilities against `ExcessiveCapability` patterns.
1119    pub fn scan_capabilities(&self, requested_caps: &[&str]) -> Vec<MalwareFinding> {
1120        let mut findings = Vec::new();
1121        for cap in requested_caps {
1122            for pat in &self.patterns {
1123                if pat.pattern_type != PatternType::ExcessiveCapability {
1124                    continue;
1125                }
1126                if *cap == pat.pattern.as_str() {
1127                    findings.push(MalwareFinding {
1128                        severity: pat.severity,
1129                        description: pat.description.clone(),
1130                        file_path: String::new(),
1131                        pattern_matched: pat.pattern.clone(),
1132                    });
1133                }
1134            }
1135        }
1136        findings
1137    }
1138}
1139
1140impl Default for SecurityScanner {
1141    fn default() -> Self {
1142        Self::new()
1143    }
1144}
1145
1146// ============================================================================
1147// Vulnerability Tracking
1148// ============================================================================
1149
1150/// A vulnerability advisory for one or more packages.
1151#[derive(Debug, Clone)]
1152pub struct VulnerabilityAdvisory {
1153    /// CVE or advisory identifier (e.g. "CVE-2024-12345").
1154    pub id: String,
1155    /// Affected packages as `(package_name, affected_version_range)` pairs.
1156    pub affected_packages: Vec<(String, String)>,
1157    /// Severity of the vulnerability.
1158    pub severity: Severity,
1159    /// Human-readable description.
1160    pub description: String,
1161    /// Version that fixes the vulnerability, if known.
1162    pub fixed_version: Option<String>,
1163}
1164
1165/// Database of known vulnerability advisories.
1166#[derive(Debug, Clone)]
1167pub struct VulnerabilityDatabase {
1168    /// All registered advisories.
1169    advisories: Vec<VulnerabilityAdvisory>,
1170}
1171
1172impl VulnerabilityDatabase {
1173    /// Create an empty vulnerability database.
1174    pub fn new() -> Self {
1175        Self {
1176            advisories: Vec::new(),
1177        }
1178    }
1179
1180    /// Add an advisory to the database.
1181    pub fn add_advisory(&mut self, advisory: VulnerabilityAdvisory) {
1182        self.advisories.push(advisory);
1183    }
1184
1185    /// Check whether an installed package has known vulnerabilities.
1186    ///
1187    /// Uses simple equality matching on the package name and substring
1188    /// matching on the version range string.
1189    pub fn check_package(&self, name: &str, version: &str) -> Vec<&VulnerabilityAdvisory> {
1190        self.advisories
1191            .iter()
1192            .filter(|adv| {
1193                adv.affected_packages.iter().any(|(pkg_name, ver_range)| {
1194                    pkg_name == name
1195                        && (ver_range == "*"
1196                            || ver_range == version
1197                            || version.contains(ver_range.as_str()))
1198                })
1199            })
1200            .collect()
1201    }
1202
1203    /// Batch-check a set of installed packages.
1204    ///
1205    /// Takes `(name, version)` pairs and returns matching advisories together
1206    /// with the affected package name.
1207    pub fn check_installed<'a>(
1208        &'a self,
1209        installed: &'a [(String, String)],
1210    ) -> Vec<(&'a str, &'a VulnerabilityAdvisory)> {
1211        let mut results = Vec::new();
1212        for (name, version) in installed {
1213            for adv in self.check_package(name, version) {
1214                results.push((name.as_str(), adv));
1215            }
1216        }
1217        results
1218    }
1219
1220    /// Total number of advisories in the database.
1221    pub fn advisory_count(&self) -> usize {
1222        self.advisories.len()
1223    }
1224
1225    /// Number of `Critical`-severity advisories.
1226    pub fn critical_count(&self) -> usize {
1227        self.advisories
1228            .iter()
1229            .filter(|a| a.severity == Severity::Critical)
1230            .count()
1231    }
1232}
1233
1234impl Default for VulnerabilityDatabase {
1235    fn default() -> Self {
1236        Self::new()
1237    }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    #[allow(unused_imports)]
1243    use alloc::vec;
1244
1245    use super::*;
1246
1247    // ---- HttpResponse ----
1248
1249    #[test]
1250    fn test_http_response_new() {
1251        let r = HttpResponse::new();
1252        assert_eq!(r.status_code, 0);
1253        assert!(r.headers.is_empty());
1254        assert!(r.body.is_empty());
1255    }
1256
1257    #[test]
1258    fn test_http_response_is_success() {
1259        let mut r = HttpResponse::new();
1260        r.status_code = 200;
1261        assert!(r.is_success());
1262        r.status_code = 299;
1263        assert!(r.is_success());
1264        r.status_code = 300;
1265        assert!(!r.is_success());
1266        r.status_code = 199;
1267        assert!(!r.is_success());
1268        r.status_code = 404;
1269        assert!(!r.is_success());
1270    }
1271
1272    // ---- HttpClient URL building / parsing ----
1273
1274    #[test]
1275    fn test_http_client_build_url_with_trailing_slash() {
1276        let client = HttpClient::new(String::from("https://example.com/"));
1277        let url = client.build_url("packages.json");
1278        assert_eq!(url, "https://example.com/packages.json");
1279    }
1280
1281    #[test]
1282    fn test_http_client_build_url_without_trailing_slash() {
1283        let client = HttpClient::new(String::from("https://example.com"));
1284        let url = client.build_url("packages.json");
1285        assert_eq!(url, "https://example.com/packages.json");
1286    }
1287
1288    #[test]
1289    fn test_http_client_build_url_path_with_leading_slash() {
1290        let client = HttpClient::new(String::from("https://example.com"));
1291        let url = client.build_url("/packages.json");
1292        assert_eq!(url, "https://example.com/packages.json");
1293    }
1294
1295    #[test]
1296    fn test_http_client_parse_url_https() {
1297        let client = HttpClient::new(String::from("https://example.com"));
1298        let (host, port, path) = client.parse_url("https://example.com/path").unwrap();
1299        assert_eq!(host, "example.com");
1300        assert_eq!(port, 443);
1301        assert_eq!(path, "/path");
1302    }
1303
1304    #[test]
1305    fn test_http_client_parse_url_http() {
1306        let client = HttpClient::new(String::from("http://example.com"));
1307        let (host, port, path) = client.parse_url("http://example.com/path").unwrap();
1308        assert_eq!(host, "example.com");
1309        assert_eq!(port, 443); // Defaults to 443
1310        assert_eq!(path, "/path");
1311    }
1312
1313    #[test]
1314    fn test_http_client_parse_url_with_port() {
1315        let client = HttpClient::new(String::from(""));
1316        let (host, port, path) = client.parse_url("https://example.com:8080/api").unwrap();
1317        assert_eq!(host, "example.com");
1318        assert_eq!(port, 8080);
1319        assert_eq!(path, "/api");
1320    }
1321
1322    #[test]
1323    fn test_http_client_parse_url_no_path() {
1324        let client = HttpClient::new(String::from(""));
1325        let (host, port, path) = client.parse_url("https://example.com").unwrap();
1326        assert_eq!(host, "example.com");
1327        assert_eq!(port, 443);
1328        assert_eq!(path, "/");
1329    }
1330
1331    #[test]
1332    fn test_http_client_build_request_get() {
1333        let client = HttpClient::new(String::from(""));
1334        let req = client.build_request(HttpMethod::Get, "example.com", "/index.html");
1335        let req_str = core::str::from_utf8(&req).unwrap();
1336        assert!(req_str.starts_with("GET /index.html HTTP/1.1\r\n"));
1337        assert!(req_str.contains("Host: example.com"));
1338    }
1339
1340    #[test]
1341    fn test_http_client_build_request_head() {
1342        let client = HttpClient::new(String::from(""));
1343        let req = client.build_request(HttpMethod::Head, "example.com", "/");
1344        let req_str = core::str::from_utf8(&req).unwrap();
1345        assert!(req_str.starts_with("HEAD / HTTP/1.1\r\n"));
1346    }
1347
1348    #[test]
1349    fn test_http_client_resolve_hostname_ip() {
1350        let client = HttpClient::new(String::from(""));
1351        let ip = client.resolve_hostname("192.168.1.10").unwrap();
1352        assert_eq!(ip, [192, 168, 1, 10]);
1353    }
1354
1355    #[test]
1356    fn test_http_client_resolve_hostname_fallback() {
1357        let client = HttpClient::new(String::from(""));
1358        let ip = client.resolve_hostname("example.com").unwrap();
1359        assert_eq!(ip, [127, 0, 0, 1]);
1360    }
1361
1362    #[test]
1363    fn test_http_client_find_header_end() {
1364        let client = HttpClient::new(String::from(""));
1365        let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nbody";
1366        let pos = client.find_header_end(data);
1367        assert!(pos.is_some());
1368        // Verify that the data at pos..pos+4 is \r\n\r\n
1369        let p = pos.unwrap();
1370        assert_eq!(&data[p..p + 4], b"\r\n\r\n");
1371    }
1372
1373    #[test]
1374    fn test_http_client_find_header_end_missing() {
1375        let client = HttpClient::new(String::from(""));
1376        let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n";
1377        assert!(client.find_header_end(data).is_none());
1378    }
1379
1380    #[test]
1381    fn test_http_client_parse_response() {
1382        let client = HttpClient::new(String::from(""));
1383        let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello world";
1384        let response = client.parse_response(raw).unwrap();
1385        assert_eq!(response.status_code, 200);
1386        assert_eq!(response.headers.len(), 1);
1387        assert_eq!(response.headers[0].0, "Content-Type");
1388        assert_eq!(response.headers[0].1, "text/plain");
1389        assert_eq!(response.body, b"hello world");
1390    }
1391
1392    // ---- Repository ----
1393
1394    #[test]
1395    fn test_repository_new() {
1396        let repo = Repository::new(String::from("test"), String::from("https://test.org"), true);
1397        assert_eq!(repo.name, "test");
1398        assert_eq!(repo.url, "https://test.org");
1399        assert!(repo.trusted);
1400        assert!(repo.package_cache.is_empty());
1401        assert_eq!(repo.last_updated, 0);
1402    }
1403
1404    #[test]
1405    fn test_repository_default() {
1406        let repo = Repository::default();
1407        assert_eq!(repo.name, "default");
1408        assert!(repo.trusted);
1409    }
1410
1411    #[test]
1412    fn test_repository_has_package_empty() {
1413        let repo = Repository::default();
1414        assert!(!repo.has_package(&String::from("nonexistent")));
1415    }
1416
1417    #[test]
1418    fn test_repository_get_package_none() {
1419        let repo = Repository::default();
1420        assert!(repo.get_package(&String::from("nonexistent")).is_none());
1421    }
1422
1423    #[test]
1424    fn test_repository_invalidate_cache() {
1425        let mut repo = Repository::default();
1426        repo.last_updated = 1000;
1427        repo.invalidate_cache();
1428        assert!(repo.package_cache.is_empty());
1429        assert_eq!(repo.last_updated, 0);
1430    }
1431
1432    #[test]
1433    fn test_repository_is_cache_stale() {
1434        let repo = Repository::default();
1435        // last_updated = 0, current_time = 3601 -> stale
1436        assert!(repo.is_cache_stale(3601));
1437        // current_time = 3600 -> not stale (exactly 1 hour)
1438        assert!(!repo.is_cache_stale(3600));
1439        // current_time = 100 -> not stale
1440        assert!(!repo.is_cache_stale(100));
1441    }
1442
1443    // ---- Repository JSON parsing ----
1444
1445    #[test]
1446    fn test_repository_extract_json_string() {
1447        let repo = Repository::default();
1448        let json = r#"{"name":"curl","version":"8.5.0"}"#;
1449        assert_eq!(
1450            repo.extract_json_string(json, "name"),
1451            Some(String::from("curl"))
1452        );
1453        assert_eq!(
1454            repo.extract_json_string(json, "version"),
1455            Some(String::from("8.5.0"))
1456        );
1457        assert_eq!(repo.extract_json_string(json, "missing"), None);
1458    }
1459
1460    #[test]
1461    fn test_repository_parse_version() {
1462        let repo = Repository::default();
1463        let v = repo.parse_version("1.2.3").unwrap();
1464        assert_eq!(v, Version::new(1, 2, 3));
1465    }
1466
1467    #[test]
1468    fn test_repository_parse_version_too_short() {
1469        let repo = Repository::default();
1470        assert!(repo.parse_version("1.2").is_none());
1471    }
1472
1473    #[test]
1474    fn test_repository_parse_version_invalid() {
1475        let repo = Repository::default();
1476        assert!(repo.parse_version("abc").is_none());
1477    }
1478
1479    #[test]
1480    fn test_repository_extract_conflicts() {
1481        let repo = Repository::default();
1482        let json = r#"{"conflicts":["pkg-b","pkg-c"]}"#;
1483        let conflicts = repo.extract_conflicts(json);
1484        assert_eq!(conflicts.len(), 2);
1485        assert_eq!(conflicts[0], "pkg-b");
1486        assert_eq!(conflicts[1], "pkg-c");
1487    }
1488
1489    #[test]
1490    fn test_repository_extract_conflicts_empty() {
1491        let repo = Repository::default();
1492        let json = r#"{"name":"test"}"#;
1493        let conflicts = repo.extract_conflicts(json);
1494        assert!(conflicts.is_empty());
1495    }
1496
1497    #[test]
1498    fn test_repository_extract_dependencies() {
1499        let repo = Repository::default();
1500        let json = r#"{"dependencies":[{"name":"openssl","version":"1.0.0"}]}"#;
1501        let deps = repo.extract_dependencies(json);
1502        assert_eq!(deps.len(), 1);
1503        assert_eq!(deps[0].name, "openssl");
1504        assert_eq!(deps[0].version_req, "1.0.0");
1505    }
1506
1507    #[test]
1508    fn test_repository_parse_package_index_empty_array() {
1509        let repo = Repository::default();
1510        let data = b"[]";
1511        let pkgs = repo.parse_package_index(data);
1512        assert!(pkgs.is_empty());
1513    }
1514
1515    #[test]
1516    fn test_repository_parse_package_index_not_array() {
1517        let repo = Repository::default();
1518        let data = b"{}";
1519        let pkgs = repo.parse_package_index(data);
1520        assert!(pkgs.is_empty());
1521    }
1522
1523    #[test]
1524    fn test_repository_parse_package_index_invalid_utf8() {
1525        let repo = Repository::default();
1526        let data: &[u8] = &[0xFF, 0xFE];
1527        let pkgs = repo.parse_package_index(data);
1528        assert!(pkgs.is_empty());
1529    }
1530
1531    // ---- RepositoryIndex ----
1532
1533    #[test]
1534    fn test_repository_index_generate() {
1535        let pkgs = vec![PackageMetadata {
1536            name: String::from("test-pkg"),
1537            version: Version::new(1, 0, 0),
1538            author: String::from("author"),
1539            description: String::from("desc"),
1540            license: String::from("MIT"),
1541            dependencies: vec![],
1542            conflicts: vec![],
1543        }];
1544        let index = RepositoryIndex::generate(&pkgs);
1545        assert_eq!(index.entries.len(), 1);
1546        assert_eq!(index.entries[0].name, "test-pkg");
1547        assert_eq!(index.entries[0].version, "1.0.0");
1548        assert_eq!(index.entries[0].description, "desc");
1549        assert_eq!(index.entries[0].license, "MIT");
1550    }
1551
1552    #[test]
1553    fn test_repository_index_to_bytes() {
1554        let pkgs = vec![PackageMetadata {
1555            name: String::from("a"),
1556            version: Version::new(0, 1, 0),
1557            author: String::new(),
1558            description: String::from("d"),
1559            license: String::from("MIT"),
1560            dependencies: vec![],
1561            conflicts: vec![],
1562        }];
1563        let index = RepositoryIndex::generate(&pkgs);
1564        let bytes = index.to_bytes();
1565        let s = core::str::from_utf8(&bytes).unwrap();
1566        assert!(s.contains("\"name\":\"a\""));
1567        assert!(s.contains("\"version\":\"0.1.0\""));
1568        assert!(s.contains("\"license\":\"MIT\""));
1569    }
1570
1571    #[test]
1572    fn test_repository_index_verify_signature_empty() {
1573        let index = RepositoryIndex::generate(&[]);
1574        // Empty signature -> false
1575        assert!(!index.verify_signature(&[1, 2, 3]));
1576        // Empty public key -> false
1577        let mut index2 = RepositoryIndex::generate(&[]);
1578        index2.signature = vec![1, 2, 3];
1579        assert!(!index2.verify_signature(&[]));
1580    }
1581
1582    // ---- MirrorManager ----
1583
1584    #[test]
1585    fn test_mirror_manager_new() {
1586        let mm = MirrorManager::new();
1587        assert_eq!(mm.mirror_count(), 0);
1588        assert!(mm.list_mirrors().is_empty());
1589    }
1590
1591    #[test]
1592    fn test_mirror_manager_add_and_sort() {
1593        let mut mm = MirrorManager::new();
1594        mm.add_mirror(MirrorMetadata {
1595            url: String::from("https://mirror2.com"),
1596            priority: 20,
1597            region: String::from("us"),
1598            last_sync: 0,
1599            status: MirrorStatus::Unknown,
1600        });
1601        mm.add_mirror(MirrorMetadata {
1602            url: String::from("https://mirror1.com"),
1603            priority: 10,
1604            region: String::from("eu"),
1605            last_sync: 0,
1606            status: MirrorStatus::Unknown,
1607        });
1608        assert_eq!(mm.mirror_count(), 2);
1609        // Should be sorted by priority (lower first)
1610        assert_eq!(mm.list_mirrors()[0].url, "https://mirror1.com");
1611        assert_eq!(mm.list_mirrors()[1].url, "https://mirror2.com");
1612    }
1613
1614    #[test]
1615    fn test_mirror_manager_remove() {
1616        let mut mm = MirrorManager::new();
1617        mm.add_mirror(MirrorMetadata {
1618            url: String::from("https://mirror1.com"),
1619            priority: 10,
1620            region: String::new(),
1621            last_sync: 0,
1622            status: MirrorStatus::Online,
1623        });
1624        assert!(mm.remove_mirror("https://mirror1.com"));
1625        assert!(!mm.remove_mirror("https://nonexistent.com"));
1626        assert_eq!(mm.mirror_count(), 0);
1627    }
1628
1629    #[test]
1630    fn test_mirror_manager_select_best() {
1631        let mut mm = MirrorManager::new();
1632        mm.add_mirror(MirrorMetadata {
1633            url: String::from("https://mirror1.com"),
1634            priority: 10,
1635            region: String::new(),
1636            last_sync: 0,
1637            status: MirrorStatus::Offline,
1638        });
1639        mm.add_mirror(MirrorMetadata {
1640            url: String::from("https://mirror2.com"),
1641            priority: 20,
1642            region: String::new(),
1643            last_sync: 0,
1644            status: MirrorStatus::Online,
1645        });
1646        // Should skip offline mirror1 and return online mirror2
1647        let best = mm.select_best_mirror().unwrap();
1648        assert_eq!(best.url, "https://mirror2.com");
1649    }
1650
1651    #[test]
1652    fn test_mirror_manager_select_best_all_offline() {
1653        let mut mm = MirrorManager::new();
1654        mm.add_mirror(MirrorMetadata {
1655            url: String::from("https://mirror1.com"),
1656            priority: 10,
1657            region: String::new(),
1658            last_sync: 0,
1659            status: MirrorStatus::Offline,
1660        });
1661        // Falls back to first mirror when all offline
1662        let best = mm.select_best_mirror().unwrap();
1663        assert_eq!(best.url, "https://mirror1.com");
1664    }
1665
1666    #[test]
1667    fn test_mirror_manager_select_best_empty() {
1668        let mm = MirrorManager::new();
1669        assert!(mm.select_best_mirror().is_none());
1670    }
1671
1672    #[test]
1673    fn test_mirror_manager_mark_offline() {
1674        let mut mm = MirrorManager::new();
1675        mm.add_mirror(MirrorMetadata {
1676            url: String::from("https://mirror1.com"),
1677            priority: 10,
1678            region: String::new(),
1679            last_sync: 0,
1680            status: MirrorStatus::Online,
1681        });
1682        mm.mark_offline("https://mirror1.com");
1683        assert_eq!(mm.list_mirrors()[0].status, MirrorStatus::Offline);
1684    }
1685
1686    // ---- RepositoryConfig ----
1687
1688    #[test]
1689    fn test_repo_config_new() {
1690        let cfg = RepositoryConfig::new();
1691        assert!(cfg.all_repositories().is_empty());
1692        assert!(cfg.enabled_repositories().is_empty());
1693    }
1694
1695    #[test]
1696    fn test_repo_config_add_and_sort() {
1697        let mut cfg = RepositoryConfig::new();
1698        cfg.add_repository(RepositoryEntry {
1699            name: String::from("b"),
1700            url: String::from("https://b.org"),
1701            enabled: true,
1702            trusted: true,
1703            priority: 20,
1704            mirrors: vec![],
1705        });
1706        cfg.add_repository(RepositoryEntry {
1707            name: String::from("a"),
1708            url: String::from("https://a.org"),
1709            enabled: true,
1710            trusted: false,
1711            priority: 10,
1712            mirrors: vec![],
1713        });
1714        assert_eq!(cfg.all_repositories().len(), 2);
1715        assert_eq!(cfg.all_repositories()[0].name, "a");
1716    }
1717
1718    #[test]
1719    fn test_repo_config_remove() {
1720        let mut cfg = RepositoryConfig::new();
1721        cfg.add_repository(RepositoryEntry {
1722            name: String::from("test"),
1723            url: String::from("https://test.org"),
1724            enabled: true,
1725            trusted: true,
1726            priority: 10,
1727            mirrors: vec![],
1728        });
1729        assert!(cfg.remove_repository("test"));
1730        assert!(!cfg.remove_repository("test"));
1731        assert!(cfg.all_repositories().is_empty());
1732    }
1733
1734    #[test]
1735    fn test_repo_config_enable_disable() {
1736        let mut cfg = RepositoryConfig::new();
1737        cfg.add_repository(RepositoryEntry {
1738            name: String::from("test"),
1739            url: String::from("https://test.org"),
1740            enabled: false,
1741            trusted: true,
1742            priority: 10,
1743            mirrors: vec![],
1744        });
1745        assert!(cfg.enabled_repositories().is_empty());
1746        assert!(cfg.enable_repository("test"));
1747        assert_eq!(cfg.enabled_repositories().len(), 1);
1748        assert!(cfg.disable_repository("test"));
1749        assert!(cfg.enabled_repositories().is_empty());
1750        assert!(!cfg.enable_repository("nonexistent"));
1751        assert!(!cfg.disable_repository("nonexistent"));
1752    }
1753
1754    #[test]
1755    fn test_repo_config_get_repository() {
1756        let mut cfg = RepositoryConfig::new();
1757        cfg.add_repository(RepositoryEntry {
1758            name: String::from("test"),
1759            url: String::from("https://test.org"),
1760            enabled: true,
1761            trusted: true,
1762            priority: 10,
1763            mirrors: vec![],
1764        });
1765        assert!(cfg.get_repository("test").is_some());
1766        assert!(cfg.get_repository("other").is_none());
1767    }
1768
1769    // ---- AccessControl ----
1770
1771    #[test]
1772    fn test_access_control_new() {
1773        let ac = AccessControl::new(UploadPolicy::Open);
1774        assert_eq!(ac.upload_policy, UploadPolicy::Open);
1775    }
1776
1777    #[test]
1778    fn test_access_control_add_remove_uploader() {
1779        let mut ac = AccessControl::new(UploadPolicy::Restricted);
1780        let fp = [0u8; 32];
1781        ac.add_uploader(fp);
1782        // Adding duplicate should not create duplicates
1783        ac.add_uploader(fp);
1784        assert!(ac.remove_uploader(&fp));
1785        assert!(!ac.remove_uploader(&fp));
1786    }
1787
1788    #[test]
1789    fn test_access_control_default() {
1790        let ac = AccessControl::default();
1791        assert_eq!(ac.upload_policy, UploadPolicy::Restricted);
1792    }
1793
1794    // ---- SecurityScanner ----
1795
1796    #[test]
1797    fn test_security_scanner_default_patterns() {
1798        let scanner = SecurityScanner::new();
1799        // Should have default patterns loaded
1800        assert!(!scanner.patterns.is_empty());
1801    }
1802
1803    #[test]
1804    fn test_security_scanner_scan_paths_match() {
1805        let scanner = SecurityScanner::new();
1806        let paths = &["/etc/shadow"];
1807        let findings = scanner.scan_package_paths(paths);
1808        assert!(!findings.is_empty());
1809        assert_eq!(findings[0].severity, Severity::High);
1810    }
1811
1812    #[test]
1813    fn test_security_scanner_scan_paths_no_match() {
1814        let scanner = SecurityScanner::new();
1815        let paths = &["/usr/bin/hello"];
1816        let findings = scanner.scan_package_paths(paths);
1817        assert!(findings.is_empty());
1818    }
1819
1820    #[test]
1821    fn test_security_scanner_scan_capabilities() {
1822        let scanner = SecurityScanner::new();
1823        let caps = &["CAP_SYS_ADMIN"];
1824        let findings = scanner.scan_capabilities(caps);
1825        assert!(!findings.is_empty());
1826        assert_eq!(findings[0].severity, Severity::Medium);
1827    }
1828
1829    #[test]
1830    fn test_security_scanner_scan_capabilities_no_match() {
1831        let scanner = SecurityScanner::new();
1832        let caps = &["CAP_READ"];
1833        let findings = scanner.scan_capabilities(caps);
1834        assert!(findings.is_empty());
1835    }
1836
1837    #[test]
1838    fn test_security_scanner_add_pattern() {
1839        let mut scanner = SecurityScanner::new();
1840        let initial_count = scanner.patterns.len();
1841        scanner.add_pattern(MalwarePattern {
1842            pattern_type: PatternType::KnownBadHash,
1843            description: String::from("bad hash"),
1844            severity: Severity::Critical,
1845            pattern: String::from("deadbeef"),
1846        });
1847        assert_eq!(scanner.patterns.len(), initial_count + 1);
1848    }
1849
1850    // ---- Severity ordering ----
1851
1852    #[test]
1853    fn test_severity_ordering() {
1854        assert!(Severity::Low < Severity::Medium);
1855        assert!(Severity::Medium < Severity::High);
1856        assert!(Severity::High < Severity::Critical);
1857    }
1858
1859    // ---- VulnerabilityDatabase ----
1860
1861    #[test]
1862    fn test_vuln_db_new() {
1863        let db = VulnerabilityDatabase::new();
1864        assert_eq!(db.advisory_count(), 0);
1865        assert_eq!(db.critical_count(), 0);
1866    }
1867
1868    #[test]
1869    fn test_vuln_db_add_and_check() {
1870        let mut db = VulnerabilityDatabase::new();
1871        db.add_advisory(VulnerabilityAdvisory {
1872            id: String::from("CVE-2024-0001"),
1873            affected_packages: vec![(String::from("openssl"), String::from("1.0.0"))],
1874            severity: Severity::Critical,
1875            description: String::from("buffer overflow"),
1876            fixed_version: Some(String::from("1.0.1")),
1877        });
1878        assert_eq!(db.advisory_count(), 1);
1879        assert_eq!(db.critical_count(), 1);
1880
1881        let results = db.check_package("openssl", "1.0.0");
1882        assert_eq!(results.len(), 1);
1883        assert_eq!(results[0].id, "CVE-2024-0001");
1884
1885        let results = db.check_package("openssl", "1.0.1");
1886        assert!(results.is_empty());
1887    }
1888
1889    #[test]
1890    fn test_vuln_db_wildcard() {
1891        let mut db = VulnerabilityDatabase::new();
1892        db.add_advisory(VulnerabilityAdvisory {
1893            id: String::from("CVE-2024-0002"),
1894            affected_packages: vec![(String::from("curl"), String::from("*"))],
1895            severity: Severity::High,
1896            description: String::from("all versions"),
1897            fixed_version: None,
1898        });
1899        let results = db.check_package("curl", "999.0.0");
1900        assert_eq!(results.len(), 1);
1901    }
1902
1903    #[test]
1904    fn test_vuln_db_check_installed() {
1905        let mut db = VulnerabilityDatabase::new();
1906        db.add_advisory(VulnerabilityAdvisory {
1907            id: String::from("CVE-2024-0003"),
1908            affected_packages: vec![(String::from("pkg-a"), String::from("1.0.0"))],
1909            severity: Severity::Medium,
1910            description: String::from("test"),
1911            fixed_version: None,
1912        });
1913        let installed = vec![
1914            (String::from("pkg-a"), String::from("1.0.0")),
1915            (String::from("pkg-b"), String::from("2.0.0")),
1916        ];
1917        let results = db.check_installed(&installed);
1918        assert_eq!(results.len(), 1);
1919        assert_eq!(results[0].0, "pkg-a");
1920    }
1921}