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