Skip to main content

alpm_types/relation/
composite.rs

1//! Composite relation types used in metadata files.
2
3use std::{
4    fmt::{Display, Formatter},
5    str::FromStr,
6};
7
8use alpm_parsers::prelude::*;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11use winnow::{error::ErrMode, stream::Stream};
12
13use crate::{Error, PackageRelation, SonameV1, SonameV2};
14
15/// Provides either a [`PackageRelation`], a [`SonameV1`] or a [`SonameV2`].
16///
17/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_
18/// e.g. in [PKGINFO], [SRCINFO] or [alpm-db-desc] files.
19///
20/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
21/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
22/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
23/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
24#[derive(Clone, Debug, Eq, PartialEq)]
25#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
26#[cfg_attr(feature = "serde", serde(untagged))]
27pub enum RelationOrSoname {
28    /// A package relation (as [`PackageRelation`]).
29    Relation(PackageRelation),
30    /// A shared object name following [alpm-sonamev1].
31    ///
32    /// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
33    SonameV1(SonameV1),
34    /// A shared object name following [alpm-sonamev2].
35    ///
36    /// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
37    SonameV2(SonameV2),
38}
39
40impl PartialEq<PackageRelation> for RelationOrSoname {
41    fn eq(&self, other: &PackageRelation) -> bool {
42        self.to_string() == other.to_string()
43    }
44}
45
46impl PartialEq<SonameV1> for RelationOrSoname {
47    fn eq(&self, other: &SonameV1) -> bool {
48        self.to_string() == other.to_string()
49    }
50}
51
52impl PartialEq<SonameV2> for RelationOrSoname {
53    fn eq(&self, other: &SonameV2) -> bool {
54        self.to_string() == other.to_string()
55    }
56}
57
58impl ParserUntil for RelationOrSoname {
59    fn parser_until<'a, P>(delimiter: P) -> impl Parser<Input<'a>, Self, ErrMode<ParseStack<'a>>>
60    where
61        P: Parser<Input<'a>, &'a str, ErrMode<ParseStack<'a>>>,
62    {
63        let mut delimiter = delimiter;
64        let parser = move |input: &mut Input<'a>| -> PResult<Self> {
65            // In the following, we attempt all three possible representation of a version, starting
66            // with the most specific and ending with the least specific, generic PackageRelation.
67            //
68            // We are forced to use parser_until, as there are some soname expressions, such as
69            // `libalpm.so>=14` or `librtmp.so.0`, which we only encountered in the AUR until now.
70            // These expressions however, are picked up by pacman as virtual components and as such,
71            // we also have to handle these in the same way, even if it's dirty.
72            let checkpoint = input.checkpoint();
73            if let Ok(v2) = SonameV2::parser_until(delimiter.by_ref())
74                .map(RelationOrSoname::SonameV2)
75                .parse_next(input)
76            {
77                return Ok(v2);
78            };
79            input.reset(&checkpoint);
80
81            if let Ok(v1) = SonameV1::parser_until(delimiter.by_ref())
82                .map(RelationOrSoname::SonameV1)
83                .parse_next(input)
84            {
85                return Ok(v1);
86            };
87            input.reset(&checkpoint);
88
89            PackageRelation::parser_until(delimiter.by_ref())
90                .map(RelationOrSoname::Relation)
91                .parse_next(input)
92        };
93        parser.layer("alpm-package-relation or alpm-sonamev1 or alpm-sonamev2")
94    }
95}
96
97impl Display for RelationOrSoname {
98    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99        match self {
100            RelationOrSoname::Relation(version) => write!(f, "{version}"),
101            RelationOrSoname::SonameV1(soname) => write!(f, "{soname}"),
102            RelationOrSoname::SonameV2(soname) => write!(f, "{soname}"),
103        }
104    }
105}
106
107impl FromStr for RelationOrSoname {
108    type Err = Error;
109
110    /// Creates a [`RelationOrSoname`] from a string slice.
111    ///
112    /// Relies on [`RelationOrSoname::parser_until_eof`] to recognize types in `input` and create a
113    /// [`RelationOrSoname`] accordingly.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1, SonameV2};
123    ///
124    /// # fn main() -> Result<(), alpm_types::Error> {
125    /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
126    /// assert_eq!(
127    ///     relation,
128    ///     RelationOrSoname::Relation(PackageRelation::new(
129    ///         "example".parse()?,
130    ///         Some("=1.0.0".parse()?)
131    ///     ))
132    /// );
133    ///
134    /// let sonamev2: RelationOrSoname = "lib:example.so.1".parse()?;
135    /// assert_eq!(
136    ///     sonamev2,
137    ///     RelationOrSoname::SonameV2(SonameV2::new("lib".parse()?, "example.so.1".parse()?))
138    /// );
139    ///
140    /// let sonamev1: RelationOrSoname = "example.so".parse()?;
141    /// assert_eq!(
142    ///     sonamev1,
143    ///     RelationOrSoname::SonameV1(SonameV1::new("example.so".parse()?, None, None)?)
144    /// );
145    /// # Ok(())
146    /// # }
147    /// ```
148    fn from_str(s: &str) -> Result<Self, Self::Err> {
149        Self::parser_until_eof
150            .parse(Input::new(s))
151            .map_err(|error| Error::ParseError(error.to_string()))
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use insta::assert_snapshot;
158    use rstest::rstest;
159    use testresult::TestResult;
160
161    use super::*;
162    use crate::{ElfArchitectureFormat, Soname, VersionOrSoname, configure_insta};
163
164    #[rstest]
165    #[case("libexample.so.1")]
166    #[case("lib:libexample.so-abc")]
167    #[case("lib:libexample.so.10-10")]
168    #[case("lib:libexample.so.1.0.0-64")]
169    fn invalid_sonamev2_parser(#[case] input: &str) {
170        let (test_name, _guard) = configure_insta();
171
172        let Err(Error::ParseError(err_msg)) = SonameV2::from_str(input) else {
173            panic!("'{input}' did not fail to parse as expected")
174        };
175
176        assert_snapshot!(test_name, err_msg.to_string());
177    }
178
179    #[rstest]
180    #[case(
181        "example",
182        RelationOrSoname::Relation(PackageRelation::new("example".parse()?, None))
183    )]
184    #[case(
185        "example=1.0.0",
186        RelationOrSoname::Relation(PackageRelation::new("example".parse()?, "=1.0.0".parse().ok()))
187    )]
188    #[case(
189        "example>=1.0.0",
190        RelationOrSoname::Relation(PackageRelation::new("example".parse()?, ">=1.0.0".parse().ok()))
191    )]
192    // The following two cases are sonames that are of unsupported shape by us.
193    // We simply re-expose them as PackageRelations, i.e. virtual components, as that's also how
194    // pacman handles them.
195    #[case(
196        "example.so>=1-64",
197        RelationOrSoname::Relation(PackageRelation::new("example.so".parse()?, ">=1-64".parse().ok()))
198    )]
199    #[case(
200        "example.so.1",
201        RelationOrSoname::Relation(PackageRelation::new("example.so.1".parse()?, None))
202    )]
203    #[case(
204        "lib:example.so.1",
205        RelationOrSoname::SonameV2(
206            SonameV2::new(
207                "lib".parse()?,
208                Soname::from_str("example.so.1")?,
209            )
210        )
211    )]
212    #[case(
213        "lib:example.so",
214        RelationOrSoname::SonameV2(
215            SonameV2::new(
216                "lib".parse()?,
217                Soname::from_str("example.so")?,
218            )
219        )
220    )]
221    #[case(
222        "example.so",
223        RelationOrSoname::SonameV1(
224            SonameV1::new(
225                "example.so".parse()?,
226                None,
227                None,
228            )?
229        )
230    )]
231    #[case(
232        "example.so=1.0.0-64",
233        RelationOrSoname::SonameV1(
234            SonameV1::new(
235                "example.so".parse()?,
236                Some(VersionOrSoname::Version("1.0.0".parse()?)),
237                Some(ElfArchitectureFormat::Bit64),
238            )?
239        )
240    )]
241    #[case(
242        "libexample.so=otherlibexample.so-64",
243        RelationOrSoname::SonameV1(
244            SonameV1::new(
245                "libexample.so".parse()?,
246                Some(VersionOrSoname::Soname("otherlibexample.so".parse()?)),
247                Some(ElfArchitectureFormat::Bit64),
248            )?
249        )
250    )]
251    fn test_relation_or_soname_parser(
252        #[case] input: &str,
253        #[case] expected: RelationOrSoname,
254    ) -> TestResult {
255        let input_str = input.to_string();
256        let result = RelationOrSoname::parser_until_eof(&mut Input::new(input))?;
257        assert_eq!(result, expected);
258        assert_eq!(result.to_string(), input_str);
259        Ok(())
260    }
261}