1use std::str::FromStr;
3
4#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
6pub enum Priority {
7 Required,
9
10 Important,
12
13 Standard,
15
16 Optional,
18
19 Extra,
21}
22
23impl std::fmt::Display for Priority {
24 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
25 f.write_str(match self {
26 Priority::Required => "required",
27 Priority::Important => "important",
28 Priority::Standard => "standard",
29 Priority::Optional => "optional",
30 Priority::Extra => "extra",
31 })
32 }
33}
34
35impl std::str::FromStr for Priority {
36 type Err = String;
37
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
39 match s {
40 "required" => Ok(Priority::Required),
41 "important" => Ok(Priority::Important),
42 "standard" => Ok(Priority::Standard),
43 "optional" => Ok(Priority::Optional),
44 "extra" => Ok(Priority::Extra),
45 _ => Err(format!("Invalid priority: {}", s)),
46 }
47 }
48}
49
50pub trait Checksum {
52 fn filename(&self) -> &str;
54
55 fn size(&self) -> usize;
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
61pub struct Sha1Checksum {
62 pub sha1: String,
64
65 pub size: usize,
67
68 pub filename: String,
70}
71
72impl Checksum for Sha1Checksum {
73 fn filename(&self) -> &str {
74 &self.filename
75 }
76
77 fn size(&self) -> usize {
78 self.size
79 }
80}
81
82impl std::fmt::Display for Sha1Checksum {
83 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
84 write!(f, "{} {} {}", self.sha1, self.size, self.filename)
85 }
86}
87
88impl std::str::FromStr for Sha1Checksum {
89 type Err = String;
90
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 let mut parts = s.split_whitespace();
93 let sha1 = parts.next().ok_or_else(|| "Missing sha1".to_string())?;
94 let size = parts
95 .next()
96 .ok_or_else(|| "Missing size".to_string())?
97 .parse()
98 .map_err(|e: std::num::ParseIntError| e.to_string())?;
99 let filename = parts
100 .next()
101 .ok_or_else(|| "Missing filename".to_string())?
102 .to_string();
103 Ok(Self {
104 sha1: sha1.to_string(),
105 size,
106 filename,
107 })
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
113pub struct Sha256Checksum {
114 pub sha256: String,
116
117 pub size: usize,
119
120 pub filename: String,
122}
123
124impl Checksum for Sha256Checksum {
125 fn filename(&self) -> &str {
126 &self.filename
127 }
128
129 fn size(&self) -> usize {
130 self.size
131 }
132}
133
134impl std::fmt::Display for Sha256Checksum {
135 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
136 write!(f, "{} {} {}", self.sha256, self.size, self.filename)
137 }
138}
139
140impl std::str::FromStr for Sha256Checksum {
141 type Err = String;
142
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 let mut parts = s.split_whitespace();
145 let sha256 = parts.next().ok_or_else(|| "Missing sha256".to_string())?;
146 let size = parts
147 .next()
148 .ok_or_else(|| "Missing size".to_string())?
149 .parse()
150 .map_err(|e: std::num::ParseIntError| e.to_string())?;
151 let filename = parts
152 .next()
153 .ok_or_else(|| "Missing filename".to_string())?
154 .to_string();
155 Ok(Self {
156 sha256: sha256.to_string(),
157 size,
158 filename,
159 })
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
165pub struct Sha512Checksum {
166 pub sha512: String,
168
169 pub size: usize,
171
172 pub filename: String,
174}
175
176impl Checksum for Sha512Checksum {
177 fn filename(&self) -> &str {
178 &self.filename
179 }
180
181 fn size(&self) -> usize {
182 self.size
183 }
184}
185
186impl std::fmt::Display for Sha512Checksum {
187 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
188 write!(f, "{} {} {}", self.sha512, self.size, self.filename)
189 }
190}
191
192impl std::str::FromStr for Sha512Checksum {
193 type Err = String;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 let mut parts = s.split_whitespace();
197 let sha512 = parts.next().ok_or_else(|| "Missing sha512".to_string())?;
198 let size = parts
199 .next()
200 .ok_or_else(|| "Missing size".to_string())?
201 .parse()
202 .map_err(|e: std::num::ParseIntError| e.to_string())?;
203 let filename = parts
204 .next()
205 .ok_or_else(|| "Missing filename".to_string())?
206 .to_string();
207 Ok(Self {
208 sha512: sha512.to_string(),
209 size,
210 filename,
211 })
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
217pub struct Md5Checksum {
218 pub md5sum: String,
220 pub size: usize,
222 pub filename: String,
224}
225
226impl std::fmt::Display for Md5Checksum {
227 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
228 write!(f, "{} {} {}", self.md5sum, self.size, self.filename)
229 }
230}
231
232impl std::str::FromStr for Md5Checksum {
233 type Err = ();
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 let mut parts = s.split_whitespace();
237 let md5sum = parts.next().ok_or(())?;
238 let size = parts.next().ok_or(())?.parse().map_err(|_| ())?;
239 let filename = parts.next().ok_or(())?.to_string();
240 Ok(Self {
241 md5sum: md5sum.to_string(),
242 size,
243 filename,
244 })
245 }
246}
247
248impl Checksum for Md5Checksum {
249 fn filename(&self) -> &str {
250 &self.filename
251 }
252
253 fn size(&self) -> usize {
254 self.size
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct PackageListEntry {
261 pub package: String,
263
264 pub package_type: String,
266
267 pub section: String,
269
270 pub priority: Priority,
272
273 pub extra: std::collections::HashMap<String, String>,
275}
276
277impl PackageListEntry {
278 pub fn new(package: &str, package_type: &str, section: &str, priority: Priority) -> Self {
280 Self {
281 package: package.to_string(),
282 package_type: package_type.to_string(),
283 section: section.to_string(),
284 priority,
285 extra: std::collections::HashMap::new(),
286 }
287 }
288}
289
290impl std::fmt::Display for PackageListEntry {
291 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
292 write!(
293 f,
294 "{} {} {} {}",
295 self.package, self.package_type, self.section, self.priority
296 )?;
297 for (k, v) in &self.extra {
298 write!(f, " {}={}", k, v)?;
299 }
300 Ok(())
301 }
302}
303
304impl std::str::FromStr for PackageListEntry {
305 type Err = String;
306
307 fn from_str(s: &str) -> Result<Self, Self::Err> {
308 let mut parts = s.split_whitespace();
309 let package = parts
310 .next()
311 .ok_or_else(|| "Missing package".to_string())?
312 .to_string();
313 let package_type = parts
314 .next()
315 .ok_or_else(|| "Missing package type".to_string())?
316 .to_string();
317 let section = parts
318 .next()
319 .ok_or_else(|| "Missing section".to_string())?
320 .to_string();
321 let priority = parts
322 .next()
323 .ok_or_else(|| "Missing priority".to_string())?
324 .parse()?;
325 let mut extra = std::collections::HashMap::new();
326 for part in parts {
327 let mut kv = part.split('=');
328 let k = kv
329 .next()
330 .ok_or_else(|| "Missing key".to_string())?
331 .to_string();
332 let v = kv
333 .next()
334 .ok_or_else(|| "Missing value".to_string())?
335 .to_string();
336 extra.insert(k, v);
337 }
338 Ok(Self {
339 package,
340 package_type,
341 section,
342 priority,
343 extra,
344 })
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
350pub enum Urgency {
351 #[default]
353 Low,
354 Medium,
356 High,
358 Emergency,
360 Critical,
362}
363
364impl std::fmt::Display for Urgency {
365 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
366 match self {
367 Urgency::Low => f.write_str("low"),
368 Urgency::Medium => f.write_str("medium"),
369 Urgency::High => f.write_str("high"),
370 Urgency::Emergency => f.write_str("emergency"),
371 Urgency::Critical => f.write_str("critical"),
372 }
373 }
374}
375
376impl FromStr for Urgency {
377 type Err = String;
378
379 fn from_str(s: &str) -> Result<Self, Self::Err> {
380 match s.to_lowercase().as_str() {
381 "low" => Ok(Urgency::Low),
382 "medium" => Ok(Urgency::Medium),
383 "high" => Ok(Urgency::High),
384 "emergency" => Ok(Urgency::Emergency),
385 "critical" => Ok(Urgency::Critical),
386 _ => Err(format!("invalid urgency: {}", s)),
387 }
388 }
389}
390
391#[derive(PartialEq, Eq, Debug, Default)]
393pub enum MultiArch {
394 Same,
396 Foreign,
398 #[default]
400 No,
401 Allowed,
403}
404
405impl std::str::FromStr for MultiArch {
406 type Err = String;
407
408 fn from_str(s: &str) -> Result<Self, Self::Err> {
409 match s {
410 "same" => Ok(MultiArch::Same),
411 "foreign" => Ok(MultiArch::Foreign),
412 "no" => Ok(MultiArch::No),
413 "allowed" => Ok(MultiArch::Allowed),
414 _ => Err(format!("Invalid multiarch: {}", s)),
415 }
416 }
417}
418
419impl std::fmt::Display for MultiArch {
420 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
421 f.write_str(match self {
422 MultiArch::Same => "same",
423 MultiArch::Foreign => "foreign",
424 MultiArch::No => "no",
425 MultiArch::Allowed => "allowed",
426 })
427 }
428}
429
430pub fn format_description(short: &str, long: &str) -> String {
458 let mut result = short.to_string();
459
460 for line in long.lines() {
461 result.push('\n');
462 if line.trim().is_empty() {
463 result.push_str(" .");
464 } else {
465 result.push(' ');
466 result.push_str(line);
467 }
468 }
469
470 result
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
496pub struct StandardsVersion {
497 major: u8,
498 minor: u8,
499 patch: u8,
500 micro: u8,
501}
502
503impl StandardsVersion {
504 pub fn new(major: u8, minor: u8, patch: u8, micro: u8) -> Self {
506 Self {
507 major,
508 minor,
509 patch,
510 micro,
511 }
512 }
513
514 pub fn major(&self) -> u8 {
516 self.major
517 }
518
519 pub fn minor(&self) -> u8 {
521 self.minor
522 }
523
524 pub fn patch(&self) -> u8 {
526 self.patch
527 }
528
529 pub fn micro(&self) -> u8 {
531 self.micro
532 }
533
534 pub fn as_tuple(&self) -> (u8, u8, u8, u8) {
536 (self.major, self.minor, self.patch, self.micro)
537 }
538}
539
540impl std::fmt::Display for StandardsVersion {
541 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
542 if self.micro != 0 {
543 write!(
544 f,
545 "{}.{}.{}.{}",
546 self.major, self.minor, self.patch, self.micro
547 )
548 } else if self.patch != 0 {
549 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
550 } else if self.minor != 0 {
551 write!(f, "{}.{}", self.major, self.minor)
552 } else {
553 write!(f, "{}", self.major)
554 }
555 }
556}
557
558impl std::str::FromStr for StandardsVersion {
559 type Err = String;
560
561 fn from_str(s: &str) -> Result<Self, Self::Err> {
562 let parts: Vec<&str> = s.split('.').collect();
563 if parts.is_empty() || parts.len() > 4 {
564 return Err(format!(
565 "Invalid standards version format: {} (expected 1-4 dot-separated components)",
566 s
567 ));
568 }
569
570 let major = parts[0]
571 .parse()
572 .map_err(|_| format!("Invalid major version: {}", parts[0]))?;
573 let minor = if parts.len() > 1 {
574 parts[1]
575 .parse()
576 .map_err(|_| format!("Invalid minor version: {}", parts[1]))?
577 } else {
578 0
579 };
580 let patch = if parts.len() > 2 {
581 parts[2]
582 .parse()
583 .map_err(|_| format!("Invalid patch version: {}", parts[2]))?
584 } else {
585 0
586 };
587 let micro = if parts.len() > 3 {
588 parts[3]
589 .parse()
590 .map_err(|_| format!("Invalid micro version: {}", parts[3]))?
591 } else {
592 0
593 };
594
595 Ok(Self {
596 major,
597 minor,
598 patch,
599 micro,
600 })
601 }
602}
603
604impl PartialOrd for StandardsVersion {
605 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
606 Some(self.cmp(other))
607 }
608}
609
610impl Ord for StandardsVersion {
611 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
612 self.as_tuple().cmp(&other.as_tuple())
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
621 fn test_sha1_checksum_filename() {
622 let checksum = Sha1Checksum {
623 sha1: "abc123".to_string(),
624 size: 1234,
625 filename: "test.deb".to_string(),
626 };
627 assert_eq!(checksum.filename(), "test.deb".to_string());
628 }
629
630 #[test]
631 fn test_md5_checksum_filename() {
632 let checksum = Md5Checksum {
633 md5sum: "abc123".to_string(),
634 size: 1234,
635 filename: "test.deb".to_string(),
636 };
637 assert_eq!(checksum.filename(), "test.deb".to_string());
638 }
639
640 #[test]
641 fn test_sha256_checksum_filename() {
642 let checksum = Sha256Checksum {
643 sha256: "abc123".to_string(),
644 size: 1234,
645 filename: "test.deb".to_string(),
646 };
647 assert_eq!(checksum.filename(), "test.deb".to_string());
648 }
649
650 #[test]
651 fn test_sha512_checksum_filename() {
652 let checksum = Sha512Checksum {
653 sha512: "abc123".to_string(),
654 size: 1234,
655 filename: "test.deb".to_string(),
656 };
657 assert_eq!(checksum.filename(), "test.deb".to_string());
658 }
659
660 #[test]
661 fn test_format_description_basic() {
662 let formatted = format_description(
663 "A great package",
664 "This package does amazing things.\nIt is very useful.",
665 );
666 assert_eq!(
667 formatted,
668 "A great package\n This package does amazing things.\n It is very useful."
669 );
670 }
671
672 #[test]
673 fn test_format_description_empty_lines() {
674 let formatted = format_description("Summary", "First paragraph.\n\nSecond paragraph.");
675 assert_eq!(
676 formatted,
677 "Summary\n First paragraph.\n .\n Second paragraph."
678 );
679 }
680
681 #[test]
682 fn test_format_description_short_only() {
683 let formatted = format_description("Short description", "");
684 assert_eq!(formatted, "Short description");
685 }
686
687 #[test]
688 fn test_format_description_multiple_empty_lines() {
689 let formatted = format_description("Test", "Line 1\n\n\nLine 2");
690 assert_eq!(formatted, "Test\n Line 1\n .\n .\n Line 2");
691 }
692
693 #[test]
694 fn test_format_description_whitespace_only_line() {
695 let formatted = format_description("Test", "Line 1\n \nLine 2");
696 assert_eq!(formatted, "Test\n Line 1\n .\n Line 2");
697 }
698
699 #[test]
700 fn test_format_description_complex() {
701 let long_desc = "This is a test package.\n\nIt has multiple paragraphs.\n\nAnd even lists:\n - Item 1\n - Item 2";
702 let formatted = format_description("Test package", long_desc);
703 assert_eq!(
704 formatted,
705 "Test package\n This is a test package.\n .\n It has multiple paragraphs.\n .\n And even lists:\n - Item 1\n - Item 2"
706 );
707 }
708
709 #[test]
710 fn test_standards_version_parse() {
711 let v = "4.6.2".parse::<StandardsVersion>().unwrap();
712 assert_eq!(v.major(), 4);
713 assert_eq!(v.minor(), 6);
714 assert_eq!(v.patch(), 2);
715 assert_eq!(v.micro(), 0);
716 assert_eq!(v.as_tuple(), (4, 6, 2, 0));
717 }
718
719 #[test]
720 fn test_standards_version_parse_two_components() {
721 let v = "3.9".parse::<StandardsVersion>().unwrap();
722 assert_eq!(v.major(), 3);
723 assert_eq!(v.minor(), 9);
724 assert_eq!(v.patch(), 0);
725 assert_eq!(v.micro(), 0);
726 }
727
728 #[test]
729 fn test_standards_version_parse_four_components() {
730 let v = "4.6.2.1".parse::<StandardsVersion>().unwrap();
731 assert_eq!(v.major(), 4);
732 assert_eq!(v.minor(), 6);
733 assert_eq!(v.patch(), 2);
734 assert_eq!(v.micro(), 1);
735 }
736
737 #[test]
738 fn test_standards_version_parse_single_component() {
739 let v = "4".parse::<StandardsVersion>().unwrap();
740 assert_eq!(v.major(), 4);
741 assert_eq!(v.minor(), 0);
742 assert_eq!(v.patch(), 0);
743 assert_eq!(v.micro(), 0);
744 }
745
746 #[test]
747 fn test_standards_version_display() {
748 let v = StandardsVersion::new(4, 6, 2, 0);
749 assert_eq!(v.to_string(), "4.6.2");
750
751 let v = StandardsVersion::new(3, 9, 8, 0);
752 assert_eq!(v.to_string(), "3.9.8");
753
754 let v = StandardsVersion::new(4, 6, 2, 1);
755 assert_eq!(v.to_string(), "4.6.2.1");
756
757 let v = StandardsVersion::new(3, 9, 0, 0);
758 assert_eq!(v.to_string(), "3.9");
759
760 let v = StandardsVersion::new(4, 0, 0, 0);
761 assert_eq!(v.to_string(), "4");
762 }
763
764 #[test]
765 fn test_standards_version_comparison() {
766 let v1 = "4.6.2".parse::<StandardsVersion>().unwrap();
767 let v2 = "4.5.1".parse::<StandardsVersion>().unwrap();
768 assert!(v1 > v2);
769
770 let v3 = "4.6.2".parse::<StandardsVersion>().unwrap();
771 assert_eq!(v1, v3);
772
773 let v4 = "3.9.8".parse::<StandardsVersion>().unwrap();
774 assert!(v1 > v4);
775
776 let v5 = "4.6.2.1".parse::<StandardsVersion>().unwrap();
777 assert!(v5 > v1);
778 }
779
780 #[test]
781 fn test_standards_version_roundtrip() {
782 let versions = vec!["4.6.2", "3.9.8", "4.6.2.1", "3.9", "4"];
783 for version_str in versions {
784 let v = version_str.parse::<StandardsVersion>().unwrap();
785 assert_eq!(v.to_string(), version_str);
786 }
787 }
788
789 #[test]
790 fn test_standards_version_invalid() {
791 assert!("".parse::<StandardsVersion>().is_err());
792 assert!("a.b.c".parse::<StandardsVersion>().is_err());
793 assert!("1.2.3.4.5".parse::<StandardsVersion>().is_err());
794 assert!("1.2.3.-1".parse::<StandardsVersion>().is_err());
795 }
796}