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