assorted_debian_utils/
archive.rs

1// Copyright 2022-2024 Sebastian Ramacher
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! # Helpers to handle Debian archives
5//!
6//! These helpers includes enums to handle suites, codenames, and other fields found in Debian archive files.
7
8use std::{
9    fmt::{Display, Formatter},
10    hash::{Hash, Hasher},
11    str::FromStr,
12};
13
14use serde::{Deserialize, Serialize, Serializer};
15
16pub use crate::ParseError;
17use crate::utils::TryFromStrVisitor;
18
19/// "Extensions" to a codename or a suite
20///
21/// This enum covers the archives for backports, security updates, (old)stable
22/// updates and (old)stable proposed-updates.
23#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
24pub enum Extension {
25    /// The backports extension
26    Backports,
27    /// The security extension
28    Security,
29    /// The updates extension
30    Updates,
31    /// The proposed-upates extension
32    ProposedUpdates,
33}
34
35impl AsRef<str> for Extension {
36    fn as_ref(&self) -> &str {
37        match self {
38            Self::Backports => "backports",
39            Self::Security => "security",
40            Self::Updates => "updates",
41            Self::ProposedUpdates => "proposed-updates",
42        }
43    }
44}
45
46impl Display for Extension {
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.as_ref())
49    }
50}
51
52impl TryFrom<&str> for Extension {
53    type Error = ParseError;
54
55    fn try_from(value: &str) -> Result<Self, Self::Error> {
56        match value {
57            "backports" => Ok(Self::Backports),
58            "security" => Ok(Self::Security),
59            "updates" => Ok(Self::Updates),
60            "proposed-updates" => Ok(Self::ProposedUpdates),
61            _ => Err(ParseError::InvalidExtension),
62        }
63    }
64}
65
66impl FromStr for Extension {
67    type Err = ParseError;
68
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        Self::try_from(s)
71    }
72}
73
74/// Trait to add/remove archive extensions from `Suite` and `Codename`
75pub trait WithExtension {
76    /// Extend suite with an extension archive.
77    ///
78    /// An existing extension will overriden and the method has no effect for`unstable` and `experimental`.
79    fn with_extension(&self, extension: Extension) -> Self;
80
81    /// Remove an extension archive from the suite.
82    ///
83    /// The method has no effect for`unstable` and `experimental`.
84    fn without_extension(&self) -> Self;
85}
86
87/// Debian archive suites
88///
89/// This enum describes the suite names found in the Debian archive.
90#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
91pub enum Suite {
92    /// The unstable suite
93    Unstable,
94    /// The testing suite
95    Testing(Option<Extension>),
96    /// The stable suite
97    Stable(Option<Extension>),
98    /// The oldstable suite
99    OldStable(Option<Extension>),
100    /// The experimental suite
101    Experimental,
102}
103
104impl WithExtension for Suite {
105    fn with_extension(&self, extension: Extension) -> Self {
106        match self {
107            Self::Unstable | Self::Experimental => *self,
108            Self::Testing(_) => Self::Testing(Some(extension)),
109            Self::Stable(_) => Self::Stable(Some(extension)),
110            Self::OldStable(_) => Self::OldStable(Some(extension)),
111        }
112    }
113
114    fn without_extension(&self) -> Self {
115        match self {
116            Self::Unstable | Self::Experimental => *self,
117            Self::Testing(_) => Self::Testing(None),
118            Self::Stable(_) => Self::Stable(None),
119            Self::OldStable(_) => Self::OldStable(None),
120        }
121    }
122}
123
124impl Display for Suite {
125    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
126        match self {
127            Self::Unstable => write!(f, "unstable"),
128            Self::Testing(None) => write!(f, "testing"),
129            Self::Stable(None) => write!(f, "stable"),
130            Self::OldStable(None) => write!(f, "oldstable"),
131            Self::Experimental => write!(f, "experimental"),
132            Self::Testing(Some(ext)) => write!(f, "testing-{ext}"),
133            Self::Stable(Some(ext)) => write!(f, "stable-{ext}"),
134            Self::OldStable(Some(ext)) => write!(f, "oldstable-{ext}"),
135        }
136    }
137}
138
139impl TryFrom<&str> for Suite {
140    type Error = ParseError;
141
142    fn try_from(value: &str) -> Result<Self, Self::Error> {
143        match value {
144            "unstable" => Ok(Self::Unstable),
145            "testing" => Ok(Self::Testing(None)),
146            "stable" => Ok(Self::Stable(None)),
147            "oldstable" => Ok(Self::OldStable(None)),
148            // The Release file from stable-proposed-updates calls the suite proposed-updaptes.
149            "proposed-updates" => Ok(Self::Stable(Some(Extension::ProposedUpdates))),
150            "experimental" => Ok(Self::Experimental),
151            _ => {
152                let s = value.split_once('-').ok_or(ParseError::InvalidSuite)?;
153                let ext = Extension::try_from(s.1)?;
154                match s.0 {
155                    "testing" => Ok(Self::Testing(Some(ext))),
156                    "stable" => Ok(Self::Stable(Some(ext))),
157                    "oldstable" => Ok(Self::OldStable(Some(ext))),
158                    _ => Err(ParseError::InvalidSuite),
159                }
160            }
161        }
162    }
163}
164
165impl FromStr for Suite {
166    type Err = ParseError;
167
168    fn from_str(s: &str) -> Result<Self, Self::Err> {
169        Self::try_from(s)
170    }
171}
172
173impl Serialize for Suite {
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: Serializer,
177    {
178        serializer.serialize_str(&self.to_string())
179    }
180}
181
182impl<'de> Deserialize<'de> for Suite {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: serde::Deserializer<'de>,
186    {
187        deserializer.deserialize_str(TryFromStrVisitor::new("a suite name"))
188    }
189}
190
191/// Debian archive codenames
192///
193/// This enum describes the codenames names found in the Debian archive.
194#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
195pub enum Codename {
196    /// The unstable suite
197    Sid,
198    /// The testing suite
199    Trixie(Option<Extension>),
200    /// The stable suite
201    Bookworm(Option<Extension>),
202    /// The oldstable suite
203    Bullseye(Option<Extension>),
204    /// The experimental suite
205    RCBuggy,
206}
207
208impl WithExtension for Codename {
209    fn with_extension(&self, extension: Extension) -> Self {
210        match self {
211            Self::Sid | Self::RCBuggy => *self,
212            Self::Trixie(_) => Self::Trixie(Some(extension)),
213            Self::Bookworm(_) => Self::Bookworm(Some(extension)),
214            Self::Bullseye(_) => Self::Bullseye(Some(extension)),
215        }
216    }
217
218    fn without_extension(&self) -> Self {
219        match self {
220            Self::Sid | Self::RCBuggy => *self,
221            Self::Trixie(_) => Self::Trixie(None),
222            Self::Bookworm(_) => Self::Bookworm(None),
223            Self::Bullseye(_) => Self::Bullseye(None),
224        }
225    }
226}
227
228impl Display for Codename {
229    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
230        match self {
231            Self::Sid => write!(f, "sid"),
232            Self::Trixie(None) => write!(f, "trixie"),
233            Self::Bookworm(None) => write!(f, "bookworm"),
234            Self::Bullseye(None) => write!(f, "bullseye"),
235            Self::RCBuggy => write!(f, "rc-buggy"),
236            Self::Trixie(Some(ext)) => write!(f, "trixie-{ext}"),
237            Self::Bookworm(Some(ext)) => write!(f, "bookworm-{ext}"),
238            Self::Bullseye(Some(ext)) => write!(f, "bullseye-{ext}"),
239        }
240    }
241}
242
243impl TryFrom<&str> for Codename {
244    type Error = ParseError;
245
246    fn try_from(value: &str) -> Result<Self, Self::Error> {
247        match value {
248            "sid" => Ok(Self::Sid),
249            "trixie" => Ok(Self::Trixie(None)),
250            "bookworm" => Ok(Self::Bookworm(None)),
251            "bullseye" => Ok(Self::Bullseye(None)),
252            "rc-buggy" => Ok(Self::RCBuggy),
253            _ => {
254                let s = value.split_once('-').ok_or(ParseError::InvalidCodename)?;
255                let ext = Extension::try_from(s.1)?;
256                match s.0 {
257                    "trixie" => Ok(Self::Trixie(Some(ext))),
258                    "bookworm" => Ok(Self::Bookworm(Some(ext))),
259                    "bullseye" => Ok(Self::Bullseye(Some(ext))),
260                    _ => Err(ParseError::InvalidCodename),
261                }
262            }
263        }
264    }
265}
266
267impl FromStr for Codename {
268    type Err = ParseError;
269
270    fn from_str(s: &str) -> Result<Self, Self::Err> {
271        Self::try_from(s)
272    }
273}
274
275impl From<Suite> for Codename {
276    fn from(suite: Suite) -> Self {
277        match suite {
278            Suite::Unstable => Self::Sid,
279            Suite::Testing(ext) => Self::Trixie(ext),
280            Suite::Stable(ext) => Self::Bookworm(ext),
281            Suite::OldStable(ext) => Self::Bullseye(ext),
282            Suite::Experimental => Self::RCBuggy,
283        }
284    }
285}
286
287impl From<Codename> for Suite {
288    fn from(codename: Codename) -> Self {
289        match codename {
290            Codename::Sid => Self::Unstable,
291            Codename::Trixie(ext) => Self::Testing(ext),
292            Codename::Bookworm(ext) => Self::Stable(ext),
293            Codename::Bullseye(ext) => Self::OldStable(ext),
294            Codename::RCBuggy => Self::Experimental,
295        }
296    }
297}
298
299impl Serialize for Codename {
300    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
301    where
302        S: Serializer,
303    {
304        serializer.serialize_str(&self.to_string())
305    }
306}
307
308impl<'de> Deserialize<'de> for Codename {
309    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
310    where
311        D: serde::Deserializer<'de>,
312    {
313        deserializer.deserialize_str(TryFromStrVisitor::new("a codename"))
314    }
315}
316
317/// Represents either a suite or codename
318///
319/// This enum is useful whenever a suite name or codename works
320#[derive(Clone, Copy, Debug, Eq)]
321pub enum SuiteOrCodename {
322    /// A suite
323    Suite(Suite),
324    /// A codename
325    Codename(Codename),
326}
327
328impl SuiteOrCodename {
329    /// Unstable
330    pub const UNSTABLE: Self = Self::Suite(Suite::Unstable);
331    /// Testing
332    pub const TESTING: Self = Self::Suite(Suite::Testing(None));
333    /// Stable
334    pub const STABLE: Self = Self::Suite(Suite::Stable(None));
335    /// Oldstable
336    pub const OLDSTABLE: Self = Self::Suite(Suite::OldStable(None));
337    /// Experimental
338    pub const EXPERIMENTAL: Self = Self::Suite(Suite::Experimental);
339    /// Stable proposed-updates
340    pub const STABLE_PU: Self = Self::Suite(Suite::Stable(Some(Extension::ProposedUpdates)));
341    /// Oldstable propoused-updates
342    pub const OLDSTABLE_PU: Self = Self::Suite(Suite::OldStable(Some(Extension::ProposedUpdates)));
343    /// Stable backports
344    pub const STABLE_BACKPORTS: Self = Self::Suite(Suite::Stable(Some(Extension::Backports)));
345}
346
347impl PartialEq for SuiteOrCodename {
348    fn eq(&self, other: &Self) -> bool {
349        match (self, other) {
350            (Self::Suite(l0), Self::Suite(r0)) => l0 == r0,
351            (Self::Codename(l0), Self::Codename(r0)) => l0 == r0,
352            (Self::Suite(l0), Self::Codename(r0)) => Suite::from(*r0) == *l0,
353            (Self::Codename(l0), Self::Suite(r0)) => Suite::from(*l0) == *r0,
354        }
355    }
356}
357
358impl Hash for SuiteOrCodename {
359    fn hash<H>(&self, state: &mut H)
360    where
361        H: Hasher,
362    {
363        match self {
364            Self::Suite(suite) => suite.hash(state),
365            Self::Codename(codename) => Suite::from(*codename).hash(state),
366        }
367    }
368}
369
370impl WithExtension for SuiteOrCodename {
371    fn with_extension(&self, extension: Extension) -> Self {
372        match self {
373            Self::Suite(suite) => Self::Suite(suite.with_extension(extension)),
374            Self::Codename(suite) => Self::Codename(suite.with_extension(extension)),
375        }
376    }
377
378    fn without_extension(&self) -> Self {
379        match self {
380            Self::Suite(suite) => Self::Suite(suite.without_extension()),
381            Self::Codename(suite) => Self::Codename(suite.without_extension()),
382        }
383    }
384}
385
386impl From<Codename> for SuiteOrCodename {
387    fn from(codename: Codename) -> Self {
388        Self::Codename(codename)
389    }
390}
391
392impl From<Suite> for SuiteOrCodename {
393    fn from(suite: Suite) -> Self {
394        Self::Suite(suite)
395    }
396}
397
398impl TryFrom<&str> for SuiteOrCodename {
399    type Error = ParseError;
400
401    fn try_from(value: &str) -> Result<Self, Self::Error> {
402        match Suite::try_from(value) {
403            Ok(suite) => Ok(Self::Suite(suite)),
404            Err(_) => match Codename::try_from(value) {
405                Ok(codename) => Ok(Self::Codename(codename)),
406                Err(_) => Err(ParseError::InvalidSuiteOrCodename),
407            },
408        }
409    }
410}
411
412impl FromStr for SuiteOrCodename {
413    type Err = ParseError;
414
415    fn from_str(s: &str) -> Result<Self, Self::Err> {
416        Self::try_from(s)
417    }
418}
419
420impl Display for SuiteOrCodename {
421    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
422        match self {
423            Self::Suite(suite) => suite.fmt(f),
424            Self::Codename(codename) => codename.fmt(f),
425        }
426    }
427}
428
429impl From<SuiteOrCodename> for Suite {
430    fn from(value: SuiteOrCodename) -> Self {
431        match value {
432            SuiteOrCodename::Suite(suite) => suite,
433            SuiteOrCodename::Codename(codename) => Self::from(codename),
434        }
435    }
436}
437
438impl From<SuiteOrCodename> for Codename {
439    fn from(value: SuiteOrCodename) -> Self {
440        match value {
441            SuiteOrCodename::Suite(suite) => Self::from(suite),
442            SuiteOrCodename::Codename(codename) => codename,
443        }
444    }
445}
446
447impl Serialize for SuiteOrCodename {
448    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
449    where
450        S: Serializer,
451    {
452        serializer.serialize_str(&self.to_string())
453    }
454}
455
456impl<'de> Deserialize<'de> for SuiteOrCodename {
457    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
458    where
459        D: serde::Deserializer<'de>,
460    {
461        deserializer.deserialize_str(TryFromStrVisitor::new("a suite or a codename"))
462    }
463}
464
465/// Allowed values of the multi-arch field
466#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Hash)]
467#[serde(rename_all = "lowercase")]
468pub enum MultiArch {
469    /// MA: allowed
470    Allowed,
471    /// MA: foreign
472    Foreign,
473    /// MA: no
474    No,
475    /// MA: same
476    Same,
477}
478
479impl AsRef<str> for MultiArch {
480    fn as_ref(&self) -> &str {
481        match self {
482            Self::Allowed => "allowed",
483            Self::Foreign => "foreign",
484            Self::No => "no",
485            Self::Same => "same",
486        }
487    }
488}
489
490impl Display for MultiArch {
491    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
492        write!(f, "{}", self.as_ref())
493    }
494}
495
496impl TryFrom<&str> for MultiArch {
497    type Error = ParseError;
498
499    fn try_from(value: &str) -> Result<Self, Self::Error> {
500        match value {
501            "allowed" => Ok(Self::Allowed),
502            "foreign" => Ok(Self::Foreign),
503            "no" => Ok(Self::No),
504            "same" => Ok(Self::Same),
505            _ => Err(ParseError::InvalidMultiArch),
506        }
507    }
508}
509
510impl FromStr for MultiArch {
511    type Err = ParseError;
512
513    fn from_str(s: &str) -> Result<Self, Self::Err> {
514        Self::try_from(s)
515    }
516}
517
518/// Debian archive components
519#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Hash)]
520#[serde(rename_all = "lowercase")]
521pub enum Component {
522    /// The `main` archive component
523    Main,
524    /// The `contrib` archive component
525    Contrib,
526    /// The `non-free` archive component
527    #[serde(rename = "non-free")]
528    NonFree,
529    /// The `non-free-firmware` archive component
530    #[serde(rename = "non-free-firmware")]
531    NonFreeFirmware,
532}
533
534impl AsRef<str> for Component {
535    fn as_ref(&self) -> &str {
536        match self {
537            Self::Main => "main",
538            Self::Contrib => "contrib",
539            Self::NonFree => "non-free",
540            Self::NonFreeFirmware => "non-free-firmware",
541        }
542    }
543}
544
545impl Display for Component {
546    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
547        write!(f, "{}", self.as_ref())
548    }
549}
550
551impl TryFrom<&str> for Component {
552    type Error = ParseError;
553
554    fn try_from(value: &str) -> Result<Self, Self::Error> {
555        match value {
556            "main" => Ok(Self::Main),
557            "contrib" => Ok(Self::Contrib),
558            "non-free" => Ok(Self::NonFree),
559            "non-free-firmware" => Ok(Self::NonFreeFirmware),
560            _ => Err(ParseError::InvalidComponent),
561        }
562    }
563}
564
565impl FromStr for Component {
566    type Err = ParseError;
567
568    fn from_str(s: &str) -> Result<Self, Self::Err> {
569        Self::try_from(s)
570    }
571}
572
573#[cfg(test)]
574mod test {
575    use std::hash::DefaultHasher;
576
577    use super::*;
578
579    #[test]
580    fn suite_from_str() {
581        assert_eq!(Suite::try_from("unstable").unwrap(), Suite::Unstable);
582        assert_eq!(Suite::try_from("stable").unwrap(), Suite::Stable(None));
583        assert_eq!(
584            Suite::try_from("stable-backports").unwrap(),
585            Suite::Stable(Some(Extension::Backports))
586        );
587    }
588
589    #[test]
590    fn codename_from_str() {
591        assert_eq!(Codename::try_from("sid").unwrap(), Codename::Sid);
592        assert_eq!(
593            Codename::try_from("bullseye").unwrap(),
594            Codename::Bullseye(None)
595        );
596        assert_eq!(
597            Codename::try_from("bullseye-backports").unwrap(),
598            Codename::Bullseye(Some(Extension::Backports))
599        );
600    }
601
602    #[test]
603    fn codename_from_suite() {
604        assert_eq!(Codename::from(Suite::Unstable), Codename::Sid);
605        assert_eq!(
606            Codename::from(Suite::Stable(Some(Extension::Backports))),
607            Codename::Bookworm(Some(Extension::Backports))
608        );
609    }
610
611    #[test]
612    fn suite_from_codename() {
613        assert_eq!(Suite::from(Codename::Sid), Suite::Unstable);
614        assert_eq!(
615            Suite::from(Codename::Bookworm(Some(Extension::Backports))),
616            Suite::Stable(Some(Extension::Backports))
617        );
618    }
619
620    #[test]
621    fn suite_or_codename_from_str() {
622        assert_eq!(
623            SuiteOrCodename::try_from("unstable").unwrap(),
624            SuiteOrCodename::from(Suite::Unstable)
625        );
626        assert_eq!(
627            SuiteOrCodename::try_from("sid").unwrap(),
628            SuiteOrCodename::from(Codename::Sid)
629        );
630    }
631
632    #[test]
633    fn suite_or_codename_eq() {
634        assert_eq!(
635            SuiteOrCodename::UNSTABLE,
636            SuiteOrCodename::Codename(Codename::Sid)
637        );
638        assert_eq!(
639            SuiteOrCodename::STABLE_PU,
640            SuiteOrCodename::Codename(Codename::Bookworm(Some(Extension::ProposedUpdates)))
641        );
642    }
643
644    #[test]
645    fn suite_or_codename_hash() {
646        let mut hasher_1 = DefaultHasher::new();
647        let mut hasher_2 = DefaultHasher::new();
648
649        SuiteOrCodename::UNSTABLE.hash(&mut hasher_1);
650        SuiteOrCodename::Codename(Codename::Sid).hash(&mut hasher_2);
651        assert_eq!(hasher_1.finish(), hasher_2.finish());
652    }
653
654    #[test]
655    fn multi_arch_from_str() {
656        assert_eq!(MultiArch::try_from("foreign").unwrap(), MultiArch::Foreign);
657    }
658
659    #[test]
660    fn compoment_from_str() {
661        assert_eq!(Component::try_from("main").unwrap(), Component::Main);
662    }
663}