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 = ();
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/// A package list entry
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct PackageListEntry {
273    /// Package name
274    pub package: String,
275
276    /// Package type
277    pub package_type: String,
278
279    /// Section
280    pub section: String,
281
282    /// Priority
283    pub priority: Priority,
284
285    /// Extra fields
286    pub extra: std::collections::HashMap<String, String>,
287}
288
289impl PackageListEntry {
290    /// Create a new package list entry
291    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/// Urgency of a particular package version
361#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
362pub enum Urgency {
363    /// Low
364    #[default]
365    Low,
366    /// Medium
367    Medium,
368    /// High
369    High,
370    /// Emergency
371    Emergency,
372    /// Critical
373    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/// Multi-arch policy
404#[derive(PartialEq, Eq, Debug, Default, Clone)]
405pub enum MultiArch {
406    /// Indicates that the package is identical across all architectures. The package can satisfy dependencies for other architectures.
407    Same,
408    /// The package can be installed alongside the same package of other architectures. It doesn't provide files that conflict with other architectures.
409    Foreign,
410    /// The package is only for its native architecture and cannot satisfy dependencies for other architectures.
411    #[default]
412    No,
413    /// Similar to "foreign", but the package manager may choose not to install it for foreign architectures if a native package is available.
414    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
442/// Format a Debian package description according to Debian policy.
443///
444/// Package descriptions consist of a short description (synopsis) and a long description.
445/// The long description lines are indented with a single space, and empty lines are
446/// represented as " ." (space followed by a period).
447///
448/// # Arguments
449///
450/// * `short` - The short description (synopsis), typically one line
451/// * `long` - The long description, can be multiple lines
452///
453/// # Returns
454///
455/// A formatted description string suitable for use in a Debian control file.
456///
457/// # Examples
458///
459/// ```
460/// use debian_control::fields::format_description;
461///
462/// let formatted = format_description("A great package", "This package does amazing things.\nIt is very useful.");
463/// assert_eq!(formatted, "A great package\n This package does amazing things.\n It is very useful.");
464///
465/// // Empty lines become " ."
466/// let with_empty = format_description("Summary", "First paragraph.\n\nSecond paragraph.");
467/// assert_eq!(with_empty, "Summary\n First paragraph.\n .\n Second paragraph.");
468/// ```
469pub 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/// Standards-Version field value
486///
487/// Represents a Debian standards version as a tuple of up to 4 components.
488/// Commonly used versions include "3.9.8", "4.6.2", etc.
489///
490/// # Examples
491///
492/// ```
493/// use debian_control::fields::StandardsVersion;
494/// use std::str::FromStr;
495///
496/// let version = StandardsVersion::from_str("4.6.2").unwrap();
497/// assert_eq!(version.major(), 4);
498/// assert_eq!(version.minor(), 6);
499/// assert_eq!(version.patch(), 2);
500/// assert_eq!(version.to_string(), "4.6.2");
501///
502/// // Versions can be compared
503/// let v1 = StandardsVersion::from_str("4.6.2").unwrap();
504/// let v2 = StandardsVersion::from_str("4.5.1").unwrap();
505/// assert!(v1 > v2);
506/// ```
507#[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    /// Create a new standards version
517    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    /// Get the major version component
527    pub fn major(&self) -> u8 {
528        self.major
529    }
530
531    /// Get the minor version component
532    pub fn minor(&self) -> u8 {
533        self.minor
534    }
535
536    /// Get the patch version component
537    pub fn patch(&self) -> u8 {
538        self.patch
539    }
540
541    /// Get the micro version component
542    pub fn micro(&self) -> u8 {
543        self.micro
544    }
545
546    /// Convert to a tuple (major, minor, patch, micro)
547    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}