alpm_pkginfo/
utils.rs

1//! Utilities used by some or all PackageInfo versions.
2use std::{
3    fmt::{Display, Formatter},
4    str::FromStr,
5};
6
7use alpm_types::{PackageRelation, SonameV1, SonameV2};
8use winnow::{
9    ModalResult,
10    Parser,
11    combinator::{cut_err, fail},
12    error::{StrContext, StrContextValue},
13    stream::Stream,
14    token::rest,
15};
16
17use crate::Error;
18#[cfg(doc)]
19use crate::{PackageInfoV1, PackageInfoV2};
20
21/// Provides either a [`PackageRelation`] or a [`SonameV1`].
22///
23/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_ in
24/// [`PackageInfoV1`] or [`PackageInfoV2`].
25///
26/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum RelationOrSoname {
29    Relation(PackageRelation),
30    SonameV1(SonameV1),
31    SonameV2(SonameV2),
32}
33
34impl RelationOrSoname {
35    /// Recognizes a [`SonameV2`], a [`SonameV1`] or a [`PackageRelation`] in a string slice.
36    ///
37    /// First attempts to recognize a [`SonameV2`], then a [`SonameV1`] and if that fails, falls
38    /// back to recognizing a [`PackageRelation`].
39    /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
40    pub fn parser(input: &mut &str) -> ModalResult<Self> {
41        // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
42        // such a way that they return errors on unexpected input instead of backtracking.
43        let checkpoint = input.checkpoint();
44        let sonamev2_result = SonameV2::parser.parse_next(input);
45        if sonamev2_result.is_ok() {
46            let sonamev2 = sonamev2_result?;
47            return Ok(RelationOrSoname::SonameV2(sonamev2));
48        }
49
50        input.reset(&checkpoint);
51        let sonamev1_result = SonameV1::parser.parse_next(input);
52        if sonamev1_result.is_ok() {
53            let sonamev1 = sonamev1_result?;
54            return Ok(RelationOrSoname::SonameV1(sonamev1));
55        }
56
57        input.reset(&checkpoint);
58        let relation_result = rest.and_then(PackageRelation::parser).parse_next(input);
59        if relation_result.is_ok() {
60            let relation = relation_result?;
61            return Ok(RelationOrSoname::Relation(relation));
62        }
63
64        cut_err(fail)
65            .context(StrContext::Expected(StrContextValue::Description(
66                "alpm-sonamev2, alpm-sonamev1 or alpm-package-relation",
67            )))
68            .parse_next(input)
69    }
70}
71
72impl Display for RelationOrSoname {
73    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
74        match self {
75            RelationOrSoname::Relation(version) => write!(f, "{version}"),
76            RelationOrSoname::SonameV1(soname) => write!(f, "{soname}"),
77            RelationOrSoname::SonameV2(soname) => write!(f, "{soname}"),
78        }
79    }
80}
81
82impl FromStr for RelationOrSoname {
83    type Err = Error;
84
85    /// Creates a [`RelationOrSoname`] from a string slice.
86    ///
87    /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
88    /// [`RelationOrSoname`] accordingly.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use alpm_pkginfo::RelationOrSoname;
98    /// use alpm_types::{PackageRelation, SonameV1, SonameV2};
99    ///
100    /// # fn main() -> Result<(), alpm_pkginfo::Error> {
101    /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
102    /// assert_eq!(
103    ///     relation,
104    ///     RelationOrSoname::Relation(PackageRelation::new(
105    ///         "example".parse()?,
106    ///         Some("=1.0.0".parse()?)
107    ///     ))
108    /// );
109    ///
110    /// let sonamev2: RelationOrSoname = "lib:example.so.1".parse()?;
111    /// assert_eq!(
112    ///     sonamev2,
113    ///     RelationOrSoname::SonameV2(SonameV2::new("lib".parse()?, "example.so.1".parse()?))
114    /// );
115    ///
116    /// let sonamev1: RelationOrSoname = "example.so".parse()?;
117    /// assert_eq!(
118    ///     sonamev1,
119    ///     RelationOrSoname::SonameV1(SonameV1::new("example.so".parse()?, None, None)?)
120    /// );
121    /// # Ok(())
122    /// # }
123    /// ```
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        Self::parser
126            .parse(s)
127            .map_err(|error| Error::AlpmType(alpm_types::Error::ParseError(error.to_string())))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use alpm_types::{ElfArchitectureFormat, VersionOrSoname};
134    use rstest::rstest;
135    use testresult::TestResult;
136
137    use super::*;
138
139    #[rstest]
140    #[case(
141        "example",
142        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
143    )]
144    #[case(
145        "example=1.0.0",
146        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok()))
147    )]
148    #[case(
149        "example>=1.0.0",
150        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
151    )]
152    #[case(
153        "lib:example.so.1",
154        RelationOrSoname::SonameV2(
155            SonameV2::new(
156                "lib".parse().unwrap(),
157                "example.so.1".parse().unwrap(),
158            )
159        )
160    )]
161    #[case(
162        "lib:example.so",
163        RelationOrSoname::SonameV2(
164            SonameV2::new(
165                "lib".parse().unwrap(),
166                "example.so".parse().unwrap(),
167            )
168        )
169    )]
170    #[case(
171        "example.so",
172        RelationOrSoname::SonameV1(
173            SonameV1::new(
174                "example.so".parse().unwrap(),
175                None,
176                None,
177            ).unwrap()
178        )
179    )]
180    #[case(
181        "example.so=1.0.0-64",
182        RelationOrSoname::SonameV1(
183            SonameV1::new(
184                "example.so".parse().unwrap(),
185                Some(VersionOrSoname::Version("1.0.0".parse().unwrap())),
186                Some(ElfArchitectureFormat::Bit64),
187            ).unwrap()
188        )
189    )]
190    #[case(
191        "libexample.so=otherlibexample.so-64",
192        RelationOrSoname::SonameV1(
193            SonameV1::new(
194                "libexample.so".parse().unwrap(),
195                Some(VersionOrSoname::Soname("otherlibexample.so".parse().unwrap())),
196                Some(ElfArchitectureFormat::Bit64),
197            ).unwrap()
198        )
199
200    )]
201    fn test_relation_or_soname_parser(
202        #[case] mut input: &str,
203        #[case] expected: RelationOrSoname,
204    ) -> TestResult {
205        let input_str = input.to_string();
206        let result = RelationOrSoname::parser(&mut input)?;
207        assert_eq!(result, expected);
208        assert_eq!(result.to_string(), input_str);
209        Ok(())
210    }
211}