Skip to main content

debian_control/
fields.rs

1//! Fields for the control file
2use std::str::FromStr;
3
4/// Priority of a package
5#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
6pub enum Priority {
7    /// Required
8    Required,
9
10    /// Important
11    Important,
12
13    /// Standard
14    Standard,
15
16    /// Optional
17    Optional,
18
19    /// Extra
20    Extra,
21
22    /// Source
23    ///
24    /// Note: This priority is not officially documented in Debian policy,
25    /// but is commonly used to indicate source packages.
26    ///
27    /// While packages generally follow the priority values defined in policy, for source packages
28    /// the archive-management software (such as dak, the
29    /// Debian Archive Kit) may set "Priority: source".
30    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
62/// A checksum of a file
63pub trait Checksum {
64    /// Filename
65    fn filename(&self) -> &str;
66
67    /// Size of the file, in bytes
68    fn size(&self) -> usize;
69}
70
71/// SHA1 checksum
72#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
73pub struct Sha1Checksum {
74    /// SHA1 checksum
75    pub sha1: String,
76
77    /// Size of the file, in bytes
78    pub size: usize,
79
80    /// Filename
81    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/// SHA-256 checksum
124#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
125pub struct Sha256Checksum {
126    /// SHA-256 checksum
127    pub sha256: String,
128
129    /// Size of the file, in bytes
130    pub size: usize,
131
132    /// Filename
133    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/// SHA-512 checksum
176#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
177pub struct Sha512Checksum {
178    /// SHA-512 checksum
179    pub sha512: String,
180
181    /// Size of the file, in bytes
182    pub size: usize,
183
184    /// Filename
185    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/// An MD5 checksum of a file
228#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
229pub struct Md5Checksum {
230    /// The MD5 checksum
231    pub md5sum: String,
232    /// The size of the file
233    pub size: usize,
234    /// The filename
235    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/// A package list entry
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct PackageListEntry {
280    /// Package name
281    pub package: String,
282
283    /// Package type
284    pub package_type: String,
285
286    /// Section
287    pub section: String,
288
289    /// Priority
290    pub priority: Priority,
291
292    /// Extra fields
293    pub extra: std::collections::HashMap<String, String>,
294}
295
296impl PackageListEntry {
297    /// Create a new package list entry
298    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/// Urgency of a particular package version
368#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
369pub enum Urgency {
370    /// Low
371    #[default]
372    Low,
373    /// Medium
374    Medium,
375    /// High
376    High,
377    /// Emergency
378    Emergency,
379    /// Critical
380    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/// Multi-arch policy
411#[derive(PartialEq, Eq, Debug, Default, Clone)]
412pub enum MultiArch {
413    /// Indicates that the package is identical across all architectures. The package can satisfy dependencies for other architectures.
414    Same,
415    /// The package can be installed alongside the same package of other architectures. It doesn't provide files that conflict with other architectures.
416    Foreign,
417    /// The package is only for its native architecture and cannot satisfy dependencies for other architectures.
418    #[default]
419    No,
420    /// Similar to "foreign", but the package manager may choose not to install it for foreign architectures if a native package is available.
421    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
449/// Format a Debian package description according to Debian policy.
450///
451/// Package descriptions consist of a short description (synopsis) and a long description.
452/// The long description lines are indented with a single space, and empty lines are
453/// represented as " ." (space followed by a period).
454///
455/// # Arguments
456///
457/// * `short` - The short description (synopsis), typically one line
458/// * `long` - The long description, can be multiple lines
459///
460/// # Returns
461///
462/// A formatted description string suitable for use in a Debian control file.
463///
464/// # Examples
465///
466/// ```
467/// use debian_control::fields::format_description;
468///
469/// let formatted = format_description("A great package", "This package does amazing things.\nIt is very useful.");
470/// assert_eq!(formatted, "A great package\n This package does amazing things.\n It is very useful.");
471///
472/// // Empty lines become " ."
473/// let with_empty = format_description("Summary", "First paragraph.\n\nSecond paragraph.");
474/// assert_eq!(with_empty, "Summary\n First paragraph.\n .\n Second paragraph.");
475/// ```
476pub 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/// Standards-Version field value
493///
494/// Represents a Debian standards version as a tuple of up to 4 components.
495/// Commonly used versions include "3.9.8", "4.6.2", etc.
496///
497/// # Examples
498///
499/// ```
500/// use debian_control::fields::StandardsVersion;
501/// use std::str::FromStr;
502///
503/// let version = StandardsVersion::from_str("4.6.2").unwrap();
504/// assert_eq!(version.major(), 4);
505/// assert_eq!(version.minor(), 6);
506/// assert_eq!(version.patch(), 2);
507/// assert_eq!(version.to_string(), "4.6.2");
508///
509/// // Versions can be compared
510/// let v1 = StandardsVersion::from_str("4.6.2").unwrap();
511/// let v2 = StandardsVersion::from_str("4.5.1").unwrap();
512/// assert!(v1 > v2);
513/// ```
514#[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    /// Create a new standards version
524    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    /// Get the major version component
534    pub fn major(&self) -> u8 {
535        self.major
536    }
537
538    /// Get the minor version component
539    pub fn minor(&self) -> u8 {
540        self.minor
541    }
542
543    /// Get the patch version component
544    pub fn patch(&self) -> u8 {
545        self.patch
546    }
547
548    /// Get the micro version component
549    pub fn micro(&self) -> u8 {
550        self.micro
551    }
552
553    /// Convert to a tuple (major, minor, patch, micro)
554    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/// Dgit information
636///
637/// The Dgit field format is: `<commit-hash> <suite> <ref> <url>`
638/// For example: `c1370424e2404d3c22bd09c828d4b28d81d897ad debian archive/debian/1.1.0 https://git.dgit.debian.org/cltl`
639#[derive(Debug, Clone, PartialEq, Eq)]
640pub struct DgitInfo {
641    /// Git commit hash
642    pub commit: String,
643    /// Suite (e.g., "debian")
644    pub suite: String,
645    /// Git reference (e.g., "archive/debian/1.1.0")
646    pub git_ref: String,
647    /// Git repository URL
648    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        // Too few parts
897        assert!("abc123 debian".parse::<DgitInfo>().is_err());
898        // Too many parts
899        assert!("abc123 debian ref url extra".parse::<DgitInfo>().is_err());
900        // Empty string
901        assert!("".parse::<DgitInfo>().is_err());
902    }
903}