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