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

veridian_kernel/pkg/
repo_server.rs

1//! Package Repository Hosting
2//!
3//! HTTP-based package repository server for VeridianOS. Hosts binary packages
4//! with metadata indexing, Ed25519 signature verification, paginated search,
5//! and JSON-like index generation for client consumption.
6//!
7//! ## HTTP Endpoints (conceptual)
8//!
9//! - `GET /api/packages` -- paginated package listing
10//! - `GET /api/packages/{name}` -- all versions of a package
11//! - `GET /api/packages/{name}/{version}` -- specific version metadata
12//! - `POST /api/packages` -- upload with Ed25519 signature auth
13
14#[cfg(feature = "alloc")]
15use alloc::{
16    collections::BTreeMap,
17    string::{String, ToString},
18    vec::Vec,
19};
20
21#[cfg(feature = "alloc")]
22use super::build_package::PackageSignature;
23#[cfg(feature = "alloc")]
24use crate::error::KernelError;
25
26/// Package metadata stored in the repository index.
27///
28/// Distinct from `build_package::PackageMetadata` -- this adds
29/// repository-specific fields (sha256_hash, upload_time).
30#[cfg(feature = "alloc")]
31#[derive(Debug, Clone)]
32pub struct RepoPackageMeta {
33    pub name: String,
34    pub version: String,
35    pub architecture: String,
36    pub description: String,
37    pub size: u64,
38    pub sha256_hash: [u8; 32],
39    pub dependencies: Vec<String>,
40    pub upload_time: u64,
41}
42
43#[cfg(feature = "alloc")]
44impl RepoPackageMeta {
45    pub fn new(name: &str, version: &str) -> Self {
46        Self {
47            name: name.to_string(),
48            version: version.to_string(),
49            architecture: String::from("x86_64"),
50            description: String::new(),
51            size: 0,
52            sha256_hash: [0u8; 32],
53            dependencies: Vec::new(),
54            upload_time: 0,
55        }
56    }
57
58    /// Format metadata as a JSON-like key-value string.
59    pub fn to_json(&self) -> String {
60        let mut out = String::from("{");
61        out.push_str("\"name\":\"");
62        out.push_str(&self.name);
63        out.push_str("\",\"version\":\"");
64        out.push_str(&self.version);
65        out.push_str("\",\"arch\":\"");
66        out.push_str(&self.architecture);
67        out.push_str("\",\"description\":\"");
68        out.push_str(&self.description);
69        out.push_str("\",\"size\":");
70        push_u64(&mut out, self.size);
71        out.push_str(",\"sha256\":\"");
72        for b in &self.sha256_hash {
73            push_hex_byte(&mut out, *b);
74        }
75        out.push_str("\",\"deps\":[");
76        for (i, dep) in self.dependencies.iter().enumerate() {
77            if i > 0 {
78                out.push(',');
79            }
80            out.push('"');
81            out.push_str(dep);
82            out.push('"');
83        }
84        out.push_str("],\"upload_time\":");
85        push_u64(&mut out, self.upload_time);
86        out.push('}');
87        out
88    }
89}
90
91/// Repository package index.
92#[cfg(feature = "alloc")]
93#[derive(Debug, Clone)]
94pub struct RepoIndex {
95    /// Packages keyed by name, each with a list of version entries.
96    pub packages: BTreeMap<String, Vec<RepoPackageMeta>>,
97    /// Tick count of last index update.
98    pub last_updated: u64,
99}
100
101#[cfg(feature = "alloc")]
102impl Default for RepoIndex {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[cfg(feature = "alloc")]
109impl RepoIndex {
110    pub fn new() -> Self {
111        Self {
112            packages: BTreeMap::new(),
113            last_updated: 0,
114        }
115    }
116
117    /// Total number of unique package names.
118    pub fn package_count(&self) -> usize {
119        self.packages.len()
120    }
121
122    /// Total number of individual package versions.
123    pub fn version_count(&self) -> usize {
124        self.packages.values().map(|v| v.len()).sum()
125    }
126}
127
128/// Repository configuration.
129#[cfg(feature = "alloc")]
130#[derive(Debug, Clone)]
131pub struct RepoConfig {
132    pub listen_port: u16,
133    pub storage_path: String,
134    /// Maximum package upload size in bytes (default 256 MB).
135    pub max_package_size: u64,
136    /// Whether uploads must include a valid Ed25519 signature.
137    pub require_signatures: bool,
138}
139
140#[cfg(feature = "alloc")]
141impl Default for RepoConfig {
142    fn default() -> Self {
143        Self {
144            listen_port: 8080,
145            storage_path: String::from("/var/repo/packages"),
146            max_package_size: 256 * 1024 * 1024,
147            require_signatures: true,
148        }
149    }
150}
151
152/// HTTP request method (subset relevant to the repo API).
153#[cfg(feature = "alloc")]
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum HttpMethod {
156    Get,
157    Post,
158}
159
160/// Parsed HTTP request for the repository API.
161#[cfg(feature = "alloc")]
162#[derive(Debug, Clone)]
163pub struct RepoRequest {
164    pub method: HttpMethod,
165    pub path: String,
166    pub body: Vec<u8>,
167}
168
169/// HTTP response from the repository.
170#[cfg(feature = "alloc")]
171#[derive(Debug, Clone)]
172pub struct RepoResponse {
173    pub status: u16,
174    pub body: String,
175}
176
177#[cfg(feature = "alloc")]
178impl RepoResponse {
179    pub fn ok(body: String) -> Self {
180        Self { status: 200, body }
181    }
182
183    pub fn not_found(msg: &str) -> Self {
184        Self {
185            status: 404,
186            body: msg.to_string(),
187        }
188    }
189
190    pub fn bad_request(msg: &str) -> Self {
191        Self {
192            status: 400,
193            body: msg.to_string(),
194        }
195    }
196
197    pub fn forbidden(msg: &str) -> Self {
198        Self {
199            status: 403,
200            body: msg.to_string(),
201        }
202    }
203}
204
205/// Package repository server.
206///
207/// Manages the package index, handles uploads with optional signature
208/// verification, and provides search/listing capabilities.
209#[cfg(feature = "alloc")]
210pub struct RepoServer {
211    pub config: RepoConfig,
212    pub index: RepoIndex,
213    /// Raw package data keyed by "name-version".
214    packages: BTreeMap<String, Vec<u8>>,
215}
216
217#[cfg(feature = "alloc")]
218impl RepoServer {
219    /// Initialize a new repository server with default configuration.
220    pub fn init() -> Self {
221        Self::with_config(RepoConfig::default())
222    }
223
224    /// Initialize with custom configuration.
225    pub fn with_config(config: RepoConfig) -> Self {
226        Self {
227            config,
228            index: RepoIndex::new(),
229            packages: BTreeMap::new(),
230        }
231    }
232
233    /// Register a package in the index.
234    ///
235    /// If `require_signatures` is enabled, the caller must provide a valid
236    /// signature. The package data is stored in the internal map.
237    pub fn add_package(
238        &mut self,
239        meta: RepoPackageMeta,
240        data: Vec<u8>,
241        signature: Option<&PackageSignature>,
242    ) -> Result<(), KernelError> {
243        // Size check
244        if data.len() as u64 > self.config.max_package_size {
245            return Err(KernelError::InvalidArgument {
246                name: "package_data",
247                value: "exceeds max_package_size",
248            });
249        }
250
251        // Signature verification
252        if self.config.require_signatures {
253            if let Some(sig) = signature {
254                if !self.verify_upload(sig, &data) {
255                    return Err(KernelError::PermissionDenied {
256                        operation: "package_upload",
257                    });
258                }
259            } else {
260                return Err(KernelError::PermissionDenied {
261                    operation: "package_upload",
262                });
263            }
264        }
265
266        let key = package_key(&meta.name, &meta.version);
267        self.packages.insert(key, data);
268
269        let entry = self
270            .index
271            .packages
272            .entry(meta.name.clone())
273            .or_insert_with(Vec::new);
274
275        // Replace existing version or append
276        if let Some(pos) = entry.iter().position(|e| e.version == meta.version) {
277            entry[pos] = meta;
278        } else {
279            entry.push(meta);
280        }
281
282        self.index.last_updated += 1;
283        Ok(())
284    }
285
286    /// Remove a package (specific version) from the index and storage.
287    pub fn remove_package(&mut self, name: &str, version: &str) -> Result<(), KernelError> {
288        let key = package_key(name, version);
289        self.packages.remove(&key);
290
291        if let Some(versions) = self.index.packages.get_mut(name) {
292            versions.retain(|v| v.version != version);
293            if versions.is_empty() {
294                self.index.packages.remove(name);
295            }
296            self.index.last_updated += 1;
297            Ok(())
298        } else {
299            Err(KernelError::NotFound {
300                resource: "package",
301                id: 0,
302            })
303        }
304    }
305
306    /// Search packages by substring match on name.
307    pub fn search(&self, pattern: &str) -> Vec<&RepoPackageMeta> {
308        let mut results = Vec::new();
309        for versions in self.index.packages.values() {
310            for meta in versions {
311                if meta.name.contains(pattern) {
312                    results.push(meta);
313                }
314            }
315        }
316        results
317    }
318
319    /// Get metadata for a specific package name and version.
320    pub fn get_package_info(&self, name: &str, version: &str) -> Option<&RepoPackageMeta> {
321        self.index
322            .packages
323            .get(name)?
324            .iter()
325            .find(|m| m.version == version)
326    }
327
328    /// List all versions for a given package name.
329    pub fn list_versions(&self, name: &str) -> Option<&Vec<RepoPackageMeta>> {
330        self.index.packages.get(name)
331    }
332
333    /// Paginated listing of all packages.
334    ///
335    /// Returns at most `page_size` entries starting from `offset`.
336    pub fn list_packages(&self, offset: usize, page_size: usize) -> Vec<&RepoPackageMeta> {
337        let mut all: Vec<&RepoPackageMeta> = Vec::new();
338        for versions in self.index.packages.values() {
339            for meta in versions {
340                all.push(meta);
341            }
342        }
343        // Apply pagination
344        all.into_iter().skip(offset).take(page_size).collect()
345    }
346
347    /// Generate a JSON-like index string for HTTP serving.
348    pub fn generate_index_json(&self) -> String {
349        let mut out = String::from("{\"packages\":[");
350        let mut first = true;
351        for versions in self.index.packages.values() {
352            for meta in versions {
353                if !first {
354                    out.push(',');
355                }
356                first = false;
357                out.push_str(&meta.to_json());
358            }
359        }
360        out.push_str("],\"last_updated\":");
361        push_u64(&mut out, self.index.last_updated);
362        out.push('}');
363        out
364    }
365
366    /// Verify an upload signature using Ed25519 marker check.
367    pub fn verify_upload(&self, signature: &PackageSignature, archive_data: &[u8]) -> bool {
368        // Delegate to PackageSignature's verification with a placeholder key
369        let public_key = [0u8; 32];
370        signature.verify(archive_data, &public_key)
371    }
372
373    /// Route an HTTP request to the appropriate handler.
374    pub fn handle_request(&self, req: &RepoRequest) -> RepoResponse {
375        match req.method {
376            HttpMethod::Get => self.handle_get(&req.path),
377            HttpMethod::Post => {
378                // POST /api/packages -- upload not fully implemented here
379                RepoResponse::bad_request("upload requires multipart body parsing")
380            }
381        }
382    }
383
384    fn handle_get(&self, path: &str) -> RepoResponse {
385        // GET /api/packages
386        if path == "/api/packages" {
387            return RepoResponse::ok(self.generate_index_json());
388        }
389
390        // GET /api/packages/{name}
391        // GET /api/packages/{name}/{version}
392        if let Some(rest) = path.strip_prefix("/api/packages/") {
393            let parts: Vec<&str> = rest.splitn(2, '/').collect();
394            match parts.len() {
395                1 => {
396                    let name = parts[0];
397                    if let Some(versions) = self.list_versions(name) {
398                        let mut out = String::from("[");
399                        for (i, meta) in versions.iter().enumerate() {
400                            if i > 0 {
401                                out.push(',');
402                            }
403                            out.push_str(&meta.to_json());
404                        }
405                        out.push(']');
406                        RepoResponse::ok(out)
407                    } else {
408                        RepoResponse::not_found("package not found")
409                    }
410                }
411                2 => {
412                    let (name, version) = (parts[0], parts[1]);
413                    if let Some(meta) = self.get_package_info(name, version) {
414                        RepoResponse::ok(meta.to_json())
415                    } else {
416                        RepoResponse::not_found("version not found")
417                    }
418                }
419                _ => RepoResponse::not_found("invalid path"),
420            }
421        } else {
422            RepoResponse::not_found("unknown endpoint")
423        }
424    }
425
426    /// Get the raw package data for download.
427    pub fn get_package_data(&self, name: &str, version: &str) -> Option<&Vec<u8>> {
428        let key = package_key(name, version);
429        self.packages.get(&key)
430    }
431
432    /// Total stored package data in bytes.
433    pub fn total_storage_bytes(&self) -> u64 {
434        self.packages.values().map(|d| d.len() as u64).sum()
435    }
436}
437
438// ---------------------------------------------------------------------------
439// Helpers
440// ---------------------------------------------------------------------------
441
442/// Build a storage key from package name and version.
443#[cfg(feature = "alloc")]
444fn package_key(name: &str, version: &str) -> String {
445    let mut key = String::from(name);
446    key.push('-');
447    key.push_str(version);
448    key
449}
450
451/// Append a u64 as decimal digits to a string (no formatting crate needed).
452#[cfg(feature = "alloc")]
453fn push_u64(out: &mut String, mut val: u64) {
454    if val == 0 {
455        out.push('0');
456        return;
457    }
458    let start = out.len();
459    while val > 0 {
460        let digit = (val % 10) as u8 + b'0';
461        out.push(digit as char);
462        val /= 10;
463    }
464    // Reverse the digits we just pushed
465    let bytes = unsafe { out.as_bytes_mut() };
466    bytes[start..].reverse();
467}
468
469/// Append a single byte as two hex characters.
470#[cfg(feature = "alloc")]
471fn push_hex_byte(out: &mut String, byte: u8) {
472    const HEX: &[u8; 16] = b"0123456789abcdef";
473    out.push(HEX[(byte >> 4) as usize] as char);
474    out.push(HEX[(byte & 0x0F) as usize] as char);
475}
476
477// ---------------------------------------------------------------------------
478// Tests
479// ---------------------------------------------------------------------------
480
481#[cfg(test)]
482mod tests {
483    #[allow(unused_imports)]
484    use alloc::vec;
485
486    use super::*;
487
488    fn make_meta(name: &str, version: &str) -> RepoPackageMeta {
489        let mut m = RepoPackageMeta::new(name, version);
490        m.description = String::from("test package");
491        m.size = 1024;
492        m
493    }
494
495    fn signed_sig() -> PackageSignature {
496        let mut sig = PackageSignature::new("test-signer");
497        sig.sign(&[], &[0u8; 32]);
498        sig
499    }
500
501    #[test]
502    fn test_repo_server_init() {
503        let server = RepoServer::init();
504        assert_eq!(server.config.listen_port, 8080);
505        assert_eq!(server.index.package_count(), 0);
506    }
507
508    #[test]
509    fn test_add_package_with_signature() {
510        let mut server = RepoServer::init();
511        let meta = make_meta("hello", "1.0.0");
512        let sig = signed_sig();
513        assert!(server.add_package(meta, vec![1, 2, 3], Some(&sig)).is_ok());
514        assert_eq!(server.index.package_count(), 1);
515    }
516
517    #[test]
518    fn test_add_package_no_sig_rejected() {
519        let mut server = RepoServer::init();
520        let meta = make_meta("hello", "1.0.0");
521        assert!(server.add_package(meta, vec![1, 2, 3], None).is_err());
522    }
523
524    #[test]
525    fn test_add_package_no_sig_allowed() {
526        let mut config = RepoConfig::default();
527        config.require_signatures = false;
528        let mut server = RepoServer::with_config(config);
529        let meta = make_meta("hello", "1.0.0");
530        assert!(server.add_package(meta, vec![1, 2, 3], None).is_ok());
531    }
532
533    #[test]
534    fn test_remove_package() {
535        let mut config = RepoConfig::default();
536        config.require_signatures = false;
537        let mut server = RepoServer::with_config(config);
538        let meta = make_meta("hello", "1.0.0");
539        server.add_package(meta, vec![1], None).unwrap();
540        assert!(server.remove_package("hello", "1.0.0").is_ok());
541        assert_eq!(server.index.package_count(), 0);
542    }
543
544    #[test]
545    fn test_remove_nonexistent() {
546        let server_config = RepoConfig {
547            require_signatures: false,
548            ..RepoConfig::default()
549        };
550        let mut server = RepoServer::with_config(server_config);
551        assert!(server.remove_package("nope", "1.0.0").is_err());
552    }
553
554    #[test]
555    fn test_search_packages() {
556        let mut config = RepoConfig::default();
557        config.require_signatures = false;
558        let mut server = RepoServer::with_config(config);
559        server
560            .add_package(make_meta("libfoo", "1.0.0"), vec![1], None)
561            .unwrap();
562        server
563            .add_package(make_meta("libbar", "2.0.0"), vec![2], None)
564            .unwrap();
565        server
566            .add_package(make_meta("hello", "1.0.0"), vec![3], None)
567            .unwrap();
568
569        let results = server.search("lib");
570        assert_eq!(results.len(), 2);
571    }
572
573    #[test]
574    fn test_get_package_info() {
575        let mut config = RepoConfig::default();
576        config.require_signatures = false;
577        let mut server = RepoServer::with_config(config);
578        server
579            .add_package(make_meta("hello", "1.0.0"), vec![1], None)
580            .unwrap();
581        let info = server.get_package_info("hello", "1.0.0");
582        assert!(info.is_some());
583        assert_eq!(info.unwrap().size, 1024);
584    }
585
586    #[test]
587    fn test_list_packages_paginated() {
588        let mut config = RepoConfig::default();
589        config.require_signatures = false;
590        let mut server = RepoServer::with_config(config);
591        for i in 0..5 {
592            let name = alloc::format!("pkg{}", i);
593            server
594                .add_package(make_meta(&name, "1.0.0"), vec![1], None)
595                .unwrap();
596        }
597        let page1 = server.list_packages(0, 3);
598        assert_eq!(page1.len(), 3);
599        let page2 = server.list_packages(3, 3);
600        assert_eq!(page2.len(), 2);
601    }
602
603    #[test]
604    fn test_generate_index_json() {
605        let mut config = RepoConfig::default();
606        config.require_signatures = false;
607        let mut server = RepoServer::with_config(config);
608        server
609            .add_package(make_meta("hello", "1.0.0"), vec![1], None)
610            .unwrap();
611        let json = server.generate_index_json();
612        assert!(json.contains("\"name\":\"hello\""));
613        assert!(json.contains("\"version\":\"1.0.0\""));
614    }
615
616    #[test]
617    fn test_handle_get_packages() {
618        let mut config = RepoConfig::default();
619        config.require_signatures = false;
620        let mut server = RepoServer::with_config(config);
621        server
622            .add_package(make_meta("hello", "1.0.0"), vec![1], None)
623            .unwrap();
624
625        let req = RepoRequest {
626            method: HttpMethod::Get,
627            path: String::from("/api/packages"),
628            body: Vec::new(),
629        };
630        let resp = server.handle_request(&req);
631        assert_eq!(resp.status, 200);
632        assert!(resp.body.contains("hello"));
633    }
634
635    #[test]
636    fn test_handle_get_package_versions() {
637        let mut config = RepoConfig::default();
638        config.require_signatures = false;
639        let mut server = RepoServer::with_config(config);
640        server
641            .add_package(make_meta("hello", "1.0.0"), vec![1], None)
642            .unwrap();
643
644        let req = RepoRequest {
645            method: HttpMethod::Get,
646            path: String::from("/api/packages/hello"),
647            body: Vec::new(),
648        };
649        let resp = server.handle_request(&req);
650        assert_eq!(resp.status, 200);
651
652        let req_missing = RepoRequest {
653            method: HttpMethod::Get,
654            path: String::from("/api/packages/nonexistent"),
655            body: Vec::new(),
656        };
657        let resp_missing = server.handle_request(&req_missing);
658        assert_eq!(resp_missing.status, 404);
659    }
660
661    #[test]
662    fn test_handle_get_specific_version() {
663        let mut config = RepoConfig::default();
664        config.require_signatures = false;
665        let mut server = RepoServer::with_config(config);
666        server
667            .add_package(make_meta("hello", "2.0.0"), vec![1], None)
668            .unwrap();
669
670        let req = RepoRequest {
671            method: HttpMethod::Get,
672            path: String::from("/api/packages/hello/2.0.0"),
673            body: Vec::new(),
674        };
675        let resp = server.handle_request(&req);
676        assert_eq!(resp.status, 200);
677        assert!(resp.body.contains("2.0.0"));
678    }
679
680    #[test]
681    fn test_total_storage_bytes() {
682        let mut config = RepoConfig::default();
683        config.require_signatures = false;
684        let mut server = RepoServer::with_config(config);
685        server
686            .add_package(make_meta("a", "1.0.0"), vec![0; 100], None)
687            .unwrap();
688        server
689            .add_package(make_meta("b", "1.0.0"), vec![0; 200], None)
690            .unwrap();
691        assert_eq!(server.total_storage_bytes(), 300);
692    }
693
694    #[test]
695    fn test_replace_existing_version() {
696        let mut config = RepoConfig::default();
697        config.require_signatures = false;
698        let mut server = RepoServer::with_config(config);
699        let mut m1 = make_meta("hello", "1.0.0");
700        m1.size = 100;
701        server.add_package(m1, vec![1], None).unwrap();
702
703        let mut m2 = make_meta("hello", "1.0.0");
704        m2.size = 200;
705        server.add_package(m2, vec![2], None).unwrap();
706
707        // Should have replaced, not duplicated
708        assert_eq!(server.index.version_count(), 1);
709        let info = server.get_package_info("hello", "1.0.0").unwrap();
710        assert_eq!(info.size, 200);
711    }
712
713    #[test]
714    fn test_package_too_large() {
715        let mut config = RepoConfig::default();
716        config.require_signatures = false;
717        config.max_package_size = 10;
718        let mut server = RepoServer::with_config(config);
719        let meta = make_meta("big", "1.0.0");
720        assert!(server.add_package(meta, vec![0; 100], None).is_err());
721    }
722
723    #[test]
724    fn test_repo_meta_to_json() {
725        let mut m = RepoPackageMeta::new("test", "1.0.0");
726        m.dependencies.push(String::from("libfoo"));
727        let json = m.to_json();
728        assert!(json.contains("\"name\":\"test\""));
729        assert!(json.contains("\"deps\":[\"libfoo\"]"));
730    }
731}