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#[derive(Debug, Clone, PartialEq, Eq)]
633pub struct DgitInfo {
634 pub commit: String,
636 pub suite: String,
638 pub git_ref: String,
640 pub url: String,
642}
643
644impl FromStr for DgitInfo {
645 type Err = String;
646
647 fn from_str(s: &str) -> Result<Self, Self::Err> {
648 let parts: Vec<&str> = s.split_whitespace().collect();
649 if parts.len() != 4 {
650 return Err(format!(
651 "Invalid Dgit field format: expected 4 parts (commit suite ref url), got {}",
652 parts.len()
653 ));
654 }
655 Ok(Self {
656 commit: parts[0].to_string(),
657 suite: parts[1].to_string(),
658 git_ref: parts[2].to_string(),
659 url: parts[3].to_string(),
660 })
661 }
662}
663
664impl std::fmt::Display for DgitInfo {
665 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
666 write!(
667 f,
668 "{} {} {} {}",
669 self.commit, self.suite, self.git_ref, self.url
670 )
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 #[test]
679 fn test_sha1_checksum_filename() {
680 let checksum = Sha1Checksum {
681 sha1: "abc123".to_string(),
682 size: 1234,
683 filename: "test.deb".to_string(),
684 };
685 assert_eq!(checksum.filename(), "test.deb".to_string());
686 }
687
688 #[test]
689 fn test_md5_checksum_filename() {
690 let checksum = Md5Checksum {
691 md5sum: "abc123".to_string(),
692 size: 1234,
693 filename: "test.deb".to_string(),
694 };
695 assert_eq!(checksum.filename(), "test.deb".to_string());
696 }
697
698 #[test]
699 fn test_sha256_checksum_filename() {
700 let checksum = Sha256Checksum {
701 sha256: "abc123".to_string(),
702 size: 1234,
703 filename: "test.deb".to_string(),
704 };
705 assert_eq!(checksum.filename(), "test.deb".to_string());
706 }
707
708 #[test]
709 fn test_sha512_checksum_filename() {
710 let checksum = Sha512Checksum {
711 sha512: "abc123".to_string(),
712 size: 1234,
713 filename: "test.deb".to_string(),
714 };
715 assert_eq!(checksum.filename(), "test.deb".to_string());
716 }
717
718 #[test]
719 fn test_format_description_basic() {
720 let formatted = format_description(
721 "A great package",
722 "This package does amazing things.\nIt is very useful.",
723 );
724 assert_eq!(
725 formatted,
726 "A great package\n This package does amazing things.\n It is very useful."
727 );
728 }
729
730 #[test]
731 fn test_format_description_empty_lines() {
732 let formatted = format_description("Summary", "First paragraph.\n\nSecond paragraph.");
733 assert_eq!(
734 formatted,
735 "Summary\n First paragraph.\n .\n Second paragraph."
736 );
737 }
738
739 #[test]
740 fn test_format_description_short_only() {
741 let formatted = format_description("Short description", "");
742 assert_eq!(formatted, "Short description");
743 }
744
745 #[test]
746 fn test_format_description_multiple_empty_lines() {
747 let formatted = format_description("Test", "Line 1\n\n\nLine 2");
748 assert_eq!(formatted, "Test\n Line 1\n .\n .\n Line 2");
749 }
750
751 #[test]
752 fn test_format_description_whitespace_only_line() {
753 let formatted = format_description("Test", "Line 1\n \nLine 2");
754 assert_eq!(formatted, "Test\n Line 1\n .\n Line 2");
755 }
756
757 #[test]
758 fn test_format_description_complex() {
759 let long_desc = "This is a test package.\n\nIt has multiple paragraphs.\n\nAnd even lists:\n - Item 1\n - Item 2";
760 let formatted = format_description("Test package", long_desc);
761 assert_eq!(
762 formatted,
763 "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"
764 );
765 }
766
767 #[test]
768 fn test_standards_version_parse() {
769 let v = "4.6.2".parse::<StandardsVersion>().unwrap();
770 assert_eq!(v.major(), 4);
771 assert_eq!(v.minor(), 6);
772 assert_eq!(v.patch(), 2);
773 assert_eq!(v.micro(), 0);
774 assert_eq!(v.as_tuple(), (4, 6, 2, 0));
775 }
776
777 #[test]
778 fn test_standards_version_parse_two_components() {
779 let v = "3.9".parse::<StandardsVersion>().unwrap();
780 assert_eq!(v.major(), 3);
781 assert_eq!(v.minor(), 9);
782 assert_eq!(v.patch(), 0);
783 assert_eq!(v.micro(), 0);
784 }
785
786 #[test]
787 fn test_standards_version_parse_four_components() {
788 let v = "4.6.2.1".parse::<StandardsVersion>().unwrap();
789 assert_eq!(v.major(), 4);
790 assert_eq!(v.minor(), 6);
791 assert_eq!(v.patch(), 2);
792 assert_eq!(v.micro(), 1);
793 }
794
795 #[test]
796 fn test_standards_version_parse_single_component() {
797 let v = "4".parse::<StandardsVersion>().unwrap();
798 assert_eq!(v.major(), 4);
799 assert_eq!(v.minor(), 0);
800 assert_eq!(v.patch(), 0);
801 assert_eq!(v.micro(), 0);
802 }
803
804 #[test]
805 fn test_standards_version_display() {
806 let v = StandardsVersion::new(4, 6, 2, 0);
807 assert_eq!(v.to_string(), "4.6.2");
808
809 let v = StandardsVersion::new(3, 9, 8, 0);
810 assert_eq!(v.to_string(), "3.9.8");
811
812 let v = StandardsVersion::new(4, 6, 2, 1);
813 assert_eq!(v.to_string(), "4.6.2.1");
814
815 let v = StandardsVersion::new(3, 9, 0, 0);
816 assert_eq!(v.to_string(), "3.9");
817
818 let v = StandardsVersion::new(4, 0, 0, 0);
819 assert_eq!(v.to_string(), "4");
820 }
821
822 #[test]
823 fn test_standards_version_comparison() {
824 let v1 = "4.6.2".parse::<StandardsVersion>().unwrap();
825 let v2 = "4.5.1".parse::<StandardsVersion>().unwrap();
826 assert!(v1 > v2);
827
828 let v3 = "4.6.2".parse::<StandardsVersion>().unwrap();
829 assert_eq!(v1, v3);
830
831 let v4 = "3.9.8".parse::<StandardsVersion>().unwrap();
832 assert!(v1 > v4);
833
834 let v5 = "4.6.2.1".parse::<StandardsVersion>().unwrap();
835 assert!(v5 > v1);
836 }
837
838 #[test]
839 fn test_standards_version_roundtrip() {
840 let versions = vec!["4.6.2", "3.9.8", "4.6.2.1", "3.9", "4"];
841 for version_str in versions {
842 let v = version_str.parse::<StandardsVersion>().unwrap();
843 assert_eq!(v.to_string(), version_str);
844 }
845 }
846
847 #[test]
848 fn test_standards_version_invalid() {
849 assert!("".parse::<StandardsVersion>().is_err());
850 assert!("a.b.c".parse::<StandardsVersion>().is_err());
851 assert!("1.2.3.4.5".parse::<StandardsVersion>().is_err());
852 assert!("1.2.3.-1".parse::<StandardsVersion>().is_err());
853 }
854
855 #[test]
856 fn test_dgit_info_parse() {
857 let input = "c1370424e2404d3c22bd09c828d4b28d81d897ad debian archive/debian/1.1.0 https://git.dgit.debian.org/cltl";
858 let dgit: DgitInfo = input.parse().unwrap();
859 assert_eq!(dgit.commit, "c1370424e2404d3c22bd09c828d4b28d81d897ad");
860 assert_eq!(dgit.suite, "debian");
861 assert_eq!(dgit.git_ref, "archive/debian/1.1.0");
862 assert_eq!(dgit.url, "https://git.dgit.debian.org/cltl");
863 }
864
865 #[test]
866 fn test_dgit_info_display() {
867 let dgit = DgitInfo {
868 commit: "c1370424e2404d3c22bd09c828d4b28d81d897ad".to_string(),
869 suite: "debian".to_string(),
870 git_ref: "archive/debian/1.1.0".to_string(),
871 url: "https://git.dgit.debian.org/cltl".to_string(),
872 };
873 let output = dgit.to_string();
874 assert_eq!(
875 output,
876 "c1370424e2404d3c22bd09c828d4b28d81d897ad debian archive/debian/1.1.0 https://git.dgit.debian.org/cltl"
877 );
878 }
879
880 #[test]
881 fn test_dgit_info_roundtrip() {
882 let original = "90f40df9c40b0ceb59c207bcbec0a729e90d7ea9 debian archive/debian/1.0.debian1-5 https://git.dgit.debian.org/crafty-books-medium";
883 let dgit: DgitInfo = original.parse().unwrap();
884 assert_eq!(dgit.to_string(), original);
885 }
886
887 #[test]
888 fn test_dgit_info_invalid() {
889 assert!("abc123 debian".parse::<DgitInfo>().is_err());
891 assert!("abc123 debian ref url extra".parse::<DgitInfo>().is_err());
893 assert!("".parse::<DgitInfo>().is_err());
895 }
896}