1#[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#[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 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#[cfg(feature = "alloc")]
93#[derive(Debug, Clone)]
94pub struct RepoIndex {
95 pub packages: BTreeMap<String, Vec<RepoPackageMeta>>,
97 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 pub fn package_count(&self) -> usize {
119 self.packages.len()
120 }
121
122 pub fn version_count(&self) -> usize {
124 self.packages.values().map(|v| v.len()).sum()
125 }
126}
127
128#[cfg(feature = "alloc")]
130#[derive(Debug, Clone)]
131pub struct RepoConfig {
132 pub listen_port: u16,
133 pub storage_path: String,
134 pub max_package_size: u64,
136 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#[cfg(feature = "alloc")]
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum HttpMethod {
156 Get,
157 Post,
158}
159
160#[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#[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#[cfg(feature = "alloc")]
210pub struct RepoServer {
211 pub config: RepoConfig,
212 pub index: RepoIndex,
213 packages: BTreeMap<String, Vec<u8>>,
215}
216
217#[cfg(feature = "alloc")]
218impl RepoServer {
219 pub fn init() -> Self {
221 Self::with_config(RepoConfig::default())
222 }
223
224 pub fn with_config(config: RepoConfig) -> Self {
226 Self {
227 config,
228 index: RepoIndex::new(),
229 packages: BTreeMap::new(),
230 }
231 }
232
233 pub fn add_package(
238 &mut self,
239 meta: RepoPackageMeta,
240 data: Vec<u8>,
241 signature: Option<&PackageSignature>,
242 ) -> Result<(), KernelError> {
243 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 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 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 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 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 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 pub fn list_versions(&self, name: &str) -> Option<&Vec<RepoPackageMeta>> {
330 self.index.packages.get(name)
331 }
332
333 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 all.into_iter().skip(offset).take(page_size).collect()
345 }
346
347 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 pub fn verify_upload(&self, signature: &PackageSignature, archive_data: &[u8]) -> bool {
368 let public_key = [0u8; 32];
370 signature.verify(archive_data, &public_key)
371 }
372
373 pub fn handle_request(&self, req: &RepoRequest) -> RepoResponse {
375 match req.method {
376 HttpMethod::Get => self.handle_get(&req.path),
377 HttpMethod::Post => {
378 RepoResponse::bad_request("upload requires multipart body parsing")
380 }
381 }
382 }
383
384 fn handle_get(&self, path: &str) -> RepoResponse {
385 if path == "/api/packages" {
387 return RepoResponse::ok(self.generate_index_json());
388 }
389
390 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 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 pub fn total_storage_bytes(&self) -> u64 {
434 self.packages.values().map(|d| d.len() as u64).sum()
435 }
436}
437
438#[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#[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 let bytes = unsafe { out.as_bytes_mut() };
466 bytes[start..].reverse();
467}
468
469#[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#[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 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}