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::traits::AlpmParser;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11use winnow::{
12    ModalResult,
13    Parser,
14    combinator::alt,
15    error::{StrContext, StrContextValue},
16};
17
18use crate::{Error, PackageRelation, SonameV1, SonameV2};
19
20/// Provides either a [`PackageRelation`], a [`SonameV1`] or a [`SonameV2`].
21///
22/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_
23/// e.g. in [PKGINFO], [SRCINFO] or [alpm-db-desc] files.
24///
25/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
26/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
27/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
28/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
29#[derive(Clone, Debug, Eq, PartialEq)]
30#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
31#[cfg_attr(feature = "serde", serde(untagged))]
32pub enum RelationOrSoname {
33    /// A package relation (as [`PackageRelation`]).
34    Relation(PackageRelation),
35    /// A shared object name following [alpm-sonamev1].
36    ///
37    /// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
38    SonameV1(SonameV1),
39    /// A shared object name following [alpm-sonamev2].
40    ///
41    /// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
42    SonameV2(SonameV2),
43}
44
45impl PartialEq<PackageRelation> for RelationOrSoname {
46    fn eq(&self, other: &PackageRelation) -> bool {
47        self.to_string() == other.to_string()
48    }
49}
50
51impl PartialEq<SonameV1> for RelationOrSoname {
52    fn eq(&self, other: &SonameV1) -> bool {
53        self.to_string() == other.to_string()
54    }
55}
56
57impl PartialEq<SonameV2> for RelationOrSoname {
58    fn eq(&self, other: &SonameV2) -> bool {
59        self.to_string() == other.to_string()
60    }
61}
62
63impl AlpmParser for RelationOrSoname {
64    /// Recognizes a [`SonameV2`], a [`SonameV1`] or a [`PackageRelation`] in a string slice.
65    ///
66    /// First attempts to recognize a [`SonameV2`], then a [`SonameV1`] and if that fails, falls
67    /// back to recognizing a [`PackageRelation`].
68    /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if `input` does not begin with a valid
73    /// [`SonameV2`], [`SonameV1`] or [`PackageRelation`].
74    fn parser(input: &mut &str) -> ModalResult<Self> {
75        // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
76        // such a way that they return errors on unexpected input instead of backtracking.
77        alt((
78            SonameV2::parser.map(RelationOrSoname::SonameV2),
79            SonameV1::parser.map(RelationOrSoname::SonameV1),
80            PackageRelation::parser.map(RelationOrSoname::Relation),
81        ))
82        .context(StrContext::Expected(StrContextValue::Description(
83            "alpm-sonamev2, alpm-sonamev1 or alpm-package-relation",
84        )))
85        .parse_next(input)
86    }
87}
88
89impl Display for RelationOrSoname {
90    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91        match self {
92            RelationOrSoname::Relation(version) => write!(f, "{version}"),
93            RelationOrSoname::SonameV1(soname) => write!(f, "{soname}"),
94            RelationOrSoname::SonameV2(soname) => write!(f, "{soname}"),
95        }
96    }
97}
98
99impl FromStr for RelationOrSoname {
100    type Err = Error;
101
102    /// Creates a [`RelationOrSoname`] from a string slice.
103    ///
104    /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
105    /// [`RelationOrSoname`] accordingly.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1, SonameV2};
115    ///
116    /// # fn main() -> Result<(), alpm_types::Error> {
117    /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
118    /// assert_eq!(
119    ///     relation,
120    ///     RelationOrSoname::Relation(PackageRelation::new(
121    ///         "example".parse()?,
122    ///         Some("=1.0.0".parse()?)
123    ///     ))
124    /// );
125    ///
126    /// let sonamev2: RelationOrSoname = "lib:example.so.1".parse()?;
127    /// assert_eq!(
128    ///     sonamev2,
129    ///     RelationOrSoname::SonameV2(SonameV2::new("lib".parse()?, "example.so.1".parse()?))
130    /// );
131    ///
132    /// let sonamev1: RelationOrSoname = "example.so".parse()?;
133    /// assert_eq!(
134    ///     sonamev1,
135    ///     RelationOrSoname::SonameV1(SonameV1::new("example.so".parse()?, None, None)?)
136    /// );
137    /// # Ok(())
138    /// # }
139    /// ```
140    fn from_str(s: &str) -> Result<Self, Self::Err> {
141        Self::parser
142            .parse(s)
143            .map_err(|error| Error::ParseError(error.to_string()))
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use insta::assert_snapshot;
150    use rstest::rstest;
151    use testresult::TestResult;
152
153    use super::*;
154    use crate::{ElfArchitectureFormat, Soname, VersionOrSoname, configure_insta};
155
156    #[rstest]
157    #[case("libexample.so.1")]
158    #[case("lib:libexample.so-abc")]
159    #[case("lib:libexample.so.10-10")]
160    #[case("lib:libexample.so.1.0.0-64")]
161    fn invalid_sonamev2_parser(#[case] input: &str) {
162        let Err(Error::ParseError(err_msg)) = SonameV2::from_str(input) else {
163            panic!("'{input}' did not fail to parse as expected")
164        };
165
166        let (test_name, _guard) = configure_insta();
167        assert_snapshot!(test_name, err_msg.to_string());
168    }
169
170    #[rstest]
171    #[case(
172        "example",
173        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
174    )]
175    #[case(
176        "example=1.0.0",
177        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok()))
178    )]
179    #[case(
180        "example>=1.0.0",
181        RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
182    )]
183    #[case(
184        "lib:example.so.1",
185        RelationOrSoname::SonameV2(
186            SonameV2::new(
187                "lib".parse().unwrap(),
188                Soname::from_str("example.so.1").unwrap(),
189            )
190        )
191    )]
192    #[case(
193        "lib:example.so",
194        RelationOrSoname::SonameV2(
195            SonameV2::new(
196                "lib".parse().unwrap(),
197                Soname::from_str("example.so").unwrap(),
198            )
199        )
200    )]
201    #[case(
202        "example.so",
203        RelationOrSoname::SonameV1(
204            SonameV1::new(
205                "example.so".parse().unwrap(),
206                None,
207                None,
208            ).unwrap()
209        )
210    )]
211    #[case(
212        "example.so=1.0.0-64",
213        RelationOrSoname::SonameV1(
214            SonameV1::new(
215                "example.so".parse().unwrap(),
216                Some(VersionOrSoname::Version("1.0.0".parse().unwrap())),
217                Some(ElfArchitectureFormat::Bit64),
218            ).unwrap()
219        )
220    )]
221    #[case(
222        "libexample.so=otherlibexample.so-64",
223        RelationOrSoname::SonameV1(
224            SonameV1::new(
225                "libexample.so".parse().unwrap(),
226                Some(VersionOrSoname::Soname("otherlibexample.so".parse().unwrap())),
227                Some(ElfArchitectureFormat::Bit64),
228            ).unwrap()
229        )
230    )]
231    fn test_relation_or_soname_parser(
232        #[case] mut input: &str,
233        #[case] expected: RelationOrSoname,
234    ) -> TestResult {
235        let input_str = input.to_string();
236        let result = RelationOrSoname::parser(&mut input)?;
237        assert_eq!(result, expected);
238        assert_eq!(result.to_string(), input_str);
239        Ok(())
240    }
241}