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