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