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 = String;
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_else(|| "Missing md5sum".to_string())?;
250 let size = parts
251 .next()
252 .ok_or_else(|| "Missing size".to_string())?
253 .parse()
254 .map_err(|e: std::num::ParseIntError| e.to_string())?;
255 let filename = parts
256 .next()
257 .ok_or_else(|| "Missing filename".to_string())?
258 .to_string();
259 Ok(Self {
260 md5sum: md5sum.to_string(),
261 size,
262 filename,
263 })
264 }
265}
266
267impl Checksum for Md5Checksum {
268 fn filename(&self) -> &str {
269 &self.filename
270 }
271
272 fn size(&self) -> usize {
273 self.size
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct PackageListEntry {
280 pub package: String,
282
283 pub package_type: String,
285
286 pub section: String,
288
289 pub priority: Priority,
291
292 pub extra: std::collections::HashMap<String, String>,
294}
295
296impl PackageListEntry {
297 pub fn new(package: &str, package_type: &str, section: &str, priority: Priority) -> Self {
299 Self {
300 package: package.to_string(),
301 package_type: package_type.to_string(),
302 section: section.to_string(),
303 priority,
304 extra: std::collections::HashMap::new(),
305 }
306 }
307}
308
309impl std::fmt::Display for PackageListEntry {
310 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
311 write!(
312 f,
313 "{} {} {} {}",
314 self.package, self.package_type, self.section, self.priority
315 )?;
316 for (k, v) in &self.extra {
317 write!(f, " {}={}", k, v)?;
318 }
319 Ok(())
320 }
321}
322
323impl std::str::FromStr for PackageListEntry {
324 type Err = String;
325
326 fn from_str(s: &str) -> Result<Self, Self::Err> {
327 let mut parts = s.split_whitespace();
328 let package = parts
329 .next()
330 .ok_or_else(|| "Missing package".to_string())?
331 .to_string();
332 let package_type = parts
333 .next()
334 .ok_or_else(|| "Missing package type".to_string())?
335 .to_string();
336 let section = parts
337 .next()
338 .ok_or_else(|| "Missing section".to_string())?
339 .to_string();
340 let priority = parts
341 .next()
342 .ok_or_else(|| "Missing priority".to_string())?
343 .parse()?;
344 let mut extra = std::collections::HashMap::new();
345 for part in parts {
346 let mut kv = part.split('=');
347 let k = kv
348 .next()
349 .ok_or_else(|| "Missing key".to_string())?
350 .to_string();
351 let v = kv
352 .next()
353 .ok_or_else(|| "Missing value".to_string())?
354 .to_string();
355 extra.insert(k, v);
356 }
357 Ok(Self {
358 package,
359 package_type,
360 section,
361 priority,
362 extra,
363 })
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
369pub enum Urgency {
370 #[default]
372 Low,
373 Medium,
375 High,
377 Emergency,
379 Critical,
381}
382
383impl std::fmt::Display for Urgency {
384 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
385 match self {
386 Urgency::Low => f.write_str("low"),
387 Urgency::Medium => f.write_str("medium"),
388 Urgency::High => f.write_str("high"),
389 Urgency::Emergency => f.write_str("emergency"),
390 Urgency::Critical => f.write_str("critical"),
391 }
392 }
393}
394
395impl FromStr for Urgency {
396 type Err = String;
397
398 fn from_str(s: &str) -> Result<Self, Self::Err> {
399 match s.to_lowercase().as_str() {
400 "low" => Ok(Urgency::Low),
401 "medium" => Ok(Urgency::Medium),
402 "high" => Ok(Urgency::High),
403 "emergency" => Ok(Urgency::Emergency),
404 "critical" => Ok(Urgency::Critical),
405 _ => Err(format!("invalid urgency: {}", s)),
406 }
407 }
408}
409
410#[derive(PartialEq, Eq, Debug, Default, Clone)]
412pub enum MultiArch {
413 Same,
415 Foreign,
417 #[default]
419 No,
420 Allowed,
422}
423
424impl std::str::FromStr for MultiArch {
425 type Err = String;
426
427 fn from_str(s: &str) -> Result<Self, Self::Err> {
428 match s {
429 "same" => Ok(MultiArch::Same),
430 "foreign" => Ok(MultiArch::Foreign),
431 "no" => Ok(MultiArch::No),
432 "allowed" => Ok(MultiArch::Allowed),
433 _ => Err(format!("Invalid multiarch: {}", s)),
434 }
435 }
436}
437
438impl std::fmt::Display for MultiArch {
439 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
440 f.write_str(match self {
441 MultiArch::Same => "same",
442 MultiArch::Foreign => "foreign",
443 MultiArch::No => "no",
444 MultiArch::Allowed => "allowed",
445 })
446 }
447}
448
449pub fn format_description(short: &str, long: &str) -> String {
477 let mut result = short.to_string();
478
479 for line in long.lines() {
480 result.push('\n');
481 if line.trim().is_empty() {
482 result.push_str(" .");
483 } else {
484 result.push(' ');
485 result.push_str(line);
486 }
487 }
488
489 result
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
515pub struct StandardsVersion {
516 major: u8,
517 minor: u8,
518 patch: u8,
519 micro: u8,
520}
521
522impl StandardsVersion {
523 pub fn new(major: u8, minor: u8, patch: u8, micro: u8) -> Self {
525 Self {
526 major,
527 minor,
528 patch,
529 micro,
530 }
531 }
532
533 pub fn major(&self) -> u8 {
535 self.major
536 }
537
538 pub fn minor(&self) -> u8 {
540 self.minor
541 }
542
543 pub fn patch(&self) -> u8 {
545 self.patch
546 }
547
548 pub fn micro(&self) -> u8 {
550 self.micro
551 }
552
553 pub fn as_tuple(&self) -> (u8, u8, u8, u8) {
555 (self.major, self.minor, self.patch, self.micro)
556 }
557}
558
559impl std::fmt::Display for StandardsVersion {
560 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
561 if self.micro != 0 {
562 write!(
563 f,
564 "{}.{}.{}.{}",
565 self.major, self.minor, self.patch, self.micro
566 )
567 } else if self.patch != 0 {
568 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
569 } else if self.minor != 0 {
570 write!(f, "{}.{}", self.major, self.minor)
571 } else {
572 write!(f, "{}", self.major)
573 }
574 }
575}
576
577impl std::str::FromStr for StandardsVersion {
578 type Err = String;
579
580 fn from_str(s: &str) -> Result<Self, Self::Err> {
581 let parts: Vec<&str> = s.split('.').collect();
582 if parts.is_empty() || parts.len() > 4 {
583 return Err(format!(
584 "Invalid standards version format: {} (expected 1-4 dot-separated components)",
585 s
586 ));
587 }
588
589 let major = parts[0]
590 .parse()
591 .map_err(|_| format!("Invalid major version: {}", parts[0]))?;
592 let minor = if parts.len() > 1 {
593 parts[1]
594 .parse()
595 .map_err(|_| format!("Invalid minor version: {}", parts[1]))?
596 } else {
597 0
598 };
599 let patch = if parts.len() > 2 {
600 parts[2]
601 .parse()
602 .map_err(|_| format!("Invalid patch version: {}", parts[2]))?
603 } else {
604 0
605 };
606 let micro = if parts.len() > 3 {
607 parts[3]
608 .parse()
609 .map_err(|_| format!("Invalid micro version: {}", parts[3]))?
610 } else {
611 0
612 };
613
614 Ok(Self {
615 major,
616 minor,
617 patch,
618 micro,
619 })
620 }
621}
622
623impl PartialOrd for StandardsVersion {
624 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
625 Some(self.cmp(other))
626 }
627}
628
629impl Ord for StandardsVersion {
630 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
631 self.as_tuple().cmp(&other.as_tuple())
632 }
633}
634
635#[derive(Debug, Clone, PartialEq, Eq)]
640pub struct DgitInfo {
641 pub commit: String,
643 pub suite: String,
645 pub git_ref: String,
647 pub url: String,
649}
650
651impl FromStr for DgitInfo {
652 type Err = String;
653
654 fn from_str(s: &str) -> Result<Self, Self::Err> {
655 let parts: Vec<&str> = s.split_whitespace().collect();
656 if parts.len() != 4 {
657 return Err(format!(
658 "Invalid Dgit field format: expected 4 parts (commit suite ref url), got {}",
659 parts.len()
660 ));
661 }
662 Ok(Self {
663 commit: parts[0].to_string(),
664 suite: parts[1].to_string(),
665 git_ref: parts[2].to_string(),
666 url: parts[3].to_string(),
667 })
668 }
669}
670
671impl std::fmt::Display for DgitInfo {
672 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
673 write!(
674 f,
675 "{} {} {} {}",
676 self.commit, self.suite, self.git_ref, self.url
677 )
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 #[test]
686 fn test_sha1_checksum_filename() {
687 let checksum = Sha1Checksum {
688 sha1: "abc123".to_string(),
689 size: 1234,
690 filename: "test.deb".to_string(),
691 };
692 assert_eq!(checksum.filename(), "test.deb".to_string());
693 }
694
695 #[test]
696 fn test_md5_checksum_filename() {
697 let checksum = Md5Checksum {
698 md5sum: "abc123".to_string(),
699 size: 1234,
700 filename: "test.deb".to_string(),
701 };
702 assert_eq!(checksum.filename(), "test.deb".to_string());
703 }
704
705 #[test]
706 fn test_sha256_checksum_filename() {
707 let checksum = Sha256Checksum {
708 sha256: "abc123".to_string(),
709 size: 1234,
710 filename: "test.deb".to_string(),
711 };
712 assert_eq!(checksum.filename(), "test.deb".to_string());
713 }
714
715 #[test]
716 fn test_sha512_checksum_filename() {
717 let checksum = Sha512Checksum {
718 sha512: "abc123".to_string(),
719 size: 1234,
720 filename: "test.deb".to_string(),
721 };
722 assert_eq!(checksum.filename(), "test.deb".to_string());
723 }
724
725 #[test]
726 fn test_format_description_basic() {
727 let formatted = format_description(
728 "A great package",
729 "This package does amazing things.\nIt is very useful.",
730 );
731 assert_eq!(
732 formatted,
733 "A great package\n This package does amazing things.\n It is very useful."
734 );
735 }
736
737 #[test]
738 fn test_format_description_empty_lines() {
739 let formatted = format_description("Summary", "First paragraph.\n\nSecond paragraph.");
740 assert_eq!(
741 formatted,
742 "Summary\n First paragraph.\n .\n Second paragraph."
743 );
744 }
745
746 #[test]
747 fn test_format_description_short_only() {
748 let formatted = format_description("Short description", "");
749 assert_eq!(formatted, "Short description");
750 }
751
752 #[test]
753 fn test_format_description_multiple_empty_lines() {
754 let formatted = format_description("Test", "Line 1\n\n\nLine 2");
755 assert_eq!(formatted, "Test\n Line 1\n .\n .\n Line 2");
756 }
757
758 #[test]
759 fn test_format_description_whitespace_only_line() {
760 let formatted = format_description("Test", "Line 1\n \nLine 2");
761 assert_eq!(formatted, "Test\n Line 1\n .\n Line 2");
762 }
763
764 #[test]
765 fn test_format_description_complex() {
766 let long_desc = "This is a test package.\n\nIt has multiple paragraphs.\n\nAnd even lists:\n - Item 1\n - Item 2";
767 let formatted = format_description("Test package", long_desc);
768 assert_eq!(
769 formatted,
770 "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"
771 );
772 }
773
774 #[test]
775 fn test_standards_version_parse() {
776 let v = "4.6.2".parse::<StandardsVersion>().unwrap();
777 assert_eq!(v.major(), 4);
778 assert_eq!(v.minor(), 6);
779 assert_eq!(v.patch(), 2);
780 assert_eq!(v.micro(), 0);
781 assert_eq!(v.as_tuple(), (4, 6, 2, 0));
782 }
783
784 #[test]
785 fn test_standards_version_parse_two_components() {
786 let v = "3.9".parse::<StandardsVersion>().unwrap();
787 assert_eq!(v.major(), 3);
788 assert_eq!(v.minor(), 9);
789 assert_eq!(v.patch(), 0);
790 assert_eq!(v.micro(), 0);
791 }
792
793 #[test]
794 fn test_standards_version_parse_four_components() {
795 let v = "4.6.2.1".parse::<StandardsVersion>().unwrap();
796 assert_eq!(v.major(), 4);
797 assert_eq!(v.minor(), 6);
798 assert_eq!(v.patch(), 2);
799 assert_eq!(v.micro(), 1);
800 }
801
802 #[test]
803 fn test_standards_version_parse_single_component() {
804 let v = "4".parse::<StandardsVersion>().unwrap();
805 assert_eq!(v.major(), 4);
806 assert_eq!(v.minor(), 0);
807 assert_eq!(v.patch(), 0);
808 assert_eq!(v.micro(), 0);
809 }
810
811 #[test]
812 fn test_standards_version_display() {
813 let v = StandardsVersion::new(4, 6, 2, 0);
814 assert_eq!(v.to_string(), "4.6.2");
815
816 let v = StandardsVersion::new(3, 9, 8, 0);
817 assert_eq!(v.to_string(), "3.9.8");
818
819 let v = StandardsVersion::new(4, 6, 2, 1);
820 assert_eq!(v.to_string(), "4.6.2.1");
821
822 let v = StandardsVersion::new(3, 9, 0, 0);
823 assert_eq!(v.to_string(), "3.9");
824
825 let v = StandardsVersion::new(4, 0, 0, 0);
826 assert_eq!(v.to_string(), "4");
827 }
828
829 #[test]
830 fn test_standards_version_comparison() {
831 let v1 = "4.6.2".parse::<StandardsVersion>().unwrap();
832 let v2 = "4.5.1".parse::<StandardsVersion>().unwrap();
833 assert!(v1 > v2);
834
835 let v3 = "4.6.2".parse::<StandardsVersion>().unwrap();
836 assert_eq!(v1, v3);
837
838 let v4 = "3.9.8".parse::<StandardsVersion>().unwrap();
839 assert!(v1 > v4);
840
841 let v5 = "4.6.2.1".parse::<StandardsVersion>().unwrap();
842 assert!(v5 > v1);
843 }
844
845 #[test]
846 fn test_standards_version_roundtrip() {
847 let versions = vec!["4.6.2", "3.9.8", "4.6.2.1", "3.9", "4"];
848 for version_str in versions {
849 let v = version_str.parse::<StandardsVersion>().unwrap();
850 assert_eq!(v.to_string(), version_str);
851 }
852 }
853
854 #[test]
855 fn test_standards_version_invalid() {
856 assert!("".parse::<StandardsVersion>().is_err());
857 assert!("a.b.c".parse::<StandardsVersion>().is_err());
858 assert!("1.2.3.4.5".parse::<StandardsVersion>().is_err());
859 assert!("1.2.3.-1".parse::<StandardsVersion>().is_err());
860 }
861
862 #[test]
863 fn test_dgit_info_parse() {
864 let input = "c1370424e2404d3c22bd09c828d4b28d81d897ad debian archive/debian/1.1.0 https://git.dgit.debian.org/cltl";
865 let dgit: DgitInfo = input.parse().unwrap();
866 assert_eq!(dgit.commit, "c1370424e2404d3c22bd09c828d4b28d81d897ad");
867 assert_eq!(dgit.suite, "debian");
868 assert_eq!(dgit.git_ref, "archive/debian/1.1.0");
869 assert_eq!(dgit.url, "https://git.dgit.debian.org/cltl");
870 }
871
872 #[test]
873 fn test_dgit_info_display() {
874 let dgit = DgitInfo {
875 commit: "c1370424e2404d3c22bd09c828d4b28d81d897ad".to_string(),
876 suite: "debian".to_string(),
877 git_ref: "archive/debian/1.1.0".to_string(),
878 url: "https://git.dgit.debian.org/cltl".to_string(),
879 };
880 let output = dgit.to_string();
881 assert_eq!(
882 output,
883 "c1370424e2404d3c22bd09c828d4b28d81d897ad debian archive/debian/1.1.0 https://git.dgit.debian.org/cltl"
884 );
885 }
886
887 #[test]
888 fn test_dgit_info_roundtrip() {
889 let original = "90f40df9c40b0ceb59c207bcbec0a729e90d7ea9 debian archive/debian/1.0.debian1-5 https://git.dgit.debian.org/crafty-books-medium";
890 let dgit: DgitInfo = original.parse().unwrap();
891 assert_eq!(dgit.to_string(), original);
892 }
893
894 #[test]
895 fn test_dgit_info_invalid() {
896 assert!("abc123 debian".parse::<DgitInfo>().is_err());
898 assert!("abc123 debian ref url extra".parse::<DgitInfo>().is_err());
900 assert!("".parse::<DgitInfo>().is_err());
902 }
903}